进程相关概念与知识点
2026/5/8 17:24:59 网站建设 项目流程

一、子进程与父进程的关系

1.基本概念

在 Linux 中,fork() 系统调用会创建一个新进程(子进程),并在父子进程中分别返回不同的值:
父进程中:返回新创建的子进程的 PID(一个大于 0 的整数)。
子进程中:返回值为 0。
创建失败时:返回值为 -1,不会创建任何进程。
因此,父进程中 fork() 的返回值是子进程的 PID。

#include <stdio.h> #include <unistd.h> int main() { pid_t pid = fork(); if (pid == 0) { printf("这是子进程,fork返回值:%d,自身PID:%d\n", pid, getpid()); } else if (pid > 0) { printf("这是父进程,fork返回值:%d,自身PID:%d\n", pid, getpid()); } else { printf("fork创建失败!\n"); } return 0; }

2.fork返回值

返回值含义对应进程
>0新创建的子进程 PID父进程
0代表 “当前是子进程”子进程
-1创建失败(如进程数达到上限、内存不足)无进程创建

易错点:fork() 调用一次,返回两次(父子进程各返回一次),这是它最特殊的地方。

3.核心特质

1. 资源复制与独立性
子进程会拷贝父进程的地址空间(代码段、数据段、栈、堆、文件描述符等),父子进程的内存完全独立,修改互不影响。
优化机制:写时复制(Copy-On-Write, COW)
刚创建时,父子进程共享同一份物理内存。
只有当任意一方尝试修改内存时,才会真正复制一份副本,避免不必要的开销。
2. 执行顺序
父子进程谁先执行,完全由操作系统调度器决定,顺序不确定。
想要控制顺序,需要用 wait() / waitpid() 让父进程阻塞等待子进程退出。
3. 父子进程的 PID 关系
子进程的 getppid() = 父进程的 getpid()。
父进程退出后,子进程会变成孤儿进程,被 init 进程(PID=1)收养。
子进程先退出、父进程未调用 wait() 时,子进程会变成僵尸进程(PCB 未释放,占用 PID 资源)。

n 次 fork() 后,总进程数 = 2^n(前提是所有 fork() 都成功)

4.父子进程共享的资源
✅ 共享:文件描述符表、文件偏移量(比如父子进程同时写同一个文件,会互相覆盖)
❌ 不共享:变量、栈、堆、进程状态、PID

4.常见考点

1.为什么 fork() 要返回子进程的 PID 给父进程?
父进程需要通过 PID 管理子进程(如 wait() 回收、kill() 发送信号),所以必须知道子进程的 PID。
2.僵尸进程和孤儿进程的区别?
孤儿进程:父进程先退出,子进程被 init 收养,无危害。
僵尸进程:子进程先退出,父进程未调用 wait() 回收,PCB (process control block 进程控制块)残留,占用 PID 资源,大量僵尸进程会导致系统无法创建新进程。
3.如何避免僵尸进程?
父进程调用 wait() / waitpid() 阻塞等待子进程退出。
父进程捕获 SIGCHLD 信号,在信号处理函数中调用 waitpid() 回收所有子进程。

5.核心模板代码

#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t pid = fork(); if (pid == -1) { perror("fork failed"); return 1; } else if (pid == 0) { // 子进程逻辑 printf("Child process: PID=%d, PPID=%d\n", getpid(), getppid()); sleep(2); // 模拟子进程执行 printf("Child process exiting\n"); return 0; } else { // 父进程逻辑 printf("Parent process: PID=%d, Child PID=%d\n", getpid(), pid); // 等待子进程退出,避免僵尸进程 int status; waitpid(pid, &status, 0); printf("Parent process: Child exited with status %d\n", WEXITSTATUS(status)); } return 0; }

写时复制:只有写操作才会触发复制,只读访问完全不需要拷贝,大幅提升 fork() 性能。
子进程刚创建时和父进程共享内存,修改时才会 “分家”,这就是 “写时复制” 的由来。

二、linux0/1/2 号进程 核心考点清单

进程PID名称核心角色父进程主要职责
0 号进程0swapper/idle内核根进程(所有进程的祖先)无(内核创建)1. 系统启动时创建,运行在内核态2. 初始化系统,创建 1 号和 2 号进程3. 系统空闲时作为 idle 进程调度
1 号进程1init/systemd用户态进程的 “祖先”0 号进程1. 接管用户空间初始化,启动系统服务2. 收养所有孤儿进程(父进程退出的进程)3. 负责管理和回收僵尸进程
2 号进程2kthreadd内核线程管理器0 号进程1. 负责创建和管理所有内核线程(如kworkerkswapd)2. 内核线程的父进程都是 2 号进程3. 内核态线程不会进入用户空间

高频考点 & 易错点


1. 进程创建关系
0 号进程是唯一的父进程:1 号和 2 号进程都是由 0 号进程创建的,二者是兄弟关系,不是父子关系。
验证方式:用 ps -ef 查看 PPID(父进程号),PID=1 和 PID=2 的 PPID 都是 0。
2. 孤儿进程 & 僵尸进程的处理
孤儿进程:父进程先退出,子进程被 1 号进程收养,1 号进程会成为它的新父进程,避免子进程成为 “无主进程”。
僵尸进程:子进程退出后,父进程未调用wait()/waitpid()回收,子进程 PCB 残留。1 号进程会定期调用wait()回收孤儿进程的僵尸状态,但用户进程的僵尸进程需要父进程主动处理。
3. 内核线程 vs 用户进程
内核线程(由 2 号进程创建):
运行在内核态,没有独立的用户地址空间,共享内核地址空间。
不执行用户代码,只执行内核函数,不能被用户直接管理。
用户进程(由 1 号进程及其后代创建):
运行在用户态,有独立的地址空间,可通过系统调用切换到内核态。
可被用户创建、管理、终止。
4. 0 号进程的特殊性
它不是普通进程,是内核在启动阶段创建的内核线程,没有用户态上下文。
当 CPU 没有任务可调度时,会切换回 0 号进程运行,也就是 “idle 状态”。


Linux 进程完整生命周期

整条链路:
创建 → 运行 → 阻塞 / 就绪 → 终止 → 资源回收

一、进程创建:fork /vfork/clone


0 号进程 最先存在,创建 1 号 (systemd)、2 号 (kthreadd)
普通用户进程:
用户进程调用 fork()
复制父进程页表、栈、数据、缓冲区
写时复制 COW,只读共享,写才拷贝
子进程:从fork 返回处继续执行,不重跑前面代码
fork 一次,两次返回
父:返回子进程 PID
子:返回 0
失败:返回 -1
补充:
vfork:子进程共享父进程地址空间,父进程阻塞
clone:Linux 底层,可定制共享资源,线程也靠它

二、进程替换:exec 系列


fork 只是复制代码,子进程和父进程跑一样的程序。
想要跑新程序 → 调用 exec
覆盖当前进程代码段、数据段、堆
保留:PID、PPID、文件描述符、进程属性
执行成功无返回,失败才返回 - 1
典型搭配:

fork() 创建子进程 → 子进程调用 exec 跑新程序

三、进程三种基本状态(必考)


就绪态
一切准备好,等 CPU 调度
运行态
正在 CPU 上执行代码
阻塞态 (等待)
等资源、等 IO、等信号、sleep
不占用 CPU,唤醒后回到就绪
状态切换:
就绪 ↔ 运行
运行 → 阻塞
阻塞 → 就绪

四、进程终止 3 种方式


正常退出
return
exit() // 库函数,刷新缓冲区、调用退出钩子
异常退出
段错误、除 0、非法指令 → 内核发信号杀死
人为终止
kill 命令 / 信号终止

五、退出后:僵尸进程 & 孤儿进程


1. 僵尸进程
子进程先退出,父进程没调用 wait/waitpid
子进程:代码、资源全部释放
只剩 PCB 进程控制块 残留,记录退出状态
危害:占用 PID,大量僵尸会导致无法新建进程
2. 孤儿进程
父进程先退出,子进程没人管
自动被 1 号进程 (systemd) 收养
无害,1 号进程会自动回收它

六、资源回收(收尾)


父进程通过:
wait()
waitpid()
获取子进程退出状态,释放 PCB,彻底消灭僵尸进程。

#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t pid; int status; // 1. 创建子进程:fork() pid = fork(); if (pid < 0) { perror("fork failed"); exit(EXIT_FAILURE); } if (pid == 0) { // ---------------------- // 子进程:替换程序 + 执行 + 退出 // ---------------------- printf("Child: PID=%d, Parent PID=%d\n", getpid(), getppid()); // 2. exec() 替换为新程序(这里用 ls 命令举例) // execlp(程序名, 命令, 参数, NULL); execlp("ls", "ls", "-l", NULL); // 只有 exec 失败才会执行到这里 perror("execlp failed"); // 3. exit() 终止进程 exit(EXIT_FAILURE); } else { // ---------------------- // 父进程:等待子进程 + 回收资源 // ---------------------- printf("Parent: PID=%d, Child PID=%d\n", getpid(), pid); // 4. wait() 阻塞等待子进程退出 wait(&status); // 解析子进程退出状态 if (WIFEXITED(status)) { printf("Parent: Child exited normally, exit code=%d\n", WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf("Parent: Child killed by signal %d\n", WTERMSIG(status)); } } return 0; }

三、进程状态宏 总结表

场景判断宏(先判断)取值宏(再取值)说明
子进程正常退出WIFEXITED(wstatus)返回:非 0 表示真WEXITSTATUS(wstatus)返回:子进程退出码 (0~255)子进程调用exit()/return正常结束
子进程被信号杀死WIFSIGNALED(wstatus)返回:非 0 表示真WTERMSIG(wstatus)返回:终止信号编号kill、段错误等信号强制终止
子进程被暂停 / 停止WIFSTOPPED(wstatus)返回:非 0 表示真WSTOPSIG(wstatus)返回:暂停信号编号收到SIGSTOP/SIGTSTP暂停运行
子进程暂停后恢复WIFCONTINUED(wstatus)返回:非 0 表示真收到SIGCONT信号恢复运行
子进程生成 core dumpWIFSIGNALED(wstatus)返回:非 0 表示真WCOREDUMP(wstatus)返回:非 0 表示生成被信号杀死且生成 core 文件(系统支持)

正常退出:WIFEXITED → WEXITSTATUS(退出码)
被信号杀死:WIFSIGNALED → WTERMSIG(信号号)
被暂停:WIFSTOPPED → WSTOPSIG(信号号)
恢复运行:WIFCONTINUED(无取值)
生成 core:WIFSIGNALED → WCOREDUMP

最重要规则(考试必考)
必须先判断,再取值
判断宏返回非 0 = 条件成立
取值宏只有在对应判断成立时才有意义

四、waitpid 选项常量表

选项常量作用具体行为典型应用场景
0实现阻塞等待父进程暂定直到子进程状态变化(退出 / 暂停)常规子进程同步等待(如批处理)
WNOHANG实现非阻塞等待调用waitpid时,若指定子进程未退出,函数立即返回0,父进程无需阻塞等待父进程需轮询监控子进程状态(如后台任务管理)
WUNTRACED捕获子进程暂停状态当子进程因信号(如SIGSTOP)停止运行时,waitpid返回该子进程 PID调试场景、需处理子进程暂停逻辑的程序
WCONTINUED捕获子进程恢复运行状态已停止的子进程通过SIGCONT信号恢复运行时,waitpid返回该子进程 PID需跟踪子进程完整生命周期(暂停 - 恢复)场景

注:这些选项可以用 | 组合使用,比如 WNOHANG | WUNTRACED

1. waitpid(pid, &status, 0) — 阻塞等待(等价于 wait)

#include <stdio.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> int main() { pid_t pid = fork(); if (pid == 0) { printf("子进程:开始执行任务\n"); sleep(3); // 模拟耗时任务 printf("子进程:任务完成,退出\n"); exit(0); } else { printf("父进程:等待子进程...\n"); int status; // 阻塞等待子进程退出 waitpid(pid, &status, 0); printf("父进程:子进程已退出,状态码:%d\n", WEXITSTATUS(status)); } return 0; }

2. WNOHANG — 非阻塞等待(父进程不卡住,可做其他事)

#include <stdio.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> int main() { pid_t pid = fork(); if (pid == 0) { printf("子进程:开始执行任务\n"); sleep(3); printf("子进程:任务完成,退出\n"); exit(0); } else { printf("父进程:轮询监控子进程...\n"); int status; while (1) { // 非阻塞等待,子进程未结束时立即返回 0 pid_t ret = waitpid(pid, &status, WNOHANG); if (ret == 0) { printf("父进程:子进程还在运行,我先做别的事...\n"); sleep(1); } else if (ret > 0) { printf("父进程:子进程已退出,状态码:%d\n", WEXITSTATUS(status)); break; } else { perror("waitpid error"); exit(1); } } } return 0; }

3. WUNTRACED — 捕获子进程暂停状态

#include <stdio.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> #include <signal.h> int main() { pid_t pid = fork(); if (pid == 0) { printf("子进程:运行中,等待被暂停...\n"); while (1) sleep(1); // 子进程一直运行 } else { printf("父进程:发送 SIGSTOP 暂停子进程\n"); kill(pid, SIGSTOP); // 暂停子进程 int status; // 等待子进程退出 或 被暂停 waitpid(pid, &status, WUNTRACED); if (WIFSTOPPED(status)) { printf("父进程:子进程被信号 %d 暂停\n", WSTOPSIG(status)); } } return 0; }

4. WCONTINUED — 捕获子进程恢复运行状态

#include <stdio.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> #include <signal.h> int main() { pid_t pid = fork(); if (pid == 0) { printf("子进程:运行中\n"); while (1) sleep(1); } else { printf("父进程:发送 SIGSTOP 暂停子进程\n"); kill(pid, SIGSTOP); sleep(2); printf("父进程:发送 SIGCONT 恢复子进程\n"); kill(pid, SIGCONT); int status; // 等待子进程退出/暂停/恢复 waitpid(pid, &status, WUNTRACED | WCONTINUED); if (WIFCONTINUED(status)) { printf("父进程:子进程已恢复运行\n"); } } return 0; }

五、回收僵尸进程方法

回收僵尸子进程是系统编程中常见的问题。僵尸进程本身不占用内存或CPU,但会占用进程ID(PID)表项,若大量存在会导致系统无法创建新进程。

以下是回收僵尸子进程的所有有效方法,按使用场景分类:

1. 父进程主动回收(最推荐)

这是最标准、最可控的方式。父进程在子进程退出后,应主动调用wait()waitpid()系统调用,以获取子进程的退出状态并释放其进程表项。

  • 阻塞式回收wait(NULL)会阻塞父进程,直到任意一个子进程结束。
  • 非阻塞式回收waitpid(-1, NULL, WNOHANG)会立即返回,若没有子进程结束则返回0,适合在父进程主循环中轮询使用。
  • 信号驱动回收:在父进程中注册SIGCHLD信号处理函数。当子进程退出时,内核会向父进程发送SIGCHLD信号,处理函数中调用waitpid回收所有已退出的子进程。这是异步、高效的处理方式。

2. 父进程退出,由 init 进程接管

如果父进程意外退出或设计缺陷未回收子进程,子进程会成为“孤儿进程”,并被 PID 为 1 的init进程(现代系统中通常是systemd)收养。init进程会定期调用wait回收其所有子进程,包括这些被接管的僵尸进程。这是一种“兜底”机制,但不应作为常规手段,因为它依赖于父进程的异常退出。

3. 系统级处理(极端情况)

当僵尸进程的父进程是init(即PPID=1),但仍未被回收时,可能是内核或系统层面的异常。此时可尝试:

  • 检查系统日志(如dmesg/var/log/syslog),排查硬件或驱动问题。
  • 在极端情况下,重启系统是最终解决方案。

简易背诵版:

父进程主动回收:通过wait()或waitpid()阻塞等待子进程结束,获取其退出状态并释放资源。其中waitpid()可通过WNOHANG参数实现非阻塞回收。
信号处理机制:父进程注册SIGCHLD信号处理函数,当子进程退出时自动触发wait()回收,避免阻塞主线程。
init 进程收养:若父进程先于子进程退出,子进程会被 init 进程(PID=1)收养,init 会定期回收其僵尸子进程。

重要提醒

不要尝试用kill -9命令杀死僵尸进程。僵尸进程已经“死亡”,它不响应任何信号,包括SIGKILL。发送信号对僵尸进程无效,只会徒劳无功。

综上,回收僵尸进程的核心在于父进程的主动管理,通过wait系列系统调用或信号处理机制,是健壮程序设计的基本要求。

守护进程

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询