进程是 Linux 操作系统调度资源的基本单位而进程控制则是 Linux 编程中最核心的知识点之一。无论是日常开发、底层学习还是面试考察fork创建子进程、exec系列函数实现程序替换、wait/waitpid完成进程等待与资源回收都是绕不开的重点。本文将从原理入手结合代码案例完整梳理进程从创建、运行、替换到退出、回收的全流程带你彻底掌握 Linux 进程控制的核心逻辑。目录一、进程创建fork() 函数详解1. fork() 初识一个调用两个返回值2. fork() 的内核工作流程3. 代码实例理解父子进程的执行流程4. 写时拷贝Copy-On-Write, COW5. fork() 的常规用法与失败原因二、进程终止程序结束的 3 种方式1. 进程退出的场景1.代码运行完毕结果正确如main函数return 02.代码运行完毕结果不正确如main函数返回非 0 值3.代码异常终止如收到信号被杀死段错误、CtrlC中断等2.进程退出的常见退出方法1.正常终止1.exit()与_exit()的区别2.通过代码直观体现1.exit()​编辑2._exit()2.异常退出3. 退出码程序状态的反馈1.查看进程退出码对应的原因共134个三、进程等待回收子进程避免僵尸进程1. 进程等待的必要性2. 进程等待的两种方式1.wait() 函数等待任意子进程2.waitpid() 函数更灵活的等待3. 解析子进程的退出状态1.查看所有信号4. 阻塞与非阻塞等待示例1.阻塞状态2.非阻塞状态四、进程程序替换让子进程执行全新程序1. 替换原理2. exec() 系列函数详解1.命名规律2.函数对比表3. 代码实例exec() 函数的使用1. execl2. execlp3. execle4. execv5. execvp6. execve4.以新增的方式给子进程添加环境变量1.putenv()(针对的是非e结尾的函数)2.继承原环境 追加新变量一、进程创建fork() 函数详解1. fork() 初识一个调用两个返回值在 Linux 中fork()是创建新进程的核心系统调用定义在unistd.h头文件中pid_t fork(void);返回值规则1.父进程返回新创建子进程的 PID正数2.子进程返回 03.调用失败返回 - 1这也是fork()最特殊的地方一个函数调用会在两个进程中分别返回两次。2. fork() 的内核工作流程当进程调用fork()时内核会完成以下关键操作1.为子进程分配新的内存块和内核数据结构PCB2.将父进程的部分数据结构内容拷贝至子进程3.将子进程添加到系统进程列表中4.调度器开始调度子进程因此fork()之后系统中会出现两个二进制代码完全相同的进程它们都会从fork()调用之后的位置开始执行。因此fork()之后系统中会出现两个二进制代码完全相同的进程它们都会从fork()调用之后的位置开始执行。3. 代码实例理解父子进程的执行流程​ #include stdio.h #include unistd.h #include stdlib.h #include sys/types.h int main(void) { pid_t pid; printf(Before: pid is %d\n, getpid()); if ((pid fork()) -1) { perror(fork()); exit(1); } printf(After: pid is %d, fork return %d\n, getpid(), pid); sleep(1); return 0; } ​4. 写时拷贝Copy-On-Write, COW默认情况下父子进程的代码段是共享的数据段在子进程未写入时也会共享。当任意一方尝试修改数据时内核会以写时拷贝的方式为修改方生成一份数据副本从而保证进程的独立性。写时拷贝是一种延时申请技术它避免了fork()时直接拷贝整个进程地址空间大幅提升了内存使用效率和进程创建速度。5. fork() 的常规用法与失败原因常见用法父进程创建子进程让父子进程同时执行不同的代码段如父进程等待客户端请求子进程处理请求子进程调用exec()系列函数执行一个全新的程序失败原因系统中进程数量过多达到系统上限实际用户的进程数超过了系统限制关于进程创建在前面的文章中也有讲解大家不理解可以在前面的文章中看一下。二、进程终止程序结束的 3 种方式进程终止的本质是释放系统资源包括进程申请的内核数据结构、代码和数据。1. 进程退出的场景1.代码运行完毕结果正确如main函数return 02.代码运行完毕结果不正确如main函数返回非 0 值而是提供了一个 “信号终止状态码”用来告诉你进程是怎么死的3.代码异常终止如收到信号被杀死段错误、CtrlC中断等一旦出现异常退出码将无意义操作系统将提供一个 “信号终止状态码”用来告诉你进程是怎么死的#include unistd.h #include signal.h int main(void) { kill(getpid(), SIGKILL); return 0; }2.进程退出的常见退出方法1.正常终止从main函数return返回return n等价于调用exit(n)调用exit()函数C 标准库函数会执行清理函数、刷新缓冲区再调用_exit()调用_exit()函数系统调用直接终止进程不刷新缓冲区1.exit()与_exit()的区别1.exit()是C语言标准库里面提供的函数调用_exit()是系统提供的接口2.exit()会对缓冲区进行刷新_exit()不会对缓冲器进行刷新2.通过代码直观体现1.exit()​ #include stdio.h #include stdlib.h #include unistd.h int main() { printf(hello); // 未加\n数据存在缓冲区 sleep(1); printf(\n); exit(0); // 会刷新缓冲区输出hello } ​2._exit()#include stdio.h #include unistd.h int main() { printf(hello); _exit(0); // 不刷新缓冲区无输出 }2.异常退出通过信号终止进程如CtrlC发送SIGINT信号或程序触发段错误信号3. 退出码程序状态的反馈退出码可以告诉我们命令执行的结果Linux 中约定退出码0表示命令成功执行非 0 值表示执行失败不同值对应不同错误原因退出码解释0命令成功执行1通用错误代码2命令或参数使用不当126权限被拒绝无法执行127未找到命令或PATH错误130通过CtrlCSIGINT终止1.查看进程退出码对应的原因共134个#include stdio.h #include string.h #include unistd.h int main() { int i 0; for(; i 130; i) { char *msg strerror(i); if (msg ! NULL) { printf(%d-%s\n, i, msg); } } return 0; }三、进程等待回收子进程避免僵尸进程1. 进程等待的必要性如果子进程退出后父进程没有回收它的资源子进程就会变成僵尸进程占用系统资源且无法被kill -9杀死。进程等待的核心目的就是回收子进程资源避免内存泄漏获取子进程的退出状态判断任务是否正常完成2. 进程等待的两种方式在我们之前写的程序中创建一个子进程之后父进程没有等待在子进程结束后就会变成僵尸状态#include stdio.h #include unistd.h #include sys/types.h //没有wait子进程变成僵尸状态 int main() { pid_t t fork(); if(t 0) { printf(我是一个子进程:%d\n, getpid()); sleep(5); } else while(1) { sleep(1); printf(我是一个父进程%d\n, getpid()); } return 0; }1.wait() 函数等待任意子进程pid_t wait(int *wstatus);返回值成功返回被等待进程的 PID失败返回 - 1参数wstatus输出型参数用于获取子进程的退出状态不关心可设为NULL#include stdio.h #include unistd.h #include sys/wait.h int main() { pid_t pid fork(); if (pid 0) { // 子进程你可以改成 exit(42) 或者段错误代码测试 printf(我是子进程pid%d\n, getpid()); sleep(2); // 测试1正常退出 exit(42); // 测试2段错误 // int *p NULL; *p 1; } else { int status; wait(status); // 等子进程同时把状态存在 status 里 if (WIFEXITED(status)) { printf(✅ 子进程正常退出退出码%d\n, WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf(❌ 子进程被信号杀死信号号%d\n, WTERMSIG(status)); } } return 0; }2.waitpid() 函数更灵活的等待pid_t waitpid(pid_t pid, int *wstatus, int options);1.参数pid指定要等待的进程pid 0等待 PID 等于pid的子进程pid -1等待任意子进程与wait()等价2.参数options等待方式控制0阻塞等待子进程未退出时父进程一直阻塞(一直等待子进程)WNOHANG非阻塞等待子进程未退出时直接返回 0父进程可继续执行其他任务3. 解析子进程的退出状态status参数是一个位图仅低 16 位有效正常终止高 8 位为退出码低 7 位为 0异常终止低 7 位为终止信号第 8 位为core dump标志可以通过宏来解析statusWIFEXITED(status)判断进程是否正常退出WEXITSTATUS(status)提取进程的退出码仅正常退出时有效1.查看所有信号4. 阻塞与非阻塞等待示例1.阻塞状态#include stdio.h #include unistd.h #include sys/wait.h int main() { pid_t pid fork(); if (pid 0) { printf(child is run, pid is: %d\n, getpid()); sleep(5); while(1) {} exit(257); } else { printf(我是父进程%d\n, getpid()); int status 0; pid_t ret waitpid(pid, status, 0); // 阻塞等待 if (ret 0 WIFEXITED(status)) { printf(wait success, child return code is: %d\n, WEXITSTATUS(status)); } } return 0; }2.非阻塞状态#include stdio.h #include sys/wait.h #include unistd.h #include stdlib.h int main() { pid_t pid fork(); if (pid 0) { printf(child is run, pid is: %d\n, getpid()); sleep(5); while(1) {} exit(257); } else { int status 0; pid_t ret waitpid(pid, status, WNOHANG); // 非阻塞等待 while(1) { sleep(1); printf(我是父进程%d\n, getpid()); } if (ret 0 WIFEXITED(status)) { printf(wait success, child return code is: %d\n, WEXITSTATUS(status)); } } return 0; }四、进程程序替换让子进程执行全新程序fork()创建的子进程会和父进程执行相同的代码而程序替换可以让子进程加载并执行一个全新的程序且不改变进程的 PID。1. 替换原理当进程调用exec()系列函数时内核会将新程序的代码和数据加载到进程的地址空间中覆盖原有的代码段和数据段进程从新程序的启动例程开始执行。调用exec()前后进程的 PID 保持不变。首先创建一个other.cc的C文件编译形成other可执行文件​ ​ ​//other.cc #include iostream #include cstdio #include unistd.h int main(int argc, char *argv[], char *env[]) { std::cout hello C, My Pid Is: getpid() std::endl; return 0; }#include stdio.h #include unistd.h #include sys/wait.h int main() { printf(我的程序要运行了\n); if(fork() 0) { printf(I am Child, My Pid Is: %d\n, getpid()); sleep(1); // 用 execl 调用当前目录下的 other 程序 execl(./other, other, NULL); exit(1); } waitpid(-1, NULL, 0); printf(我的程序运行完毕了\n); return 0; }2. exec() 系列函数详解1.Linux 提供了 6 个以exec开头的函数统称为exec函数族#include unistd.h int execl(const char *path, const char *arg, ...); int execlp(const char *file, const char *arg, ...); int execle(const char *path, const char *arg, ..., char *const envp[]); int execv(const char *path, char *const argv[]); int execvp(const char *file, char *const argv[]); int execve(const char *path, char *const argv[], char *const envp[]);1.命名规律llist参数以列表形式传递必须以NULL结尾vvector参数以字符串数组形式传递数组末尾必须为NULLppath自动搜索PATH环境变量无需写全路径eenv自定义传入环境变量数组2.函数对比表函数名参数格式是否自动用PATH是否自定义环境变量execl列表否否使用当前环境execlp列表是否使用当前环境execle列表否是execv数组否否使用当前环境execvp数组是否使用当前环境execve数组否是真正的系统调用3. 代码实例exec() 函数的使用1. execlint execl(const char *path, const char *arg, ...);特点需要完整路径参数以列表形式传递继承父进程环境变量#include stdio.h #include stdlib.h #include unistd.h #include sys/wait.h int main() { pid_t pid fork(); if (pid -1) { perror(fork); exit(EXIT_FAILURE); } if (pid 0) { // 子进程用 execl 执行 /bin/ls -l // 参数列表必须以 NULL 结尾 execl(/bin/ls, ls, -l, NULL); // 如果 execl 成功下面的代码不会执行 perror(execl failed); exit(EXIT_FAILURE); } else { // 父进程等待子进程结束 wait(NULL); printf(execl 测试完成\n); } return 0; }2. execlpint execlp(const char *file, const char *arg, ...);特点自动在 PATH 中搜索程序参数以列表形式传递继承父进程环境变量。#include stdio.h #include stdlib.h #include unistd.h #include sys/wait.h int main() { pid_t pid fork(); if (pid -1) { perror(fork); exit(EXIT_FAILURE); } if (pid 0) { // 子进程用 execlp 执行 ls -a // 不用写 /bin/ls会自动从 PATH 找 execlp(ls, ls, -a, NULL); perror(execlp failed); exit(EXIT_FAILURE); } else { wait(NULL); printf(execlp 测试完成\n); } return 0; }3. execleint execle(const char *path, const char *arg, ..., char *const envp[]);特点需要完整路径参数以列表形式传递可以自定义环境变量#include stdio.h #include stdlib.h #include unistd.h #include sys/wait.h int main() { // 自定义环境变量列表必须以 NULL 结尾 char *const env[] { MY_NAMETom, MY_AGE18, NULL }; pid_t pid fork(); if (pid -1) { perror(fork); exit(EXIT_FAILURE); } if (pid 0) { // 子进程用 execle 执行 /usr/bin/env查看环境变量 execle(/usr/bin/env, env, NULL, env); perror(execle failed); exit(EXIT_FAILURE); } else { wait(NULL); printf(execle 测试完成\n); } return 0; }4. execvint execv(const char *path, char *const argv[]);特点需要完整路径参数以数组形式传递继承父进程环境变量。#include stdio.h #include stdlib.h #include unistd.h #include sys/wait.h int main() { pid_t pid fork(); if (pid -1) { perror(fork); exit(EXIT_FAILURE); } if (pid 0) { // 子进程用 execv 执行 /bin/ls -lh // 参数数组必须以 NULL 结尾 char *const argv[] {ls, -lh, NULL}; execv(/bin/ls, argv); perror(execv failed); exit(EXIT_FAILURE); } else { wait(NULL); printf(execv 测试完成\n); } return 0; }5. execvpint execvp(const char *file, char *const argv[]);特点自动在 PATH 中搜索程序参数以数组形式传递继承父进程环境变量#include stdio.h #include unistd.h #include sys/wait.h int main() { printf(我的程序要运行了!\n); if (fork() 0) { printf(I am Child, My Pid Is: %d\n, getpid()); sleep(1); char *const argv[] { (char *const)other, (char *const)-a, (char *const)-b, (char *const)-c, (char *const)-d, NULL }; execvp(./other, argv); exit(1); } waitpid(-1, NULL, 0); printf(我的程序运行完毕了\n); }6. execveint execve(const char *path, char *const argv[], char *const envp[]);特点需要完整路径参数和环境变量都以数组形式传递是所有 exec 函数的底层实现。#include stdio.h #include unistd.h #include sys/wait.h int main() { printf(我的程序要运行了!\n); if (fork() 0) { printf(I am Child, My Pid Is: %d\n, getpid()); sleep(1); char *const argv[] { (char *const)other, (char *const)-a, (char *const)-b, (char *const)-c, (char *const)-d, NULL }; extern char **environ; execve(/home/wyy/my-project/linux-practice/进程控制/other, argv, environ); exit(1); } waitpid(-1, NULL, 0); printf(我的程序运行完毕了\n); }再次观察新的代码我们发现如果传入自己的环境变量那么就不会继承父进程的环境变量了而是自己新的环境变量也就是说进程替换会覆盖之前进程的环境变量char *const addenv[] { (char *const)MYVAL123456789, (char *const)MYVAL1123456789, (char *const)MYVAL2123456789, NULL }; int main() { printf(我的程序要运行了!\n); if(fork() 0) { printf(I am Child, My Pid Is: %d\n, getpid()); sleep(1); char *const argv[] { (char *const)other, (char *const)-a, (char *const)-b, (char *const)-c, (char *const)-d, NULL }; execve(/home/wyy/my-project/linux-practice/进程控制/other, argv, addenv); exit(1); } waitpid(-1, NULL, 0); printf(我的程序运行完毕了\n); }4.以新增的方式给子进程添加环境变量1.putenv()(针对的是非e结尾的函数)#include stdio.h #include unistd.h #include sys/wait.h #include stdlib.h char *const addenv[] { (char *const)MYVAL123456789, (char *const)MYVAL1123456789, (char *const)MYVAL2123456789, NULL }; int main() { printf(我的程序要运行了!\n); if(fork() 0) { printf(I am Child, My Pid Is: %d\n, getpid()); sleep(1); char *const argv[] { (char*const)other, (char*const)-a, (char*const)-b, (char*const)-c, (char*const)-d, NULL }; for(int i 0; addenv[i]; i) { putenv(addenv[i]); } execvp(./other, argv); exit(1); } waitpid(-1, NULL, 0); printf(我的程序运行完毕了\n); }2.继承原环境 追加新变量#include stdio.h #include unistd.h #include sys/wait.h #include stdlib.h char *newnew (char *)myVAL66666666; char *const addenv[] { (char *const)MYVAL123456789, (char *const)MYVAL1123456789, (char *const)MYVAL2123456789, NULL }; int main() { printf(我的程序要运行了!\n); if (fork() 0) { printf(I am Child, My Pid Is: %d\n, getpid()); sleep(1); char *const argv[] { (char *const)other, (char *const)-a, (char *const)-b, (char *const)-c, (char *const)-d, NULL }; for (int i 0; addenv[i]; i) { putenv(addenv[i]); } extern char **environ; execvpe(./other, argv, environ); exit(1); } waitpid(-1, NULL, 0); printf(我的程序运行完毕了\n); }掌握了这四大核心你就真正握住了 Linux 系统编程的骨架。无论是以后深入研究 Shell 解释器的模拟实现、多进程网络服务器如早期的 Apache还是理解 Container容器底层的隔离机制今天筑起的这堵进程控制的高墙都将是你最坚实的底座。保持这种死磕底层原理的劲头继续在代码的世界里纵横驰骋吧