孤儿进程、僵尸进程和守护进程
孤儿进程
如果父进程先于子进程退出,则子进程成为孤儿进程,此时将自动被PID为1的进程收养, PID为1的进程就成为了这个进程的父进程。
当一个孤儿进程退出以后,它的资源清理会交给它的父进程来处理。
#include <testfun.h>
int main(){if(fork() == 0){while(1){sleep(1);}}else{}return 0;
}
ps -elf
查看
再使用 kill -9 <PID> # 强制终止进程 我这里是kill -9 28667
僵尸进程
如果子进程先退出,系统不会自动清理掉子进程的环境,而必须由父进程调用wait或waitpid函数来完成清理工作。
如果父进程不做清理工作,则已经退出的子进程将成为僵尸进程(defunct),在系统中如果存在的僵尸进程过多,将会影响系统的性能, 所以必须对僵尸进程进行处理。
当一个进程执行结束时,它会向它的父进程发送一个SIGCHLD/终止信号,从而父进程可以根据子进程的终止情况进行处理。
在父进程处理之前,内核必须要在进程队列当中维持已经终止的子进程的PCB。如果僵尸进程过多,将会占据过多的内核态空间。并且僵尸进程的状态无法转换成其他任何进程状态。
#include <unistd.h> // 提供 fork() 函数声明int main() {// 调用 fork() 创建子进程if (fork() == 0) { // fork() 返回 0 表示当前是子进程// 子进程执行的代码块// 这里为空,子进程会直接执行后面的 return 0} else {// 父进程执行的代码块(fork() 返回子进程的 PID)while (1) { // 无限循环,使父进程持续运行// 循环体为空,父进程会一直占用 CPU}}// 子进程会执行到这里(因为 if 块为空)// 父进程不会执行到这里(因为 while(1) 是无限循环)return 0; // 子进程正常退出
}
ps -elf
查看
孤儿进程 vs 僵尸进程
类型 | 定义 | 如何终止 |
---|---|---|
孤儿进程 | 父进程已终止,由 init 接管 | kill -9 <PID> |
僵尸进程 | 已终止但未被父进程 wait() | 需终止其父进程或重启系统 |
wait函数
wait函数是一个系统调用函数。
wait函数的作用是等待一个已经退出的子进程,并进行清理回收子进程资源工作。
- wait 随机地等待一个已经退出的子进程,并返回该子进程的PID, 并给子进程进程资源清理和回收
- wait是一个阻塞函数, wait会一直阻塞直到等到一个结束的子进程, 解除阻塞.(前提是有子进程)(没有子进程的时候, 直接返回-1)
其函数声明如下:
// man 2 wait#include <sys/types.h>
#include <sys/wait.h>
//wait for process to change state
pid_t wait(int *wstatus);
// 返回值, 成功返回子进程的ID, 失败返回-1
wstatus: 了解
- 该参数是一个整型指针, 用来存储进程的退出状态(可以用宏解析)。
- 这个wstatus的整型内存区域中由两部分组成,其中一些位用来表示退出状态(当正常退出时),而另外一些位用来指示发生异常时的信号编号。
- 我们可以通过一些宏检查状态的情况。 (参考: man 2 wait)
宏 | 说明 |
---|---|
WIFEXITED(wstatus) | 子进程正常退出的时候返回真,此时可以使用WEXITSTATUS(wstatus),获取子进程的返回情况 |
WIFSIGNALED(wstatus) | 子进程异常退出的时候返回真,此时可以使用 WTERMSIG(wstatus)获取信号编号,可以使用 WCOREDUMP(wstatus)获取是否产生core文件 |
WIFSTOPPED(wstatus) | 子进程暂停的时候返回真,此时可以使用WSTOPSIG(wstatus)获取信号编号 |
... |
EgCode:
#include <testfun.h> // 自定义头文件(假设包含必要的函数声明)
#include <stdio.h> // 提供 printf() 函数声明
#include <unistd.h> // 提供 fork(), sleep(), wait() 等函数声明
#include <sys/wait.h> // 提供 wait() 相关的宏(WIFEXITED等)int main() {// 创建子进程if (fork() == 0) { // 子进程执行的代码块(fork()返回0)printf("child process \n"); // 打印子进程标识sleep(20); // 子进程休眠20秒(模拟耗时操作)return 100; // 子进程退出,返回状态码100} else {// 父进程执行的代码块(fork()返回子进程PID)int status; // 存储子进程退出状态int s_pid = wait(&status); // 阻塞等待任意子进程结束,并获取其PID和状态printf("wait child: child pid=%d \n", s_pid); // 打印被回收的子进程PID// 判断子进程退出方式if (WIFEXITED(status)) { // 子进程正常退出(调用return或exit)printf("child status: %d \n", WEXITSTATUS(status)); // 打印子进程退出码(100)} else if (WIFSIGNALED(status)) { // 子进程因信号终止(如kill -9)printf("child signed: %d \n", WTERMSIG(status)); // 打印终止信号的编号}}return 0; // 父进程退出
}
waitpid函数
函数 | 区别 |
---|---|
wait(&status) | 等待 任意一个子进程 结束。 |
waitpid(pid, &status, options) | 可以指定等待某个子进程(pid ),并支持非阻塞模式(WNOHANG )。 |
waitpid函数也是一个系统调用函数。
waitpid函数的作用是等待一个已经退出的子进程,并进行清理工作。
- waitpid 等待一个指定退出的子进程,并返回该子进程的PID
- waitpid 是一个阻塞函数
其函数声明如下:
#include <sys/types.h>
#include <sys/wait.h>
// wait for process to change state
pid_t waitpid(pid_t pid, // 指定等待的PID的子进程int *wstatus, // 存储进程的退出状态int options // 修改 waitpid 的行为的选项, 默认0
);
// 返回值: 返回值, 成功返回子进程的ID, 失败返回-1。
// 返回值: 如果在options参数上使用WNOHANG选项,且没有子进程退出:返回0
pid参数:pid参数可以控制支持更多种模式的等待方式。
表 3. PID数值效果
PID数值 | 效果 |
---|---|
< -1 | 等待进程PID和pid绝对值的进程 |
== -1 | 等待任一个子进程, 等价于wait |
== 0 | 等待同一进程组的任意子进程 |
什么是进程组 ?
进程组是一个或多个进程的集合:
- 每个进程除了是一个单独的进程,还归属于某一个进程组; 所以进程不仅有进程ID, 还有进程组ID。
- 当使用shell创建进程的时候,除了这个进程被创建,这个进程将会创建一个进程组, 并作为进程组的组长。
- 组长的PID就是进程组ID。
- 通过fork创建一个子进程时,子进程默认和父进程属于同一个进程组。
- 只要进程组当中存在至少一个进程(这个进程即使不是组长),该进程组就存在。
- getpgrp()函数获得进程组ID。
options参数:
- waitpid是阻塞函数,如果给waitpid 的options参数设置一个名为WNOHANG的宏,则系统调用会变成非阻塞模式。
- 如果默认阻塞: 填0。
EgCode:
#include <testfun.h> // 自定义头文件(假设包含必要的函数声明)
#include <stdio.h> // 提供 printf() 函数声明
#include <unistd.h> // 提供 fork(), sleep() 函数声明
#include <sys/wait.h> // 提供 waitpid() 及相关宏定义int main() {// 创建子进程if (fork() == 0) {// 子进程执行的代码块printf("child process \n"); // 打印子进程标识信息sleep(20); // 子进程休眠20秒(模拟耗时操作)return 100; // 子进程退出,返回状态码100} else {// 父进程执行的代码块int status; // 用于存储子进程退出状态/* 两种waitpid调用方式:* 1. 阻塞方式(0):父进程会一直等待子进程结束* 2. 非阻塞方式(WNOHANG):立即返回,不等待*/// int s_pid = waitpid(-1, &status, WNOHANG); // 非阻塞方式int s_pid = waitpid(-1, &status, 0); // 阻塞方式(常用)// 判断waitpid的返回结果if (s_pid == 0) {// 只有在WNOHANG模式下可能返回0,表示没有子进程结束printf("no child process end \n");} else {// 正常获取到结束的子进程printf("wait child: child pid=%d \n", s_pid); // 打印被回收的子进程PID// 解析子进程退出状态if (WIFEXITED(status)) {// 子进程正常退出printf("child status: %d \n", WEXITSTATUS(status)); // 打印退出码} else if (WIFSIGNALED(status)) {// 子进程被信号终止printf("child signed: %d \n", WTERMSIG(status)); // 打印信号编号}}}return 0; // 父进程退出
}
守护进程
守护进程是 在后台运行的特殊进程,它脱离终端控制,通常用于提供系统服务(如网络、日志、定时任务等)。它没有控制终端(TTY),生命周期长,随系统启动而运行,直到系统关闭才结束。
特征 | 说明 |
---|---|
后台运行 | 不占用终端,用户无法直接交互(如 sshd 、nginx )。 |
脱离终端控制 | 不受用户登录/注销影响(即使终端关闭,守护进程仍运行)。 |
生命周期长 | 通常在系统启动时运行,直到系统关闭。 |
无控制终端 | 其 stdin/stdout/stderr 通常重定向到 /dev/null 或日志文件。 |
独立会话组 | 调用 setsid() 创建新会话,脱离原进程组和终端关联。 |
守护进程的创建流程
- 父进程创建子进程,然后让父进程终止。
- 在子进程当中创建新会话。
- 修改当前工作目录为根目录。(因为如果使用当前目录, 意味着当前目录活跃, 则当前目录无法在文件系统中卸载; 而根目录所在的文件系统正常来讲是一直挂载的)
- 重设文件权限掩码为0,避免创建文件的权限受限。
- 关闭不需要的文件描述符,比如0、1、2。
#include <unistd.h> // 提供 fork(), setsid(), chdir(), close(), sleep() 等函数
#include <sys/stat.h> // 提供 umask() 函数int main(int argc, char* argv[]) {// 创建子进程if (fork() == 0) { // 子进程进入该分支(fork()返回0)// 1. 创建新会话,脱离终端控制setsid(); // 使子进程成为新会话的领头进程,脱离原终端// 2. 更改工作目录到根目录chdir("/"); // 避免守护进程阻止文件系统卸载// 3. 重设文件权限掩码umask(0); // 确保守护进程创建文件时有最大权限(模式0777)// 4. 关闭所有文件描述符(防止资源泄漏)for (int i = 0; i < 1024; i++) { // 遍历可能的文件描述符close(i); // 关闭每个描述符(包括标准输入/输出/错误)}// 5. 守护进程主循环while (1) { // 无限循环保持守护进程运行sleep(1); // 避免CPU占用过高(实际应用会替换为任务逻辑)}}// 父进程直接退出(子进程由init接管)return 0;
}
补充:job、bg、fg
关键区别总结
特性 | jobs | bg | fg |
---|---|---|---|
主要作用 | 查看作业状态 | 恢复暂停的任务到后台back | 将任务调回前台front |
依赖场景 | 需先有后台/暂停的作业 | 需先用 Ctrl+Z 暂停任务 | 需先用 & 或 Ctrl+Z |
输出示例 | [1] Running sleep 100 & | [1] + Continued vim & | 前台占用终端(如 vim ) |
+
:最近一个被放入后台的作业。(fg %+=fg)
-
:倒数第二个被放入后台的作业。
&: 表示放到后台运行
快捷键 | 功能 | 信号 | 进程状态 | 恢复方式 | 适用场景 |
---|---|---|---|---|---|
Ctrl+C | 强制终止当前前台进程 | SIGINT | 进程直接退出 | 无法恢复,需重新启动 | 想立即结束任务(如卡死的程序) |
Ctrl+Z | 暂停当前前台进程 | SIGTSTP | 进程暂停(Stopped) | 可通过 fg 或 bg 恢复 | 临时释放终端(如暂停 vim 去执行其他命令) |
符号 | 含义 | 等价命令 |
---|---|---|
%1 | 作业编号 1 | fg %1 、bg %1 |
%+ 或 %% | 最近操作的任务(带 + 标记) | fg (不指定编号) |
%- | 倒数第二个操作的任务 | fg %- |
fg %1
:将作业 1 调回前台运行。
bg %1
:将作业 1 在后台继续运行(针对已暂停的任务)。
补充:Kill
kill
是 Linux/Unix 系统中用于 终止进程 的核心命令,通过向目标进程发送信号(Signal)来实现控制。
kill命令可以用来给指定的进程发送信号。
- 通常用户经常会从终端启动shell再启动进程,当进程正在运行时,它可以接受一些键盘发送的信号:比如
ctrl+c
表示终止信号,ctrl+z
表示暂停信号。这种可以直接接受键盘信号的状态被称为前台,否则称为后台。 - 当进程处于后台的时候,只能通过kill命令发送信号 给它。
kill [选项] <PID或作业号> # 通过进程ID或作业号终止
kill -<信号名或编号> <PID> # 发送指定信号
信号编号 | 信号名 | 作用 | 是否可捕获 |
---|---|---|---|
1 | SIGHUP | 终端挂断(重启配置) | 是 |
2 | SIGINT | 键盘中断(Ctrl+C) | 是 |
9 | SIGKILL | 强制终止(不可捕获/忽略) | 否 |
15 | SIGTERM | 默认终止信号(允许清理) | 是 |
19 | SIGSTOP | 暂停进程(Ctrl+Z) | 否 |
kill -15 1234 # 先尝试正常退出(如保存数据)
kill -9 1234 # 若无效,强制终止
kill -9 -1 # 终止当前用户的所有进程(危险!)
kill %1 # 终止作业编号为1的任务(Shell作业控制)