进程备忘录
目录一、概念1. 僵尸进程Zombie2. 孤儿进程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; }输出示例父进程: PID1234, 子进程PID1235 子进程: PID1235, PPID12341. 僵尸进程Zombie定义子进程已终止但其父进程尚未调用wait()/waitpid()来回收其退出状态导致子进程的进程描述符仍保留在内核中。状态ps aux中显示为ZDefunct。危害少量僵尸无大碍大量会耗尽 PID 和内存资源。产生原因父进程未处理SIGCHLD信号或未主动回收。解决方法父进程调用wait()/waitpid()。父进程忽略SIGCHLDsignal(SIGCHLD, SIG_IGN)内核会自动回收。父进程终止僵尸子进程会被 initPID1收养并回收。2. 孤儿进程Orphan定义父进程先于子进程终止此时子进程变为孤儿。处理孤儿进程会被 init 进程PID1自动收养并负责回收调用wait因此不会变成僵尸。应用常用于守护进程daemon——通过 fork 让父进程退出子进程成为孤儿被 init 收养脱离终端控制。二、wait 系列函数回收子进程1.pid_t wait(int *status);阻塞等待任意子进程终止。返回终止子进程 PIDstatus存储退出状态。使用宏解析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);更高级的等待接口支持实时信号信息。idtypeP_PID等待指定 PID。P_PGID等待指定进程组。P_ALL任意子进程。optionsWNOHANG、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它就会重新获得一个控制终端。一旦重新获得终端用户的键盘信号比如CtrlC就有可能意外地发送给这个守护进程导致它被杀死。这违背了守护进程“默默在后台运行不受终端干扰”的初衷。此时的孙子进程它继承了会话 ID但它不是会话组长因为它的 PID 不等于会话组的 SID。在 Linux 规则中只有会话组长才能获取控制终端。既然孙子进程不是组长它就永远、绝对、丧失了重新获得终端的资格。它变成了一个纯粹的、不受任何终端信号干扰的后台进程。WIFEXITED(state)检查是否正常退出WEXITSTATUS(state)提取退出码WIFSIGNALED(state)检查是否被信号终止WTERMSIG(state)提取终止信号编号WIFSTOPPED(state)检查是否被暂停WSTOPSIG(state)提取暂停信号编号