资讯详情

Linux进程万字详解(五) | 进程控制全解析:创建、终止、等待与程序替换

📅 2026/11/4 7:07:39 | 华诺云谱 👁 阅读
Linux进程万字详解(五) | 进程控制全解析:创建、终止、等待与程序替换
目录引言一、进程创建1.1 fork函数回顾1.2 写时拷贝的底层实现1.3 fork的常规用法与失败原因1.4 进程创建与虚拟机隔离二、进程终止2.1 进程终止的本质2.2 进程退出码2.3 错误码与错误描述2.4 异常终止与信号2.5 进程退出方法方法1main函数return方法2exit函数方法3_exit系统调用三、进程等待3.1 进程等待的必要性3.2 wait接口3.3 waitpid接口3.4 status位图解析3.5 宏封装3.6 阻塞等待 vs 非阻塞等待阻塞等待options 0非阻塞等待options WNOHANG四、进程程序替换4.1 替换原理4.2 exec系列函数详解execl列表传参execlp自动搜索PATHexecv向量传参execvpPATH 向量execvpe / execle自定义环境变量4.3 execve真正的系统调用4.4 程序替换的本质加载器结语引言进程是操作系统资源分配的基本单位而进程控制则是操作系统最核心的功能之一。从进程的创建到终止从父进程的等待到程序的执行替换这一系列机制共同构成了Linux进程管理的完整生命周期。理解进程控制不仅能让我们洞察操作系统如何调度与管理程序执行更能为后续学习Shell原理、进程间通信、信号机制等高级主题打下坚实基础。本文将围绕进程控制的四大核心话题展开进程创建、进程终止、进程等待、进程程序替换。我们将深入剖析fork的写时拷贝原理解读进程退出码的设计哲学揭示wait/waitpid的状态位图结构详解exec系列函数的加载机制。从内核数据结构到用户接口调用从原理分析到代码实战带你完整掌握Linux进程由生到死的全过程。一、进程创建1.1 fork函数回顾在Linux中fork函数是最重要的进程创建接口。它从已存在进程中创建一个新进程原进程为父进程新进程为子进程。#include unistd.h pid_t fork(void); // 返回值子进程中返回0父进程返回子进程PID出错返回-1围绕fork有三个经典问题为什么给子进程返回0父进程返回子进程PID为什么一个fork函数会有两个返回值为什么同一个变量既等于0又大于0结合前文所学这些问题现在可以深入理解了。创建进程时内核会分配新的内存块和内核数据结构给子进程将父进程大部分数据结构内容拷贝至子进程添加子进程到系统进程列表fork返回开始调度器调度子进程拥有独立的task_struct、mm_struct和页表。默认情况下父子进程通过页表映射到同一物理内存共享代码和数据。当任一进程尝试修改数据时触发写时拷贝Copy-On-Write在物理内存开辟新空间保证进程独立性。1.2 写时拷贝的底层实现权限陷阱机制父进程创建子进程后内核会将父子进程的代码段和数据段页表权限都设为只读。当子进程试图写入数据时CPU检查发现页表权限为只读触发缺页异常Page Fault操作系统介入识别这是合法的写时拷贝场景开辟新物理内存拷贝原数据更新子进程页表映射虚拟地址不变物理地址指向新空间恢复数据段读写权限重新执行写入操作为什么需要写时拷贝优势说明减少创建时间避免一次性完整拷贝所有内存减少内存浪费子进程大概率不会修改所有变量提高内存利用率按需分配延迟拷贝写时拷贝是一种精细的内存控制技术属于延迟申请策略能显著提高整机内存使用效率。1.3 fork的常规用法与失败原因常规用法代码分流父子进程执行不同代码段如父进程监听请求子进程处理请求程序替换子进程调用exec函数执行全新程序调用失败原因系统进程过多内存空间不足PCB、虚拟地址空间、页表分配失败用户进程数超过系统限制1.4 进程创建与虚拟机隔离如果一个进程要执行另一款操作系统的代码呢这正是内核级虚拟机KVM的原理。Linux下可以运行多个操作系统实例每个实例都是一个独立进程。虚拟机之间的隔离性正是基于进程的独立性——每个虚拟机拥有独立的PCB、虚拟地址空间和页表通过页表映射到不同物理内存实现硬件级别的隔离。二、进程终止2.1 进程终止的本质进程终止的本质是释放系统资源销毁内核数据结构task_struct、mm_struct、页表等回收物理内存和打开的文件描述符。进程退出的三种场景场景说明代码运行完毕结果正确正常退出退出码为0代码运行完毕结果不正确正常退出退出码非0代码异常终止被信号中断退出码无意义子进程的执行结果需要告知父进程父进程据此进行决策。因此进程退出是为了给父进程一个交代。2.2 进程退出码C/C程序中main函数的返回值就是进程退出码。返回0表示成功非0表示不同的错误原因。查看退出码echo $? # 打印最近一个进程的退出码示例#include iostream #include unistd.h #include stdio.h using namespace std; int main() { FILE* pr fopen(xixi.txt, r); if(pr NULL) return 1; // 文件打开失败返回错误码1 return 0; // 成功 }[qyhVM-0-2-centos linux-sys-prog]$ ./test1 [qyhVM-0-2-centos linux-sys-prog]$ echo $? 1./test1的父进程是BashBash通过wait获取子进程的退出码存储在task_struct中。echo $?就是读取这个值。注意再次执行echo $?会显示0因为这次打印的是上一次echo命令自身的退出码。2.3 错误码与错误描述系统提供了一套错误码与错误描述的映射C标准库实现通过strerror函数转换#include iostream #include unistd.h #include stdio.h #include errno.h #include cstring using namespace std; int main() { FILE* pr fopen(xixi.txt, r); if(pr NULL) { cout errno - strerror(errno) endl; return errno; } return 0; }[qyhVM-0-2-centos linux-sys-prog]$ ./test1 2 - No such file or directory [qyhVM-0-2-centos linux-sys-prog]$ echo $? 2指令同样遵循此机制[qyhVM-0-2-centos linux-sys-prog]$ ls -l xixi.txt ls: cannot access xixi.txt: No such file or directory [qyhVM-0-2-centos linux-sys-prog]$ echo $? 2ls也是进程其退出码与我们的程序一致。当然也可以自定义退出码规则不遵循C标准库的约定。2.4 异常终止与信号一旦进程出现异常退出码失去意义。异常通常由信号导致#include iostream #include unistd.h using namespace std; int main() { int a 10; a / 0; // 除零错误 return 66; }[qyhVM-0-2-centos linux-sys-prog]$ ./test1 Floating point exception [qyhVM-0-2-centos linux-sys-prog]$ echo $? 136进程因浮点异常被信号终止返回的136不是退出码而是包含信号信息的编码。异常退出的详细机制将在信号章节深入讲解。2.5 进程退出方法方法1main函数returnmain函数的return表示进程结束。其他函数的return仅表示函数调用完成。方法2exit函数在代码任何位置调用exit(status)立即终止进程并返回退出码给父进程。其内部执行执行用户通过atexit或on_exit注册的清理函数刷新所有打开流的缓冲区调用_exit系统调用#include iostream #include stdlib.h using namespace std; int main() { exit(1); // 进程立即终止不会执行后续代码 cout xixi endl; // 不会执行 return 66; }方法3_exit系统调用_exit是系统级接口直接终止调用进程不刷新缓冲区#include iostream #include unistd.h using namespace std; int main() { cout xixi; // 注意无换行数据在缓冲区 _exit(1); // 直接终止缓冲区不刷新 return 0; }[qyhVM-0-2-centos linux-sys-prog]$ ./test1 [qyhVM-0-2-centos linux-sys-prog]$ echo $? 1对比特性exit_exit层级C库函数系统调用缓冲区刷新✅ 刷新❌ 不刷新清理函数执行✅ 执行❌ 不执行关系封装了_exit底层接口关键结论之前讨论的缓冲区是C语言库缓冲区不是系统级缓冲区。因为_exit不刷新缓冲区而exit会刷新说明缓冲区存在于用户空间由C标准库维护。三、进程等待3.1 进程等待的必要性防止僵尸进程子进程退出父进程不回收造成内存泄漏僵尸进程刀枪不入kill -9无法杀死已死去的进程获取子进程执行结果判断任务完成质量结果对错、是否正常退出回收资源这是进程等待最重要的功能3.2 wait接口#include sys/types.h #include sys/wait.h pid_t wait(int* status);status输出型参数获取子进程退出状态不关心可传NULL返回值成功返回回收的子进程PID失败返回-1特性阻塞等待任意一个子进程退出示例#include iostream #include unistd.h #include sys/wait.h using namespace std; int main() { pid_t id fork(); if(id 0) { int cnt 5; while(cnt--) { cout 子进程pid getpid() endl; sleep(1); } exit(0); } pid_t rid wait(NULL); // 阻塞等待子进程 if(rid 0) { cout wait success, rid: rid endl; } return 0; }子进程pid5446 子进程pid5446 子进程pid5446 子进程pid5446 子进程pid5446 wait success, rid:5446父进程创建子进程后在wait处阻塞直到子进程退出才继续执行。3.3 waitpid接口pid_t waitpid(pid_t pid, int* status, int options);参数说明pid0等待指定PID子进程0等待同进程组任一子进程-1等待任一子进程同wait-1等待指定进程组任一子进程status输出型参数获取详细退出信息options0阻塞等待WNOHANG非阻塞等待waitpid失败情况父进程没有子进程指定的pid子进程不存在3.4 status位图解析status不是整存整取的整数而是位图结构高16位不考虑 ├─ 低16位 ├─ 高8位第8-15位退出码正常退出时有效 └─ 低8位 ├─ 第7位Core Dump标志 └─ 低7位第0-6位终止信号异常退出时有效验证#include iostream #include unistd.h #include sys/wait.h using namespace std; int main() { pid_t id fork(); if(id 0) { // 子进程 sleep(1); exit(1); // 退出码设为1 } int _status 0; pid_t rid waitpid(id, _status, 0); if(rid 0) { cout wait success, rid: rid _status: _status endl; } return 0; }wait success, rid:4633 _status:256退出码1对应status为256即1 8验证了位图结构。判定规则低7位为0正常退出读取高8位退出码低7位非0异常退出退出码无意义读取信号编号这就是为什么信号没有0号信号——0被保留用于表示无异常。3.5 宏封装手动位操作繁琐系统提供宏函数宏功能WIFEXITED(status)判断是否正常退出真/假WEXITSTATUS(status)提取退出码需先判断WIFEXITED为真3.6 阻塞等待 vs 非阻塞等待阻塞等待options 0父进程调用waitpid后若子进程未结束父进程挂起等待直到子进程退出。非阻塞等待options WNOHANGWNOHANG Wait No Hang即等待但不挂起。若子进程未结束立即返回0父进程可以继续执行其他任务。现实类比阻塞等待张三在李四楼下等一直打电话直到李四下来电话不挂非阻塞轮询张三隔段时间打电话问下来了吗没下来就挂电话做别的事反复询问直到李四下来非阻塞轮询示例#include iostream #include unistd.h #include sys/wait.h #include vector using namespace std; typedef void (*handler_t)(); vectorhandler_t handlers; void fun_one() { cout 临时任务1整理文件 endl; } void fun_two() { cout 临时任务2发送心跳包 endl; } void Load() { handlers.push_back(fun_one); handlers.push_back(fun_two); } void handler() { if(handlers.empty()) Load(); for(auto iter : handlers) iter(); } int main() { pid_t pid fork(); if(pid 0) { return 1; } else if(pid 0) { cout 子进程运行中pid: getpid() endl; sleep(5); exit(1); } else { int status 0; pid_t ret 0; do { ret waitpid(-1, status, WNOHANG); // 非阻塞等待 if(ret 0) { cout 子进程仍在运行父进程执行临时任务... endl; handler(); sleep(1); } } while(ret 0); if(WIFEXITED(status) ret pid) { cout 等待成功退出码: WEXITSTATUS(status) endl; } } return 0; }waitpid返回值0子进程结束返回PID0调用成功但子进程未结束非阻塞模式0调用失败非阻塞等待让父进程并行处理多项任务提高整体效率。四、进程程序替换4.1 替换原理fork创建子进程后父子进程执行相同程序的不同分支。如果子进程想执行全新的程序呢这就需要进程程序替换。核心机制通过特定接口加载磁盘上的新程序代码和数据到调用进程的地址空间中覆盖原有内容。关键特性不创建新进程PID不变替换代码段和数据段更新页表映射PCB和虚拟地址空间保持不变执行特点exec*函数只有替换失败时才有返回值。一旦成功原代码后半部分已不存在无法返回。因此无需判断成功返回值只需处理失败情况。4.2 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 execvpe(const char* file, char* const argv[], char* const envp[]); int execve(const char* filename, char* const argv[], char* const envp[]);execl列表传参int execl(const char* path, const char* arg, ...); // path可执行文件路径我要执行谁 // arg...命令行参数我怎么执行它必须以NULL结尾示例#include iostream #include unistd.h #include sys/wait.h using namespace std; int main() { cout begin... endl; if(fork() 0) { execl(/usr/bin/ls, ls, -al, NULL); exit(1); // 只有execl失败才会执行到这里 } waitpid(-1, NULL, 0); cout end... endl; return 0; }begin... total 68 drwxrwxr-x 3 qyh qyh 4096 Oct 30 15:34 . ... end...为什么不直接替换当前进程因为替换后原程序后续代码丢失。创建子进程后子进程被替换父进程不受影响——这正是进程独立性的体现。execlp自动搜索PATHint execlp(const char* file, const char* arg, ...); // p PATH自动在环境变量PATH中查找可执行文件execlp(ls, ls, -al, NULL); // 无需完整路径execv向量传参int execv(const char* path, char* const argv[]); // v vector通过指针数组传递参数char* const argv[] {(char*const)ls, (char*const)-al, NULL}; execv(/usr/bin/ls, argv);命令行参数的传递我们之前讲main函数的argc、argv时提到程序的命令行参数由父进程通过execv这类函数传递。所有程序都是其他进程的子进程命令行参数就是这样层层传递的。execvpPATH 向量execvp(ls, argv); // 自动搜索PATH 向量传参execvpe / execle自定义环境变量int execvpe(const char* file, char* const argv[], char* const envp[]); int execle(const char* path, const char* arg, ..., char* const envp[]); // e environment传递自定义环境变量表示例char* const argv[] {(char*const)ls, (char*const)-al, NULL}; char* const envp[] {(char*const)XIXI10086, NULL}; execvpe(./test2, argv, envp);注意传递envp意味着使用全新的环境变量列表老的环境变量不再保留。保留原环境变量并新增// 方法1使用putenv推荐 putenv(XIXI10086); // 添加到当前进程环境变量 // 子进程自动继承 // 方法2通过environ传递 extern char** environ; // 当前进程完整环境变量表 execvpe(./test2, argv, environ); // 传递包含新增变量的完整表4.3 execve真正的系统调用上述6个函数都是C库函数封装底层都调用唯一的系统调用int execve(const char* filename, char* const argv[], char* const envp[]);设计原因库函数提供多种参数风格方便用户调用系统调用只需一个统一接口未显式传递环境变量时库函数自动传入environ指向的默认环境变量表这就是为什么不传环境变量子进程仍有环境变量传了则老环境变量消失的根本原因。4.4 程序替换的本质加载器Shell执行命令的原理Bash父进程创建子进程子进程调用exec*加载新程序新程序执行完毕子进程退出Bash通过wait回收子进程获取退出码我们每天在Bash中执行的每个指令都是这个流程的实例。exec*系列函数本质上就是加载器Loader的实现。跨语言调用进程程序替换是系统级概念与编程语言无关。只要最终能在系统中运行的程序无论C、C、Python、Java编译后的字节码都可以被替换执行。验证PID不变begin... pid:13676 // 子进程原PID 替换了:pid13676 // 替换后PID相同 end...结语通过本文的系统讲解我们完整梳理了Linux进程控制的四大核心机制从fork的写时拷贝到进程终止的资源回收从wait的状态位图解析到exec的程序替换原理。这些机制环环相扣共同构成了进程由生到死的完整生命周期。特别值得深入理解的是写时拷贝的权限陷阱设计——通过将页表权限设为只读利用缺页异常触发拷贝这种以退为进的设计思想体现了操作系统工程的精妙。进程等待的状态位图同样令人叹服用16个比特位同时编码退出码、终止信号和Core Dump标志信息密度极高。exec系列函数的加载器本质则揭示了Shell命令执行的底层真相——我们每天使用的每个命令都是进程创建与替换的实例。进程控制不仅是操作系统理论的核心更是实际系统编程的基石。理解这些机制后我们已经具备了手写简易Shell的能力也为后续学习进程间通信、信号机制、线程控制等高级主题奠定了坚实基础。操作系统的设计之美在于它将复杂的资源管理隐藏在简洁的接口背后而进程控制正是这种设计哲学的最佳体现之一。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑