柴犬: 你好啊,屏幕前的大帅哥or大美女,和我一起享受美好的今天叭😃😃😃
文章目录
- 一、进程创建
- 1.调用fork之后,内核都做了什么?
- 2.如何理解fork函数有两个返回值?
- 3.如何理解fork返回之后,给父进程返回子进程的pid,而给子进程返回0?
- 4.如何理解一个id变量,怎么能保存两个值,并且if和elseif语句同时执行?
- 5.fork常规用法 && fork调用失败的原因
- 二、进程终止(进程退出的三种情况)
- 1.退出码(你可以不关心进程退出信息,但OS不能不提供获取信息的方式)
- 2.进程如何退出(return、exit( )、_exit( ) )
- 三、进程等待(回收子进程所有资源,读取子进程退出信息)
- 1.wait(等待任意的子进程,只能是阻塞等待)
- 2.输出型参数status(用于修改status(存放进程退出信息)变量的值)
- 3.waitpid(可以等待特定的子进程,可以非阻塞等待)
- 4.宏WIFEXITED和WEXITSTATUS(获取进程终止情况和退出码)
- 5.进程的阻塞和非阻塞等待(多次非阻塞等待 ⇒ 轮询)
- 四、进程的程序替换(子进程执行新程序的代码和数据)
- 1.创建子进程的目的?
- 2.进程的程序替换
- 2.1 单个进程的程序替换
- 2.2 父进程派生子进程的程序替换
- 3.程序替换原理
- 4.exec系列替换函数详解(6个封装函数,1个系统调用)
- 5.派生子进程替换我们自己写的程序
- 五、实现一个简易shell
- 1.shell代码实现
- 2.什么是当前路径?(当前进程的工作目录 && cd底层实现用chdir)
- 3.shell内建/内置命令(shell自己执行的命令,而不是派生子进程进行程序替换来执行)
一、进程创建
1.调用fork之后,内核都做了什么?
1.
在调用fork函数之后,当执行的程序代码转移到内核中的fork代码后,内核需要分配新的内存块和内核数据结构给子进程,内核数据结构包括PCB、mm_struct和页表,然后构建起映射关系,同时将父进程内核数据结构中的部分内容拷贝到子进程,并且内核还会将子进程添加到系统进程列表当中,最后内核空间中的fork代码执行完毕,操作系统中也就已经创建出来了子进程,最后返回用户空间,父子进程执行程序fork之后的剩余代码。
2.
所以,fork之前父进程独立执行,fork之后,父子进程两个执行流一起执行fork之后的剩余代码。但值得注意的是:fork之后,父子进程谁先执行,完全由调度器决定。
调度器
3.
将子进程添加到系统进程列表中,实际上是通过一张哈希表来完成的,Linux利用hash表来管理进程,指向PCB的指针会存到pidhash里面,然后在通过pid_hashfn哈希函数,将进程的pid转换为hash表的索引,通过这个函数可以将进程的pid均匀的散列在他们的域里面,也就是0到PIDHASH_SZ的位置,当出现不同的pid散列到相同的索引时,发生冲突,LInux利用链地址法来处理冲突的PID。(这部分了解即可)
#define PIDHASH_SZ (4096 >> 2)
extern struct task_struct *pidhash[PIDHASH_SZ];
#define pid_hashfn(x) ((((x) >> 8) ^ (x)) &(PIDHASH_SZ - 1))
2.如何理解fork函数有两个返回值?
1.
先将代码呈现一下,这个代码的运行结果和解析在上一篇博文已经讲过了,这里就不在细说了。上一篇博文
1 #include <stdio.h>
2 #include <unistd.h>
3
4
5 int global_value = 100;
6 int main()
7 {
8 pid_t id = fork();// 一个id变量保存了两个不同的值
9 if(id < 0)
10 {
11 printf("fork error\n");
12 return 1;
13 }
14 else if(id == 0)
15 {
16 int cnt = 0;
17 while(1)
18 {
19 printf("我是子进程, pid: %d, ppid: %d | global_value: %d, &global_value: %p\n", getpid(), getppid(), global_value, &global_value);
20 sleep(1);
21 cnt++;
22 if(cnt == 10)
23 {
24 global_value = 300;
25 printf("子进程已经更改了全局的变量啦..........\n");
26 }
27 }
28 }
29 else
30 {
31 while(1)
32 {
33 printf("我是父进程, pid: %d, ppid: %d | global_value: %d, &global_value: %p\n", getpid(), getppid(), global_value, &global_value);
34 sleep(2);
35 }
36 }
37 sleep(1);
38 return 0;
39 }
2.
在fork实现的代码中,执行return语句之前,代码的核心逻辑肯定已经跑完了,这个时候OS中已经有两个进程了,所以在执行return语句的时候,其实已经有两个执行流分别执行fork当中的return语句了,那么自然在fork调用结束之后,就会出现两个返回值。
3.
所以平常所说的,在fork调用结束之后,父子进程共享代码是稍有一些不严谨的,因为在fork调用里面核心代码跑完之后,其实就已经有两个执行流了,也就是父子进程已经出现了。
3.如何理解fork返回之后,给父进程返回子进程的pid,而给子进程返回0?
1.
fork之后,子进程如果创建成功是不需要得到父进程的pid的,因为这没有意义,他完全可以通过getppid来获取父进程的pid,所以没有必要用返回值来接收,究其原因就是,子进程找父进程具有唯一性,因为子进程只能有一个父进程。
但是父进程可以有多个子进程,那么父进程找子进程是不具有唯一性的,就需要fork函数返回子进程的pid,通过子进程的pid来确定和找到具体的子进程。
4.如何理解一个id变量,怎么能保存两个值,并且if和elseif语句同时执行?
1.
在fork之后,父子进程谁先执行代码完全由调度器决定,所以父子进程谁先返回,那么就谁先对id变量进行赋值,后一个执行的进程又会对id变量进行写入,因为进程具有独立性,所以这个时候就会发生写时拷贝。写时拷贝
2.
此时id变量打印出来的地址是相同的,但是内容就会不一样了,因为分别在父子进程中各有一个id变量的值了,他们的值是不同的。(这部分内容在虚拟地址空间的时候,我们就已经谈过了)
3.
所以在fork结束之后,执行父子进程的共享代码时,是可以出现两个分支语句同时执行的情况的,那是因为父子进程在执行共享代码的时候,分别进入了不同的分支语句,而我们看到的程序的运行结果就是两个分支语句竟然同时执行了,究其原因就是因为在程序运行过程中,出现了两个进程,也就是两个执行流,才导致了分支语句的“同时执行”。
5.fork常规用法 && fork调用失败的原因
1.
fork的常规用法有两种:第一种就是一个进程执行一个程序,然后让父子进程执行不同的代码块,例如:父进程等待客户端请求,生成子进程来处理请求。
第二种就是一个进程要执行不同的程序,例如fork函数返回之后,调用程序替换函数exec,使得父子进程执行不同的程序。
2.
fork调用失败,有可能是系统中有太多的进程,导致实际用户的进程数超过了最大上限,致使fork调用失败。
跑一下下面这段代码便可以测试出,进程的最大上限数量是多少。
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6
7 int cnt=0;
8 while(1)
9 {
10 pid_t ret=fork();
11 if(ret<0)
12 {
13 printf("fork error!,cnt:%d\n",ret);
14 break;
15 }
16 else if(ret==0)
17 {
18 // child process
19 while(1) sleep(1);
20 }
21 // parent process
22 cnt++;
23 }
24 return 0;
25 }
下面是我的运行结果,可能是操作系统杀死了我的进程,为了保护它自己,所以显示出来的是-1
害害害,然后服务器好像挂掉了,凉凉了。
自己搞了一下,最好的解决办法就是登录到你的服务器后台,我用的是腾讯云服务器,找到对应的服务器的控制台,然后重启云服务器就可以解决了。
二、进程终止(进程退出的三种情况)
1.退出码(你可以不关心进程退出信息,但OS不能不提供获取信息的方式)
1 #include <stdio.h>
2 int addtotarget(int from, int to)
3 {
4 int sum=0;
5 for(int i = from; i < to; i++)
6 {
7 sum+=i;
8 }
9 return sum;
10 }
11 int main()
12 {
13 // 写代码是为了完成某种任务,想要知道任务完成的如何
14 // 就需要通过进程退出码来判断
15 int num = addtotarget(1,100);//计算结果应该是4950,所以退出码应该是1
16 if(num==5050)
17 {
18 return 0;
19 }
20 else
21 {
22 return 1;
23 }
24 // 如果不关心进程的退出码,return 0就行
25 // 如果关心进程的退出码,要返回特定的数据来表明特定的错误。
26
27
28 // 0是进程退出时,对应的退出码,标定进程执行的结果是否正确
29 // return 0;
30 }
1.
有的时候,我们在写main函数的时候,总是要写一个return 0,但是不写这个return 0好像程序运行起来又没有什么错误,这是为什么呢?其实0是进程的退出码,这个值会返回给操作系统,表明进程的退出结果是什么样的,是正常退出呢?还是异常退出?至于平常我们不写return 0也没什么事,是因为C99规定编译器要自动在生成的可重定向目标二进制文件中加入return 0,但是vc6.0并不支持这样的标准,因为他是98年的产品,所以在平常的写代码过程中,我们还是要养成加return 0的好习惯。
2.
错误码的意义:用0表示成功退出,非0表示错误退出,非0具体的数字标识了不同的错误信息。
3.
不同的退出码都必须有相应的退出码的文字描述,来确定进程是因为什么原因而退出的,这个文字描述可以自己定义,也可以使用系统的映射关系来输出错误码的文字描述,但这个使用的并不频繁。
4.
$?是shell当中的一个变量,该变量永远记录最近一个进程在命令行中执行完毕时对应的退出码,变量名是?,取变量名是 $?
把退出码转换成退出码对应的文字描述,可以用strerror,strerror是库提供给我们的输出错误码对应信息描述的一个库函数。
当然我们也可以自己定义错误码的错误信息是什么,这取决于你,当出错的时候,你输出你想输出的错误信息也可以,这就比较自由了。
例如在开辟空间失败时,我就会输出一句错误信息,malloc fail,这完全就是我自定义的错误信息,当然你也可以这么干!
printf("malloc fail\n");
2.进程如何退出(return、exit( )、_exit( ) )
1.
代码执行完毕,结果正确 — return 0
代码执行完毕,结果错误 — return !0 — 退出码起作用,标识错误信息
代码没执行完毕,程序出现异常 — 退出码无意义
以上就是进程退出的三种情况。
2.
进程在退出的时候,可以从main函数return返回,以此结束进程。
也可以调用库函数exit或者使用系统调用接口_exit
如果没有exit,这个进程应该是一直运行不会退出的,但是现在有了exit,进程就会提前终止,并且退出码被设置成了111。
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 int main()
5 {
6
7 printf("hello Linux\n");
8
9 exit(111);
10
11 while(1) sleep(1);
12 }
另外一种提前终止进程的方式是调用系统接口_exit(),此接口的参数和库函数exit()相同都是进程退出码。
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 #include <string.h>
5 int addtotarget(int from, int to)
6 {
7 int sum=0;
8 for(int i = from; i < to; i++)
9 {
10 sum+=i;
11 }
12 _exit(111);
13 // return sum;
14 }
15 int main()
16 {
17
18 printf("hello Linux\n");
19
20 addtotarget(1,100);
21
22 while(1) sleep(1);
23
24}
程序运行结果的退出码的确是111
3.
exit底层用的其实就是_exit,因为库函数实际上就是系统调用接口封装得来的。并且exit在man3号手册库函数,_exit在man2号手册系统调用。
库函数和系统调用接口的关系
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 int main()
5 {
6 printf("hello Linux!");
7 sleep(3);
E> 8 exit();
9 // _exit();
10
11 return 0;
12 }
4.可以看到在没有\n的情况下,调用exit时,程序运行会先睡眠3秒,然后在打印出hello Linux,这是为什么呢?其实是因为没有刷新缓冲区的东西,所以即使hello Linux已经加载到缓存区,也不会立即打印出来,而是等到睡眠之后调用exit结束的时候,才将缓冲区刷新,才会打印出来hello Linux。
但是当调用_exit系统接口的时候,我们看到hello Linux是不会被打印出来的,所以_exit是不会刷新缓冲区的。
5.
得出结论:exit终止进程,会主动刷新缓冲区。_exit终止进程,不会刷新缓冲区。
6.如果缓冲区在操作系统里面,那么exit和_exit都会刷新缓冲区,因为这两个接口终止进程的工作最终都是要依靠操作系统来终止的,所以操作系统更加的底层,缓冲区如果在OS的话,这两个接口都应该刷新缓冲区,但是我们看到的现象并不是这样的,所以就说明缓冲区不在OS,他其实是用户级的缓冲区,至于用户级缓冲区的详谈,放到后面的博文再说。
三、进程等待(回收子进程所有资源,读取子进程退出信息)
1.wait(等待任意的子进程,只能是阻塞等待)
1.
之前在讲进程状态的时候,我们谈到过僵尸进程,当时说僵尸进程其实是一个问题,如果子进程退出,父进程不读取子进程退出的信息,那么子进程就会变为僵尸进程,从而导致内存泄露问题的产生,我们可以通过进程等待的方式来解决僵尸进程问题。
2.
解决僵尸进程,可以让父进程通过进程等待的方式,回收子进程剩余资源(PCB,内核栈等),获取子进程退出信息,父进程需要知道子进程的退出码和执行时间等信息,形象化的比喻就是父进程通过进程等待来给僵尸进程收尸。
如何产生僵尸进程,避免产生僵尸进程
3.
wait可以回收僵尸进程剩余资源。
wait如果等待终止进程成功,将会返回终止进程的id值,如果等待失败则会返回-1,通过man手册可以查到wait具体使用方法,wait在2号手册。
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 #include <string.h>
5 #include <sys/types.h>
6 #include <sys/wait.h>
18 int main()
19 {
20 pid_t id=fork();
21 if(id==0)
22 {
23 // child process
24 int cnt=10;
25 while(cnt)
26 {
27 printf("我是子进程:%d,父进程:%d,cnt:%d\n",get pid(),getppid(),cnt--);
28 sleep(1);
29 }
30 exit(0);// 子进程退出
31 }
32 sleep(15);
33 pid_t ret=wait(NULL);
34 if(ret>0)
35 {
36 printf("wait success:%d!\n",ret);
37 }
38 sleep(5);// 让父进程先别退出,等待5秒
39 }
下面是监控脚本
while :; do ps axj | head -1 && ps axj | grep mytest | grep -v grep; sleep 1; done
4.
通过运行结果可以看出,在前10秒钟,两个进程都是休眠S状态,因为在等待显示器就绪,在接下来的5秒钟,子进程变为了僵尸进程,状态由S变为Z,然后在wait调用结束之后,子进程被父进程成功回收,只剩下继续休眠5秒的父进程,最后父进程成功退出,bash回收父进程资源。
2.输出型参数status(用于修改status(存放进程退出信息)变量的值)
1.
wait和waitpid都有status输出型参数,这个参数可以基于系统调用waitpid、wait的基础上用于获得子进程的退出信息,也就是子进程的退出码和终止信号,在获得这些信息之后,waitpid内部实现的时候,就会修改waitpid外部的status变量了。status的位图结构中的不同的区域代表了不同的进程退出信息的意义,我们只研究status整型的低16个比特位,其中的前7比特位代表子进程终止信号,后8比特位代表进程退出码。
下面可以用一段代码来验证上述所说的
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/wait.h>
4 #include <unistd.h>
5 #include <stdlib.h>
6 #include <assert.h>
7 int main()
8 {
9 pid_t id = fork();
10 assert(id!=-1);
11 if(id==0)
12 {
13 // child process
14 int cnt=5;
15 while(cnt)
16 {
17 printf("child process running,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt--);
18 }
19
20 exit(10);
21 }
22 int status=0;
23 int ret=waitpid(id,&status,0);
24 if(ret>0)
25 {
26 // printf("wait success,exit code:%d,signal number:%d\n",);
27 printf("status:%d\n",status);
28 }
29 return 0;
30 }
2.
status的输出结果为2560,写成16比特位的形式就是0000 1010 0000 0000正如计算器所得,1010实际上就是10,也就是僵尸进程的退出码,表示什么样的结果错误,这完全取决于我们自己,你可以写个printf语句输出你想输出的错误信息,然后终止信号是0 ,表示僵尸进程正常退出。完全符合上面所说的结论。
3.waitpid(可以等待特定的子进程,可以非阻塞等待)
1.
waitpid可以获取子进程退出结果,并且回收僵尸进程剩余资源。
2.
wait和waitpid都有一个参数叫做status,如果传递NULL表示不关心子进程的退出状态信息,否则操作系统会根据该参数,将子进程的退出信息反馈给父进程,另外status不能简单看作一个整型,它具有自己的位图结构,不同区域代表不同的含义。
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 #include <string.h>
5 #include <sys/types.h>
6 #include <sys/wait.h>
18 int main()
19 {
20 pid_t id=fork();
21 if(id==0)
22 {
23 // child process
24 int cnt=5;
25 while(cnt)
26 {
27 printf("我是子进程:%d,父进程:%d,cnt:%d\n",getpid(),getppid(),cnt--);
28 sleep(1);
29 //int*p=NULL;
30 //*p=100;
31 //int a=10;
32 //a/=0;
33 }
34 exit(10);// 子进程退出,设置退出码为10
35 }
36 int status = 0;// 不能直接拿来使用,他有自己的位图结构
37 pid_t ret=waitpid(id,&status,0);
38 if(id >0)
39 {
40 printf("wait success:%d,sig number:%d,child exit code:%d\n",ret,(status & 0x7F),(status>>8)&0x7F);
41 // status>>8 don't change status only >>=8 can change status.
42
43 }
44 sleep(5);// 让父进程先别退出,等待5秒
45}
3.
可以利用位运算符来得到status的前7比特位和后8个比特位,以此来获取到子进程退出信息,是异常终止呢?还是正常终止,结果出错,或结果正确呢?这些进程退出信息都可以通过waitpid和status得到。
4.
僵尸进程的数据和代码资源被操作系统释放了,但是进程的PCB是没有释放的,依旧保留在操作系统里面,例如子进程的退出码和进程终止信号都会被保存到它的PCB当中,当父进程调用系统调用waitpid的时候,子进程PCB中的退出码和终止信号就会被父进程waitpid里的status参数获取到,然后status变量的值就会被status输出型参数给修改掉。
下面就是task_struct中的进程退出码和终止信号以及退出状态等信息,这些信息都会在进程等待的系统调用接口中获取到。
5.
下面是进程正常退出和进程异常终止时,终止信号和退出码的数值,我将退出码设置为10,用于检测status变量的正确性,另外当进程异常终止时,退出码失去意义,linux此时自动将退出码默认设置为0,但我们知道,这个退出码是什么都无所谓了,因为它没有意义。
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 #include <string.h>
5 #include <sys/types.h>
6 #include <sys/wait.h>
18 int main()
19 {
20 pid_t id=fork();
21 if(id==0)
22 {
23 // child process
24 int cnt=5;
25 while(cnt)
26 {
27 printf("我是子进程:%d,父进程:%d,cnt:%d\n",getpid(),getppid(),cnt--);
28 sleep(1);
29 int*p=NULL;// 演示进程终止的段错误
30 *p=100;
31 int a=10;// 演示进程终止的浮点错误
32 a/=0;
33 }
34 exit(10);// 子进程退出,设置退出码为10
35 }
36 int status = 0;// 不能直接拿来使用,他有自己的位图结构
37 pid_t ret=waitpid(id,&status,0);
38 if(id >0)
39 {
40 printf("wait success:%d,sig number:%d,child exit code:%d\n",ret,(status & 0x7F),(status>>8)&0x7F);
41 // status>>8 don't change status only >>=8 can change status.
42
43 }
44 sleep(5);// 让父进程先别退出,等待5秒
45}
11终止信号代表段错误,段错误就是地址错误,因为我们的代码中故意访问了野指针,所以进程会异常退出,打印出进程异常退出的终止信号
8终止信号涵盖所有的算术错误,例如浮点异常等等。
5.
进程等待的本质就是检测子进程的退出信息,然后父进程将子进程的退出信息(退出码和终止信号等)通过status变量获取,也就是通过waitpid或wait等系统调用获取。
4.宏WIFEXITED和WEXITSTATUS(获取进程终止情况和退出码)
WIFEXITED(status):若子进程是正常终止,则返回结果为真,用于查看进程是否正常退出。
WEXITSTATUS(status):若进程正常终止,也就是进程终止信号为0,这时候会返回子进程的退出码。
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/wait.h>
4 #include <unistd.h>
5 #include <stdlib.h>
6 #include <assert.h>
7 int main()
8 {
9 pid_t id = fork();
10 assert(id!=-1);
11 if(id==0)
12 {
13 // child process
14 int cnt=10;
15 while(cnt)
16 {
17 printf("child process running,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt--);
18 sleep(1);
19 }
20 exit(10);
21 }
22 int status=0;
23 // 1.让OS释放子进程的僵尸状态,获取子进程的退出结果
24 // 2.在父进程等待期间,子进程还没退出的时候,父进程的状态就是阻塞等待
25 int ret=waitpid(id,&status,0);
26 if(ret>0)
27 {
28 // 是否正常退出
29 if(WIFEXITED(status))
30 {
31 // 判断子进程退出码是什么
32 printf("child process exit normally,exit code:%d\n",WEXITSTATUS(status));
33 }
34 else
35 {
36 printf("child process don't exit normally\n");
37 }
38 // printf("wait success,exit code:%d,signal number:%d\n",(status>>8)&0xFF,status & 0x7F);
39 }
40 return 0;
41 }
下面是进程正常终止的结果。
下面是进程异常终止的结果。
5.进程的阻塞和非阻塞等待(多次非阻塞等待 ⇒ 轮询)
1.
当子进程还没有死的时候,也就是没有退出的时候,父进程调用的wait或waitpit需要等待子进程退出,系统调用接口也不返回,这段时间父进程什么都没做,就一直等待子进程退出,这样的等待方式,称之为阻塞式等待。
2.
非阻塞式等待就是,不停的检测子进程状态,每一次检测之后,系统调用立即返回,在waitpid中的第三个参数设置为WNOHANG,即为父进程非阻塞式等待。
3.
如果等待的子进程状态没有发生变化,则waitpid会返回0值。多次非阻塞等待子进程,直到子进程退出,这样的等待方式又称之为轮询。如果等待的进程不是当前父进程的子进程,则waitpid会调用失败。
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/wait.h>
4 #include <unistd.h>
5 #include <stdlib.h>
6 #include <assert.h>
7 int main()
8 {
9 pid_t id = fork();
10 assert(id!=-1);
11 if(id==0)
12 {
13 // child process
14 int cnt=5;
15 while(cnt)
16 {
17 printf("child process running,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt--);
18 sleep(3);
19 }
20 exit(10);
21 }
22 int status=0;
23 while(1)
24 {
25 pid_t ret=waitpid(id,&status,WNOHANG);// WNOHANG是非阻塞等待,子进程没有退出,父进程检测一次之后,立即返回
26 if(ret == 0)
27 {
28 // waitpid调用成功,子进程没有退出
29 printf("Wait for success,but the child process is still running\n");
30 }
31 else if(ret == id)
32 {
33 // waitpid调用成功,子进程退出
34 printf("wait success,exit code:%d,signal number:%d\n",(status>>8)&0xFF,status & 0x7F);
35 break;
36 }
37 else
38 {
39 // waitpid调用失败,例如等待了一个不属于该父进程的子进程
40 printf("The waitpid call failed\n");
41 break;
42 }
43 sleep(1);
44 }
45 return 0;
46 }
4.
非阻塞等待有一个好处就是,不会像阻塞式等待一样,父进程什么都做不了,而是在轮询期间,父进程还可以做其他的事情。
例如下面代码中,利用了回调函数的方式,来让父进程轮询等待子进程期间,还可以处理其他任务。
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/wait.h>
4 #include <unistd.h>
5 #include <stdlib.h>
6 #include <assert.h>
7 #include <string.h>
8 void task1()
9 {
10 printf("Process task1\n");
11 }
12 void task2()
13 {
14 printf("Process task2\n");
15 }
16 void task3()
17 {
18 printf("Process task3\n");
19 }
20 typedef void (*func_t)();// 定义一个函数指针类型。
21 func_t Process_task[10];
22 void loadtask()
23 {
24 memset(Process_task,0, sizeof(Process_task));
25 Process_task[0]=task1;
26 Process_task[1]=task2;
27 Process_task[2]=task3;
28 }
31 int main()
32 {
33 pid_t id = fork();
34 assert(id!=-1);
35 if(id==0)
36 {
37 // child process
38 int cnt=5;
39 while(cnt)
40 {
41 printf("child process running,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt--);
42 sleep(1);
43 }
44 exit(10);
45 }
46 loadtask();// 加载任务到函数指针数组里面。
47 int status=0;
48 while(1)
49 {
50 pid_t ret=waitpid(id,&status,WNOHANG);// WNOHANG是非阻塞等待,子进程没有退出,父进程检测一次之后,立即返回
51 if(ret == 0)
52 {
53 // waitpid调用成功,子进程没有退出
54 printf("Wait for success,but the child process is still running\n");
55 for(int i=0; Process_task[i]!=NULL; i++)
56 {
57 Process_task[i]();// 回调函数的方式,让父进程在轮询期间,做其他事情
58 }
59 }
60 else if(ret == id)
61 {
62 // waitpid调用成功,子进程退出
63 printf("wait success,exit code:%d,signal number:%d\n",(status>>8)&0xFF,status & 0x7F);
64 break;
65 }
66 else
67 {
68 // waitpid调用失败,例如等待了一个不属于该父进程的子进程
69 printf("The waitpid call failed\n");
70 break;
71 }
72 sleep(1);
73 }
74 return 0;
75}
四、进程的程序替换(子进程执行新程序的代码和数据)
1.创建子进程的目的?
创建子进程一般有两个目的:
1.让子进程执行父进程代码的一部分,也就是执行父进程对应的磁盘上的代码和数据的一部分。
2.让子进程加载磁盘上指定的程序到内存中,使其执行新的程序的代码和数据,这就是进程的程序替换。
2.进程的程序替换
2.1 单个进程的程序替换
下面函数参数中的…是可变参数列表,可以给C语言函数传递不同个数的参数。
int execl(const char* path,const char* arg,...); 程序替换函数
例如下面这些C函数都有可变参数列表。
=1.
要执行一个程序,首先就是找到这个程序,然后在执行这个程序,执行程序的时候,也拥有不同的执行方式,通过执行选项的不同便可以使得程序以多种不同的方式执行。
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6 // .c --> .exe --> load into memory --> process --> running
7 printf("The process is running...\n");
8
9 execl("/usr/bin/ls","ls","-a","-l","--color=auto",NULL);// 传参以NULL结尾,来表示传参结束
10
11 printf("The process finishes running...\n");
12
13 return 0;
14 }
下面是运行结果,我们可以利用execl将已经封装好的指令函数调用起来,有一个现象就是第三句printf代码没有被执行,这个问题,在你看了替换原理部分内容之后,便可得出答案。
2.
exec系列的函数只有在调用失败的时候才有返回值,这个返回值是-1,那为什么exec系列的函数没有调用成功时的返回值呢?
答案:没有必要,因为exec系列函数调用结束之后,代码就全都被替换了,就算给你返回值你也使用不了,因为代码全都替换为指定程序的代码了,所以只要exec系列函数返回,那就一定发生调用错误了。
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 int main()
5 {
6 // .c --> .exe --> load into memory --> process --> running
7 printf("The process is running...\n");
8
9 execl("/usr/bin/lsafa","ls","-a","-l","--color=auto",NULL);
//调用一定发生错误,因为lsafa是不存在的程序
10
11 perror("execl");// 打印错误信息
12
13 printf("The process finishes running...\n");
14
15 exit(1);
16 return 0;
17 }
2.2 父进程派生子进程的程序替换
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 #include <assert.h>
5 #include <sys/types.h>
6 #include <sys/wait.h>
7 int main()
8 {
9 printf("The process is running...\n");
10 pid_t id = fork();
11 assert(id!=-1);
12 if(id==0)
13 {
14 //child process
15 sleep(1);
16 execl("/usr/bin/ls","ls","-a","-l",NULL);
17 exit(1);// 如果调用失败,直接让子进程退出
18 }
19 int status = 0;
20 pid_t ret=waitpid(id,&status,0);
21 if(ret == id)
22 {
23 printf("wait success, exit code:%d , signal number:%d\n",(status>>8)&0xFF,status&0x7F);
24 }
25 }
子进程的程序替换是不会影响父进程的,因为进程具有独立性。
下面是运行结果,子进程被替换为ls进程,ls进程正常退出退出码为0,终止信号为0.
故意使得程序替换失败,则进程退出也是按照我们设定的退出码所退出的。
3.程序替换原理
1.
将磁盘中指定程序的代码和数据直接覆盖掉物理内存中原来正在运行的进程的代码和数据,以达到程序替换的效果,这就是程序替换的本质。
2.
所以在进程替换的时候是没有创建新进程的,而是在原有进程基础上,将指定程序的代码和数据覆盖到原来的代码和数据里。
3.
当父进程派生的子进程发生程序替换时,防止父子进程原先共享的代码段和数据段被修改,操作系统会进行写时拷贝,将代码段和数据段重新复制一份给子进程,让子进程程序替换之后,不会影响父进程。这就是进程之间的独立性。
4.虚拟地址空间和页表可以保证进程之间的独立性,一旦有执行流要改变代码或数据,就会发生写时拷贝。
所以不是只有数据可能发生写入,代码也是有可能发生写入的,这两种情况都会发生写时拷贝。
4.exec系列替换函数详解(6个封装函数,1个系统调用)
1.
l代表list,指的是将参数一个一个的传入execl函数
2.
p是指path,不用传程序的路径,只需要传程序的名字就够了,此函数会自动在PATH环境变量的路径下面去查找对应的程序。
execlp中的两个ls是不重复的,一个是告诉操作系统要执行什么程序,一个是告诉操作系统怎么执行程序。
3.
v是指vector,指的是该函数可以将所有的执行参数放到数组里面,统一进行传参,而不是使用可变参数列表的方式,来一个一个的传执行参数。
4.
PATH和vector,指的是不用传程序路径,并且可以将执行参数放到数组里面,统一进行传参
5.
execle中的e代表自定义环境变量。
下面定义的env指针数组就是自定义环境变量,也就意味着,程序替换的时候,不用系统环境变量,用自己定义的环境变量。
也可以不传自定义环境变量,而用系统的环境变量传给子进程替换的程序,只不过替换的程序mybin.c没有打印出来全部的环境变量,而是只打印了PATH和PWD的值。
利用putenv将指定的自定义环境变量导入到环境变量表里面,然后将environ作为参数传给替换程序,这样替换程序就可以获得自己定义的和系统默认的环境变量。
6.
当执行一个新的程序的时候,exec系列函数是要比main函数先执行的,因为将程序加载到内存中,其实是通过linux加载器exec系列函数实现的,程序肯定是先加载后执行的,所以一定是先执行exec后执行程序中的main函数。
main函数也是有参数的,它也需要被调用,它的参数其实就是来自于execle的。
7.虽然6个exec函数中有4个函数的参数是没有传环境变量的,但是被替换程序依旧可以拿到系统默认的环境变量,其实就是通过虚拟地址空间中environ的方式,让被替换程序拿到。
8.
下面是mm_struct结构体源码,可以看到虚拟地址空间中有维护环境变量的区域,这些区域会通过页表映射到物理地址中,替换后的子进程将天然的拥有这些系统默认环境变量。所以即使exec某些函数没有传环境变量给被替换函数的main函数,被替换函数依旧是可以拿到这些环境变量的。
9.
execvpe其实就是vector+PATH+env,我们需要自己传环境变量,并且不用可变参数列表的方式传执行参数,而是用指针数组的方式来一并将执行参数传递,传程序名时可以不带程序路径,系统会帮我们找。
10.
真正执行程序替换的其实只有execve这一个系统调用接口,其他的6个都是在execve的基础上封装得来的。只有execve在man2号手册,其他都在3号手册。
带e的函数都需要自己组装环境变量,可以选择自己的、或系统的、或系统和自己的环境变量。
5.派生子进程替换我们自己写的程序
1.
makefile默认只能生成一个可执行程序,从上向下扫描时,只要形成一个可执行程序之后,后面的指令就不会被执行了。
2.
所以,我们可以利用.PHONY生成伪目标all,让all依赖于两个exe文件,这样就可以编译两个源文件了。
3.
我们的程序中没有环境变量PATH,带p没有意义,所以这里使用execl函数来进行程序替换。
4.如果子进程可以替换为我们自己写的程序的话,那其他的语言程序其实是都可以调用的。python,shell,c++这些程序都可以在子进程中进行程序替换。
所以,程序替换,可以调用任何后端语言的可执行程序。
五、实现一个简易shell
1.shell代码实现
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/types.h>
5 #include <sys/wait.h>
6 #include <assert.h>
7 #include <string.h>
8 #define NUM 1024
9 #define OPT_NUM 64
10
11 char command_line_array[NUM];
12 char *myargv[OPT_NUM];//指针数组,每个指针指向命令行被切割后的字符串
13 int lastcode=0;
14 int lastsig=0;
15
16 int main()
17 {
18 while(1)
19 {
20
21 printf("[%s@%s 当前路径]#",getenv("USER"),getenv("HOSTNAME"));
22 //获取用户输入
W> 23 char *s=fgets(command_line_array,sizeof(command_line_array)-1,stdin);//读取字节数最大为1023留出一个\0
24 assert(s!=NULL);
25 //将获取输入时输入的回车赋值成反斜杠0
26 command_line_array[strlen(command_line_array)-1] = 0;
27
28 //将命令行输入的字符串,进行字符串切割,以空格为分隔符
29 //空格全都换成反斜杠0,或者用strtok
30 myargv[0]=strtok(command_line_array," ");
31 int i=1;
32 if(strcmp(myargv[0],"ls") == 0 && myargv[0]!= NULL)//我们自己在ls的命令行参数表中手动加上执行颜色命令。
33 {
34 myargv[i++]=(char*)"--color=auto";
35 }
36
W> 37 while(myargv[i++]=strtok(NULL," "));
38
39 // 如果是cd命令,不需要创建子进程,让shell进程执行cd命令就可以,本质就是执行系统接口chdir
40 // 像这种不需要派生子进程执行,而是让shell自己执行的命令,我们称之为内建或内置命令。
41 if(myargv[0] != NULL && strcmp(myargv[0],"cd")==0)
42 {
43 if(myargv[1] != NULL)
44 {
45 chdir(myargv[1]);//将shell进程的工作目录改为cd的路径
46 continue;
47 }
48 }
49 // 完成另一个内建命令echo的运行,保证$?可以运行
50 if(myargv[0]!=NULL && myargv[1]!=NULL && strcmp(myargv[0],"echo")==0)
51 {
52 if(strcmp(myargv[1],"$?") == 0)
53 {
54 printf("%d,%d\n",lastcode,lastsig);
55 }
56 else
57 {
58 printf("%s\n",myargv[1]);
59 }
60 continue;
61 }
62
63 // 最后以NULL结尾,切割的字符串中已经没有字符串时,函数返回NULL
64 #ifdef DEBUG
65 for(int i=0;myargv[i],i++)
66 {
67 printf("myargv[%d]:%s\n",myargv[i]);
68 }
69 #endif
70 //执行命令
71 pid_t id=fork();
72 assert(id!=-1);
73 if(id==0)
74 {
75 execvp(myargv[0],myargv);
76 exit(1);//如果程序替换失败,直接让子进程退出
77 }
78 int status=0;
W> 79 pid_t ret = waitpid(id,&status,0);
80 assert(ret > 0);
81 lastcode = ((status>>8) & 0xFF);
82 lastsig = (status & 0x7F);
83
84 }
85 return 0;
86 }
2.什么是当前路径?(当前进程的工作目录 && cd底层实现用chdir)
1 #include <stdio.h>
2 #include <unistd.h>
3 int main()
4 {
5 while(1)
6 {
7 printf("我是一个进程:%d\n",getpid());
8 sleep(1);
9 }
10 return 0;
11 }
查看进程的指令:ls /proc/进程pid/ -al
1.
可以看到进程有两个路径,一个是cwd一个是exe,exe路径代表当前进程执行的是磁盘上的哪个路径下的程序,可以看到执行的是myproc二进制可执行程序,cwd代表current work directory,代表当前进程的工作目录,所以实际上当前路径就是当前进程的工作目录。
2.
在模拟shell的实现代码中,cd到其他目录,pwd之后的路径实际上是没有变化的,因为pwd实际上pwd的是父进程shell的路径,而父进程的cwd路径始终是未改变的,而执行cd命令的是子进程,所以子进程的cwd路径是会改变的。
3.
系统给我们提供了一个系统调用接口叫做chdir,用于改变当前进程的工作目录cwd路径,实际上cd能够进入指定路径下的目录,底层实现上就是改变了shell(bash)进程的cwd路径,所以pwd时,随时随地打印出来的就是shell进程的工作目录。
1 #include <stdio.h>
2 #include <unistd.h>
3 int main()
4 {
5 chdir("/home/wyn"); //修改当前进程的工作目录为/home/wyn
6 while(1)
7 {
8 printf("我是一个进程:%d\n",getpid());
9 sleep(1);
10 }
11 return 0;
12 }
4.
所以如果我们模拟实现的shell也想实现cd改变路径的功能,实际上是不可以创建子进程的,因为子进程程序替换执行cd,父进程的工作目录是没有改变的,所以直接将这一种情况单独拿出来进行判断,在这种情况下,直接让父进程执行cd命令,修改父进程的工作目录即可。
5.
如下所示,直接利用系统调用接口chdir将父进程shell的工作目录改为cd的路径。
并且不去创建子进程,用continue跳过while循环中剩余的代码。
3.shell内建/内置命令(shell自己执行的命令,而不是派生子进程进行程序替换来执行)
1.
像上面的cd命令实际上就是shell的内建命令,因为这样的命令不需要派生子进程来进行程序替换执行,直接让父进程执行就ok,这样的指令就是shell自带的命令,我们称之为内建命令或内置命令。
2.
这也就能解释为什么echo能够打印本地变量了,我们之前将echo理解为一个可执行程序,也就是shell的子进程,但是我们说子进程只能继承父进程的环境变量,而不能继承本地变量,所以当时就陷入echo为什么能够打印出本地变量的疑问当中,因为如果echo是子进程的话,他是没有继承本地变量的。
但现在我们就知道原因了,echo实际上不是shell的子进程,而是shell的内建命令,是shell自己来执行的指令,shell当然拥有本地变量了,当然也就能够打印本地变量喽。