前言
本文介绍了进程终止、进程等待、进程替换等进程控制的相关概念。
一、再识fork
我们在之前的内容中已经了解过系统调用fork,今天我们再来深入了解一下fork。
- 我们已经知道了fork有两个返回值,但是为什么会出现这种现象呢?
fork函数的实现在操作系统内部,当这个函数准备返回时,它的核心代码已经执行完了也就是子进程已经被创建了,并且已经在OS的运行队列中准备被调度了。
fork后,有两个执行流,父子进程是共享的,所以return会被父子进程各自调度一次,也就是调度两次。 - 为什么给父进程返回子进程的pid,给子进程返回0呢?
子进程的父进程只有一个,而父进程可以有很多个子进程,这是一个现实的事情,一个孩子只有一个父亲,而一个父亲可以有很多个孩子。
那么给父进程返回子进程的pid,给子进程返回0,可以很方便的区分父子进程。 - 同一个变量,为什么会有两个不同的值呢?
返回的本质是写入,谁先返回,就是谁先写入,因为进程具有独立性,因此会发生写时拷贝。变量的地址相同但是内容不同。 - fork的使用场景
(1)父进程希望子进程执行一部分父进程的代码,用if和else if将父进程和子进程的代码分隔开(现实:父亲希望子女继承家业,成为自己的帮手);
(2)父进程希望子进程执行另一个程序,用程序替换实现(现实:孩子要闯出属于自己的天地)。 - fork失败
(1)系统中有太多的进程;
(2)实际用户的进程数超过了限制。
二、退出码
退出码是用来标识一个进程任务执行结果的情况。因为成功只有一种情况,而失败的情况很多,因此,一般情况下0表示执行成功,非0表示执行失败。非0的数字不同,所表示的错误不同。系统对于退出码一般都有着相应的文字藐视,当然我们也可以自定义,也可以直接使用系统给定的映射关系。(例如,strerror这个函数)
文件test4.c
1 #include<stdio.h> 2 #include<string.h> 3 int main() 4 { 5 for(int i = 0;i < 200; ++i) 6 { 7 printf("%d : %s\n",i,strerror(i)); 8 } 9 return 0; 10 }
运行:
可以看到strerror函数有134种系统映射好的文字描述的错误码。
三、进程终止
1.进程终止的情况
- 代码运行完,并且结果正确;
- 代码运行完,结果不正确;
- 代码未运行完,进程异常结果无意义。
2.进程退出的方法
- 从主函数(main函数)return返回;
- 程序的任意位置调用exit(code);
- 系统调用_exit(code);
3._exit()和exit()
_exit()和exit()不同:
- _exit是系统调用;exit是库函数,是对系统调用的上层封装。
- _exit终止进程,不会刷新缓冲区;exit终止进程,会刷新缓冲区。
- 调用exit:
文件test4.c
1 #include<stdio.h> 2 #include<stdlib.h> 3 int main() 4 { 5 printf("hello world!"); 6 sleep(1); 7 exit(1); 8 return 0; 9 }
- 调用_exit()
文件test4.c
1 #include<stdio.h> 2 #include<unistd.h> 3 #include<stdlib.h> 4 int main() 5 { 6 printf("hello world!"); 7 sleep(1); 8 _exit(1); 9 return 0; 10 }
同样的代码,调用exit和调用_exit终止进程后,得到的结果却不同。exit会主动刷新缓冲区,而_exit不会主动刷新缓冲区,这是因为缓冲区所在的位置。
四、进程等待
1.进程等待的场景
之前讲进程状态的时候,我们讲到一种特殊的转态:僵尸状态:子进程退出,父进程没有等待子进程(没有回收子进程的资源),会造成子进程处于僵尸状态,造成内存泄漏,同时这种状态的进程无法被杀死(甚至是kill -9都不能杀死这个进程,因为他已经死了)。为了避免这种情况,为了让父进程知道子进程的任务完成的怎么样,我们需要让父进程通过进程等待的方式,回收子进程退出后的资源。
2.进程为什么要等待
1.父进程通过进程等待的方式回收子进程的资源;
2.父进程要获取子进程的退出信息
3.进程等待的方式
wait
**参数:**输出型参数,一般设置为status,用于获取进程的退出状态,如果不关心进程的退出状态可以设置为NULL;
**返回值:**wait成功返回等待的子进程pid,等待失败返回-1。
waitpid
参数:
- pid,设置为-1时,表示等待任意一个进程,和wait一样;也可以指定等待的进程pid,就只能等待指定的进程。
- status,输出型参数,由操作系统填充进程退出的信息,如果传递NULL说明不关心该信息。
status不能当作一个简单的整数对待,而应该被当作一个位图看待:
它的次低8位表示它的退出状态(退出码)((status >> 8) & 0xff);
低7位表示终止信号(&0x7f)。 - 设置为0时表示,阻塞式等待进程;设置为WNOHANG时,表示非阻塞式等待进程。
返回值:如果等待成功,返回所等待的进程pid;如果等待失败,则返回0。
进程等待的信息存在哪里
子进程和父进程都有各自对应PCB,当子进程退出且父进程未回收子进程资源时,子进程的退出信息以及子进程的资源信息都保存在子进程的PCB中。waitpid系统调用,以操作系统的身份执行代码,找到子进程,将输出型参数status参数传到子进程内部,把子进程的退出信息设置进status中,再将status取出,父进程就获得了子进程的退出信息。
补充
对于获取进程的退出结果,我们可以不采用对输出型参数进行位操作,因为Linux提供了对应操作的宏。
WIFEXITED(status):若子进程为正常终止,返回的状态,则为真(查看进程是否正常退出)
WEXITSTATUS(status):若WIFEXITED为非0,则用它提取子进程的退出码(查看进程的退出码)
五、进程替换
进程替换,就是在原进程执行到一定时间是,将原进程替换成磁盘中指定的一个新的进程,然后去执行新的进程(原进程剩余部分不会执行)。
1.进程函数
注意:对于execl系列函数的参数要以NULL作为结尾(参数是可以变参数)
exec系列函数的返回值,如果替换成功,则没有返回值,而替换失败则返回-1。
2.演示
execl函数:
文件mytest.c
1 #include<stdio.h> 2 #include<unistd.h> 3 int main() 4 { 5 printf("process is running...\n"); 6 execl("/usr/bin/ls","ls","--color=auto","-a","-l",NULL); 7 printf("process running done!"); 8 return 0; 9 }
其他
其他函数的用法:
l(list):表示参数采用列表;
v(vector):表示参数采用数组(将所有执行的参数传入数组中进行统一传递,不用使用可变参数);
p(path):自动搜索环境变量(PATH);
e(env):表示自己维护环境变量。
execlp
文件mytest.c
1 #include<stdio.h> 2 #include<unistd.h> 3 int main() 4 { 5 printf("process is running...\n"); 6 execlp("ls","ls","--color=auto","-a","-l",NULL); 7 printf("process running done!"); 8 return 0; 9 }
execv
文件mytest.c
1 #include<stdio.h> 2 #include<unistd.h> 3 int main() 4 { 5 printf("process is running...\n"); 6 char* const argv[] = { 7 "ls", 8 "--color=auto", 9 "-a", 10 "-l", 11 NULL 12 }; 13 execv("/usr/bin/ls",argv); 14 printf("process running done!"); 15 return 0; 16 }
execvp
文件mytest.c
1 #include<stdio.h> 2 #include<unistd.h> 3 int main() 4 { 5 printf("process is running...\n"); 6 char* const argv[] = { 7 "ls", 8 "--color=auto", 9 "-a", 10 "-l", 11 NULL 12 }; 13 execvp("ls",argv); 14 printf("process running done!"); 15 return 0; 16 }
execle
文件myprocess.c
1 #include<stdio.h> 2 #include<stdlib.h> 3 int main() 4 { 5 printf("PATH:%s\n",getenv("PATH")); 6 printf("PWD:%s\n",getenv("PWD")); 7 printf("MYENV:%s\n",getenv("MYENV")); 8 printf("我是一个进程\n"); 9 printf("我是一个进程\n"); 10 printf("我是一个进程\n"); 11 printf("我是一个进程\n"); 12 return 0; 13 }
文件mytest.c
1 #include<stdio.h> 2 #include<unistd.h> 3 int main() 4 { 5 printf("process is running...\n"); 6 char* const envi[] = { 7 (char*)"MYENV=123454", 8 NULL 9 }; 10 execle("./myprocess","myprocess",NULL,envi); 11 printf("process running done!"); 12 return 0; 13 }
execve
只有它是系统调用,其他exec系列的函数都是基于它做的封装,目的是为了有更多的应用场景,有更多的选择。
3.替换原理
为什么调用替换函数后,原程序的剩余部分不再执行?
用fork创建子进程后,执行的可能是父进程相同的进程(也可能执行与父进程不同道德代码分支),子进程往往会调用exec系列的函数以执行另一个程序。
当进程调用exec系列函数时,该进程的用户空间的代码和朱家具完全被新程序替换,从新程序的启动例程开始执行。
要注意,调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
简单来说,程序替换的本质时将指定的程序的代码和数据加载到指定的位置,覆盖原本的代码和数据(即,进程替换时并没有创建新的进程)。
因此,原进程后续的代码会被新进程的代码和数据所覆盖,因此就不会执行剩余代码了。
这也是为什么只有当程序替换失败才会有返回值,因为如果替换成功就不会执行接下来的代码,返回值就无意义。
总结
以上就是今天要讲的内容,本文介绍了进程终止、进程等待、进程替换等的相关概念。本文作者目前也是正在学习Linux相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。
最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!