一文吃透 Linux 进程:从 PID、PCB 到僵尸回收、fork/exec、守护进程
本文全面介绍了Linux进程管理的核心概念与实践技巧。主要内容包括:进程基本概念(PID/PPID/PCB)、进程状态与生命周期管理(fork/exec/exit/wait)、僵尸进程的成因与解决方案、信号处理与环境变量操作、I/O重定向与管道通信、守护进程创建的关键步骤,以及进程调度优先级相关的nice值和实时调度策略。文章还提供了常用监控工具(如ps/top/strace/lsof)和最佳实践
文章目录
- 一文吃透 Linux 进程:从 PID、PCB 到僵尸回收、fork/exec、守护进程
-
- 1. 什么是进程(Process)
- 2. 身份三件套:PID、PPID、进程组/会话
- 3. PCB:进程控制块(task_struct)
- 4. 进程状态与 `ps`/`top`
- 5. 进程的生命周期:`fork → exec → exit → wait`
- 6. 僵尸进程与孤儿进程
- 7. 信号与处理器
- 8. 环境变量:`getenv/setenv` 与 `environ`
- 9. I/O & 重定向 & 管道(父子通信常用)
- 10. 守护进程(Daemon)要点
- 11. 调度与优先级:`nice/renice`、`sched_*`
- 12. 监控与排错工具小抄
- 13. 常见坑与最佳实践
- 14. 迷你示例:父启动子进程、捕获输出、等待退出码
- 结尾:把握主线,化零为整
一文吃透 Linux 进程:从 PID、PCB 到僵尸回收、fork/exec、守护进程
这篇博客把——进程的基本概念、PID、PPID、PCB、进程状态、僵尸进程与回收、fork/exec、wait/waitpid、信号、环境变量、execvpe、守护进程——串成一条清晰的主线。配上小而能跑的示例,方便上手与排错。
1. 什么是进程(Process)
- 进程:一个运行中的程序实例,拥有独立的地址空间、打开的文件、寄存器上下文、信号处理器、环境等。
- 线程:共享进程的地址空间与多数资源,侧重并发执行。
- 用户态/内核态:进程大多数时间在用户态运行,经由系统调用切入内核态完成 I/O、调度等。
2. 身份三件套:PID、PPID、进程组/会话
- PID(Process ID):唯一标识一个进程。
- PPID(Parent PID):父进程号。
ps -o pid,ppid,cmd -p <PID> - 进程组/会话:用于作业控制(如前台/后台)。
setsid()可建立新会话,典型用于守护进程。
ps -o pid,ppid,pgid,sid,tty,stat,cmd -p $$
3. PCB:进程控制块(task_struct)
PCB(Process Control Block) 是内核中描述进程的一整套结构信息(Linux 里是 task_struct):
- 标识:
pid / tgid / ppid - 状态:运行/就绪/睡眠/停止/僵尸
- CPU 上下文:寄存器、栈指针、程序计数器
- 内存信息:
mm_struct(虚拟内存布局) - 文件系统:打开文件表、cwd、root
- 信号:待处理信号、处理器
- 调度:优先级、时间片、调度策略
- 统计:CPU 时间、start_time
僵尸进程本质就是“逻辑已退出,但 PCB 残留等待父进程读取”的那块信息。
4. 进程状态与 ps/top
常见状态(ps 的 STAT 字段):
R运行/就绪S可中断睡眠(大多数 I/O 等待)D不可中断睡眠(设备 I/O)T停止(SIGSTOP/ptrace)Z僵尸(Zombie)X死亡(极短暂)
ps -e -o pid,ppid,stat,time,cmd | head
top # 动态观察
5. 进程的生命周期:fork → exec → exit → wait
5.1 fork():复制一份进程
- 父进程得到子进程 PID,子进程返回 0。
- 地址空间采用写时复制(COW),直到实际写入才复制页。
pid_t pid = fork();
if (pid == 0) { /* 子进程 */ }
else if (pid > 0) { /* 父进程 */ }
else { perror("fork"); }
5.2 exec*():用新程序替换自己
-
成功后不返回,当前 PID 不变,但代码/数据段变为新程序。
-
常用族谱:
execv(path, argv):绝对/相对路径 + 参数数组execvp(file, argv):按PATH搜索execve(path, argv, envp):自定义环境(最底层)execvpe(file, argv, envp):GNU 扩展,PATH搜索 + 自定义环境
const char* argv[] = {"ls","-l",NULL};
execvp("ls", (char* const*)argv);
perror("execvp"); _exit(127);
execvpe= exec + v(数组参数) + p(按PATH搜索) + e(自定义环境)。
5.3 exit/_exit:退出进程
exit()刷新 stdio 缓冲;_exit()直接退(exec失败时常用_exit(127))。- 退出码:0 表示成功,非 0 表示错误类型。
5.4 wait()/waitpid():收割与回收
-
不收割的子进程会变僵尸(只有 PCB 残留)。
-
waitpid(pid, &status, 0)精确等待指定子进程;WNOHANG非阻塞轮询。 -
宏解析:
WIFEXITED(status)/WEXITSTATUS(status)WIFSIGNALED(status)/WTERMSIG(status)WIFSTOPPED/WSTOPSIG、WIFCONTINUED
pid_t child = fork();
if (child == 0) { execlp("sleep","sleep","1",(char*)NULL); _exit(127); }
int st; pid_t r = waitpid(child, &st, 0);
if (WIFEXITED(st)) printf("exit=%d\n", WEXITSTATUS(st));
6. 僵尸进程与孤儿进程
- 僵尸(Z):子进程退出后,父进程未
wait,PCB 残留。危害:占据 PID,积多了会影响系统。 - 孤儿:父进程先于子进程退出,子进程会被
init/systemd收养;当它退出时由收养者wait,不会成为僵尸。
避免僵尸:
- 正确
wait/waitpid; - 设
SIGCHLD处理器循环waitpid(-1, &st, WNOHANG); - 或
signal(SIGCHLD, SIG_IGN)(系统自动回收,注意可移植性与副作用)。
7. 信号与处理器
- 常见信号:
SIGINT(Ctrl+C),SIGTERM(优雅退出),SIGKILL(不可捕获),SIGCHLD(子进程状态变化)。 - 安装处理器建议用
sigaction而非signal,更可控:
static volatile sig_atomic_t running = 1;
static void on_term(int){ running = 0; }
struct sigaction sa = {0};
sa.sa_handler = on_term;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0; // 或 SA_RESTART
sigaction(SIGTERM, &sa, NULL);
在处理器里只做异步安全的事(设标志、写到 pipe 等),不要做 malloc、printf 等。
8. 环境变量:getenv/setenv 与 environ
getenv("PATH"):取变量。setenv/unsetenv:修改变量(线程安全性比直接改environ好)。extern char** environ;:进程的环境表("KEY=VALUE"数组),遍历所有环境变量用它。
extern char** environ;
for (char** p = environ; *p; ++p) puts(*p);
传递给子进程:execve/execvpe 的第三个参数 envp。
9. I/O & 重定向 & 管道(父子通信常用)
- 文件描述符:
0/1/2分别是stdin/stdout/stderr。 - 重定向:
dup2(oldfd, newfd)把newfd指向oldfd的对象。 - 管道:
pipe(int p[2]);p[0]读、p[1]写。配合fork()让父子通信:
int p[2]; pipe(p);
pid_t pid = fork();
if (pid == 0) {
close(p[0]); dup2(p[1], 1); execlp("ls","ls","-l",(char*)NULL); _exit(127);
}
close(p[1]); char buf[4096]; ssize_t n = read(p[0], buf, sizeof buf);
write(1, buf, n); close(p[0]); waitpid(pid, NULL, 0);
10. 守护进程(Daemon)要点
典型步骤:
fork()让父进程退出(shell 认为命令结束);setsid()脱离控制终端,成为新会话首进程;- 可选再
fork()一次,防止重新获得控制终端; chdir("/")、umask(0)(或安全缺省掩码);- 关闭/重定向
0/1/2到/dev/null或日志; - 安装
SIGTERM等处理器,优雅退出; - 日志建议用
syslog或自行管理 log fd。
轻量骨架:
pid_t pid = fork(); if (pid<0) _exit(1); if (pid>0) _exit(0);
if (setsid()==-1) _exit(1);
pid = fork(); if (pid<0) _exit(1); if (pid>0) _exit(0);
chdir("/"); umask(0);
/* 重定向 0/1/2 到 /dev/null */
11. 调度与优先级:nice/renice、sched_*
- nice 值:-20(最高)到 19(最低),影响普通调度优先级。
- 命令行:
nice -n 10 cmd、renice 5 -p <PID> - 实时调度:
SCHED_FIFO / SCHED_RR(需 CAP_SYS_NICE),用sched_setscheduler()。
12. 监控与排错工具小抄
- 进程树:
pstree -p - 文件占用:
lsof -p <PID>/lsof <file> - 系统调用跟踪:
strace -f -p <PID>/strace cmd - /proc:
/proc/<PID>/status,fd/,cmdline,environ,stat - CPU/内存:
top,htop,pidstat,vmstat
13. 常见坑与最佳实践
- 不
wait导致僵尸 → 在合适时机waitpid或SIGCHLD收割。 exec成功不返回 → 任何错误打印必须在exec之后的分支;成功时请_exit(127)只在exec失败后调用。- 相对路径误用 → 少了
/就从 CWD 找,ENOENT很常见。程序里不展开~。 sprintf溢出 → 用snprintf或直接fprintf到流。fflushvsfsync→fflush到内核页缓存;要“落盘”用fsync(fileno(fp))。- 信号处理器里做复杂事 → 只改标志/写 pipe,复杂逻辑放主循环。
execvpe是 GNU 扩展 → 需要#define _GNU_SOURCE;可用execve代替(不查PATH)。
14. 迷你示例:父启动子进程、捕获输出、等待退出码
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
int main() {
int p[2]; pipe(p);
pid_t pid = fork();
if (pid == 0) {
close(p[0]);
dup2(p[1], STDOUT_FILENO);
execlp("echo", "echo", "hello, process", (char*)NULL);
_exit(127);
}
close(p[1]);
char buf[256]; ssize_t n = read(p[0], buf, sizeof(buf));
if (n > 0) write(STDOUT_FILENO, buf, n);
close(p[0]);
int st; waitpid(pid, &st, 0);
if (WIFEXITED(st)) printf("child exit=%d\n", WEXITSTATUS(st));
return 0;
}
结尾:把握主线,化零为整
- 进程是谁? —— PCB 记录一切(PID、资源、状态)。
- 如何出生? ——
fork复制,exec换壳。 - 如何结束? ——
exit/_exit;父用waitpid收尸,避免僵尸。 - 如何被管理? —— 信号、调度、优先级、守护化、重定向、/proc、工具链。
- 如何排错? ——
ps/top/pstree/lsof/strace+ 合理日志与错误码。
当你把这条链路打通,Linux 进程世界就不再神秘:它是可观察、可操控、可验证的系统工程。接下来建议你动手:把上面的“迷你示例”和“守护进程骨架”编译运行,对照 ps 和日志观察变化;再用 strace 看看系统调用,感受“内核与进程的对话”。
更多推荐

所有评论(0)