尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Linux进程控制全解析:fork、exit与僵尸进程的底层逻辑与实战

Linux进程控制全解析:fork、exit与僵尸进程的底层逻辑与实战 在Linux系统里摸爬滚打这些年我越来越觉得“进程控制”是绕不过去的核心基本功。不管你是做后端开发、嵌入式还是搞运维调优面试题里十有八九会碰到fork、exec、wait、exit这些概念。很多新手对着fork的返回值一脸懵或者是写出了僵尸进程不知道怎么处理其实都是因为对进程创建和终止的底层逻辑没有吃透。这篇东西我就拿实际项目经验说话把进程创建与终止这块掰开了揉碎了讲清楚。不搞教科书式的照本宣科也不写那种全是术语的云山雾罩就用我们平时写代码、查问题最真实的场景把fork是怎么一回事、进程退出时系统做了什么、僵尸进程为什么可怕这些事讲明白。这篇适合刚接触Linux编程的同学系统入门也适合有经验但一直没时间梳理底层原理的开发者查漏补缺。1. 项目概述为什么进程控制是Linux开发的基石1.1 先搞清楚进程到底是什么要说进程控制得先给“进程”画个像。在Linux里进程不是简单的“一个正在运行的程序”这么一句话能概括的。从内核的视角看进程是资源分配的基本单位它拥有独立的地址空间、文件描述符表、信号处理设置、当前工作目录等一整套上下文。内核通过task_struct结构体来管理每一个进程这个结构体里有进程状态、调度信息、打开的文件、内存管理信息、信号处理函数等等。我在实际开发中常把这个结构体类比成每个员工的“档案袋”里面记录了这个员工的所有信息领导内核调度谁干活看的就是这些档案。每一个fork调用本质上就是复制一份这样的档案然后让两个几乎一样的进程分头执行各自的任务。理解这一点特别重要因为后面讲的进程创建、终止、僵尸进程全部都是围绕task_struct和进程生命周期状态在做文章。1.2 进程控制到底控制了什么进程控制这个主题说白了就是管好进程的“生老病死”。具体拆开来看核心就三块创建、终止、回收。这三块对应的是fork/vfork、exit/_exit/return、wait/waitpid这三组系统调用和函数。很多初学者容易犯一个错误就是觉得进程控制就是学会调用几个API。但实际上你如果不知道fork之后父子进程的执行流是怎么走的不知道写时复制COW对性能的影响不知道exit和_exit在缓冲区处理上的巨大差异那你写的程序在复杂的生产环境下大概率会出莫名其妙的问题。我在做后端服务的时候遇到过子进程资源没回收导致句柄泄漏的问题也遇到过主进程fork之后没处理好父进程退出导致服务僵死的情况。这些坑的根源其实都能在进程控制的基础原理里找到答案。2. 进程创建深入fork的实现机制与使用陷阱2.1 fork到底做了什么fork是Linux里创建进程最经典的接口它的原型在unistd.h里#include sys/types.h #include unistd.h pid_t fork(void);这个函数最大的特点就是“调用一次返回两次”。调用fork之后内核会创建一个新进程这个新进程几乎是父进程的完整副本。然后父子进程分别从fork返回父进程返回子进程的PID子进程返回0。如果返回-1说明创建失败。我知道很多初学者第一次看这个返回值设计会觉得反直觉。为啥父进程拿到的子进程PID而子进程拿到的是0其实这是精心设计过的父进程需要知道子进程的ID才能去wait回收它而子进程想要知道自己变成新进程了唯一简单可靠的方式就是返回0作为约定标记。用PID不可能因为子进程不会知道自己在系统进程表里的新编号。一个标准的生产级fork用法是这样写的pid_t pid fork(); if (pid 0) { // 创建失败处理错误 perror(fork error); exit(1); } else if (pid 0) { // 子进程逻辑 printf(子进程PID%d父进程PID%d\n, getpid(), getppid()); } else { // 父进程逻辑 printf(父进程PID%d子进程PID%d\n, getpid(), pid); }这里有个细节我每次培训都要强调fork之后父子进程的执行顺序是不确定的。你以为先打印父进程、后打印子进程不一定。这取决于内核的调度器在单核CPU上轮转调度在多核CPU上甚至可能同时在跑。所以千万不要依赖打印顺序来判断谁先谁后一切以返回值分支为准。2.2 写时复制技术fork高效运行的秘密武器有人可能会问一个很自然的问题fork要复制父进程的完整地址空间那开销得多大如果父进程占用了2GB内存每次fork是不是都要实实在在复制2GB数据早期的Linux确实是这么干的效率很低。但现代Linux用了一套非常聪明的机制叫写时复制Copy-On-Write简称COW。它的核心逻辑是fork的时候并不真正复制物理内存页面而是把父子进程的虚拟地址空间都映射到同一份物理页面上并且把页面标记为“只读”。这时候如果父子进程只是读数据大家共享物理内存又快又省空间。一旦有任何一个进程尝试写入就会触发缺页异常内核这才把这块物理页面复制一份出来让写入的进程在副本上操作然后把页面的权限改回可读写。这样整个复制过程被延迟到了真正发生写入的那一刻而且只复制被写的页面没有被写的页面依然共享。我拿一个实际场景来说明这个机制的意义。假设我在写一个Web服务器主进程启动时加载了巨大的配置文件几百MB然后fork了10个子进程去处理请求。因为COW的存在这10个子进程根本不需要各自复制一份几百MB的数据而是共享同一份。当某个子进程要修改某个全局变量时只会触发个别页面的复制在这个案例里可能就是几十KB而已。这性能提升是数量级的。2.3 vfork和fork的细节差异除了fork还有一个vfork接口早期用来实现“子进程马上执行exec”的场景。vfork的设计初衷是反正子进程创建完就要去执行新程序之前的地址空间内容根本用不上那就别复制了直接把父进程的地址空间给子进程用吧。#include sys/types.h #include unistd.h pid_t vfork(void);vfork和fork最大的区别在于vfork创建的子进程会共享父进程的地址空间并且在子进程调用exec或exit之前父进程会被挂起。这也就意味着如果子进程在exec之前修改了父进程的变量父进程是能感知到的。现代Linux系统的fork已经用上了COWvfork的优势不再像以前那么明显。我在实际项目中几乎不用vfork因为它太容易踩坑了——子进程如果在exec之前不小心做了太多事情很容易影响父进程的状态。除非你有极其极端的性能要求并且确认子进程路径非常短小否则老老实实fork就够了。有些老项目里会看到vfork的使用了解它的机制有助于读老代码但新项目别用。2.4 fork失败的常见原因面试官特别喜欢问一个问题“fork会失败吗”答案是会。最常见的两个原因第一系统进程数达到上限。Linux内核通过pid_max参数控制最大PID编号也就直接限制了同时存在的进程总数。查看方式是cat /proc/sys/kernel/pid_max一般默认是32768或者更大。一台长期运行的服务器上如果某个进程疯狂fork不回收很快就可能打满这个上限。第二内存不足。fork需要为子进程分配task_struct和内核栈等资源虽然COW技术减少了物理内存复制但内核数据结构还是需要分配的。如果系统内存耗尽fork就会返回-1并设置errno为EAGAIN。我踩过这么一个坑在某次压力测试中程序每一轮请求都fork一个子进程但进程退出后父进程没有及时wait回收导致大量僵尸进程堆积到最后系统无法创建新进程服务告警。定位了很久才发现是僵尸进程问题。所以写任何涉及fork的项目第一件事就是把回收机制设计好这就是第四部分要讲的进程回收。3. 进程退出从exit到_exit再到return的完整链路3.1 exit和_exit到底差在哪进程终止这个话题看起来很简单就是一个进程跑完了要退出。但在Linux里“退出”也分三六九等。最常用的三兄弟是return、exit()和_exit()。_exit()是真正的系统调用级别退出它在内核中直接终止当前进程。而exit()是C标准库的封装它除了最终会调用_exit之外还会先做一系列“善后工作”调用atexit注册的钩子函数、清理标准I/O缓冲区、关闭文件流等。这两个的区别在实际开发中太重要了。我给你讲一个真实的例子。我早期写过一个小工具主流程里用printf打印日志然后调exit(0)退出日志正常输出了。后来改动代码把某个错误分支从exit(0)换成了_exit(1)结果发现日志少了一行。原因很简单printf的输出是先写到标准库的用户态缓冲区里的并没有立刻刷到内核。_exit不走标准库的清理逻辑缓冲区里的数据就直接丢掉了。所以记住这个原则在C/C程序里正常情况下应该用exit()而不是_exit()否则你printf的内容可能不明不白就没了。3.2 return和exit的关系很多初学者搞不清return 0和exit(0)有什么区别。在main函数里这两者看起来好像等价实际上还是有细微差别的。main函数里的return语句最终会在C运行时启动代码中被转换成exit()调用。但是要注意如果是在子函数里return那只是结束当前函数进程并不会退出。而exit不管在哪里被调用都会直接终止当前进程。还有一点非常关键return和exit在C里的行为不同。C中return会触发局部对象的析构函数而exit不会。所以在C项目里如果某个模块需要清理资源千万不能用exit跑到一半否则析构函数全被跳过了很容易导致内存泄漏或者文件句柄没释放。我在带新人时经常强调退出进程之前先想想有没有该释放的资源没释放有没有该刷的日志没刷完。如果你对自己的代码退出路径不够清晰优先用return只有在异常处理或库函数内部等不得不退出的地方才用exit。3.3 main函数的返回值与退出状态码main函数的返回值会被操作系统当作进程的退出状态码。按照惯例0表示成功非0表示异常。但很多人并不知道这个状态码还有讲究它只有低8位是有效的范围是0到255。在写脚本和做进程管理时退出码是父子进程之间传递信息的重要手段。我在写服务守护进程的时候会用退出码约定一组业务规则0表示正常退出1表示配置错误2表示资源不足3表示数据库连接失败。这样上层监控脚本只需要检查退出码就能快速定位问题方向。一个实用的技巧是在脚本里用$?获取上一个命令的退出码./my_program echo 退出码: $?在C程序里你也可以用WEXITSTATUS(status)宏从wait的返回状态中提取子进程的退出码这个在后文会详细讲。3.4 atexit钩子函数退出时的最后一道关卡atexit函数允许你注册一些在进程正常退出时自动调用的函数它的声明在stdlib.h里#include stdlib.h int atexit(void (*function)(void));我用atexit最典型的场景是做统一清理比如关闭全局的日志文件、释放全局连接池、输出一些最后的统计信息。这样即使代码里有很多个exit分支只要它们是“正常退出”经过exit()而非_exit()所有注册的钩子都会按注册顺序的逆序被调用。有个细节需要注意atexit注册的钩子函数是在exit()内部被调用的所以它们执行的时候标准I/O流还是可用的。但如果你在钩子里又调用了exit这是未定义行为会出问题。我一般习惯把钩子函数写得尽量简单不做复杂逻辑只做必要的资源释放和状态记录。还有一个在信号处理中很常见的问题如果进程是被信号杀死的比如kill -9atexit注册的函数是不会被调用的。SIGKILL信号是不能被捕获和处理的内核直接强制终止进程没有任何善后机会。所以如果要保证资源回收光靠atexit是不够的必须配合信号处理和系统设计层面的容错机制。4. 僵尸进程与孤儿进程进程终止后最容易被忽略的坑4.1 为什么会有僵尸进程进程退出并不代表进程“消失”了。当一个进程调用exit正常退出后内核并不会立刻把它彻底清理掉。进程会先进入一个特殊状态叫僵尸状态Zombie。在这种状态下进程的大部分资源已经被释放只剩一个task_struct结构体残留在内核进程表里。这个残留的目的很简单保存进程的退出状态码给父进程读取。父进程需要用wait或waitpid调用来“收尸”读取子进程的退出状态。读到之后内核进程表才会真正删除这个僵尸进程。如果父进程一直不调用wait来回收子进程僵尸进程就会一直保留在进程表里。这就是很多服务端程序会隐藏大量僵尸进程的原因。你在终端敲ps aux看到一堆STAT列是Z的进程就是这个情况。僵尸进程虽然不再占用CPU和内存资源但它的PID和内核task_struct没有被释放。当系统同时存在的进程数达到pid_max上限时就无法再创建新进程了。4.2 僵尸进程的正确处理姿势处理僵尸进程的办法说起来简单做起来有不少门道。最直接的做法是父进程调用wait或者waitpid阻塞等待子进程退出并回收。#include sys/wait.h pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options);wait会阻塞调用者直到有一个子进程退出。这在一父一子的场景下好使但如果父进程有多个子进程wait是没法指定等谁的它返回任何一个退出的子进程PID。waitpid则更灵活可以指定等待某个特定PID的子进程。但阻塞等待在实际业务里用起来太死板了。一个网络服务不可能为了等一个子进程退出就暂停全部工作。所以有一个标准做法给父进程注册一个SIGCHLD信号处理函数。当子进程退出时内核会向父进程发送SIGCHLD信号父进程在信号处理函数里调用waitpid回收子进程。下面这段代码是实践中最常用的套路void sigchld_handler(int sig) { int saved_errno errno; while (waitpid(-1, NULL, WNOHANG) 0) { // 循环回收所有已退出的子进程 } errno saved_errno; }这个写法有两点值得学习。第一while循环加上WNOHANG标志表示非阻塞回收所有已经退出的子进程防止信号丢失导致僵尸残留。第二函数开头保存errno、结尾恢复因为信号处理函数可能会中断主流程的系统调用不能破坏了errno值。在主函数里这样注册信号struct sigaction sa; sa.sa_handler sigchld_handler; sigemptyset(sa.sa_mask); sa.sa_flags SA_RESTART | SA_NOCLDSTOP; if (sigaction(SIGCHLD, sa, NULL) -1) { perror(sigaction); exit(1); }SA_RESTART标志让被信号中断的系统调用自动重启SA_NOCLDSTOP表示子进程只是被暂停SIGSTOP时父进程不收到SIGCHLD只有真正退出才通知。4.3 孤儿进程被谁收养和僵尸进程容易混淆的是孤儿进程。孤儿进程是指父进程先退出了子进程还在运行。子进程不会因此变成无人管理它的“养父”是PID为1的init进程在现代Linux系统里通常是systemd。内核会在父进程退出时把所有还没被回收的子进程的ppid统一改成init的PID由init进程负责回收。从编程角度理解孤儿进程的意义在于在后台服务的实现里我们经常用“双重fork”技巧让子进程脱离终端的控制变成守护进程。第一次fork后让父进程退出子进程被init收养这时候再setsid创建新的会话就能彻底脱离原来的终端会话。这套打法是Linux守护进程的标准流程我后面在实战部分会再提到。4.4 进程回收的心得与注意事项进程回收这块我踩过的坑和总结的心得比较多。第一点wait和waitpid返回的不只是子进程的PIDstatus里还藏着退出状态。要用宏来解读int status; pid_t child wait(status); if (WIFEXITED(status)) { printf(子进程%d正常退出退出码%d\n, child, WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf(子进程%d被信号杀死信号编号%d\n, child, WTERMSIG(status)); }第二点如果父进程自己有大量业务要做又不想错过子进程退出通知除了SIGCHLD信号也可以用waitpid配合WNOHANG参数在工作循环里非阻塞轮询子进程状态但这会浪费CPU不是推荐做法。第三点在频繁创建子进程的框架里一定要严格测试僵尸进程数量。我曾经在项目上线前用watch -n 1 ps -eo pid,ppid,stat,comm | grep defunct实时监控僵尸进程这是排查这类问题的好帮手。5. 完整实操从创建到回收的全生命周期实战5.1 环境准备与代码规划为了把这套理论落到实际行动上我写一个完整的C程序来演示进程从创建、执行任务、退出到被父进程回收的完整生命周期。这个程序模拟一个经典场景父进程作为任务调度器每接到一个“任务”这里简化为一个整数就fork一个子进程来处理处理完由父进程统一回收。开发环境就是标准的Linux环境我用的是Ubuntu 22.04 LTS内核版本5.15。代码用gcc编译命令如下gcc -o proc_demo proc_demo.c -Wall -Wextra整个程序设计思路是这样用SIGCHLD信号处理机制回收子进程而不是阻塞式wait这样父进程可以继续派发新任务。每个子进程模拟执行2秒的“业务处理”然后exit返回一个和任务ID相关的退出码模拟业务处理结果。5.2 核心代码实现#include stdio.h #include stdlib.h #include unistd.h #include sys/wait.h #include sys/types.h #include signal.h #include errno.h #define TASK_COUNT 5 void sigchld_handler(int sig) { int saved_errno errno; pid_t pid; int status; while ((pid waitpid(-1, status, WNOHANG)) 0) { if (WIFEXITED(status)) { printf([父进程] 子进程 %d 已回收退出码%d\n, pid, WEXITSTATUS(status)); } else if (WIFSIGNALED(status)) { printf([父进程] 子进程 %d 被信号 %d 终止\n, pid, WTERMSIG(status)); } } errno saved_errno; } void do_child_work(int task_id) { printf([子进程 %d] 开始处理任务 #%d\n, getpid(), task_id); sleep(2); // 模拟业务处理结果如果任务ID是3模拟一个异常退出 if (task_id 3) { printf([子进程 %d] 任务 #%d 处理失败准备异常退出\n, getpid(), task_id); exit(42); } printf([子进程 %d] 任务 #%d 处理完成正常退出\n, getpid(), task_id); exit(0); } int main() { struct sigaction sa; sa.sa_handler sigchld_handler; sigemptyset(sa.sa_mask); sa.sa_flags SA_RESTART | SA_NOCLDSTOP; if (sigaction(SIGCHLD, sa, NULL) -1) { perror(sigaction); exit(1); } printf([父进程 %d] 开始派发任务\n, getpid()); for (int i 1; i TASK_COUNT; i) { pid_t pid fork(); if (pid 0) { perror(fork); continue; } else if (pid 0) { do_child_work(i); // 正常永远不会执行到这里 _exit(0); // 防御性调用 } else { printf([父进程] 已派发任务 #%d 给子进程 %d\n, i, pid); } } // 父进程继续做自己的事情不阻塞等待子进程 printf([父进程] 所有任务已派发继续处理其他事务...\n); sleep(5); printf([父进程] 主流程退出\n); return 0; }5.3 编译运行与结果解读编译和运行过程$ gcc -o proc_demo proc_demo.c -Wall -Wextra $ ./proc_demo我实际跑下来输出类似下面这样每次运行PID和顺序会有差异[父进程 12345] 开始派发任务 [父进程] 已派发任务 #1 给子进程 12346 [父进程] 已派发任务 #2 给子进程 12347 [子进程 12346] 开始处理任务 #1 [父进程] 已派发任务 #3 给子进程 12348 [子进程 12347] 开始处理任务 #2 [父进程] 已派发任务 #4 给子进程 12349 [子进程 12348] 开始处理任务 #3 [父进程] 已派发任务 #5 给子进程 12350 [子进程 12349] 开始处理任务 #4 [子进程 12350] 开始处理任务 #5 [父进程] 所有任务已派发继续处理其他事务... [子进程 12346] 任务 #1 处理完成正常退出 [父进程] 子进程 12346 已回收退出码0 [子进程 12347] 任务 #2 处理完成正常退出 [父进程] 子进程 12347 已回收退出码0 [子进程 12348] 任务 #3 处理失败准备异常退出 [父进程] 子进程 12348 已回收退出码42 [子进程 12349] 任务 #4 处理完成正常退出 [父进程] 子进程 12349 已回收退出码0 [子进程 12350] 任务 #5 处理完成正常退出 [父进程] 子进程 12350 已回收退出码0 [父进程] 主流程退出这个输出验证了几个重要结论。第一父进程派发任务后没有被阻塞它通过信号机制完成了子进程的回收。第二任务#3的子进程用exit(42)模拟了异常退出父进程通过WEXITSTATUS准确取到了42这个自定义退出码。第三所有子进程都被及时回收没有僵尸进程残留用ps查看进程表是干净的。第四子进程完成的顺序和派发顺序不同这再次证明了调度顺序的不确定性。5.4 场景扩展把进程控制用到守护进程里理解了基础流程我分享一个更贴近生产的用法。我之前在做一个数据采集网关时需要把采集程序做成守护进程。传统的守护进程实现就需要用到进程创建和终止的知识核心步骤是pid_t pid fork(); if (pid 0) { exit(1); } else if (pid 0) { exit(0); // 父进程退出 } // 子进程继续执行 if (setsid() -1) { perror(setsid); exit(1); }第一次fork后父进程退出子进程变成孤儿进程被init收养。接着在子进程里调用setsid创建新会话。因为此时子进程不是会话首进程setsid会成功新进程组和新会话就这样诞生了进程也就彻底脱离了控制终端。后面还可以继续fork一次确保进程永远不会重新获取控制终端。这就是最经典的双重fork守护进程流程。在服务退出时守护进程的终止也很有讲究。如果直接kill -9很多清理逻辑不会执行。所以在真实项目里我一般会监听SIGTERM信号执行完清理逻辑后再用exit(0)正常退出。这样外部脚本用kill pid默认发SIGTERM就能优雅地停止服务。6. 常见问题与排查技巧实录6.1 为什么fork之后printf输出了两遍这个问题几乎每个学Linux编程的人都会遇到。你在代码里写了一句printf(hello)然后fork结果发现“hello”被打印了两次。原因在于标准I/O库的缓冲区机制printf的输出先存放在用户态缓冲区中并没有真正写到屏幕上。fork会把整个进程的地址空间复制一份缓冲区自然也被复制了。父子进程各自退出时各自刷新自己的缓冲区于是同一个“hello”就出现了两遍。解决办法有两个在fork之前调用fflush(stdout)手动刷新缓冲区或者干脆用write这种不带缓冲的系统调用直接写文件描述符。在实际项目中我一般是在fork之前把所有标准输出刷新干净养成好习惯。6.2 子进程的printf输出乱序怎么办在有多进程并发的场景下多个子进程同时往同一个终端打印信息顺序混乱是正常的因为这是内核调度决定的。如果你真的需要严格的顺序日志正确做法不是去控制调度而是让每个子进程把输出写到独立的日志文件或者通过管道/消息队列把数据统一送到日志收集进程去处理。6.3 为什么修改全局变量在子进程里没生效这个问题也是很多初学者的疑惑。子进程里修改了全局变量回到父进程一打印值还是原来的。这正是写时复制和进程地址空间隔离的体现。fork出来的子进程有一套独立的地址空间至少在写入发生后它对全局变量的修改永远不会影响父进程。如果你真的需要父子进程共享数据需要借助进程间通信IPC机制比如共享内存、消息队列等单纯靠全局变量是行不通的。6.4 fork出现Resource temporarily unavailable怎么排查这个报错通常对应errno为EAGAIN意味着系统进程数或者内存资源不足。排查步骤我一般是这样第一步用ps -eLf | wc -l查看当前系统进程和线程数量是否异常偏高。第二步用cat /proc/sys/kernel/pid_max查看PID上限如果进程数已经接近这个值基本上就是进程数打满了。第三步用top查看内存和CPU状态确认是否内存耗尽。第四步如果系统进程数不高但还报错要怀疑是当前用户或者某个cgroup的进程数限制可以用ulimit -u查看用户最大进程数限制。我在一次性能测试中遇到这个报错就是僵尸进程堆积导致的。用ps -eo stat,pid,ppid,cmd | grep ^Z一眼就找到了几百个僵尸进程根源是父进程代码里漏了waitpid调用。补上SIGCHLD处理后就再没出现过这个问题。6.5 面试高频题目速查表整理一份Linux进程控制面试题的简明速查表方便读者对照自测问题核心答案要点fork返回值的含义父进程返回子进程PID子进程返回0失败返回-1fork之后父子进程执行顺序不确定由调度器决定写时复制是什么fork不实际复制物理内存写入时触发缺页异常才复制页面exit和_exit的区别exit会刷新缓冲区、调用atexit钩子_exit直接内核退出僵尸进程如何产生子进程退出父进程未调用wait回收如何避免僵尸进程wait/waitpid或SIGCHLD信号处理中回收孤儿进程谁回收initsystemd进程main return和exit的区别main的return最终转成exit子函数return只结束当前函数守护进程怎么创建fork setsid 第二次fork脱离终端和会话7. 进程控制的日常调试心得几个提高效率的工具与方法工具用得好排查效率能提一倍。我这几年用下来最顺手的几个就是ps、pstree、strace和gdb。ps -ef配合--forest选项能直观看到进程的父子关系树pstree -p pid能快速查看某个进程的所有子进程strace -f -e traceprocess ./demo能跟踪进程相关的所有系统调用包括fork、execve、exit_group等对理解程序行为特别有帮助。gdb调试多进程时记得要设置set follow-fork-mode child或者set detach-on-fork off。前者让调试器跟踪子进程后者让父子进程都在调试器控制下。我在排查子进程退出码异常的问题时经常用这种方式在子进程里打断点直接看它走到哪一步出了错。还有一个排查神器是/proc文件系统。每个进程运行的时候/proc/pid/status里能看到进程状态、父进程PID、内存占用等信息。比如判断一个进程是不是僵尸状态看/proc/pid/stat里的状态字母是Z就清楚了。8. 从进程控制延伸出去下一阶段的学习方向进程创建和终止只是进程控制的第一个模块。学完这块接下来一般会沿着两条线深入。第一条线是进程执行。fork创建的子进程还是父进程的副本如果想要让它执行一个全新的程序就需要用到exec系列函数execl、execv、execle、execve等。fork和exec的组合是Linux下创建新进程的标准姿势。第二条线是进程间通信。进程各自独立但它们经常需要协同工作。管道、消息队列、共享内存、信号量、信号这五大IPC机制是Linux多进程开发的必修课。我是在写一个多进程日志采集系统时才真正把这些机制串起来的每个子进程往管道写数据父进程统一汇总这个架构用到了进程控制加进程通信的全部知识。还有一条隐藏线是进程调度与状态。进程有运行、就绪、阻塞、僵尸等状态状态机在写大型服务时特别重要。理解了进程生命周期后面学线程、协程也会事半功倍。我自己学习进程控制时的一个体会是把fork、exit、wait这套机制放到真实服务场景里去理解比死磕API列表有用得多。你可以动手写一个极简的Web服务器雏形主进程监听端口来一个请求就fork一个子进程去处理处理完退出。这样一个几十行的小项目基本就把进程创建、终止、回收的核心流程全部覆盖了。写完这个你会对Linux进程模型有一个别人教不会的深刻理解。
返回列表