进程备忘录
目录
一、概念
1. 僵尸进程(Zombie)
2. 孤儿进程(Orphan)
二、wait 系列函数(回收子进程)
1. pid_t wait(int *status);
2. pid_t waitpid(pid_t pid, int *status, int options);
3. pid_t waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
三、代码示例
一、概念
进程调用fork()创建子进程时,进程 ID 的变化遵循以下核心规则:
父进程:PID(进程ID)保持不变。
fork()返回子进程的 PID 给父进程。子进程:获得一个新的唯一 PID。
fork()返回 0 给子进程自身。
此外,还有一个容易被忽略的关键点:
PPID(父进程ID):子进程的 PPID 会被设置为父进程的 PID。这就像是子进程一出生就知道“谁是我的爸爸”。
关键特例:当“爸爸”先走一步时
如果父进程在子进程之前结束,那么子进程就变成了“孤儿进程”。此时,子进程会被PID 为 1 的init进程(或systemd)“收养”,它的PPID 会变成 1。
验证方法(代码示例)
下面这段 C 代码直观地验证:
c #include <stdio.h> #include <unistd.h> int main() { pid_t pid = fork(); if (pid == 0) { // 子进程 printf("子进程: PID=%d, PPID=%d\n", getpid(), getppid()); } else if (pid > 0) { // 父进程 printf("父进程: PID=%d, 子进程PID=%d\n", getpid(), pid); } return 0; }输出示例:
父进程: PID=1234, 子进程PID=1235 子进程: PID=1235, PPID=12341. 僵尸进程(Zombie)
定义:子进程已终止,但其父进程尚未调用
wait()/waitpid()来回收其退出状态,导致子进程的进程描述符仍保留在内核中。状态:
ps aux中显示为Z(Defunct)。危害:少量僵尸无大碍,大量会耗尽 PID 和内存资源。
产生原因:父进程未处理
SIGCHLD信号或未主动回收。解决方法:
父进程调用
wait()/waitpid()。父进程忽略
SIGCHLD(signal(SIGCHLD, SIG_IGN)),内核会自动回收。父进程终止,僵尸子进程会被 init(PID=1)收养并回收。
2. 孤儿进程(Orphan)
定义:父进程先于子进程终止,此时子进程变为孤儿。
处理:孤儿进程会被 init 进程(PID=1)自动收养,并负责回收(调用
wait),因此不会变成僵尸。应用:常用于守护进程(daemon)——通过 fork 让父进程退出,子进程成为孤儿,被 init 收养,脱离终端控制。
二、wait 系列函数(回收子进程)
1.pid_t wait(int *status);
阻塞等待任意子进程终止。
返回终止子进程 PID,
status存储退出状态。使用宏解析:
WIFEXITED(status)—— 正常退出 →WEXITSTATUS(status)取返回值。WIFSIGNALED(status)—— 被信号杀死 →WTERMSIG(status)取信号编号。
2.pid_t waitpid(pid_t pid, int *status, int options);
更灵活:
pid == -1:等待任意子进程(同wait)。pid > 0:等待指定 PID 的子进程。options =WNOHANG:非阻塞,立即返回 0(无子进程终止)。
常用于循环 +
WNOHANG实现非阻塞轮询。
3.pid_t waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
更高级的等待接口,支持实时信号信息。
idtype:P_PID:等待指定 PID。P_PGID:等待指定进程组。P_ALL:任意子进程。
options:WNOHANG、WEXITED、WSTOPPED、WCONTINUED。infop返回子进程状态详细信息(如 si_signo, si_code, si_pid 等)。
/****************************************************************************************************/
wait(&state)返回的是一个pid_t类型的值,但它返回的是“已终止的子进程的 PID”。
具体来说,这个返回值有三种情况:
1. 正常情况(返回 > 0)
返回的是刚刚终止的那个子进程的进程 ID。
因为一个父进程可能有多个子进程,通过这个返回值,你可以知道到底是哪一个子进程退出了。
2. 错误情况(返回 -1)
表示调用失败。最常见的错误是
ECHILD(没有子进程存在,或者子进程没有被等待)。此时可以通过
errno查看具体错误原因。
3. 特殊信号中断(返回 0)
(仅在设置了WNOHANG选项时发生)
如果你调用
waitpid(-1, &state, WNOHANG),并且当前没有任何子进程终止,返回值是0。但标准的
wait(&state)默认没有WNOHANG标志,如果子进程没结束它会一直阻塞,所以标准的wait不会返回 0。
补充一个重要细节:
虽然返回值是子进程 PID,但如果你想获取父进程自己的 PID,应该使用getpid()。
而wait返回的 PID 配合state参数(退出状态码)一起使用,可以精确处理每个子进程的退出结果
三、代码示例
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <signal.h> void daemonize() { pid_t pid; // 1. fork 并让父进程退出(成为孤儿,被 init 收养) pid = fork(); if (pid < 0) exit(EXIT_FAILURE); if (pid > 0) exit(EXIT_SUCCESS); // 父进程退出 /*exit(0); exit(EXIT_SUCCESS);通常是0 exit(EXIT_FAILURE);通常是1 exit(2); 自定义错误码 */ // 2. 创建新会话,脱离控制终端 if (setsid() < 0) exit(EXIT_FAILURE); // 3. 再次 fork(防止无意中重新获得终端) pid = fork(); if (pid < 0) exit(EXIT_FAILURE); if (pid > 0) exit(EXIT_SUCCESS); // 4. 修改工作目录为根目录(避免占用可卸载文件系统) chdir("/"); // 5. 重设文件权限掩码(更宽松) umask(0); // 6. 关闭所有打开的文件描述符(0,1,2) close(STDIN_FILENO); close(STDOUT_FILENO); close(STDERR_FILENO); // 7. 将 stdin/out/err 重定向到 /dev/null(可选) open("/dev/null", O_RDWR); // fd 0 dup(0); // fd 1 dup(0); // fd 2 //dup 是什么?复制一个“已存在的 fd” 规则:返回 最小可用 fd // 8. 忽略 SIGCHLD,避免僵尸(或使用 signal 处理) signal(SIGCHLD, SIG_IGN); } int main() { daemonize(); // 现在进程是守护进程,可以写日志、监听端口等 while (1) { // 守护进程主循环 sleep(10); } return 0; }在 Linux 内核机制中,会话组长有权申请打开一个终端。如果这个守护进程以后运气不好(比如它去打开了一个串口设备,或者某个库函数试图打开/dev/tty),它就会重新获得一个控制终端。
一旦重新获得终端,用户的键盘信号(比如
Ctrl+C)就有可能意外地发送给这个守护进程,导致它被杀死。这违背了守护进程“默默在后台运行,不受终端干扰”的初衷。
此时的孙子进程:
它继承了会话 ID,但它不是会话组长(因为它的 PID 不等于会话组的 SID)。
在 Linux 规则中,只有会话组长才能获取控制终端。既然孙子进程不是组长,它就永远、绝对、丧失了重新获得终端的资格。
它变成了一个纯粹的、不受任何终端信号干扰的后台进程。
WIFEXITED(state) | 检查是否正常退出 |
WEXITSTATUS(state) | 提取退出码 |
WIFSIGNALED(state) | 检查是否被信号终止 |
WTERMSIG(state) | 提取终止信号编号 |
WIFSTOPPED(state) | 检查是否被暂停 |
WSTOPSIG(state) | 提取暂停信号编号 |
