ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

嵌入式Linux开发:吃透父进程与子进程机制,避开僵尸进程与调试坑

嵌入式Linux开发:吃透父进程与子进程机制,避开僵尸进程与调试坑 1. 先从一块板子讲起为什么嵌入式开发绕不开“进程”这个话题用飞凌嵌入式的ElfBoard做嵌入式Linux开发时很多人第一次被“进程”这个概念绊住往往不是在看《Unix环境高级编程》的时候而是在调试一个明明很简单、却反复出怪问题的程序的时候。比如我最初在ElfBoard上写一个网络采集程序逻辑上没有任何问题打开socket、循环读取传感器数据、写到文件里。但运行一段时间后日志里总会出现一些莫名其妙的重复记录排查了很久才发现不是我的业务逻辑有Bug而是程序在启动时被反复fork出了多个子进程每个子进程都在独立地写同一个日志文件数据自然就乱套了。这个问题的根源就是父进程和子进程之间的关系没有被理清。嵌入式开发和纯应用层开发的差异在于你手里这块板子的CPU、内存、Flash都是有限的跑在它上面的每一个进程都会真实地消耗资源。如果你不理解进程的创建、继承、退出和回收机制那么你的程序在PC上跑得再好一放到板子上就会显露出各种问题——要么内存被吃光要么出现僵尸进程把进程表占满。所以搞懂父进程和子进程不是考试需要而是你真正把程序部署到ElfBoard这类嵌入式设备上的前提条件。这篇文章我不会去复述教科书上的定义而是结合我在ElfBoard上实际调试程序的经验把进程相关的核心机制、fork函数的真实行为、以及那些会让你抓狂的进程“坑”一次说清楚。不管你是刚接触嵌入式Linux的新手还是已经写过一阵子驱动和应用的老手这篇文章里的实操内容和排查方法都能直接用在你的板子上。2. 先理解进程到底是什么不是“正在运行的程序”那么简单2.1 进程是程序在内存中的一次“活化”教科书上说“进程是程序的一次执行过程”这句话没有错但它没有说出嵌入式开发中最关心的那层意思进程是一个占用着系统资源的实体。程序文件放在Flash或SD卡里它只是一堆静态的二进制指令不占用内存、不占用CPU、不占用文件描述符。当你把它加载到内存里开始执行它才变成一个进程这个时候它就有了自己的地址空间、自己的寄存器上下文、自己的打开文件列表、自己的PID。我习惯用一个生活化的比喻来想这件事程序就像是菜谱放在书架上Flash里它本身不能产生任何东西。进程则是你按照菜谱实际开火做饭的整个过程你需要占用灶台CPU、占用锅碗内存、占用食材文件、外设而且这个过程有开始、有结束。你同时照着三本菜谱做三道菜就是三个进程它们各自占着各自的灶台和锅。在ElfBoard上这个比喻有非常实际的意义。板子的内存通常只有几百MB远不如PC充裕。你每创建一个进程内核就要为它分配内存、创建task_struct结构体、分配内核栈、复制或共享文件描述符表。如果你的程序不懂得控制进程数量板子很快就会出现内存不足然后触发OOMOut Of Memory机制把某个进程杀掉——这在多进程程序里往往会引发连锁反应。2.2 PID、PPID与进程表每个进程都有“身份证”和“户口”每个进程在内核里都对应一个task_struct这个结构体里记录着进程的所有信息状态、优先级、打开的文件、信号处理方式、内存布局等等。而用户空间能直接看到的是PID和PPID这两个数字。PIDProcess ID进程的唯一标识。每次创建新进程内核会分配一个新的PID这个数字在系统运行期间是唯一的进程结束后可以被复用。PPIDParent Process ID父进程的PID。当你用shell启动一个程序时这个程序的父进程就是shell进程当你在程序里用fork创建子进程时子进程的PPID就是那个调用fork的进程的PID。查看这两个数字非常简单在ElfBoard的串口终端或SSH终端里执行ps -ef或者更直观地看进程树ps -ef --forestps -ef --forest会以树状结构展示进程之间的父子关系缩进的每一层代表一个fork层级。这对理解程序运行时的进程结构非常有帮助。让我用ElfBoard上的实际场景来说。如果你在串口终端里执行了一条命令./myapp那么myapp的父进程就是当前shell进程。如果你在myapp内部又调用了system(ls)或者fork()那么ls就是myapp的子进程形成了shell → myapp → ls这么一条链。当myapp退出时这条链就被切断后续处理方式取决于具体的场景——这也就是后面会讲到的孤儿进程与僵尸进程。提示在嵌入式板子上排查问题时养成先敲ps -ef --forest看一眼进程树的习惯往往比直接看日志更能快速定位异常。很多“程序行为诡异”的问题本质上是进程结构和你设想的不一样。3. fork函数父子进程关系的起点3.1 fork的返回值为什么设计成“一个函数返回两次”Linux中创建子进程的标准方式是调用fork()它的原型只有一句话#include unistd.h pid_t fork(void);但这个函数的行为是所有刚接触它的程序员都会困惑的一次调用两次返回。在父进程中fork返回子进程的PID在子进程中fork返回0。如果创建失败返回-1。为什么要这么设计因为fork内部会通过复制当前进程的地址空间、文件描述符表、信号处理设置等创建出一个几乎一模一样的子进程。子进程并不是从main函数开始执行而是从fork调用的下一条指令开始执行。这就意味着调用fork之前父子进程是完全一样的fork之后它们在某些属性上各走各路了。“一个函数返回两次”这个设计实际上是为了让程序员用最少的代码区分父进程和子进程的执行路径pid_t pid fork(); if (pid 0) { // fork失败 perror(fork); exit(1); } else if (pid 0) { // 子进程执行路径 printf(子进程我的PID是%d我的父进程PID是%d\n, getpid(), getppid()); } else { // 父进程执行路径 printf(父进程我的PID是%d我创建的子进程PID是%d\n, getpid(), pid); }简单解释一下这个代码的运作逻辑在父进程的上下文里fork成功返回了子进程的PID因此pid变量大于0。在子进程的上下文里fork返回0因此pid等于0。所以if/else分支自然地分开了两条执行路径。这就解释了为什么说fork之后的代码在父子进程里会被执行两次。父进程执行完fork调用后继续走pid 0的分支子进程则从fork返回点开始走pid 0的分支。3.2 fork的“写时复制”机制为什么要这么设计传统教材会告诉你说fork复制了一份进程的地址空间但在现代Linux中fork并不是直接复制所有内存而是使用了**写时复制Copy-on-WriteCOW**技术。原理是这样的fork出来的子进程最初与父进程共享同一个物理内存页。只有当其中某一个进程真正去写入某个内存页时内核才会为这个页创建一份新的副本并更新该进程的页表映射。这个设计的理由很直白大多数情况下子进程调用fork后很快就会执行exec()来运行一个新程序之前复制的那些数据大多没有用。如果每次都老老实实复制全部内存成本太高了在嵌入式板子上这种成本更是不可接受的。我在ElfBoard上做过一个粗略的测试一个占用了大约50MB内存的程序用fork创建子进程通过/proc/pid/status里的VmRSS观察内存占用发现父进程fork完成后子进程的RSS并不会立刻接近50MB而是远小于这个值——因为大部分内存页还在共享状态只有被写入的页才真正产生了物理内存开销。这就是COW的直接体现。这个机制带来的一个重要注意事项是父子进程在fork之后虽然共享了内存页但当一个进程修改了某个变量时这个修改不会反映到另一个进程里。因为COW机制会在写入时创建新页让写者看到自己的副本原始页仍然保留给另一方。所以父子进程之间的数据交换不能靠共享普通全局变量来实现——它们在逻辑上是两个独立的地址空间即便物理上共享了一部分页写入时也会被拆开。如果确实需要父子进程通信有两种常规方案使用pipe()管道适合父子进程间传小数据。使用共享内存如mmap配合MAP_SHARED适合传大数据块。这两种方式在嵌入式场景下都足够高效也都适合ElfBoard这种内存受限的设备。4. 在ElfBoard上实操编写一个能看出父子进程关系的程序4.1 环境准备确认交叉编译工具链和板端运行环境在写代码之前先确保你的交叉编译环境是好的。飞凌嵌入式的ElfBoard通常提供了配套的交叉编译工具链核心操作是把编译好的可执行文件拷贝到板子上运行。如果你的开发机是x86 PC直接在开发机上执行gcc编译出来的程序是没办法在ARM架构的板子上运行的。必须用交叉编译器比如source /opt/elfboard/toolchain/environment-setup-cortexa7t2hf-neon-poky-linux-gnueabi然后编译arm-poky-linux-gnueabi-gcc -o fork_demo fork_demo.c注意执行编译前需要确认fork_demo.c里包含了正确的头文件#include stdio.h #include stdlib.h #include unistd.h #include sys/types.h #include sys/wait.h编译完成后把fork_demo拷贝到板子上通过scp、U盘或者NFS均可然后在板子的串口终端里运行chmod x fork_demo ./fork_demo如果你用的是ElfBoard出厂的系统一般自带gcc编译器也可以在板子上直接编译省去交叉编译的步骤。不过从开发效率角度我建议还是在PC上交叉编译后拷贝到板子上毕竟板子的编译速度远不如PC。4.2 完整示例在ElfBoard上观察父子进程的运行轨迹下面这个例子基本上是所有Linux进程编程入门必写的一小段代码但我会带点Embedded特色的改动——在程序里输出进程的PID、PPID并观察fork之后两个进程是否共享同一个局部变量#include stdio.h #include stdlib.h #include unistd.h #include sys/types.h #include sys/wait.h int main(void) { int shared_var 100; pid_t pid fork(); if (pid 0) { perror(fork error); return 1; } if (pid 0) { // 子进程 printf([子进程] 我是子进程, 我的PID %d, 我的父进程PPID %d\n, getpid(), getppid()); printf([子进程] fork返回值为 %d, 我的shared_var初始值 %d\n, pid, shared_var); // 子进程修改共享变量 shared_var 200; printf([子进程] 我把shared_var改为200, 现在shared_var %d\n, shared_var); sleep(2); printf([子进程] 睡眠2秒后我再看看shared_var %d\n, shared_var); exit(0); } else { // 父进程 printf([父进程] 我是父进程, 我的PID %d, 我创建的的子进程PID %d\n, getpid(), pid); printf([父进程] 我的PPID(即shell的PID) %d\n, getppid()); printf([父进程] 我的shared_var初始值 %d\n, shared_var); // 父进程修改共享变量 shared_var 300; printf([父进程] 我把shared_var改为300, 现在shared_var %d\n, shared_var); // 等待子进程结束 wait(NULL); printf([父进程] 子进程已结束我(父进程)也要退出了。\n); } return 0; }在ElfBoard上编译运行之后输出结果是这样的PID会是板子上实际的数字[父进程] 我是父进程, 我的PID 1284, 我创建的的子进程PID 1285 [父进程] 我的PPID(即shell的PID) 1230 [父进程] 我的shared_var初始值 100 [父进程] 我把shared_var改为300, 现在shared_var 300 [子进程] 我是子进程, 我的PID 1285, 我的父进程PPID 1284 [子进程] fork返回值为 0, 我的shared_var初始值 100 [子进程] 我把shared_var改为200, 现在shared_var 200 [子进程] 睡眠2秒后我再看看shared_var 200 [父进程] 子进程已结束我(父进程)也要退出了。这个输出里有两个关键信息值得细看。第一个关键信息是fork的返回值。在子进程里fork返回0在父进程里fork返回的是子进程的PID。本例中子进程PID是1285父进程PID是1284所以PID 1285的进程其PPID是1284。这与ps -ef看到的父子关系完全对应。第二个关键信息是shared_var的输出。在fork之后父子和子进程看到的shared_var都是100。父进程把它改成300后子进程里看到的仍然是100子进程把它改成200后父进程里虽然我们没有打印但可以想到看到的仍然是300。这验证了前面说的写时复制机制。所谓“共享”在fork刚完成那一刻是物理共享的但一旦有写入动作就会分裂成两份独立的内存谁也影响不到谁。运行结束后可以用ps -ef --forest确认一下进程是否都已经退出。如果父进程没有调用wait()子进程退出之后会变成僵尸进程——这是嵌入式开发里最常见的进程问题之一下一节详细说。4.3 在板子上用ps命令实时观察进程树程序运行期间可以另开一个终端或者提前用后台运行程序在串口终端里执行ps -ef --forest | grep fork_demo你会看到类似这样的输出PID PPID CMD 1284 1230 ./fork_demo 1285 1284 \_ ./fork_demo通过这个命令你能直观地观察到父进程1284下面是子进程1285缩进关系表示父子关系。实际调试多进程程序时这个方法非常管用。有时候程序逻辑上以为只有一个进程在跑但ps一看跑了一堆子进程问题就暴露了。5. 进程生命周期创建、退出、回收以及那些让你头大的“僵尸”5.1 僵尸进程是怎么产生的为什么嵌入式板子最怕这个僵尸进程Zombie Process是Linux进程机制里最著名的一个“坑”。它的成因很清晰子进程先于父进程退出而父进程没有调用wait()或waitpid()去回收子进程的退出状态那么子进程就会变成僵尸进程。僵尸进程已经不再占用CPU、不再执行任何代码它的地址空间已经被释放了但内核仍然保留着它的task_struct原因是为了让父进程在未来的某个时刻还能查询它的退出码。你可以把僵尸进程理解为已经注销了户口但还在系统户籍册上挂着一条记录的人。内核无法主动删除这条记录因为父进程还没有“签收”子进程的退出状态。在PC上如果你不小心产生了几个僵尸进程系统未必有明显反应。但在ElfBoard这类嵌入式板子上问题会严重得多。因为嵌入式系统的进程数本来就少而且内核的PID空间虽然够大但僵尸进程如果不断累积会有两个后果进程表条目被大量僵尸进程占据导致新的进程无法创建fork返回EAGAIN。ps输出里出现大量defunct标记排查问题时严重干扰视线。看一下僵尸进程长什么样ps -eo pid,ppid,stat,comm输出中状态为Zzombie的进程就是僵尸进程它们的命令行往往显示为defunct。注意父进程自己退出后僵尸子进程会被init进程PID为1接管init会负责回收它们。所以很多情况下杀掉僵尸进程的父进程僵尸进程就会被清理掉。但这是治标不治本根因永远是父进程没有调用wait()。5.2 孤儿进程父进程先走一步子进程被“收养”与僵尸进程相对的另一个场景是孤儿进程父进程先退出子进程还在运行。这种情况下子进程并不会变成僵尸进程而是会被系统的init进程或者最近的subreaper进程收养它的PPID会变成1或者某个专门的子收割者进程。为什么会设计这种机制因为Linux内核规定当父进程退出时它的所有子进程都会被自动“过继”给init进程。之后子进程再退出就由init负责回收不会变成僵尸。在嵌入式场景下孤儿进程本身不是什么大问题但如果你在脚本或守护进程里不预期地产生了孤儿进程它们会脱离原来的会话控制不再随终端的关闭而退出。这就可能导致你在板子上关闭了一个终端程序却发现后台进程还在跑、还在占CPU。调试时看到这种现象可以先查一下PPID是不是变成了1。刚才的例子里父进程在wait(NULL)之后退出而子进程已经提前退出了所以没有孤儿进程问题。但如果我把代码改成父进程不等子进程直接return 0那么子进程就会变成孤儿进程PPID变为1。5.3 让进程正确退出wait、waitpid 与 SIGCHLD正确管理父子进程的核心就是父进程要承担“回收”责任。下面是最常用的写法pid_t pid fork(); if (pid 0) { // 子进程做自己的事 exit(0); } else if (pid 0) { int status; wait(status); // 阻塞等待子进程退出 if (WIFEXITED(status)) { printf(子进程正常退出退出码为 %d\n, WEXITSTATUS(status)); } }为了检查子进程是否正常退出可以使用两个宏WIFEXITED(status)判断子进程是否正常退出没有收到信号。WEXITSTATUS(status)获取子进程的退出码。如果你不想阻塞等待也可以使用waitpid(pid, status, WNOHANG)做非阻塞轮询。这种方式在嵌入式程序里更常见因为父进程往往有自己的循环需要跑不能因为等待子进程就卡住。例如while (1) { int status; pid_t ret waitpid(pid, status, WNOHANG); if (ret pid) { printf(子进程已回收退出码%d\n, WEXITSTATUS(status)); break; } else if (ret 0) { // 子进程还没退出继续干别的事 } else { perror(waitpid); break; } }还有一种高级做法是使用SIGCHLD信号。当子进程状态发生变化退出、停止、被信号恢复时内核会给父进程发送SIGCHLD信号。父进程可以在信号处理函数里调用waitpid来回收子进程避免在主循环里轮询void sigchld_handler(int signo) { int status; pid_t pid; while ((pid waitpid(-1, status, WNOHANG)) 0) { printf(回收了子进程 %d\n, pid); } } int main(void) { signal(SIGCHLD, sigchld_handler); // ... }注意在信号处理函数里要循环调用waitpid直到返回0或-1因为多个子进程可能在同一时刻退出而信号处理函数只能执行一次。5.4 在ElfBoard上实测僵尸进程的生成与清除我把刚才的程序稍做修改去掉wait调用让子进程退出后父进程继续休眠// fork_zombie_demo.c #include stdio.h #include unistd.h #include sys/types.h int main(void) { pid_t pid fork(); if (pid 0) { printf([子进程] PID%d, 我要退出了\n, getpid()); return 0; } else if (pid 0) { printf([父进程] PID%d, 我不等子进程, 先睡20秒\n, getpid()); sleep(20); printf([父进程] 睡醒了\n); } return 0; }在子进程退出后的20秒休眠期里在ElfBoard上执行ps -eo pid,ppid,stat,comm | grep fork_zombie输出中会看到类似PID PPID STAT COMMAND 1300 1229 S ./fork_zombie 1301 1300 Z ./fork_zombie defunct1301号进程状态为Z命令行标记为defunct这就是僵尸进程。20秒后父进程退出这个僵尸进程会被init进程回收ps里就看不到了。所以前面那个“ps里看到大量defunct进程”的问题根源不在子进程而在父进程没有履行回收义务。排查时不要被表象迷惑。6. 从fork到exec为什么嵌入式设备上最常用的是forkexec组合前面讲的fork创建出来的子进程是与父进程几乎相同的另一个进程。但在真实项目中我们通常希望子进程去执行一个全新的程序比如从主程序里启动另一个可执行文件。这时候用的模式是经典的fork exec。6.1 exec族函数让子进程“变性”exec族函数的核心作用是用一个新的程序替换当前进程的代码段、数据段、堆和栈而PID保持不变。也就是说子进程调用exec后它不再运行父进程复制过来的那段代码而是运行一个全新的程序。这个过程中最关键的点是PID不变所以对系统来说同一个进程的“身份”没有变。但它的代码、数据、打开的文件描述符带FD_CLOEXEC标志的除外等都被替换或重置了。exec族函数的常见成员有execl(path, arg0, arg1, ..., NULL)execv(path, argv[])execlp(file, arg0, arg1, ..., NULL)会在PATH中搜索execvp(file, argv[])为什么fork之后通常立刻exec因为fork出来的是一个“克隆体”如果你的本意是要运行一个完全不同的程序比如在应用程序里调用ping命令那么克隆出来的那一大堆父进程信息根本没用。先fork、再exec子进程瞬间变成一个全新的程序父进程则继续做自己的事。这种方式在嵌入式设备上非常普遍比如一个传感器采集程序需要定时调用某个处理脚本就可以用forkexec的方式启动脚本。6.2 在ElfBoard上调用系统命令并获取返回值在嵌入式Linux应用里有时需要在程序里执行一条shell命令并获取它的输出。很多人会用system(ls -l)但system函数有它的隐患它会先fork一个子进程再在子进程里执行/bin/sh -c 命令然后父进程阻塞等待它结束。如果命令本身出错或者shell环境有问题返回值的处理会比较麻烦。更可控的做法是用fork exec pipe// run_cmd.c #include stdio.h #include stdlib.h #include unistd.h #include sys/types.h #include sys/wait.h int run_system_cmd(const char *cmd) { int pipefd[2]; if (pipe(pipefd) -1) { perror(pipe); return -1; } pid_t pid fork(); if (pid 0) { perror(fork); return -1; } if (pid 0) { // 子进程把标准输出、标准错误重定向到管道的写端 dup2(pipefd[1], STDOUT_FILENO); dup2(pipefd[1], STDERR_FILENO); close(pipefd[0]); close(pipefd[1]); // 执行命令 execl(/bin/sh, sh, -c, cmd, NULL); perror(execl); exit(127); } // 父进程从管道读端读取输出 close(pipefd[1]); char buf[128]; ssize_t n; while ((n read(pipefd[0], buf, sizeof(buf) - 1)) 0) { buf[n] \0; fputs(buf, stdout); } close(pipefd[0]); int status; waitpid(pid, status, 0); if (WIFEXITED(status)) { return WEXITSTATUS(status); } return -1; } int main(void) { int ret run_system_cmd(cat /proc/cpuinfo | grep model name); printf(命令返回值: %d\n, ret); return 0; }这段代码的思路是子进程里用dup2把标准输出和标准错误都重定向到管道写端然后execl执行命令父进程从管道读端读取输出最后用waitpid回收子进程。这样就能在程序里拿到命令的完整输出并且可控性比system强得多。在ElfBoard上运行时如果命令输出里有中文终端可能需要设置UTF-8编码否则会显示乱码——这只是终端显示问题不影响实际读取。注意system()函数在执行时也会创建子进程但它本质上是“启动shell再执行命令”效率比直接fork exec低而且如果命令字符串来自外部输入还存在命令注入的安全风险。在嵌入式产品里尽量用fork exec而不是system。7. 高频问题排查实录我在ElfBoard上遇到的进程相关Bug7.1 案例一网络服务启动后端口被大量进程占用之前我调试一个基于socket的服务程序在板上反复启动、停止、再启动最后发现netstat -ap里有一堆监听同一个端口的进程而且父进程都不在了全部是孤儿进程。每次启动时新程序都会尝试绑定端口结果提示Address already in use。排查过程很简单先看进程树发现上一次启动的服务程序在退出时没有正确杀掉它fork出来的子进程导致那些子进程变成了孤儿进程并在继续运行。孤儿进程没有被init接管之后自动退出因为它们的业务逻辑本身就是常驻循环。最终处理方式是在主进程退出前给所有子进程发SIGTERM信号并统一回收。这个案例提醒我嵌入式程序里的进程生命周期管理不只是创建还包括退出清理。主进程退出前所有子进程必须是你亲手处理过的要么在业务上让它们自行退出要么显式向它们发送信号。千万不要指望设备重启来解决所有问题。7.2 案例二多进程写同一个文件日志内容错乱这个问题我在文章开头提到了。程序启动后通过fork创建了3个子进程每个子进程都在写同一个日志文件。最初用的是简单的fprintf测试时一切正常但在高频率写日志时日志行的内容会交错、残缺。原因在于多个进程对同一个文件描述符的写入操作并不是原子的。即使你打开了同一个文件各自的文件偏移量是独立的在fork时共享的文件偏移量在出现写入后也可能因为缓冲机制而错位。解决方案是在写入时用fcntl加文件锁F_SETLKW让每次写入成为一个临界区。或者让子进程把日志通过网络发送给一个专门的日志进程由唯一的进程负责写文件。或者在打开文件时使用O_APPEND标志这虽然能保证写入操作追加到文件末尾的原子性但无法保证单次fprintf的完整内容不被打断需要配合pwrite等有原子性的写操作或加锁。在嵌入式环境里最简单的做法是加锁。性能损失可以接受因为日志频率通常不会特别高。7.3 案例三内存泄漏排查用进程退出码定位崩溃点有一次在ElfBoard上运行主程序时每隔几小时就会出现一次进程异常退出但主程序没有打印任何错误日志。排查时我用了两个工具第一步在启动主程序的外层脚本里记录进程退出码./main_program echo exit with $?如果程序是被信号杀死的shell会打印类似exit with 143这样的值——143等于128加15意味着程序收到SIGTERM信号。如果是段错误会得到139128加11。第二步检查dmesg内核日志。嵌入式板子上内核会记录被OOM Killer杀掉的进程信息dmesg | grep -i oom最终发现是主程序fork出来的某个子进程不断申请内存最终触发了OOM Killer内核把整个进程族里内存占用较大的那个给杀了。这给了我一个结构性上的反思子进程的内存占用必须被监控否则一个错误的子进程可能拖着整个系统陪葬。这些问题的共同点是它们都不是“逻辑错误”那种一眼就能看出来的问题而是进程生命周期管理不当在长时间运行后暴露出来的系统性问题。嵌入式设备往往7x24小时运行任何进程泄漏或者回收不当都会在几天后变成一次故障。8. 总结一段实操心得不是套话是真话每次在ElfBoard上写多进程程序我都会先问自己三个问题这个进程必须fork吗子进程的生命周期和父进程是什么关系父进程退出时子进程由谁回收只要这三个问题想清楚了绝大多数进程相关的Bug都不会发生。我个人在实际调试中最依赖的工具组合是ps -ef --forest看进程树、cat /proc/pid/status看进程资源占用、dmesg看内核崩溃与OOM记录。这三个命令能覆盖嵌入式开发里九成以上的进程诊断需求。如果你正在学嵌入式Linux或者已经在ElfBoard上开发应用建议你花半天时间把上面那个fork示例程序亲手写一遍、编译一遍、运行一遍然后故意去掉wait制造几个僵尸进程再用ps观察它们。这个过程看起来简单但它的价值在于建立对进程机制的直觉。书上的图再清晰也不如你自己在串口终端里看到一次真实的Z状态进程来得深刻。搞清楚父进程和子进程你的嵌入式Linux开发能力算是真正迈过了一道坎。
返回列表