【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 ( (pidfork()) -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之后父子进程同时执行#includestdio.h // perror #includeunistd.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 写时拷贝工作过程父进程创建子进程时不复制父进程的内存数据而是让子进程共享父进程的同一份物理内存。操作系统把这些共享的内存页标记为只读。如果父子进程都只是读数据就继续共享没问题。一旦有某个进程比如子进程要写数据CPU会触发缺页异常。操作系统捕获异常后才真正复制一份内存页给这个进程然后允许它修改。好处fork()变得非常快不用复制大量内存。省内存很多进程可以共享只读的代码段、常量等。1. 修改前后父子进程的物理内存共享情况修改前代码段、数据段的物理页完全共享。修改后代码段仍共享数据段因修改触发复制各自拥有独立的物理页。2.页表项标记为只读的作用答用于检测写操作当进程尝试修改只读页时触发缺页异常操作系统执行写时复制操作。展示的是写时复制机制修改前父子进程共享物理内存页修改后数据段页被复制为独立物理页代码段页继续共享。写时拷贝的策略为了保证父子进程的独立性数据各自私有一份。不是所有的数据都有必要被拷贝一份比如只读的数据。写时拷贝可以节约资源。fork 时如果把所有的数据都拷贝一份是需要花费时间的降低了效率。写时拷贝可以提高 fork 执行的效率。fork 创建子进程本身就是向操作系统要资源如果把所有的数据都拷贝一份要更多的资源更容易导致 fork 失败。写时拷贝可以减少 fork 失败的概率。总结Linux 中的fork()创建子进程后通常马上调用exec()加载新程序。有了COWfork()时根本不用复制父进程内存等到exec()时直接更换地址空间即可效率极高。因为有写时拷⻉技术的存在,所以⽗⼦进程得以彻底分离离完成了进程独⽴性的技术保证!二.进程终⽌进程终⽌的本质是释放系统资源就是释放进程申请的相关内核数据结构和对应的数据和代码。2-1main 函数的返回值我们在写 C/C 代码时main 函数里面我们总是会返回 0举例如下#include stdio.h int main() { printf(hello world\n); return 0; }但这是什么原因呢main 函数中的这个返回值叫做进程退出码用来表示进程退出时其执行结果是否正确。返回的 0 是给操作系统看的来确认进程的执行结果是否正确。0 通常表示成功echo $?最后一个进程的退出码2-2 进程退出场景代码运⾏完毕结果正确退出码0代码运⾏完毕结果不正确一般是代码逻辑有问题但没有导致程序崩溃退出码非0代码没有跑完代码异常终⽌这种情况下退出码已经没有意义了是由信号来终止比如 ctrlc2-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; }运行结果ubuntuVM-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 device2-4进程常⻅退出⽅法正常终⽌正常终⽌可以通过 echo $? 查看进程退出码1. 从main返回2. 调⽤exit3. _exit4.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 进程等待的⽅法系统调用 waitwaitpid 等待任意一个子进程改变状态子进程终止时函数才会返回。其实就是等待进程由 R/S状态变成 Z状态然后父进程读取子进程的状态操作系统回收子进程3-2-1 wait⽅法wait 函数#includesys/types.h #includesys/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. pidpid -1等待任意一个子进程与 wait 等效。pid 0等待其进程 ID 与 pid 相等的子进程即传入进程 ID等待指定的子进程。思考下fork 函数在父进程中返回子进程的 ID是为什么呢为了方便父进程等待指定的子进程。status是一个输出型参数status是一个指向整型的指针用于接收子进程的退出状态信息。当父进调用wait或waitpid时内核会将子进程的终止信息写入status指向的内存中。如果父进程不关心子进程的退出状态可以传入NULL。通过宏函数解析statusstatus中编码了多种信息退出码、终止信号等不能直接解读必须使用专门的宏函数来提取WIFEXITED(status)判断子进程是否正常终止即调用exit或从main返回。如果是返回true非零。WEXITSTATUS(status)前提是WIFEXITED返回真该宏会提取子进程传递给exit或return的退出码低8位有效。c. options0—— 阻塞式等待当第三个参数设为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 获取⼦进程statuswait和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 a1/0130 SIGINT 或 ^C 和 143 SIGTERM 等终⽌信号是⾮常典型的它们属于128n 信号其中 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; // 父进程死亡时发送给本进程的信号 ... };工作流程子进程退出时无论是通过return 0还是exit(0)该值都会被写入子进程 PCB 的exit_code字段中。如果是被信号杀死则exit_signal字段会被设置为对应的信号编号。父进程调用wait/waitpid时内核从子进程的 PCB 中读取exit_code和exit_signal将它们按照 status 的位布局编码成一个int值然后拷贝到父进程的用户空间填充到status参数中。父进程解析通过WEXITSTATUS(status)和WTERMSIG(status)等宏从status中解码出退出码和终止信号。总结子进程退出时内核把退出码/终止信号保存到 PCB 的exit_code/exit_signal字段中父进程调用waitpid时内核从 PCB 读取这些值编码后填入status参数返回给父进程。【总结】如果⼦进程已经退出调⽤wait/waitpid时wait/waitpid会⽴即返回并且释放资源获得⼦进程退出信息。如果在任意时刻调⽤wait/waitpid⼦进程存在且正常运⾏则进程可能阻塞。如果不存在该⼦进程则⽴即出错返回。