持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第N天,点击查看活动详情 大部分情况下通常退出码是父进程关心的,因为父进程费了很大的劲把子进程创建出来干活,活干的怎么样,父进程得知道。野指针、/0、越界等都可能导致进程非正常结束,父进程也要关心这种情况,但此时退出码是无意义的。
🍥2.2 进程常见退出方法 正常退出 main 函数 return
🍥3.3 获取子进程status wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。 如果传递NULL,表示不关心子进程的退出状态信息。 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。 status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
阻塞和非阻塞
关注的是程序在等待调用结果(消息,返回值)时的状态. 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。 如果 waitpid 中的 options 传 WNOHANG ,那么 ————————————————阻塞调用更简单。
例子
比如你的学习很差,所以打电话给楼上学习好的同学张三,说:张三,你下来,我请你吃个饭,然后你帮我复习一下。张三说:行,没问题,但是我在写代码,半个小时之后再来。一般一个班,学习好的人总是少数,所以你怕你电话一挂,有人又跟张三打电话求助,导致你不能及时复习,所以你又跟张三说:张三,你电话不要挂,你把电话放你旁边,我喜欢看你写代码的样子。然后你什么事都不做,就在那等待,直到张三下来。当然现实中很少有这种情况,但是这样的场景是存在的,一般是比较紧急的情况,比如你爸打电话让你做件事且告诉你不要挂电话。此时张三不下来,电话就不挂就类似于调用函数,这种等待方式就叫做阻塞等待。我们目前所调用的函数,全部是阻塞函数,不管是你自己写的、库里的、系统的,阻塞函数最典型的特征是调用 ➡ 执行 ➡返回 ➡ 结束,其中调用方始终在等待,什么事情都没做。
测试用例一: 父进程 fork 派生一个子进程干活,父进程通过 status 可以知道子进程把活做的怎么样。
#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)
{
int count = 5;
while(count)
{
printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
sleep(1);
}
printf("child quit...\n");
exit(123);
}
//father
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
int code = (status >> 8) & 0xFF;
printf("%d\n", status);
printf("father wait done, ret: %d, exit code: %d\n", ret, code);
if(code == 0)
{
printf("做好了\n");
}
else
{
printf("没做好\n");
}
return 0;
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
父进程通过 waitpid 要拿子进程的退出码应该从哪里去取呢,明明子进程已经退出了。子进程是结束了,但是子进程的状态是僵尸,也就是说子进程的相关数据结构并没有被完全释放。当子进程退出时,进程的 task_struct 里会被填入当前子进程退出时的退出码,所以 waitpid 拿到的 status 值是通过 task_struct 拿到的。
测试用例二: 针对测试用例二,父进程无非就是想知道子进程的工作完成的结果,那全局变量是否可以作为子进程退出码的设置,以此告知父进程子进程的退出码。
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int code = 0;
int main()
{
pid_t id = fork();
if(id == 0)
{
int count = 5;
while(count)
{
printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
sleep(1);
}
printf("child quit...\n");
code = 123;
exit(0);
}
//father
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
printf("father wait done, ret: %d, exit code: %d\n", ret, code);
if(code == 0)
{
printf("做好了\n");
}
else
{
printf("没做好\n");
}
return 0;
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
很显然,不可以。这里对于全局变量,发生了写时拷贝,在进程地址空间里我们说过父子是具有独立性的,虽然变量是同一个,但实际上子进程或父进程所写的数据,它们都是无法看到彼此的,所以不可能让父进程拿到子进程的退出结果。
测试用例三: 野指针异常终止
#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)
{
int count = 5;
while(count)
{
printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
sleep(1);
//err
int* p = 0x12345;
*p = 100;
}
printf("child quit...\n");
exit(123);
}
//father
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
int code = (status >> 8) & 0xFF;
int sig = status & 0x7F;//0111 1111
printf("father wait done, ret: %d, exit code: %d, sig: %d\n", ret, code, sig);
return 0;
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
测试用例四: 模拟异常终止 —— 使用kill -9信号亲手杀死子进程。
#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)
{
int count = 50;
while(count)
{
printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid());
sleep(1);
}
printf("child quit...\n");
exit(123);
}
//father
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
int code = (status >> 8) & 0xFF;
int sig = status & 0x7F;//0111 1111
printf("father wait done, ret: %d, exit code: %d, sig: %d\n", ret, code, sig);
return 0;
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
测试用例五: 父进程完整的等待子进程的全过程。
#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) { int count = 5; while(count) { printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid()); //err //int* p = 0x12345; //*p = 100; sleep(1); } printf("child quit...\n"); exit(123); } //father int status = 0; pid_t ret = waitpid(id, &status, 0); if(ret > 0) { printf("wait success!\n"); if((status & 0x7F) == 0) { printf("process quit normal!\n"); printf("exit code: %d\n", (status >> 8) & 0xFF); } else { printf("process quit error!\n"); printf("sig: %d\n", status & 0x7F); } }
return 0;
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
测试用例六: 非阻塞等待
#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) { int count = 3; while(count) { printf("child is running: %d, ppid: %d, pid: %d\n", count--, getppid(), getpid()); //err //int* p = 0x12345; //*p = 100; sleep(1); } printf("child quit...\n"); exit(123); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
🍣4. 进程替换 🍥4.1 为什么要进程替换 创建子进程的目的:
执行父进程的部分代码。 我们之前所写的代码都属于这种情况。 执行其它程序的代码。 不要父进程的代码和数据。所以我们要学习进程替换。 进程替换是为了子进程能够执行其它程序的代码;进程替换就是以写时拷贝的策略,让第三方进程的代码和数据替换到父进程的代码和数据,给子进程用,因为进程间具有独立性,所以不会影响父进程。以前我们说数据是可写的,代码是不可写的,现在看来,确实如此。但是接下来要把其它程序的代码通过进程替换放在内存里让子进程与之关联,此时就要给代码进行写时拷贝。99% 的情况是对数据进行写时拷贝,1% 的情况是代码依旧是只读,本质就是对父进程不可写,子进程后续调用某些系统调用,实际给子进程重新开辟空间把新进程的代码加载,不让子进程执行父进程的代码。
🍥4.2 替换原理 用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变
🍥4.3 替换函数 有6个以exec*开头的调用接口,执行函数替换 ——
#include <unistd.h>
int execl(const char *path, const char *arg, ...); int execv(const char *path, char *const argv[]); int execlp(const char *file, const char *arg, ...); int execvp(const char *file, char *const argv[]); int execle(const char *path, const char *arg, ...,char *const envp[]); int execve(const char *path, char *const argv[], char *const envp[]); 1 2 3 4 5 6 7 8 这些函数的功能都是一样的,如果用 C++ 去设计这样的接口,一定是重载。这里是使用 C 去设计的,函数名的命名也有区分。
在下文中会测试
函数解释及使用:
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。 如果调用出错则返回 -1。 所以 exec 函数只有出错的返回值而没有成功的返回值。 命名理解 这些函数原型看起来很容易混,但只要掌握了规律就很好记。
l(list) : 表示参数采用列表 v(vector) : 参数用数组 p(path) : 有p自动搜索环境变量PATH e(env) : 表示自己维护环境变量 测试用例一:
单进程,父进程亲自干活。
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("my process begin!\n");
execl("/usr/bin/ls", "ls", "-a", "-l", "-i", NULL);
printf("my process end!\n");
return 0;
}
1 2 3 4 5 6 7 8 9 10 11
多进程,父进程创建子进程干活。
#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)
{
printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
execl("/usr/bin/ls", "ls", "-a", "-l", "-i", 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;
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
为什么图一没有输出 my process end ! && 图二的退出码是 0 ?
因为在这之前 execl 已经程序替换了,所以 execl 后面的代码已经不是当前进程的代码了,所以图二获取到的退出码 0 是 ls 的退出码。换言之,一旦程序替换,你到底执行正确与否是取决于 ls 程序。所以 exec 系列的函数不用考虑返回值,只要返回了,一定是这个函数调用或程序替换失败了。
测试用例二:
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)
{
//const char* const my_argv[] = {"ls", "-l", "-a", "-i", NULL};//err,注意要与函数原型的参数类型匹配
char* const my_argv[] = {"ls", "-l", "-a", "-i", NULL};
printf("I am child, 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;
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
测试用例三:
execlp 相比 execl 在命名上多了 1 个 p,且参数只有第 1 个不同:不同点在于 execlp 不需要带路径,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)
{
printf("I am child, pid: %d, ppid: %d\n", getpid(), getppid());
execlp("ls", "ls", "-a", "-i", "-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;
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
带 p 的含义就是不用带路径,系统会自动搜索你要执行的程序,不带 p 则相反。所以你要执行哪个程序,背后的含义是 a) 你在哪 b) 你是谁。可见 execlp 就是 b,execl 就是 ab。当然这里的搜索默认只有系统的命令才能找到,如果需要执行自己的命令,需要提前把自己的命令与 环境变量 关联。
🍣5. 实现mini_shell 写一个shell 命令行解释器,需要循环以下过程
打印提示行 获取命令行 解析命令行 fork创建子进程;替换子进程 父进程等待子进程退出 🍥5.1 打印提示行 [用户名@主机名 路径]提示符,这些都可以通过系统调用获取到,但是对于理解Linux意义不大,就直接写死了。
写进度条时候就知道,显示器的刷新策略就是行刷新,不想换行还要刷新,可以调用fflush(stdout);
🍥5.2 获取命令行
4 🍥5.3 解析命令行 解析字符串,要分割串,用strtok。传参给要解析的字符串、分隔符串儿,返回子串;第二次提取时,把还想提取的老串儿给NULL——
我们把子串儿都提取到char*的指针数组argv中。
//3.解析命令行字符串,argv[] - strtok
const char* sep = " ";
6 7 8 9 10 11 12 13 14 15 16 🍥5.4 fork创建子进程;替换子进程 不能用当前进程直接替换,会把前面的解析代码覆盖掉,因此要创建子进程。同时,父进程需要等待子进程退出。这也就解释了为什么在bash上执行出错了,echo $? 就能拿到退出码,这是因为子进程的退出结果是可以wait拿到的
🍥5.5 内建命令 wait我们发现,对于|管道和>重定向是无法处理的,因为当前代码没有组合设置,我们实现的其实是一个相当简陋的shell。然而震惊的是,我们 cd… 路径没有回退?!这是因为执行回退的是子进程,并非是父进程bash.
fork要执行的命令是第三方命令,对于cd,现在我们不想再执行第三方命令,以内建命令方式运行(即不创建子进程,让父进程shell自己执行),实际上相当于调用了自己的一个函数。更改当前进程路径,有一个系统调用接口chdir
🍥5.6 mini_shell完整代码 #include<stdio.h> #include<stdlib.h> #include<string.h> #include<sys/wait.h> #include<unistd.h>
#define NUM 128
#define CMD_NUM 64
int main() { char command[NUM]; char* argv[CMD_NUM] = {NULL};//未设置就是NULL值 for(;;) { command[0]=0;//这种方式可以做到,时间复杂度O(1),清空字符串 //1.打印提示符 printf("[you-know-who@myhostname mydir]$ "); fflush(stdout);