最近在折腾飞凌嵌入式ElfBoard这块板子,跑Linux调进程时碰到一堆和父进程、子进程相关的乱七八糟问题。嵌入式开发里进程这个概念太关键了,尤其当你开始用fork创建子进程,或者发现系统里多了个僵尸进程却不知道是谁搞出来的时候,真的会被绕晕。这篇文章我就结合在ElfBoard上的实际调试经历,把父进程和子进程这块彻底理一遍,从原理到实操到排错,一次讲透。
1. 先理清父子进程的关系,开发板上怎么看
1.1 进程为什么会有"爸爸"和"儿子"
进程不是一个孤立的概念。你写的每个程序跑到内存里,一定是由某个地方拉起来的,拉起它的那个进程就是父进程(Parent Process),被拉起来的这个就是子进程(Child Process)。这两个概念不是随便叫叫的,系统内核在创建进程的时候,真的会把父子关系记录在PCB(进程控制块)里,通过PPID字段指向父进程的PID。
刚开始在PC上玩Linux可能没太大感觉,但在嵌入式板子上,资源紧张、系统精简,进程之间的关系一旦乱了,排查起来要比服务器上麻烦得多。比如你在ElfBoard上把一个串口调试程序做成开机自启,它是由哪个进程拉起来的,这个“爸爸”的身份直接决定了程序死了之后由谁来收拾残局。如果父进程先挂了,子进程就成了孤儿,会被PID为1的init进程收养,这就要涉及孤儿进程的底层逻辑了。
每个进程都有唯一的标识符PID,同时还有一个PPID。PID是在内核分配进程描述符时从pid_max范围内递增分配的,PPID则是从父进程的task_struct里直接拷贝过来的。在/proc文件系统里,每个进程都对应一个以PID命名的目录,你看一眼/proc/1234/status里的PPid字段,就知道它的爸爸是谁了。
1.2 从命令行到进程树,一条完整的血缘链
你在ElfBoard上敲一个命令,比如ls或者top,shell(一般就是bash或者busybox的ash)会先fork一个子进程,然后在这个子进程里exec加载ls的镜像。也就是说,每次你运行命令,命令行解释器都在扮演“父亲”的角色。而且这个关系是层层传递的,init进程是系统启动后第一个用户态进程,所有用户进程最终都是它的子孙,整棵进程树根扎在init身上。
用pstree可以直观看到这棵树的形状。在ElfBoard系统的终端里敲pstree,会看到一坨嵌套的进程关系,树的顶层是PID 1,下面是各个服务。如果你写了个小程序叫test_process,运行后再开一个软件终端敲ps,就能看到bash作为父进程,下面挂着test_process,这个子进程的PPID正好指向bash的PID。
这里有一个特别要提醒的点:同一个父进程可以创建多个子进程,但一个子进程同时只有一个父进程。父进程退出后,子进程不会被一起杀掉,而是被“过继”给最近的subreaper,如果没有subreaper就给init。这是很多刚接触嵌入式Linux的人第一反应错的点,以为父进程退了子进程也跟着退了,实际上完全不是这样,父子进程是独立调度的两个实体。
1.3 在ElfBoard上快速定位进程的爸爸是谁
实际操作中,最常用的命令就是ps,而且要用带ef参数的看。在ElfBoard上装的是busybox的话,ps -ef可能不支持完整格式,这时候可以试试ps -aux或者直接用cat /proc/进程号/status。
比如你想查一个叫app_demo的进程是谁拉起的,先用pidof或者ps找到它的PID,然后执行cat /proc/PID/status,在输出里找PPid这一行,再拿这个PPid去ps里反查,就能顺藤摸瓜找到它的父进程。我在板子上调试一个GUI程序时,就发现它居然是被某个服务脚本拉起来的,而不是我自己手动跑的那个进程,这就解释了为什么程序退了又被自动拉起来,因为服务脚本里有while循环加重启逻辑。
ps -ef输出里的第一列UID、第二列PID、第三列PPID,马上就能看出来进程的家谱。看惯了之后,几乎不用进/proc,一眼就能判断异常的进程是谁生成的。这个技能在嵌入式现场非常实用,你不可能在客户的设备上装gdb慢慢调试,但ps和/proc是肯定有的。
2. fork才是子进程的真正来源
2.1 为什么嵌入式开发绕不开fork
Linux里创建子进程最根本的方式是fork系统调用,内核复制一份当前进程的描述符和内存映射,产生一个和父进程几乎一模一样的子进程。嵌入式开发里你躲不开fork,原因很简单:很多守护进程和服务框架就是用fork去派生工作进程的,比如常见的热插拔管理、网络服务监听进程,都是父进程监听端口,每来一个连接就fork一个子进程去处理。
还有一类情况是你要跑多个任务,但不想用线程,因为线程共享地址空间,一个线程把内存写坏了,整个进程都崩。用子进程隔离更安全,子进程崩了父进程还能继续跑。这个思路在我们做嵌入式软件升级时特别常用:升级程序跑在子进程里,就算升级过程中段错误了,主程序还活着,能给你留一条恢复的后路。
ElfBoard这块板子是跑Linux的,用的Linux内核本身就定义死了:进程一定是用fork(或者它的变体clone)创建的。你就算什么都不做,开机后系统初始化时也是在内核里复刻出了一个又一个进程。理解了fork,你才能真正理解Linux的进程模型,而不是停留在“进程就是运行中的程序”这种表面认知上。
2.2 fork返回值的玄机:一个函数返回两个PID
fork的返回值设计是学习进程编程时最容易犯迷糊的地方。一个fork调用,在主流程里看起来只调用了一次,但调用返回后,父子两个进程各得到一次返回值。父进程得到的返回值是子进程的PID,子进程得到的返回值是0,如果失败则返回-1。
这个设计第一次接触确实很反直觉,但它的用途非常清晰:通过判断返回值,同一个函数里就能让父子进程走不同的代码分支。返回值是全系统范围内唯一标识子进程的PID,父进程后面要wait子进程全靠它;返回值0则表示“当前在子进程里”,子进程不知道自己的PID也没关系,可以用getpid()现查。
我在ElfBoard上写第一个fork测试的时候,代码是这样:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> int main(void) { pid_t pid = fork(); if (pid < 0) { perror("fork failed"); return 1; } else if (pid == 0) { printf("我是子进程, PID=%d, 父进程PPID=%d\n", getpid(), getppid()); } else { printf("我是父进程, PID=%d, 子进程PID=%d\n", getpid(), pid); } return 0; }编译后放到ElfBoard上运行,你会第一次直观感受到:明明是写在一个程序里的代码,却有两个进程各执行了一遍,而且printf打印出来的内容会有两个不同的PID。这里有坑:printf是标准库带的缓冲IO,在终端场景下通常是行缓冲,遇到换行会flush,但如果你的程序把输出重定向到文件,或者用system()包了一层,没加换行的时候缓冲区会被父子进程各拷一份,结果一条字符串被打印两次。这是fork带来的经典双写问题,后续我会专门再讲。
2.3 说好的“复制”其实是写时复制
fork的语义是复制父进程的整个地址空间,但内核不可能在fork时真的把所有内存全部拷贝一份,那样又慢又浪费。Linux用的是写时复制(Copy-On-Write, COW):fork时父子进程共享物理内存页,页表都标记为只读,谁先写入,谁才触发缺页异常,内核把这一页真正复制一份后再写入。
这个机制对嵌入式系统意义重大。板子内存本身就紧张,假设一个父进程占了80MB内存,如果fork上来就完整复制,瞬间要160MB,很可能直接OOM。有了COW,fork只是复制了页表和task_struct,开销小得多。但要注意,这也意味着你在子进程里大面积写内存的时候,内存占用会真实涨起来,在ElfBoard这种内存可能只有256MB的板子上,写很多新数据时记得留意free的输出。
还有一个嵌入式里常被忽略的点:fork后子进程继承了很多父进程的资源,比如打开的文件描述符、信号处理函数、环境变量。这些不是空的,它们会被拷贝或者引用计数增加。如果父进程打开了串口设备、socket套接字,fork出的子进程手里也有同一份引用,那么在子进程里关掉描述符并不会真正释放资源,因为父进程还握着。这一点在做串口通信程序时很致命,父进程监听串口,fork了子进程去处理数据,子进程退出时把串口fd关了,你以为关掉了,其实父进程还占着,导致设备被占用。
3. 在ElfBoard上实操:亲手创建一个子进程
3.1 最小demo:用fork创建子进程并观察运行顺序
前面说的理论比较干,还是上手见真章。我在ElfBoard上做了一组小实验,Pi板子是通过串口终端连上去的,Linux系统起来之后先写一个最简单的fork程序,编译完扔进可执行文件里跑。
代码很简单,就是2.2节里那个demo。但这次我在fork前后加了几个计时的打印,观察调度顺序。结果很有意思:大部分时候父进程先打印,子进程后打印,但偶尔子进程会先跑。原因在于fork返回后,两个进程都处于就绪态,CPU调度器按优先级和时间片来调度,谁先拿到CPU谁先执行,并不保证父进程一定优先。这个“不确定顺序”的特性在编写依赖时序的程序时是最容易埋雷的,两个进程如果同时在写同一个文件,先后顺序乱,结果就会错。
我在板子上反复跑了20多次,统计下来父进程先打印的次数略多,但这不是规律,只是宿主的调度器行为。如果你的程序逻辑强依赖父子进程执行的先后顺序,需要显式加同步机制,比如用wait让父进程等子进程退出,或者用管道、信号做同步。绝对不要指望fork之后天然有个先后顺序。
3.2 如何区分父子进程并各干各的活
实际写业务代码时,fork之后干的第一件事就是处理返回值,然后各干各的。我经常在板子上写一种模式:父进程负责监听外部事件,比如等待按键中断或者网络连接,子进程负责去做耗时操作,比如写Flash或者处理图片数据。
这里涉及一个选择:用fork还是用线程。我的经验是,如果任务之间要共享大量数据,用线程加锁效率高;如果任务要完全隔离,想安全第一,用子进程。在嵌入式里跑第三方解码库,我宁可放子进程里,一来库内部可能申请很多内存,二来一旦库崩溃导致段错误,子进程挂了父进程还能捕获SIGCHLD信号并重启它。
各干各的活有一个要注意的细节:父子进程的内存是独立的副本(COW之后的),父进程修改一个全局变量,子进程完全看不到。很多新手从线程模型切换过来,下意识以为父进程改了变量子进程能读到,结果数据对不上。要在父子进程间传递数据,得用进程间通信(IPC),最轻量的就是管道pipe。我在ElfBoard的串口通信程序里就是父进程读串口数据,通过管道发给子进程,子进程解析协议并转发,数据单向流动,清爽不打架。
3.3 僵尸进程是怎么出现的,如何用wait回收
fork子进程之后,子进程退出,内核并不会马上把它完全清理掉。内核需要保留子进程的退出状态信息,比如退出码和资源使用统计,供它的父进程查询。这个“已经死了但还没被父进程收走尸体”的进程状态就是僵尸状态。进程信息里显示为Z(zombie),用ps能看到。
我在ElfBoard上做了个实验:子进程直接exit(42),父进程在fork之后用一个sleep(60)挂住,这时候在另一个终端里ps aux,就能看到一个PID很小的僵尸进程。父进程不调用wait或者waitpid,僵尸就一直在那占着进程表项。僵尸进程不占CPU不占内存,但它占用一个PID,而系统PID总数有限(pid_max默认值是32768),僵尸多了,PID被耗尽,新进程就fork不出来了。
正确做法是父进程调用waitpid。最简单实用的是阻塞等待:
int status; pid_t pid = waitpid(child_pid, &status, 0); if (WIFEXITED(status)) { printf("子进程退出码: %d\n", WEXITSTATUS(status)); }这样才能把子进程的残留信息回收掉。如果父进程不想阻塞等待,可以在代码里给SIGCHLD信号挂一个处理函数,或者用WNOHANG参数轮询。在ElfBoard上的服务程序里,我用的就是非阻塞的waitpid搭配轮询,不拖慢主流程。需要注意,waitpid的pid参数传-1表示等待任意子进程,传具体PID则精确等待某个子进程,这在有多个子进程的场景里特别关键,避免把别的子进程的退出状态误收了。
4. 嵌入式多进程的经典坑:孤儿态、僵尸态与退出码
4.1 init收养:孤儿进程为什么不会失控
父进程先于子进程退出,子进程就变成孤儿进程。孤儿进程不会永远没了依靠,内核会自动让PID 1的init进程(现在很多嵌入式系统里是busybox init)成为它的新父进程。子进程结束时,init会负责回收它的状态信息,所以孤儿进程一般不会变成僵尸。
但这个机制也有副作用,我在板子上写串口程序时碰到过:父进程意外退出,子进程还在跑,并且子进程里还在访问父进程留下来的管道或者socket。父进程的fd在子进程里仍然有效,但另一端已经没人读取了,子进程在写管道时会触发SIGPIPE信号,默认动作是终止进程。这算好的,如果子进程继续持有某个锁文件,父进程退出了锁没释放,下次启动新实例时就会发现资源被占。用flock锁的时候,如果持锁进程的父进程退出但子进程还活着,那锁依然被持有,因为锁是跟着文件描述符走的,子进程继承了它。
所以设计嵌入式程序时,最好明确:如果你不希望某个任务随父进程的崩溃而终止,那就让它成为孤儿,交给init去管;如果你希望它跟着父进程走,要用进程组和信号来管理,比如prctl(PR_SET_PDEATHSIG)设置父进程死亡时子进程收到某信号。这个接口在嵌入式Linux里很实用,能实现“父进程挂了子进程也自杀”的效果,避免一堆残留进程。
4.2 僵尸进程的危害与三种清理方式
僵尸进程不是bug,它是正常的进程生命周期阶段,但长期大量堆积就是问题了。在ElfBoard这种长期开机的设备上,如果某个服务每次处理完任务fork出的子进程都成了僵尸,几百上千次之后,进程表就会被占满,到时候ssh都连不进来,ps命令都跑不动。
三种清理方式我逐个试过。第一种,用wait/waitpid在父进程里主动回收,这个是最推荐的,代码层面的根本解法。第二种,如果父进程写了SIGCHLD信号处理函数,并且在函数里调用waitpid(-1, &status, WNOHANG),那么即使信号乱序到达,也能把所有退出的子进程都清理掉。注意在信号处理函数里只能调用异步信号安全函数,waitpid是安全的,printf不安全。
第三种,实在找不到是谁的父进程,就用kill把还在运行的父进程干掉,系统在父进程退出后会重新挂接子进程到init,init会自动回收。这个方法简单粗暴,但可能会影响其他功能。在嵌入式现场如果出现大量僵尸,我一般先跑一个ps -eo ppid,pid,stat,comm查看僵尸的PPID,然后定位到具体父进程,查它为什么不回收,而不是盲目kill。
4.3 从d2l安装失败的报错看子进程退出码的意义
这个话题不止一次被群里人问起。有人在装d2l(深度学习动手学代码库)时,pip install d2l报错,提示subprocess-exited-with-error。他不是嵌入式场景,但这背后正是父子进程的概念:pip是父进程,为了执行setup.py里的构建逻辑,会拉起一个子进程(通过subprocess模块),子进程退出时返回了一个非零的退出码,pip就认为构建失败,终止安装并报错。
这个错误几乎和进程模型紧密相关。子进程退出码是0表示成功,非0表示失败。你用os.system或者system()时,返回值的低位是信号编号,高位是退出码,要取WEXITSTATUS才能拿到真正的退出码。Python的subprocess模块比较友好,直接抛CalledProcessError,里面就有returncode。
排查这类问题的思路是:先看父进程到底执行了什么命令,pip的报错信息里通常会带出完整命令行;然后在板子或者本地手动跑一遍这个命令,看子进程直接失败的真正原因。我帮人排查过几次,大部分情况是缺依赖库,或者是编译环境中没有找到gcc,还有的是下载依赖时的网络原因。本质上,子进程退出码只是一个结果信号,背后原因还需要看日志和stderr。在嵌入式板子上装Python包时要特别留意,ARM架构下很多包没有预编译wheel,需要现场编译,这时如果板子上缺少编译工具链,子进程就会以非零码退出,看起来就像pip出bug了,实际是工具链的问题。
5. 真实场景里的“子进程”:CEF多进程架构与系统稳定
5.1 CEF为什么非要搞多进程
还有个常见热词是“CEF用自己的子进程”。CEF是Chromium Embedded Framework,很多嵌入式设备的HMI界面就是用CEF做的,ElfBoard这类平台性能足够,跑个CEF当UI层是很常见的方案。CEF默认是多进程架构,一个Browser主进程(父进程),下面挂Renderer子进程、GPU进程、Utility进程等等。
为什么非要搞多进程?因为浏览器引擎的渲染和解析本身就容易出错,如果全部塞进一个进程,页面一崩整个应用直接退出,在设备上就是屏幕黑掉或者界面消失,非常致命。多进程之后,一个渲染进程崩了,Browser进程还在,可以重新拉起新的Renderer,代价只是那个页面重新加载。这对嵌入式设备很有价值,毕竟设备通常7x24小时跑,不能动不动就弹崩溃框或者重启。
CEF的父进程就是Browser进程,它负责创建和管理各个子进程。子进程不是同时在同一个任务里fork出来的,而是按需创建,有渲染任务才拉起Renderer,一开始空页面的时候可能只有一个GPU进程。在ElfBoard上看到一堆cef的子进程,机器负载还高时,多半是Render进程一直在CPU跑,要用top确认一下是哪个具体子进程在吃资源。
5.2 子进程崩溃后的处理策略
CEF有自己的崩溃恢复机制,Browser进程收到子进程的退出状态后会尝试重建,但也不是万能的。我在板子上调CEF应用时,遇到渲染进程反复崩溃的情况,直观表现是界面闪一下又恢复。这种行为背后是父进程在检测到子进程异常退出后,根据配置决定要不要重启、要不要抛出异常事件。
对我自己做嵌入式应用的建议是:如果你自己用fork创建子进程来执行危险任务,一定要设计好重启策略。我的做法是维护一个简单计数器,子进程在短时间内连续崩溃超过一定次数(比如5次),就不再无限重启,而是记录日志并进入降级模式,避免变成一个死循环拉进程的“自杀式恢复”,让系统卡死。这一点在资源有限的板子上特别重要,无限重启一个内存占用大的子进程,最终会触发OOM。
另外,子进程退出时如果有未写完的日志或者未落盘的数据,父进程要负责做善后处理。比如子进程操作Flash中途退出,可能会导致数据半写状态,父进程收到SIGCHLD后要检查标志位,决定是否回滚。这是我在做掉电安全相关的更新程序时踩过坑总结出来的:fork子进程之前,先规划好崩溃后由谁来做数据一致性的补偿动作。
5.3 排查进程问题时的三板斧
最后把我在ElfBoard上排查进程问题最常用的三板斧总结一下,都是不用装额外工具就能用的。
第一板斧是ps配合/proc。ps -eo pid,ppid,stat,comm看全局,cat /proc/PID/status深入单个进程。你判断一个进程是不是僵尸,看stat列的通配符最直观,Z代表zombie,R是running,S是sleeping,D是uninterruptible sleep。
第二板斧是top的CPU占用排序。嵌入式设备上出现卡顿,经常是某个子进程在无限循环跑满CPU,top里按P键按CPU排序,马上就能揪出来。如果是多核平台,记得top里按1看每个CPU的负载分布,有些进程会绑核运行,看起来整体负载不高,但某一核已经被占满了。
第三板斧是strace。板子上如果空间允许,busybox里没有strace可以自己交叉编译一个放进去。strace -p PID可以实时跟踪进程的系统调用,能看到进程卡在哪个系统调用上。排查子进程假死特别有效,可能它阻塞在一个read上,或者等待网络socket的数据,一跟踪就明白了。我遇到过子进程占着CPU但啥也不干的情况,strace一看,它在一个nanosleep循环里自旋等待,完全是我自己逻辑写的有问题,加了个mutex但忘了释放,导致子进程一直在等锁。
这些工具和方法都不依赖图形界面,串口终端就能操作。嵌入式调试环境往往就是这么朴素,但足够解决90%的进程问题。
6. 进程编程里那些文档不会写的实战细节
6.1 文件描述符继承是最容易被忽略的隐形炸弹
fork之后子进程会继承父进程所有打开的文件描述符,包括socket、串口fd、普通文件fd。继承意味着引用计数增加,父进程关闭fd不会让底层文件对象释放,因为它还被子进程引用着。
我在ElfBoard上写一个网络服务程序时,父进程bind了端口并listen,接着fork了子进程。子进程退出前习惯性调用了close(sockfd),结果发现端口根本没有被释放,还在被占用。后来才意识到,父进程手里还保持着同一个socket fd的引用,listen还在继续。正确做法是在fork之后,父子进程各自关闭自己不需要的fd,父进程保留listen fd,子进程关闭listen fd再去做自己的事。
嵌入式场景里还有一个点容易忽略:串口fd被继承后,两个进程同时去读同一个串口,会竞争数据。如果你fork了一个子进程,又没有明确谁负责读串口,数据会被两个进程随机读取,协议直接乱掉。我的建议是:进程创建后第一时间明确fd归属,不需要的fd在子进程入口处统一关闭,这是一种代码习惯,能省掉后面一大半莫名其妙的bug。
6.2 内存占用与OOM风险的评估
COW机制下fork本身不占多少物理内存,但子进程一旦开始大量写操作,内存占用会逐渐分离出来。在ElfBoard这种内存有限的板子上,开启一个进程前先估算它的内存开销非常有必要。
我的做法是:在程序启动时记录/proc/meminfo里的MemAvailable,一段时间后反复对比,看内存是否持续下降。如果子进程里加载了比较大的静态数据或者图资文件,父进程的页缓存可能不算进程占用,但子进程写入了大量数据就会真实计入。使用malloc分配但不写时,内存不会立刻全部分配,只有写的时候才消耗物理页,这个理解COW后就会很清楚。
如果板子内存较小,还有一个建议:用vfork代替fork,在某些极端情况下更快,但vfork的语义是子进程先运行且共享父进程地址空间直到exec或exit,使用要特别小心,子进程里不能乱改父进程的变量。现在的Linux里vfork基本是fork的优化变体,但我在嵌入式系统上依然会用vfork来做简单的任务拉起,因为它避免了复制页表的过程,适合那种fork后马上exec的场景。不过用vfork时一旦子进程有往stdout打印缓冲区的操作,很容易出问题,建议优先用标准fork,更稳健。
6.3 进程上限与PID耗尽问题
嵌入式设备上pid_max默认值一般是32768,也就是说系统同时最多只有32768个PID可用。频繁fork子进程并且不回收,PID分配会逐渐攀升,最终出新进程失败“Resource temporarily unavailable”。
这个问题的典型场景就是前面说的僵尸进程积累。还有一种情况是程序里循环fork,每次都等子进程退出,但父进程忘了waitpid,僵尸堆积。在长期运行的设备上,这种bug可能要跑几个星期才暴露,但在实验室里很难发现,因为测试时间根本不够。我的建议是在代码里加入子进程数量的监控,通过读取/proc/loadavg或者sysconf(_SC_CHILD_MAX),在逼近阈值时打日志。
另外,Linux里每个用户都有进程数限制,通过ulimit -u查看。嵌入式系统中往往只用一个root用户,root默认不受限制,但容器或者安全配置可能会加上限制。如果你在板子上跑Docker或者使用systemd服务,注意LimitNPROC的设置,别让默认限制卡死你的服务。
我在实际中碰到的比较隐蔽的问题是systemd服务长时间运行后,重启服务时提示失败,查日志发现是子进程未被正确回收导致服务启动时PID文件被锁。这个问题表面上是锁文件冲突,底层就是僵尸进程没清理干净。遇到这类问题,先清理系统里的僵尸进程,再看看服务代码里的wait逻辑,多半是父进程在初始化阶段没设置SIGCHLD的回收。
7. 最后的一点实践经验
父亲进程和子进程的概念,在嵌入式Linux里可以说是基础中的基础,但你真正把它用到极致,需要一次次的实战踩坑。我在ElfBoard上从最开始的fork打印,到后来写串口服务、CEF界面集成、远程升级程序,几乎每个环节都会遇到父子进程的衍生问题,说实在的,Linux内核里这一套进程模型设计得已经很优雅了,但用得好不好,全看开发者的理解和习惯。
如果让我给刚开始接触嵌入式Linux进程编程的朋友一个建议,那就是:动笔写fork之前,先想清楚三个问题。第一,子进程的目的是什么,独立任务还是协同处理;第二,父进程怎么感知子进程的退出,是用wait阻塞还是用SIGCHLD;第三,子进程出问题之后,父进程要不要恢复它,恢复的边界条件是什么。这三个问题想清楚了,进程代码基本不会出大乱子。
另外,在嵌入式板子上一定要多看系统的原始信息,不要只看自己程序的调试输出。ps、/proc、dmesg,这些免费的观察窗口,往往能一眼看出问题本质。比如子进程崩溃了,dmesg里可能会有segment fault的提示,ps里能看到退出状态,这些比你在代码里printf一万句都管用。做嵌入式就是这么朴素,有时候一个ps命令,比上gdb调试半天还来得快。