【Linux】十一.进程概念--进程的控制

一.进程创建

1-1 fork函数初识

在 linux 中 fork 函数是⾮常重要的函数,它从已存在进程中创建⼀个新进程。新进程为⼦进程,⽽原进程为⽗进程。

#include <unistd.h> pid_t fork(void); 返回值:⼦进程中返回0,⽗进程返回⼦进程id,出错返回-1

进程调⽤ fork ,当控制转移到内核中的 fork 代码后,内核做:

  • 分配新的内存块和内核数据结构给⼦进程
  • 将⽗进程部分数据结构内容拷⻉⾄⼦进程
  • 添加⼦进程到系统进程列表当中
  • fork 返回,开始调度器调度

当⼀个进程调⽤fork之后,就有两个⼆进制代码相同的进程。⽽且它们都运⾏到相同的地⽅。但每个进程都将可以开始它们⾃⼰的旅程,看如下程序。

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; } 运⾏结果: Before: pid is 43676 After:pid is 43676, fork return 43677 After:pid is 43677, fork return 0

这⾥看到了三⾏输出,⼀⾏before,两⾏after。进程43676先打印before消息,然后它有打印after。另⼀个after消息有43677打印的。注意到进程43677没有打印before,原因如下图所⽰.

所以,fork之前⽗进程独⽴执⾏,fork之后,⽗⼦两个执⾏流分别执⾏。注意,fork之后,谁先执⾏完全由调度器决定。

1-2 fork 的常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。

举例:父进程等待客户端请求,生成子进程来处理请求。

  • 一个进程要执行一个不同的程序。

举例:子进程从 fork 返回后,调用 exec 函数。

fork 调用失败的原因:

  • 系统中有太多的进程,系统资源不足。
  • 实际用户的进程数超过了限制。

1-2 fork函数返回值

  • ⼦进程返回0
  • ⽗进程返回的是⼦进程的pid。

当一个进程调用 fork 函数之后(在不写入的情况下)用户的代码和数据是两个进程共享的。就有两个二进制代码相同的进程。而且它们都调度到相同的地方。之后每个进程都将可以开始它们自己的运行之路。fork之前父进程执行,fork之后父子进程同时执行

#include<stdio.h> // perror #include<unistd.h> // getpid, getppid, fork int main() { pid_t ret = fork(); // 返回时发生写时拷贝 if (ret == 0) { // 子进程 while (1) { printf("child process, pid:%u, ppid:%u\n", getpid(), getppid()); sleep(1); } } else if (ret > 0) { // 父进程 while (1) { printf("father process, pid:%u, ppid:%u\n", getpid(), getppid()); sleep(1); } } else { // failure perror("fork"); } return 0; }

注意:fork 之后谁先执行完全由调度器决定。


请问:

为什么 fork 有两个返回值,从而使父子进程进入不同的业务逻辑;为什么 fork 的返回值会返回两次呢?

fork 函数中的 return 语句是被父子进程共享的,所以都会被父子进程执行。当 fork 返回时,会往变量 ret 中写入数据(如:pid_t ret = fork(); ),发生了写时拷贝,导致 ret 有两份,分别被父子进程私有。(代码共享,数据各自承担)

返回值 ret 变量名相同,为什么会有两个不同的值呢?

变量名相同,有两个不同的值,本质是虚拟地址通过页表被映射到了不同的物理地址处。


1-3 写时拷贝

工作过程:

  1. 父进程创建子进程时,不复制父进程的内存数据,而是让子进程共享父进程的同一份物理内存。

  2. 操作系统把这些共享的内存页标记为只读

  3. 如果父子进程都只是数据,就继续共享,没问题。

  4. 一旦有某个进程(比如子进程)要数据,CPU会触发缺页异常

  5. 操作系统捕获异常后,才真正复制一份内存页给这个进程,然后允许它修改。

好处:

  • fork()变得非常快(不用复制大量内存)。

  • 省内存(很多进程可以共享只读的代码段、常量等)。

1. 修改前后父子进程的物理内存共享情况?

修改前:代码段、数据段的物理页完全共享。

修改后:代码段仍共享,数据段因修改触发复制,各自拥有独立的物理页。

2.页表项标记为只读的作用?
答:用于检测写操作,当进程尝试修改只读页时,触发缺页异常,操作系统执行写时复制操作。
展示的是写时复制机制,修改前父子进程共享物理内存页,修改后数据段页被复制为独立物理页,代码段页继续共享。

写时拷贝的策略:

  • 为了保证父子进程的独立性!(数据各自私有一份)。
  • 不是所有的数据,都有必要被拷贝一份(比如只读的数据)。写时拷贝可以节约资源。
  • fork 时,如果把所有的数据都拷贝一份,是需要花费时间的,降低了效率。写时拷贝可以提高 fork 执行的效率。
  • fork 创建子进程本身就是向操作系统要资源,如果把所有的数据都拷贝一份,要更多的资源,更容易导致 fork 失败。写时拷贝可以减少 fork 失败的概率。

总结Linux 中的fork()创建子进程后,通常马上调用exec()加载新程序。有了COW,fork()时根本不用复制父进程内存,等到exec()时直接更换地址空间即可,效率极高。

因为有写时拷⻉技术的存在,所以⽗⼦进程得以彻底分离离!完成了进程独⽴性的技术保证!


二.进程终⽌

进程终⽌本质释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。

2-1main 函数的返回值

我们在写 C/C++ 代码时,main 函数里面我们总是会返回 0,举例如下:

#include <stdio.h> int main() { printf("hello world\n"); return 0; }

但这是什么原因呢?

  1. main 函数中的这个返回值叫做:进程退出码,用来表示进程退出时,其执行结果是否正确。
  2. 返回的 0 是给操作系统看的,来确认进程的执行结果是否正确。(0 通常表示成功)

echo $?:最后一个进程的退出码


2-2 进程退出场景

  • 代码运⾏完毕,结果正确(退出码:0
  • 代码运⾏完毕,结果不正确(一般是代码逻辑有问题,但没有导致程序崩溃,退出码:非0)
  • 代码没有跑完,代码异常终⽌(这种情况下,退出码已经没有意义了,是由信号来终止,比如 ctrl+c)

2-3 进程退出码


退出码(退出状态)可以告诉我们最后⼀次执⾏的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表⽰执⾏成功,没有问题。代码 1 或 0 以外的任何代码都被视为不成功。

  • 父进程创建子进程的目的是为了让子进程给我们完成任务,父进程需要通过子进程的退出码知道子进程把任务完成的怎么样。
  • 退出码可以人为的定义,也可以使用系统的错误码列表,程序如下:

    比如:C 语言库中提供一个接口,可以把错误码转换成对应的错误码描述,程序如下:

#include <stdio.h> #include <string.h> // strerror int main() { for (int i = 0; i < 20; i++) { printf("%d -- %s\n", i, strerror(i)); } return 0; }

运行结果:

ubuntu@VM-0-2-ubuntu:~/code/lesson2$ ./test 0->Success 1->Operation not permitted 2->No such file or directory 3->No such process 4->Interrupted system call 5->Input/output error 6->No such device or address 7->Argument list too long 8->Exec format error 9->Bad file descriptor 10->No child processes 11->Resource temporarily unavailable 12->Cannot allocate memory 13->Permission denied 14->Bad address 15->Block device required 16->Device or resource busy 17->File exists 18->Invalid cross-device link 19->No such device

2-4进程常退出⽅法

正常终⽌

正常终⽌(可以通过 echo $? 查看进程退出码):
1. 从main返回
2. 调⽤exit
3. _exit

4.return退出

  • 只有 main 函数中的return 表示的是终止进程非 main函数中的 return 不是终止进程,而是结束函数
  • 在任何函数中调用exit 函数,都表示直接终止该进程

库函数:exit

#include <stdlib.h> void exit(int status); // 终止正常进程 // 参数 status: 定义了进程的终止状态,父进程通过 wait 函数来获取该值

说明:虽然status是int,但是仅有低8位可以被⽗进程所⽤。所以_exit(-1)时,在终端执⾏$?发现返回值是255。

系统调用:_exit

#include <unistd.h> void _exit(int status); // 终止正在调用的进程

系统调用接口 _exit 的功能也是终止正在调用的进程,它和库函数 的exit 有什么区别呢?

  • exit:在进程退出的时候,会进行后续资源处理(比如刷新缓冲区)。
  • _exit:在进程退出的时候,不会进行后续资源处理,直接终止进程。

【补充】

exit最后也会调⽤_exit, 但在调⽤_exit之前,还做了其他⼯作:
1. 执⾏⽤⼾通过 atexit或on_exit定义的清理函数。
2. 关闭所有打开的流,所有的缓存数据均被写⼊
3. 调⽤_exit

异常退出:

ctrl + c,信号终⽌

return退出

return是⼀种更常⻅的退出进程⽅法。执⾏return n等同于执⾏exit(n),因为调⽤main的运⾏时函数会将main的返回值当做 exit的参数。


站在操作系统角度,如何理解进程终止?

进程终止时,内核做的三件事

1. 回收内核数据结构(如PCB)

  • 不是销毁,而是将PCB对象标记为“未使用”,放回数据结构池(如Linux的slab分配器管理)。

在内核空间中维护一个内存池,减少了用户频繁申请和释放空间的操作,提高了用户使用内存的效率,但每次从内存池中申请和使用一块空间时,还需要先对这块空间进行类型强转,再初始化 。现在有了这些 “数据结构池” ,比如:当创建新进程时,需要创建新的 PCB,不需要再从内存池中申请一块空间,进行类型强转并初始化,而是从 “ 数据结构池 ” 中直接获取一块不用的 PCB 覆盖初始化即可,减少了频繁申请和释放空间的过程,提高了使用内存的效率。这种内存分配机制在 Linux 中叫做 slab 分配器。

  • 好处:下次创建新进程时,直接从池中取一个PCB覆盖初始化,避免频繁向内存池申请/释放/强转/初始化,效率更高。

2. 回收程序代码和数据占用的内存

  • 不是清空,只是把物理内存页标记为“未使用”(空闲)。

  • 之后OS可以重新分配给其他进程。

3. 解除该进程的所有链接关系

  • 例如:从进程链表里摘除、从父进程的孩子列表里删除、关闭所有打开的文件描述符(减少文件引用计数)、释放信号量等IPC资源等。

总结:把占用的内核对象放回池子,占用的内存标记为空闲,同时从各种管理结构中摘掉它。”

这就好比图书馆退卡:不是把你的记录抹掉,而是在系统里标记“此卡已注销”,卡号回收给下一个人用;你借的书标记为“在馆”,同时从你名下解除借阅关系。slab分配器——进程复用PCB比新创建PCB要快得多,这正是现代操作系统高效的原因之一。


三.进程等待

3-1 进程等待必要性.

  • ⼦进程退出,⽗进程如果不管不顾,就可能造成‘僵⼫进程’的问题,进⽽造成内存泄漏。
  • 另外,进程⼀旦变成僵⼫状态,那就⼑枪不⼊,“杀⼈不眨眼”的kill -9 也⽆能为⼒,因为谁也没有办法杀死⼀个已经死去的进程。
  • 最后,⽗进程派给⼦进程的任务完成的如何,我们需要知道。如,⼦进程运⾏完成,结果对还是不对,或者是否正常退出。
  • ⽗进程通过进程等待的⽅式,回收⼦进程资源,获取⼦进程退出信息

总结:父进程通过进程等待的方式:回收子进程资源,防止内存泄漏获取子进程的退出信息


3-2 进程等待的⽅法

系统调用 wait,waitpid 等待任意一个子进程改变状态,子进程终止时,函数才会返回。(其实就是等待进程由 R/S状态变成 Z状态,然后父进程读取子进程的状态,操作系统回收子进程)

3-2-1 wait⽅法(wait 函数)

#include<sys/types.h> #include<sys/wait.h> pid_t wait(int* status); 返回值: 成功返回被等待进程pid,失败返回-1。 参数: 输出型参数,获取⼦进程退出状态,不关⼼则可以设置成为NULL

举例:等待一个子进程

#include <stdio.h> #include <stdlib.h> // exit #include <sys/types.h> // getpid, getppid #include <sys/wait.h> // wait #include <unistd.h> // fork, sleep, getpid, getppid int main() { pid_t id = fork(); if (id == 0) { // 子进程 int count = 5; while (count) { // 子进程运行5s printf("child is running: %ds, pid: %d, ppid: %d\n", count--, getpid(), getppid()); sleep(1); } printf("child quit...!\n"); exit(1); // 终止子进程 } else if (id > 0) { // 父进程 printf("father is waiting...\n"); pid_t ret = wait(NULL); // 等待子进程终止,不关心子进程退出状态 printf("father waits for success, pid: %d\n", ret); // 输出终止子进程的pid } else { // fork failure perror("fork"); return 1; // 退出码设为1,表示fork失败 } return 0; }

子进程运行期间,父进程一直在等待子进程,最后父进程返回的时子进程的id

举例:多个进程等待

#include <stdio.h> #include <stdlib.h> // exit #include <sys/types.h> // getpid, getppid #include <sys/wait.h> // wait #include <unistd.h> // fork, sleep, getpid, getppid int main() { for (int i = 0; i < 5; i++) // 创建5个子进程 { pid_t id = fork(); if (id == 0) { //子进程 int count = 5; while (count) { // 子进程运行5s printf("child is running: %ds, pid: %d, ppid: %d\n", count--, getpid(), getppid()); sleep(1); } printf("child quit!\n"); exit(0); // 终止子进程 } else if (id < 0) { // fork failure perror("fork"); return 1; } } sleep(10); // 休眠10s // 父进程等待 for (int i = 0; i < 5; i++) { printf("father is waiting...\n"); pid_t ret = wait(NULL); // 等待任意一个子进程终止,不关心子进程退出状态 printf("father waits for success, ret: %d\n", ret); // 输出终止子进程的id sleep(2); } printf("father quit!\n"); // 父进程退出 return 0; }

可以看到子进程退出后,因为父进程在休眠,没有进行进程等待子进程全部变成了僵尸进,随着父进程进行进程等待,5 个僵尸进程被操作系统一一回收。

总结:一般而言,我们在 fork 之后,是需要让父进程进行进程等待的。上述两个例子,父进程只是等待子进程终止,并没有关心子进程的退出状态。

3-2-2waitpid 函数

pid_ t waitpid(pid_t pid, int *status, int options);

有如下几种设置参数的方式。

  • a. pid:

pid = -1,等待任意一个子进程,与 wait 等效。
pid > 0,等待其进程 ID 与 pid 相等的子进程,即传入进程 ID,等待指定的子进程。
思考下,fork 函数在父进程中返回子进程的 ID,是为什么呢?

为了方便父进程等待指定的子进程。


  • status是一个输出型参数

status是一个指向整型的指针,用于接收子进程的退出状态信息。当父进调用waitwaitpid时,内核会将子进程的终止信息写入status指向的内存中。如果父进程不关心子进程的退出状态,可以传入NULL

通过宏函数解析status

status中编码了多种信息(退出码、终止信号等),不能直接解读,必须使用专门的宏函数来提取:

WIFEXITED(status):判断子进程是否正常终止(即调用exit或从main返回)。如果是,返回true(非零)。

WEXITSTATUS(status)前提是WIFEXITED返回真,该宏会提取子进程传递给exitreturn退出码(低8位有效)。


  • c. options:

0—— 阻塞式等待

  • 当第三个参数设为0时,waitpid采用阻塞等待模式。

  • 此时,如果pid指定的子进程尚未结束,父进程会一直挂起在该调用上,直到该子进程终止才会返回。

  • wait等效wait(&status)完全等价于waitpid(-1, &status, 0)

    -1表示等待任意一个子进程,0表示阻塞等待。

WNOHANG—— 非阻塞等待

  • WNOHANG是 "WaitNoHanging" 的缩写,意为"不挂起"。
  • 采用非阻塞模式:如果pid指定的子进程还没有结束waitpid不会等待,而是立即返回 0

  • 返回 0 意味着:这一次等待失败了,子进程仍在运行,需要父进程稍后再次调用waitpid去轮询检查。

  • 如果子进程已经正常结束,则返回该子进程的pid,表示等待成功。

总结:第三个参数为0= 阻塞等待(不结束不返回);WNOHANG= 非阻塞轮询(没结束就返回 0,结束则返回 pid)。wait(&status)waitpid(-1, &status, 0)的简写形式。


  • 返回值:

成功时,返回状态已更改的子进程 ID,
如果参数 options 指定了 WNOHANG(非阻塞等待),并且存在一个或多个由参数 pid 指定的子进程,尚未更改状态,则返回 0,轮询检测。
出错时,返回 -1。

总结:waitpid成功返回子进程 PID(有子进程结束),非阻塞且无结束返回 0,出错返回 -1。

3-2-3 获取⼦进程status

  • wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作系统填充。
  • 如果传递NULL,表⽰不关⼼⼦进程的退出状态信息。
  • 否则,操作系统会根据该参数,将⼦进程的退出信息反馈给⽗进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16⽐特位)

备注:一般进程提前(异常)终止,本质是该进程收到了操作系统发送的信号。

我们通过检测 status 参数的次低 8 位,可以得到该进程的退出码,检测 status 参数的低 7 位,可以知道该进程是否被信号所杀,以及被哪个信号所杀。
信号是从 1 号开始的,没有 0 号。如果低 7 位全为 0,说明该进程一定是正常终止的,没有收到任何退出信号;如果 status 参数的低 7 位不为 0,说明该进程是被信号终止的。

3-2-3获取进程的退出码

  • 退出码 0 表⽰命令执⾏⽆误,这是完成命令的理想状态。
  • 退出码 1 我们也可以将其解释为 “不被允许的操作”。例如在没有 sudo 权限的情况下使⽤yum;再例如除以 0 等操作也会返回错误码 1 ,对应的命令为 let a=1/0
  • 130 ( SIGINT 或 ^C )和 143 ( SIGTERM )等终⽌信号是⾮常典型的,它们属于128+n 信号,其中 n 代表终⽌码。
  • 可以使⽤ strerror 函数来获取退出码对应的描述。

本质就是第二个参数 status 进行操作,得到 status 次低 8 位的值,即子进程退出码(status >> 8) & 0xFF

#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> int main() { pid_t id = fork(); if (id == 0) // child process { int count = 5; while (count) { printf("child is running: %ds, pid: %d, ppid: %d\n", count--, getpid(), getppid()); sleep(1); } printf("child quit...!\n"); exit(20); // 终止子进程,退出码为20 } else if (id > 0) // father process { int status = 0; // 进程退出状态 pid_t ret = waitpid(-1, &status, 0); // 等待子进程终止 int exit_code = (status >> 8) & 0xff; // 计算子进程的退出码 // 输出子进程id,退出码 printf("father waits for success, ret: %d, exit code: %d\n", ret, exit_code); // 通过子进程退出码判断子进程把事情办的结果 if (exit_code == 0) printf("子进程成功!\n"); else printf("子进程没有成功!\n"); } else { //fork失败 } return 0; }

运行结果:

通过 waitpid 函数的status 参数父进程拿到了子进程的退出码

问题一:

为什么父进程非得要拿子进程的退出码不能在全局定义一个变量(为子进程的退出码)吗?通过这个全局变量来反馈父进程

用户数据被父子进程各自私有进程之间具有独立性waitpid是系统调用,利用内核中转子进程的退出状态,不依赖父子进程共享用户内存;这和写时拷贝保证的进程用户数据独立性是两套机制,相互配合但各司其职

问题二:

子进程的退出码是如何进入到 waitpid 函数的 status 参数中的呢?

子进程退出时把退出码存到自己的PCB中,父进程调用waitpid时,内核(OS核心部分)从子进程PCB读出退出码,编码后写入父进程的status变量内存中。


3-2-4 获取子进程的终止信号

本质就是对waitpid 函数的第二个参数 status 进行操作,得到 status 低 7 位的值,即子进程终止信号:status & 0x7FF

代码如下:

#include <stdio.h> #include <unistd.h> #include <sys/wait.h> #include <signal.h> int main() { pid_t id = fork(); if (id == 0) // 子进程 { printf("子进程 pid: %d, 父进程 ppid: %d\n", getpid(), getppid()); sleep(2); // 自己发送信号杀死自己(模拟异常终止) raise(SIGKILL); // 9号信号 printf("这句话不会被执行\n"); } else if (id > 0) // 父进程 { int status = 0; pid_t ret = waitpid(-1, &status, 0); // 位运算解析 status int exit_code = (status >> 8) & 0xff; // 退出码(高8位) int term_signal = status & 0x7f; // 终止信号(低7位) int core_dump = (status >> 7) & 1; // 是否产生 core dump(第7位) printf("ret: %d\n", ret); printf("status 原始值: 0x%04x (%d)\n", status, status); printf("退出码 exit_code: %d\n", exit_code); printf("终止信号 term_signal: %d\n", term_signal); printf("是否 core dump: %d\n", core_dump); if (term_signal != 0) { printf("子进程被信号 %d 杀死\n", term_signal); } } else { perror("fork error"); } return 0; }

运行结果:父进程通过 waitpid 函数的status 参数拿到了子进程的终止信号

方式二:用宏(后面仔细讲解)


3-2-5waitpid 的两种等待方式:阻塞和非阻塞

  • 阻塞等待(给 options 参数传 0)
  • 非阻塞等待(给 options 参数传 WNOHANG)

举例子1:

张三打电话问李四作业写完没,李四说没有,过了一会儿,张三又打电话问李四作业写完没,李四说没有,张三没有挂掉电话一直和李四保持通话联系,问李四作业写完没,直到李四作业写完,张三才会停止打电话,这就是阻塞调用。

我们学到的的大多数接口,都是阻塞函数(调用 --> 执行 --> 返回 --> 结束),因为都是单执行流,同时实现起来也比较简单。

阻塞等待:调用方需要一直等着,不能做其他事情,直到函数返回

举例子2:

张三打电话问李四作业写完没,李四说没有,过了一会儿,张三又打电话问李四作业写完没,李四说没有,张三多次打电话问李四作业写完没,直到李四作业写完,张三才会停止打电话。

本质是,张三打电话不会把自己一直卡住,张三可以忙自己的事情,通过间隔多次打电话,检测李四的状态。张三每一次打电话,称之为非阻塞等待多次打电话的过程,称之为非阻塞轮询检测方案。


  • 进程的阻塞等待:

父进程中的 wait 和 waitpid 函数默认是阻塞调用,调用该函数后,只要子进程没有退出,父进程就得一直等,什么事情都做不了,直到子进程退出,函数才返回。

  • 进程的非阻塞等待:

想让父进程中的 waitpid 函数是非阻塞调用(即父进程边运行边调用),需要将函数的第三个参数设为 WNOHANG,这样不需要父亲一直在那边光等待不能够干自己的事情。

父进程中 waitpid 函数如果是非阻塞调用,返回值有三种情况:

  • 等待失败:此次等待失败,需要再次检测(0)
  • 等待失败:真的失败(-1)
  • 等待成功:已经返回(>0)

代码实现:

进程的阻塞等待⽅式:

#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t pid = fork(); if (pid < 0) { perror("fork error"); return 1; } else if (pid == 0) { // 子进程:运行 5 秒后退出 int count = 5; while (count) { printf("子进程工作中... 还剩 %d 秒\n", count--); sleep(1); } printf("子进程退出!\n"); return 99; // 退出码 99 } else { // 父进程:阻塞等待 int status = 0; printf("父进程开始等待子进程...\n"); pid_t ret = waitpid(-1, &status, 0); // 0 = 阻塞等待 if (ret == -1) { printf("waitpid 出错\n"); return 1; } // 等待成功 printf("父进程:子进程 %d 已结束\n", ret); printf("退出码:%d\n", WEXITSTATUS(status)); } return 0; }

运行结果:

进程的⾮阻塞等待⽅式:

#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t pid = fork(); if (pid < 0) { // fork 失败 perror("fork error"); return 1; } else if (pid == 0) { // 子进程:运行 5 秒后退出 int count = 5; while (count) { printf("子进程工作中... 还剩 %d 秒\n", count--); sleep(1); } printf("子进程退出!\n"); return 88; // 退出码 88 } else { // 父进程:非阻塞等待 int status = 0; while (1) { pid_t ret = waitpid(pid, &status, WNOHANG); // 非阻塞等待 if (ret == -1) { // 真的失败 printf("waitpid 出错\n"); break; } else if (ret == 0) { // 此次无结果:子进程还没结束 printf("父进程:子进程还没好,我先干点别的...\n"); // 父进程可以做自己的事情 sleep(1); // 模拟干别的事 } else { // 等待成功:子进程已结束 printf("父进程:子进程结束,退出码 = %d\n", WEXITSTATUS(status)); break; } } } return 0; }

运行结果:

【补充】

如何理解阻塞 / 等待?

进程等待:即父进程在等待子进程终止,而子进程在跑自己的代码。
进程阻塞 :阻塞的本质就是进程被卡住了,没有被 CPU 执行。
操作系统将当前进程放入等待队列,暂时先不会被 CPU 执行,当需要的时候,会唤醒等待队列(即把进程从等待队列移出,放回运行队列,并把进程状态设置为 运行(R) 状态,让 CPU 去调度)。

比如:我们电脑上运行的软件太多,发现某个软件卡住了,其实是当前运行队列中的进程太多,系统资源不足,操作系统把一些进程放入等待队列中了。

内核源码中的退出码与终止信号

在 Linux 内核 2.6 的源码中,每个进程的进程控制块(PCB)struct task_struct结构体表示。其中专门有两个字段用于保存进程退出相关的信息:

struct task_struct { ... int exit_state; // 退出状态 int exit_code; // 退出码(正常退出时) int exit_signal; // 终止信号(被信号杀死时) int pdeath_signal; // 父进程死亡时发送给本进程的信号 ... };

工作流程:

  1. 子进程退出时:无论是通过return 0还是exit(0),该值都会被写入子进程 PCB 的exit_code字段中。如果是被信号杀死,则exit_signal字段会被设置为对应的信号编号。

  2. 父进程调用wait/waitpid:内核从子进程的 PCB 中读取exit_codeexit_signal,将它们按照 status 的位布局编码成一个int值,然后拷贝到父进程的用户空间,填充到status参数中。

  3. 父进程解析:通过WEXITSTATUS(status)WTERMSIG(status)等宏,从status中解码出退出码和终止信号。

总结:子进程退出时,内核把退出码/终止信号保存到 PCB 的exit_code/exit_signal字段中;父进程调用waitpid时,内核从 PCB 读取这些值,编码后填入status参数返回给父进程。


【总结】

  1. 如果⼦进程已经退出,调⽤wait/waitpid时,wait/waitpid会⽴即返回,并且释放资源,获得⼦进程退出信息。
  2. 如果在任意时刻调⽤wait/waitpid,⼦进程存在且正常运⾏,则进程可能阻塞。
  3. 如果不存在该⼦进程,则⽴即出错返回。