进程控制——fork-and-exec、system、wait
forc-and-exec流程
父进程与子进程之间的关系十分复杂,最大的复杂点在于进程间相互调用。Linux下这一流程称为fork-and-exec。父进程通过fork的方式产生一个一模一样的子进程,然后被复制出来的子进程再以exec的方式来执行实际要进行的进程,最终成为一个子进程的存在。整个流程如下
API解释
fork
原型
#include <unistd.h>
pid_t fork(void);
功能
从调用该函数的进程复制出子进程,被复制的进程则被称为父进程,复制出来的进程称为子进程。
复制后有两个结果:
1)依照父进程内存空间样子,原样复制地开辟出子进程的内存空间
2)由于子进程的空间是原样复制的父进程空间,因此子进程内存空间中的代码和数据和父进程完全相同
其实复制父进程的主要目的,就是为了复制出一块内存空间,只不过复制的附带效果是,子进程原样的拷贝了一份其实复制父进程的主要目的,就是为了复制出一块内存空间,只不过复制的附带效果是,子进程原样的拷贝了一份
参数
无
返回值
由于子进程原样复制了父进程的代码,因此父子进程都会执行fork函数,当然这个说法有些欠妥,但是暂且这么理解。
1)父进程的fork,成功返回子进程的PID,失败返回-1,errno被设置。
2)子进程的fork,成功返回0,失败返回-1,errno被设置。
代码演示
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <sys/types.h>
4 #include <unistd.h>
5
6 int main(void)
7 {
8 pid_t ret = 0;
9
10
11 printf("befor fork\n");
12
13 ret = fork();
14 if(ret > 0)
15 {
16 printf("parent PID = %d\n", getpid());
17 printf("parent ret = %d\n", ret);
18 sleep(1);
19 }
20 else if(ret == 0)
21 {
22 printf("child PID = %d\n", getpid());
23 printf("child ret = %d\n", ret);
24 }
25
26 printf("after fork\n\n");
27
28 // while(1);
29 return 0;
30 }
依据fork返回值不同来区分父子进程,进而在父子进程中执行不同代码
浅析复制原理
Linux有虚拟内存机制,所以父进程是运行在虚拟内存上的,虚拟内存是OS通过数据结构基于物理内存模拟出来的,因此底层的对应的还是物理内存。复制时子进程时,会复制父进程的虚拟内存数据结构,那么就得到了子进程的虚拟内存,相应的底层会对应着一片新的物理内存空间,里面放了与父进程一模一样代码和数据。
父子进程各自会执行哪些代码
复制出子进程后,父子进程各自都有一份相同的代码,而且子进程也会被运行起来,那么我们来看一下,父子进程各自会执行哪些代码。
父进程
①执行fork前的代码
②执行fork函数。父进程执行fork函数时,调用成功会返回值为子进程的PID,进入if(ret > 0){}中,执行里面的代码。if(ret > 0){}中的代码只有父进程才会执行。
③执行fork函数后的代码
子进程
①fork前的代码。尽管子进程复制了这段代码,但是子进程并不会执行,子进程只从fork开始执行。
②子进程调用fork时,返回值为0,注意0不是PID。进入if(ret == 0){},执行里面的代码。if(ret == 0){}中的代码只有子进程执行。
③执行fork后的代码
子进程会继承父进程的哪些属性
子进程继承如下性质
①用户ID,用户组ID
②进程组ID
③会话期ID
④控制终端
⑤当前工作目录
⑥根目录
⑦文件创建方式屏蔽字
⑧环境变量
⑨打开的文件描述符
子进程独立的属性
①进程ID。
②不同的父进程ID。
③父进程设置的锁,子进程不能被继承。
exec加载器
父进程fork复制出子进程的内存空间后,子进程内存空间的代码和数据和父进程是相同的,这样没有太大的意义,我们需要在子进程空间里面运行全新的代码,这样才有意义。
怎么运行新代码?
我们可以在if(ret==0){}里面直接写新代码,但是这样子很麻烦,如果新代码有上万行甚至更多的话,这种做法显然是不行的,因此就有了exec加载器。有了exec后,我们可以单独的另写一个程序,将其编译好后,使用exec来加载即可。
exec函数族
exec的函数有很多个,它们分别是execve、execl、execv、execle、execlp、execvp,都是加载函数。其中execve是系统函数,其它的execl、execv、execle、execlp、execvp都是基于execve封装得到的库函数,因此我们这里重点介绍execve函数
原型
#include <unistd.h>
int execve(const char *filename, char **const argv, char **const envp);
功能
向子进程空间加载新程序代码(编译后的机器指令)。
将新程序代码加载(拷贝)到子进程的内存空间,替换掉原有的与父进程一模一样的代码和数据,让子进程空间运行全新的程序。
参数
filename:新程序(可执行文件)所在的路径名。
可以是任何编译型语言所写的程序,比如可以是c、c++、汇编等,这些语言所写的程序被编译为机器指令后,都可以被execve这函数加载执行。正是由于这一点特性,我们才能够在C语言所实现的OS上,运行任何一种编译型语言所编写的程序。
疑问:java可以吗?
java属于解释性语言,它所写的程序被编译后只是字节码,并不是能被CPU直接执行的机器指令,所以不能被execve直接加载执行,而是被虚拟机解释执行。execve需要先加载运行java虚拟机程序,然后再由虚拟机程序去将字节码解释为机器指令,再有cpu去执行
argv:传给main函数的参数,比如我可以将命令行参数传过去
envp:环境变量表
返回值
函数调用成功不返回,失败则返回-1,且errno被设置。
代码演示
new_process.c
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 //extern char **environ;
5
6 int main(int argc, char **argv, char **environ)
7 {
8 int i = 0;
9
10 for(i=0; i<argc; i++)
11 {
12 printf("%s ", argv[i]);
13 }
14 printf("\n---------------------\n");
15
16
17 for(i=0; NULL!=environ[i]; i++)
18 {
19 printf("%s\n", environ[i]);
20 }
21 printf("\n---------------------\n");
22
23
24 return 0;
25 }
new_process.c编译成可执行文件new_pro
main.c
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <sys/types.h>
4 #include <unistd.h>
5
6
7
8 int main(int argc, char **argv)
9 {
10 pid_t ret = 0;
11
12 ret = fork();
13 if(ret > 0)
14 {
15 sleep(1);
16 }
17 else if(ret == 0)
18 {
19 extern char **environ;
20 //int execve(const char *filename, char **const argv, char **const envp);
21 char *my_argv[] = {"fds", "dsfds", NULL};
22 char *my_env[] = {"AA=aaaaa", "BB=bbbbb", NULL};
23 execve("./new_pro", my_argv, my_env);
24 }
25
26 return 0;
27 }
Linux在命令行执行./a.out,程序是如何运行起来的
①窗口进程先fork出子进程空间
②调用exec函数加载./a.out程序,并把命令行参数和环境变量表传递给新程序的main函数的形参
Windows双击快捷图标,程序是怎么运行起来的
①图形界面进程fork出子进程空间
②调用exec函数,加载快捷图标所指向程序的代码.以图形界面方式运行时,就没有命令行参数了,但是会传递环境变量表。
system函数
如果我们需要创建一个进子进程,让子进程运行另一个程序的话,可以自己fork、execve来实现,但是这样的操作很麻烦,所以就有了system这个库函数,这函数封装了fork和execve函数,调用时会自动的创建子进程空间,并把新程序的代码加载到子进程空间中,然后运行起来。
原型
#include <stdlib.h>
int system(const char *command);
功能
创建子进程,并加载新程序到子进程空间,运行起来。
参数
command:新程序的路径名
新程序的路径名如果包含在$PATH环境变量中,则可以直接写程序名。否则要写出新程序的路径(绝对路径 or 相对路径)
返回值
有点复杂,参考Linux system函数返回值
进程资源回收
进程运行终止后,不管进程是正常终止还是异常终止的,必须回收进程所占用的资源。
为什么要回收进程的资源?
①程序代码在内存中动态运行起来后,才有了进程,进程既然结束了,就需要将代码占用的内存空间让出来(释放)。
②OS为了管理进程,为每个进程在内存中开辟了一个task_stuct结构体变量,进程结束了,那么这个结构体所占用的内存空间也需要被释放。
③等其它资源
由谁来回收进程资源
由父进程来回收,父进程运行结束时,会负责释放子进程资源。
僵尸进程
子进程终止了,但是父进程还活着,父进程在没有回收子进程资源之前,子进程就是僵尸进程。
为什么子进程会变成僵尸进程?
子进程已经终止不再运行,但是父进程还在运行,它没有释放子进程占用的资源,所以就变成了占着资源不拉屎僵尸进程。就好比人死后不腐烂,身体占用的资源得不到回收是一样的,像这种情况就是所谓的僵尸。
代码演示
# include <stdio.h>
# include <stdlib.h>
# include <sys/types.h>
# include <unistd.h>
int main(void)
{
pid_t ret=0;
ret=fork();
if(ret>0)
{
while(1);
}
else if(ret==0)
{ }
return e;
}
ps查看到的进程状态
R 正在运行
S 处于休眠状态
Z 僵尸进程,进程运行完了,等待被回收资源。
孤儿进程
没爹没妈的孩子就是孤儿,子进程活着,但是父进程终止了,子进程就是孤儿进程。
为了能够回收孤进程终止后的资源,孤儿进程会被托管给我们前面介绍的pid==1的init进程,每当被托管的子进程终止时,init会立即主动回收孤儿进程资源,回收资源的速度很快,所以孤儿进程没有变成僵尸进程的机会。
代码演示
# include <stdio.h>
# include <stdlib.h>
# include <sys/types.h>
# include <unistd.h>
int main(void)
{
pid_t ret=0;
ret=fork();
if(ret>0)
{ }
else if(ret==0)
{
while(1);
}
return e;
}
进程的终止
正常终止
①main调用return
②任意位置调用exit
③任意位置调用_exit
异常终止
如果是被某个信号终止的,就是异常终止。
①自杀:自己调用abort函数,自己给自己发一个SIGABRT信号将自己杀死。
②他杀:由别人发一个信号,将其杀死。
进程终止状态
return、exit、_exit的返回值严格来说应该叫“退出状态”,当退出状态被函数(return、exit、_exit)交给OS内核,OS对其进行加工之后得到的才是“进程终止状态”,父进程调用wait函数便可以得到这个“进程终止状态”。
OS是怎么加工的?
正常终止
进程终止状态 = 终止原因(正常终止)<< 8 | 退出状态的低8位
终止原因用一个数表示
不管return、exit、_exit返回的返回值有多大,只有低8位有效,所以如果返回值太大,只取低8位的值。
举例:返回值是1000
十进制1000
二进制0011 1110 1000
低八位1110 1000
低八位对应十进制232
异常终止
进程终止状态 = 是否产生core文件位 | 终止原因(异常终止)<< 8 | 终止该进程的信号编号
父进程调用wait函数,得到“进程终止状态”有什么用
父进程得到进程终止状态后,就可以判断子进程终止的原因是什么,如果是正常终止的,可以提取出返回值,如果是异常终止的,可以提取出异常终止进程的信号编号。当有OS支持时,进程return、exit、_exit正常终止时,所返回的返回值(退出状态),最终通过“进程终止状态”返回给了父进程。这有什么用,比如,父进程可以根据子进程的终止状态来判断子进程的终止原因,返回值等等,以决定是否重新启动子进程,或则做一些其它的操作,不过一般来说,子进程的终止状态对父进程并没有太大意义。
父进程从内核获取子终止状态
如何获取
①父进程调用wait等子进程结束,如果子进程没有结束的话,父进程调用wait时会一直休眠的等(或者说阻塞的等)。
②子进程终止返回内核,内核构建“进程终止状态”
参考 阴影文字
③内核向父进程发送SIGCHLD信号,通知父进程子进程结束了,你可以获取子进程的“进程终止状态”了。如果父进程没有调用wait函数的话,会忽略这个信号,表示不关心子进程的“进程终止状态”。如果父进程正在调用wait函数等带子进程的“进程终止状态”的话,wait会被SIGCHLD信号唤醒,并获取进“进程终止状态”。一般情况下,父进程都不关心子进程的终止状态是什么,所以我们经常看到的情况是,不管子进程返回什么返回值,其实都无所谓,因为父进程不关心。不过如果我们的程序是一个多进程的程序,而且父进程有获取子进程“终止状态”的需求,此时我们就可以使用wait函数来获取了。
wait函数
原型
#include <sys/types.h>
#include <sys/wait.h> pid_t wait(int *status);
功能
获取子进程的终止状态,主动释放子进程占用的资源(释放资源这一条即使不调用wait,父进程也会自动释放)
参数
status:用于存放“进程终止状态”的缓存
返回值
成功返回子进程的PID,失败返回-1,errno被设置。
代码演示
父进程代码
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <sys/types.h>
4 #include <unistd.h>
5 #include <sys/wait.h>
6
7
8 int main(int argc, char **argv)
9 {
10 pid_t ret = 0;
11
12 ret = fork();
13 if(ret > 0)
14 {
15 int status = 0;
16
17 wait(&status);
18 printf("status = %d\n", status);
19 if(WIFEXITED(status))
20 {
21 printf("exited:%d\n", WEXITSTATUS(status));
22 }
23 else if(WIFSIGNALED(status))
24 {
25 printf("signal killed:%d\n", WTERMSIG(status));
26 }
27
28 }
29 else if(ret == 0)
30 {
31 extern char **environ;
32 execve("./new_pro", argv, environ);
33 }
34
35 return 0;
36 }
子进程代码
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 //extern char **environ;
5
6 int main(int argc, char **argv, char **environ)
7 {
8 int i = 0;
9
10 for(i=0; i<argc; i++)
11 {
12 printf("%s ", argv[i]);
13 }
14 printf("\n---------------------\n");
15
16 //while(1);
17
18
19 return 20;
20 }
子进程需要使用命令gcc child.c -o new_pro
OS处理进程终止状态的带参宏
WIFEXITED(status)
提取出终止原因,判断是否是正常终止
①如果表达式为真:表示进程是正常终止的
②为假:不是正常终止的
WIFSIGNALED(status)
提取出终止原因,判断是否是被信号杀死的(异常终止)
①如果表达式为真:是异常终止的
②为假:不是异常终止的
wait的缺点
如果父进程fork创建出了好多子进程,wait只能获取最先终止的那个子进程的“终止”状态,其它的将无法获取,如果你想获取所有子进程终止状态,或者只想获取指定子进程的进程终止状态,需要使用wait的兄弟函数waitpid,它们的原理是相似的。
进程控制——fork-and-exec、system、wait的更多相关文章
- APUE8进程控制 fork vfork exec
- 进程控制fork与vfork
1. 进程标识符 在前面进程描述一章节里已经介绍过进程的两个基本标识符pid和ppid,现在将详细介绍进程的其他标识符. 每个进程都有非负的整形表示唯一的进程ID.一个进程终止后,其进程ID就可以再次 ...
- APUE学习之进程控制 - fork 与 vfork
最后编辑: 2019-11-6 版本: gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.11) 一.进程标识 每一个进程都有一个唯一的非 ...
- 进程控制fork vfork,父子进程,vfork保证子进程先运行
主要函数: fork 用于创建一个新进程 exit 用于终止进程 exec 用于执行一个程序 wait 将父进程挂起,等待子进程结束 getpid 获取当前进程的进程ID nice 改变进程的优先级 ...
- linux系统编程:进程控制(fork)
在linux中,用fork来创建一个子进程,该函数有如下特点: 1)执行一次,返回2次,它在父进程中的返回值是子进程的 PID,在子进程中的返回值是 0.子进程想要获得父进程的 PID 需要调用 ge ...
- 第6章 进程控制(3)_wait、exec和system函数
5. 等待函数 (1)wait和waitpid 头文件 #include <sys/types.h> #include <sys/wait.h> 函数 pid_t wait(i ...
- UNIX高级环境编程(10)进程控制(Process Control)- 竞态条件,exec函数,解释器文件和system函数
本篇主要介绍一下几个内容: 竞态条件(race condition) exec系函数 解释器文件 1 竞态条件(Race Condition) 竞态条件:当多个进程共同操作一个数据,并且结果依赖 ...
- 进程控制之fork函数
一个现有进程可以调用fork函数创建一个新进程. #include <unistd.h> pid_t fork( void ); 返回值:子进程中返回0,父进程中返回子进程ID,出错返回- ...
- 进程控制之exec函数
用fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序.当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行.因为调用exec并不创 ...
随机推荐
- response返回特性
1. response 返回特性 r=requests.get("http://www.baidu.com")print(r.text) #打印返回正文print(r.status ...
- Kafka分区分配策略(Partition Assignment Strategy)
众所周知,Apache Kafka是基于生产者和消费者模型作为开源的分布式发布订阅消息系统(当然,目前Kafka定位于an open-source distributed event streamin ...
- 转 Fiddler4 手机抓包
Fiddler4 手机抓包 文章转自:https://www.cnblogs.com/zhengna/p/10876954.html 1.要对计算机Fiddler进行配置,允许远程计算机连接. 2. ...
- jvm源码解析java对象头
认真学习过java的同学应该都知道,java对象由三个部分组成:对象头,实例数据,对齐填充,这三大部分扛起了java的大旗对象,实例数据其实就是我们对象中的数据,对齐填充是由于为了规则分配内存空间,j ...
- 前端面试之JavaScript中的闭包!
前端面试之JavaScript中的闭包! 闭包 闭包( closure )指有权访问另一个函数作用域中变量的函数. ----- JavaScript 高级程序设计 闭包其实可以理解为是一个函数 简单理 ...
- Linux 从4.12内核版本开始移除了 tcp_tw_recycle 配置。 tcp_max_tw_buckets TIME-WAIT 稳定值
被抛弃的tcp_recycle_小米云技术-CSDN博客_sysctl: cannot stat /proc/sys/net/ipv4/tcp_tw_recy https://blog.csdn.ne ...
- Git:.gitignore和.gitkeep文件的使用 让空文件夹被跟踪
Git:.gitignore和.gitkeep文件的使用 Git:.gitignore和.gitkeep文件的使用 https://majing.io/posts/10000001781172 .gi ...
- Python学习【第3篇】:列表魔法
##########################深灰魔法-list类中提供的方法###################list 类,列表list = [1,12,9,"age" ...
- 利用PWM脉宽调制实现呼吸灯
1.设计目标 完成一个呼吸灯,从亮到灭的时间为2秒,从灭到亮的时间为2秒,以此不断往复. 2.设计步骤 2.1设计分析 利用PWM(脉冲宽度调制)实现led灯亮度的变化,只需要改变占空比就可以实现,具 ...
- git回退版本,再返回最新分支git pull失败的解决经验
点击"蓝字"关注我吧 作者:良知犹存 转载授权以及围观:欢迎添加微信公众号:Conscience_Remains 总述 一篇解决gti分支切换问题的文章,大家应该都有过 ...