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的更多相关文章

  1. APUE8进程控制 fork vfork exec

  2. 进程控制fork与vfork

    1. 进程标识符 在前面进程描述一章节里已经介绍过进程的两个基本标识符pid和ppid,现在将详细介绍进程的其他标识符. 每个进程都有非负的整形表示唯一的进程ID.一个进程终止后,其进程ID就可以再次 ...

  3. APUE学习之进程控制 - fork 与 vfork

    最后编辑: 2019-11-6 版本: gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.11) 一.进程标识 每一个进程都有一个唯一的非 ...

  4. 进程控制fork vfork,父子进程,vfork保证子进程先运行

    主要函数: fork 用于创建一个新进程 exit 用于终止进程 exec 用于执行一个程序 wait 将父进程挂起,等待子进程结束 getpid 获取当前进程的进程ID nice 改变进程的优先级 ...

  5. linux系统编程:进程控制(fork)

    在linux中,用fork来创建一个子进程,该函数有如下特点: 1)执行一次,返回2次,它在父进程中的返回值是子进程的 PID,在子进程中的返回值是 0.子进程想要获得父进程的 PID 需要调用 ge ...

  6. 第6章 进程控制(3)_wait、exec和system函数

    5. 等待函数 (1)wait和waitpid 头文件 #include <sys/types.h> #include <sys/wait.h> 函数 pid_t wait(i ...

  7. UNIX高级环境编程(10)进程控制(Process Control)- 竞态条件,exec函数,解释器文件和system函数

    本篇主要介绍一下几个内容: 竞态条件(race condition) exec系函数 解释器文件    1 竞态条件(Race Condition) 竞态条件:当多个进程共同操作一个数据,并且结果依赖 ...

  8. 进程控制之fork函数

    一个现有进程可以调用fork函数创建一个新进程. #include <unistd.h> pid_t fork( void ); 返回值:子进程中返回0,父进程中返回子进程ID,出错返回- ...

  9. 进程控制之exec函数

    用fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序.当进程调用一种exec函数时,该进程执行的程序完全替换为新程序,而新程序则从其main函数开始执行.因为调用exec并不创 ...

随机推荐

  1. SW3516中文资料书

    SW3516 是一款高集成度的快充车充芯片, 支持 A+C 口任意口快充输出, 支持双口独立限流.其集成了 5A 高效率同步降压变换器, 支持 PPS/PD/QC/AFC/FCP/SCP/PE/SFC ...

  2. C++中输出变量类型的方法

    C++中输出变量类型的方法 在c++中输出变量或者数据类型,使用typeid().name()的方法.如下例子: #include <iostream> #include <stri ...

  3. 糊糊的学习笔记--Fiddle抓包

    Fiddle简述 Fiddler是一个http调试代理,它能 够记录所有的你电脑和互联网之间的http通讯,Fiddler 可以也可以让你检查所有的http通讯,设置断点,以及Fiddle 所有的&q ...

  4. NodeJS连接MongoDB数据库

    NodeJS连接MongoDB数据库 连接数据库的js文件[我将其命名为(connect.js)] // 引入mongoose第三方模块 const mongoose = require('mongo ...

  5. Tensorflow-交叉熵&过拟合

    交叉熵 二次代价函数 原理 缺陷 假如我们目标是收敛到0.A点为0.82离目标比较近,梯度比较大,权值调整比较大.B点为0.98离目标比较远,梯度比较小,权值调整比较小.调整方案不合理. 交叉熵代价函 ...

  6. 获取当前文件路径 import 原理 一般把模块组成的集合称为包(package)

    获取当前文件路径 testpath.py import sysprint(sys.path) [root@d mapReduceLog]# python testpath.py['/data/mapR ...

  7. makefile自动生成学习

    https://www.cnblogs.com/jrglinux/p/6964169.html 关键是如何写Makefile.am  其他的交给 自动工具完成 添加一个 很好的博客 学习下 https ...

  8. RabbitMQ入门看这一篇就够了

    一文搞懂 RabbitMQ 的重要概念以及安装 一 RabbitMQ 介绍 这部分参考了 <RabbitMQ实战指南>这本书的第 1 章和第 2 章. 1.1 RabbitMQ 简介 Ra ...

  9. Golang--Directional Channel(定向通道)

    Directional Channel 通道可以是定向的(directional).在默认情况下,通道将以双向的(bidirectional)形式运作,用户既可以把值放人通道,也可以从通道取出值;但是 ...

  10. Spring Boot 系列总结

    Spring Boot 系列总结 1.SpringBoot自动装配 1.1 Spring装配方式 1.2 Spring @Enable 模块驱动 1.3 Spring 条件装配 2.自动装配正文 2. ...