Fork me on GitHub

进程控制——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 }
View Code

依据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 }
View Code

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 }
View Code

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;
}
View Code

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;
}
View Code

进程的终止

正常终止

①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 }
View Code

子进程代码

 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 }
View Code

子进程需要使用命令gcc child.c -o new_pro

OS处理进程终止状态的带参宏

WIFEXITED(status)

提取出终止原因,判断是否是正常终止

①如果表达式为真:表示进程是正常终止的

②为假:不是正常终止的

WIFSIGNALED(status)

提取出终止原因,判断是否是被信号杀死的(异常终止)

①如果表达式为真:是异常终止的

②为假:不是异常终止的

wait的缺点

如果父进程fork创建出了好多子进程,wait只能获取最先终止的那个子进程的“终止”状态,其它的将无法获取,如果你想获取所有子进程终止状态,或者只想获取指定子进程的进程终止状态,需要使用wait的兄弟函数waitpid,它们的原理是相似的。

posted @ 2018-09-22 10:09  克拉默与矩阵  阅读(2275)  评论(0编辑  收藏  举报