将磁盘中指定的程序加载到内存中,让指定的进程进行执行。不论是哪种后端语言写的程序,exec*
类的函数都可以调用。
创建子进程的目的:
所以 进程替换是为了让子进程能够执行其它程序的代码;进程替换就是以写时拷贝的策略,让第三方进程的代码和数据替换到父进程的代码和数据,给子进程用,因为进程间具有独立性,所以不会影响父进程。以前我们说数据是可写的,代码是不可写的,现在看来,确实如此。但是接下来要把其它程序的代码通过进程替换放在内存里让子进程与之关联,此时就要给代码进行写时拷贝。99%
的情况是对数据进行写时拷贝,1%
的情况是代码依旧是只读,本质就是对父进程不可写,子进程后续调用某些系统调用,实际给子进程重新开辟空间把新进程的代码加载,不让子进程执行父进程的代码(其实也是因为新程序的代码覆盖了子进程的代码)。
程序替换的本质:用磁盘指定位置上的程序的代码和数据,覆盖进程自身的代码和数据,达到让进程执行指定程序的目的。
用 fork
创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支), 子进程往往要调用一种 exec
函数以执行另一个程序。当进程调用一种 exec
函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行(也就是说替换后原进程后面的代码就被覆盖了,不会再被执行)。调用 exec
并不创建新进程,所以调用 exec
前后该进程的 id
并未改变。
所以进程替换不会改变进程内核的数据结构,只会修改部分页表数据,然后把新进程的代码和数据加载至内存,重新构建页表映射关系,和父进程彻底脱离。
一共有七个替换函数,统称为 exec
函数:
#include <unistd.h>
// 系统调用
int execve(const char *filename, char *const argv[],char *const envp[]);
// execve的封装(也就是拓展版本)
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
上面的函数中 execve
是系统级别的函数,而其它的是它的一个封装,也就是功能上的一个拓展,底层还是调用的 execve
!
这些函数的功能都是一样的,如果用 C++
去设计这样的接口,一定是重载。这里是使用 C
去设计的,函数名的命名也有区分。下面我们会对这些接口进行演示,但实际在后面常用的也只是部分而已。
🔴 注意:上面参数中的 ...
代表的是参数列表,也就是我们可以传多个参数,就像 printf
一样;除此之外,最后一个参数必须以 NULL
结尾代表结束!(包括 envp[]
中也要以 NULL
结尾)
-1
exec
函数 只有出错的返回值 而没有成功的返回值(因为成功调用的时候已经开始执行新程序,所以没必要有成功的返回值) 🤽♂ 这些函数原型看起来很容易混,但只要掌握了规律就很好记。
list
):表示参数采用列表,将参数一个一个传入 exec*
vector
):表示参数采用数组,也就是将参数列表写进数组后传数组即可path
):表示自动搜索环境变量 PATH
,只需要传入对应环境变量 PATH
的字符串即可,系统会进行可执行程序的查找env
):表示自定义环境变量#include <unistd.h>
int main()
{
char *const argv[] = {"ps", "-ef", NULL};
char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要自己组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
// 带v的,可以使用数组传参
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
// 带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);
exit(0);
}
下面我们来讲解几个 exec*
函数,其它类似拓展功能的都是一样的,学会了每个关键字功能就行!
int execl(const char *path, const char *arg, ...);
// path代表要执行文件的路径
// arg以及...(可变参数列表)就是我们要输入的参数列表,最后以NULL结尾
如果只有单进程的情况:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("single process running...\n");
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
printf("single process done\n");
return 0;
}
运行结果:
[liren@VM-8-2-centos pro_replace]$ ./mypro
single process running...
total 28
drwxrwxr-x 2 liren liren 4096 Jan 19 12:47 .
drwxrwxr-x 6 liren liren 4096 Jan 19 12:42 ..
-rw-rw-r-- 1 liren liren 72 Jan 19 12:43 makefile
-rwxrwxr-x 1 liren liren 8408 Jan 19 12:47 mypro
-rw-rw-r-- 1 liren liren 246 Jan 19 12:47 repro.c
[liren@VM-8-2-centos pro_replace]$
可以看到执行完 execl
后最后一句 printf
是被覆盖了,不会执行的!
多进程的情况,子进程完成调用函数工作:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// child
printf("我是子进程,pid:%d,ppid:%d\n", getpid(), getppid());
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
exit(1);
}
// father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
printf("child status -> sig: %d, code: %d\n", status & 0x7F, (status >> 8) & 0xFF);
else
printf("wait error!\n");
return 0;
}
运行结果:
[liren@VM-8-2-centos pro_replace]$ ./mypro
我是子进程,pid:20462,ppid:20461
total 28
drwxrwxr-x 2 liren liren 4096 Jan 19 12:54 .
drwxrwxr-x 6 liren liren 4096 Jan 19 12:42 ..
-rw-rw-r-- 1 liren liren 72 Jan 19 12:43 makefile
-rwxrwxr-x 1 liren liren 8720 Jan 19 12:54 mypro
-rw-rw-r-- 1 liren liren 604 Jan 19 12:54 repro.c
child status -> sig: 0, code: 0
[liren@VM-8-2-centos pro_replace]$
可以看到子进程中调用 execl
后并没有去调用到 exit(1)
,同样验证了上面的说法!
int execv(const char *path, char *const argv[]);
// path代表要执行文件的路径
// argv代表传入的是一个参数数组,其中每个元素都是字符指针也就是字符串,其中要注意的是这个数组的类型是char* const
execv
与 execl
较为类似,它们的唯一差别是,如果需要传多个参数,那么 execl
是以可变参数的形式进行列表传参,而 execv
是以指针数组的形式传参。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// child
//const char* const my_argv[] = {"ls", "-l", "-a", "-i", NULL}; ❌ 注意要与函数原型的参数类型匹配
char* const my_argv[] = {"ls", "-l", "-a", "-i", NULL};
printf("我是子进程,pid:%d,ppid:%d\n", getpid(), getppid());
execv("/usr/bin/ls", my_argv);
exit(1);
}
// father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
printf("child status -> sig: %d, code: %d\n", status & 0x7F, (status >> 8) & 0xFF);
else
printf("wait error!\n");
return 0;
}
运行结果:
[liren@VM-8-2-centos pro_replace]$ ./mypro
我是子进程,pid:22396,ppid:22395
total 28
1180646 drwxrwxr-x 2 liren liren 4096 Jan 19 13:03 .
789602 drwxrwxr-x 6 liren liren 4096 Jan 19 12:42 ..
1180647 -rw-rw-r-- 1 liren liren 72 Jan 19 12:43 makefile
1180649 -rwxrwxr-x 1 liren liren 8720 Jan 19 13:03 mypro
1180648 -rw-rw-r-- 1 liren liren 776 Jan 19 13:03 repro.c
child status -> sig: 0, code: 0
[liren@VM-8-2-centos pro_replace]$
int execlp(const char *file, const char *arg, ...);
// file代表的是可执行文件的名称,如"ls"、"touch"等等
// arg以及...(可变参数列表)就是我们要输入的参数列表,最后以NULL结尾
execlp
相比 execl
在命名上多了 1
个 p
,且参数只有第 1
个不同:即 execlp
不需要带路径,在执行时它会拿着你要执行的程序自动的在系统 PATH
环境变量中查找你要执行的目标程序。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// child
printf("我是子进程,pid:%d,ppid:%d\n", getpid(), getppid());
execlp("ls", "ls", "-a", "-l", NULL);
exit(1);
}
// father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
printf("child status -> sig: %d, code: %d\n", status & 0x7F, (status >> 8) & 0xFF);
else
printf("wait error!\n");
return 0;
}
运行结果:
[liren@VM-8-2-centos pro_replace]$ ./mypro
我是子进程,pid:23307,ppid:23306
total 28
drwxrwxr-x 2 liren liren 4096 Jan 19 13:07 .
drwxrwxr-x 6 liren liren 4096 Jan 19 12:42 ..
-rw-rw-r-- 1 liren liren 72 Jan 19 12:43 makefile
-rwxrwxr-x 1 liren liren 8720 Jan 19 13:07 mypro
-rw-rw-r-- 1 liren liren 785 Jan 19 13:07 repro.c
child status -> sig: 0, code: 0
[liren@VM-8-2-centos pro_replace]$
带 p
的含义就是不用带路径,系统会自动搜索你要执行的程序,不带 p
则相反。当然这里的 搜索默认只有系统的命令才能找到,如果需要执行自己的命令,需要提前把自己的命令与 PATH
关联。
int execvp(const char *file, char *const argv[]);
// file代表的是可执行文件的名称,如"ls"、"touch"等等
// argv代表传入的是一个参数数组,其中每个元素都是字符指针也就是字符串,其中要注意的是这个数组的类型是char* const
通过上面我们知道 execvp
无非就是不带路径,使用指针数组传参。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// child
char* const my_argv[] = {"ls", "-l", "-a", NULL};
printf("我是子进程,pid:%d,ppid:%d\n", getpid(), getppid());
execvp("ls", my_argv);
exit(1);
}
// father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
printf("child status -> sig: %d, code: %d\n", status & 0x7F, (status >> 8) & 0xFF);
else
printf("wait error!\n");
return 0;
}
运行结果:
[liren@VM-8-2-centos pro_replace]$ ./mypro
我是子进程,pid:24517,ppid:24516
total 28
drwxrwxr-x 2 liren liren 4096 Jan 19 13:13 .
drwxrwxr-x 6 liren liren 4096 Jan 19 12:42 ..
-rw-rw-r-- 1 liren liren 72 Jan 19 12:43 makefile
-rwxrwxr-x 1 liren liren 8720 Jan 19 13:13 mypro
-rw-rw-r-- 1 liren liren 639 Jan 19 13:13 repro.c
child status -> sig: 0, code: 0
[liren@VM-8-2-centos pro_replace]$
e
的 exec*
函数以及调用自定义的可执行文件 带 e
表示传入默认的或者自定义的环境变量给目标可执行程序。
下面我们分别演示传入系统中的环境变量以及我们自己写的环境变量,在这之前我们先演示一下如何调用我们自己的写的程序,所以我们创建一个 mycmd.c
文件:
// mycmd.c
#include <stdio.h>
int main()
{
for(int i = 0; i < 5; ++i)
printf("cmd:%d\n", i);
// 自定义的环境变量
printf("MYENV:%s\n", getenv("MYENV"));
// 系统自带的环境变量
printf("PATH:%s\n", getenv("PATH"));
printf("PWD:%s\n", getenv("PWD"));
return 0;
}
由于我们需要生成多个可执行文件,但是由于 makefile
中的机制是默认只生成从上往下的第一个可执行文件,所以我们在 makefile
中添加一个伪目标 all
,这个时候 makefile
从上往下扫描 all
中定义的可执行文件名称,从而达到生成它们的目的!
值得注意的是 all
要放在文件中的开头!
# makefile文件
.PHONY:all
all: mypro mycmd
mypro:repro.c
gcc -o $@ $^ -std=c99
mycmd:mycmd.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -f mypro mycmd
接下来我们修改一下 repro.c
中的一些细节:
// repro.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// child
char* const my_env[] = {"MYENV=helloLinux", NULL};
printf("我是子进程,pid:%d,ppid:%d\n", getpid(), getppid());
// 调用自己写的mycmd文件,并将my_env传过去
execle("./mycmd", "mycmd", NULL, my_env);
exit(1);
}
// father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
printf("child status -> sig: %d, code: %d\n", status & 0x7F, (status >> 8) & 0xFF);
else
printf("wait error!\n");
return 0;
}
嘶~~怎么这么奇怪对不对,我们明明第二个和第三个打印的是 PATH
和 BASH
,但是最后打印出来的是 null
❓❓❓
其实原因就是因为只是替换了程序,而我们调用 exec*e
的时候,只是将参数中的 envp
数组中内的环境变量传了过去,所以替换后只能查看到我们传过去的环境变量,而不能看到系统中的环境变量!
那如果我们要实现既能看到系统中的环境变量,又想自己传环境变量过去,该怎么办呢 ❓❓❓
这里就要用到我们之前在讲环境变量的时候埋下的伏笔,也就是 putenv
函数,将指定环境变量导入到系统中 environ
指向的环境变量表!
🔴 下面程序在 mycmd.c
中做了一些小改动,将 BASH
改成了 PWD
,请注意!
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// child
printf("我是子进程,pid:%d,ppid:%d\n", getpid(), getppid());
putenv("MYENV=helloLinux"); // 将自定义环境变量添加到系统中去
extern char** environ; // 声明一下防止报错
execle("./mycmd", "mycmd", NULL, environ); // 将系统中的环境变量传过去
exit(1);
}
// father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
printf("child status -> sig: %d, code: %d\n", status & 0x7F, (status >> 8) & 0xFF);
else
printf("wait error!\n");
return 0;
}
执行结果:
程序替换可以调用任何后端语言对应的可执行文件!
我们这里用 C
语言来调用 C++
举个例子,首先写一个 C++
文件叫做 otherLanguage.cc
,并用 C
程序调用:
// otherLanguage.cc
#include <iostream>
using namespace std;
int main()
{
cout << "hello C++" << endl;
cout << "hello C++" << endl;
cout << "hello C++" << endl;
cout << "hello C++" << endl;
return 0;
}
// repro.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
// child
char* const my_env[] = {"MYENV=helloLinux", NULL};
printf("我是子进程,pid:%d,ppid:%d\n", getpid(), getppid());
// 调用otherLanguage生成的可执行文件
execl("./otherLanguage", "otherLanguage", NULL);
exit(1);
}
// father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
printf("child status -> sig: %d, code: %d\n", status & 0x7F, (status >> 8) & 0xFF);
else
printf("wait error!\n");
return 0;
}
执行结果:
既然我们可以用自己的程序调用自己的程序,main
函数也是一个程序,它也需要被调用,也要被传参。
它的 原理其实就是被 execve
这个系统函数调用的,通过 execve
函数传递 argc
、argv
、env
给 main
函数!所以 exec*
在 linux
中也称为 加载器!
一个完整的集成开发环境的组件肯定包括编辑器、编译器、调试器、加载器等。一个软件被加载到内存里,肯定是运行起来,形成进程,进程再调用 exec
系列的函数就可以完成加载的过程。所以 exec
可以理解成一种特殊的加载器。
用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell
由标识为 sh
的方块代表,它随着时间的流逝从左向右移动。shell
从用户读入字符串 "ls"
。shell
建立一个新的进程,然后在那个进程中运行 ls
程序并等待那个进程结束。
然后我们的 shell
读取新的一行输入,建立一个新的进程,在这个进程中运行程序并等待这个进程结束。所以要写一个shell
,需要循环以下过程:
fork
)
execvp
)
wait
)
根据这些思路,和我们前面的学的技术,就可以自己来实现一个 shell
了。
这里命令行的格式比较随意,我们就用 用户+主机名+当前地址
为命令行格式!
printf("[%s@%s %s]~ ", getenv("USER"), getenv("HOSTNAME"), getenv("PWD"));
fflush(stdout); // 刷新一下缓冲区
调用效果:
因为用的是 C
语言,所以我们可以用宏来定义字符数组的大小和指针数组的大小,要注意的是因为每次我们 fgets
后要按一次回车,回车也会被放到缓冲区,也就是说加入输入了 "123\n"
那么我们要将这个 \n
换成 \0
才能保证字符数组的正确性!
在切割字符串的时候调用的是 strtok
这个函数,详细的参数可以自行去网上或者手册查看,第一个参数是要切割的目标,第二个参数是分割标志,然后利用其最后没有子串也就是切割完后的返回值是 NULL
,我们利用 while
循环赋值给 myargv[i]
,最后让 myargv[i]
去判断是否终止即可!
#define NUM 1024 // 定义用户输入的最大数,其中个数为1023个,最后一位给'\0'
#define OPTION_NUM 64 // 定义切割后每个选项的最大个数
char lineCommand[NUM]; // 存放用户输入的字符串数组
char* myargv[OPTION_NUM]; // 存放切割后单词的指针数组
// 获取用户输入
char* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
assert(s != NULL); // 检测一下输入是否正确
lineCommand[strlen(lineCommand) - 1] = '\0'; // 由于我们按回车会有一个'\n',我们将其设为'\0'
(void)s; // 防止编译器在release模式下告警
// 切割字符串
myargv[0] = strtok(lineCommand, " ");
int i = 1;
// 没有子串后,strtok最后切割完返回的是NULL,用myargv[i]接收判断后退出
while(myargv[i++] = strtok(NULL, " "))
{}
将 myargv
中的子串打印出来看看是否正确,编译的时候需要在 makefile
中加上 -DDEBUG
即可!
// 用条件编译,测试是否成功
#ifdef DEBUG
for(int i = 0; myargv[i]; ++i)
printf("myargv[%d]:%s\n", i, myargv[i]);
#endif
因为我们用的是 myargv
数组存放我们的子串,并且我们习惯输入指令是不带路径的,所以我们要用带有 v
和 p
的,最后选择的函数就是 execvp
!
// 执行命令
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
// 由于指令输入习惯,所以选择最适合的execvp
execvp(myargv[0], myargv);
// 若失败终止信号设为1
exit(1);
}
int status = 0;
pid_t ret = waitpid(id, &status, 0); // 回收已退出的子进程的信息
assert(ret > 0);
(void)ret; // 防止编译器在release模式下告警
这些代码在完整代码中可以看,主要说一下几个指令:
第一个就是 ls
指令,如果我们想要它每次打印的时候带上颜色,我们可以在一开始切割之前就判断是否为 ls
,是的话我们将第二个参数变成 --color=auto
这样子就能带上颜色了!
第二个就是 cd
指令,这里就要扯到一个知识点,就是 内建/内置指令
,其实就是不用子进程帮我们去完成,直接让当前父进程自己完成该命令即可,因为 cd
的目的是为了让当前父进程抵达某个路径,但是如果我们用子进程去帮我们的话,由于进程之间的独立性,最后并不会让父进程也就是当前进程得到任何效果,所以对于这种指令,我们只需要在父进程里面直接完成即可!!!并且我们可以调用一个叫做 chdir
的指令帮我们更改当前的路径,因为当前路径的本质其实就是当前文件!
下面通过进入 /proc
中查看 myshell
进程,其中 cwd
才是我们需要去更改的当前目录也就是当前路径!
第三个就是 echo
指令了,和 cd
指令一样,它们都是 内建/内置指令
,所以执行它们的时候不需要去创建子进程去帮我们完成任务,直接在当前进程中执行即可~
要注意的是我们这里主要实现的还是 echo $?
,就是维护两个全局变量 lastSig
和 lastCode
,然后每次调用的时候将它们打印出来即可!
// makefile
myshell:myshell.c
gcc -o myshell myshell.c -std=c99 #-DDEBUG
.PHONY:clean
clean:
rm -f myshell
------------------------------------------------------
// myshell.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <assert.h>
#include <string.h>
#define NUM 1024 // 定义用户输入的最大数,其中个数为1023个,最后一位给'\0'
#define OPTION_NUM 64 // 定义切割后每个选项的最大个数
char lineCommand[NUM]; // 存放用户输入的字符串数组
char* myargv[OPTION_NUM]; // 存放切割后单词的指针数组
int lastSig = 0; // 终止信号
int lastCode = 0; // tui'chu
int main()
{
// 因为shell是循环输入的,所以要套在死循环里面
while(1)
{
// 获取命令行
printf("[%s@%s %s]~ ", getenv("USER"), getenv("HOSTNAME"), getenv("PWD"));
fflush(stdout);
// 获取用户输入
char* s = fgets(lineCommand, sizeof(lineCommand) - 1, stdin);
assert(s != NULL); // 检测一下输入是否正确
lineCommand[strlen(lineCommand) - 1] = '\0'; // 由于我们按回车会有一个'\n',我们将其设为'\0'
(void)s; // 防止编译器在release模式下告警
// 切割字符串
myargv[0] = strtok(lineCommand, " ");
int i = 1;
// 若是ls,可以给它加上颜色
if(myargv[0] != NULL && strcmp("ls", myargv[0]) == 0)
myargv[i++] = (char*)"--color=auto";
// 没有子串后,strtok最后切割完返回的是NULL,用myargv[i]接收判断后退出
while(myargv[i++] = strtok(NULL, " "))
{}
// 若是cd,则可以直接在当前进程修改
// 像这种指令不需要子进程来执行,而是让shell自己执行的命令,叫做内建/内置命令
if(myargv[0] != NULL & strcmp("cd", myargv[0]) == 0)
{
if(myargv[1] != NULL)
chdir(myargv[1]);
continue;
}
// echo $? 时候打印出退出码和终止信号
if(myargv[0] && myargv[1] && strcmp(myargv[0], "echo") == 0)
{
if(strcmp(myargv[1], "$?") == 0)
printf("退出状态:%d,终止信号:%d\n", lastCode, lastSig);
else
printf("%s\n", myargv[1]);
continue;
}
// 用条件编译,测试是否成功
#ifdef DEBUG
for(int i = 0; myargv[i]; ++i)
printf("myargv[%d]:%s\n", i, myargv[i]);
#endif
// 执行命令
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
// 由于指令输入习惯,所以选择最适合的execvp
execvp(myargv[0], myargv);
// 若失败终止信号设为1
exit(1);
}
int status = 0;
pid_t ret = waitpid(id, &status, 0); // 回收已退出的子进程的信息
assert(ret > 0);
(void)ret; // 防止编译器在release模式下告警
// 将最近一次的退出码和终止信号更新
lastCode = (status >> 8) & 0xFF;
lastSig = status & 0x7F;
}
return 0;
}