【Linux&操作系统】6. 进程控制

62 阅读19分钟

6 进程控制

6.1 写时拷贝

  • fork()函数创建子进程时,会将父进程的task_struct,mm_struct,vm_area_struct,页表等全部拷贝到子进程的相关结构中去,此时,因为相关指针是浅拷贝,父进程会和子进程公用相同部分的代码和数据,此时,父进程和子进程中vm_area_struct的数据部分的权限会被设置为只读

  • 当子进程创建完毕之后,如果父子进程中,发生了写操作,此时OS会检查发生该操作的进程的vm_area_struct中管理相关区域的权限,此时发现该区域被标记为"只读",且和父/子进程共享该区域,此时就会触发写时拷贝

  • 写时拷贝发生时,OS会检查发生写操作的进程的页表,发现其页被标记为只读权限,于是没法写,并触发缺页中断,并开辟新的物理内存,拷贝原本共享数据的进程的数据部分到新的物理内存,并修改发生写操作的进程的页表的物理地址部分,接着正常修改相关数据

  • 我们假设修改数据的是子进程,子进程修改数据后,如果父进程没有其他子进程,父进程和子进程的数据部分的被修改的页重新设置回"读写"权限,如果父进程还有其他子进程,并且这个子进程和父进程同样也共享该数据部分,那么父进程和其他子进程依旧保持共享关系,权限依旧为只读

  • PS:不难发现,其实在上面这几段的形容中,修改某个数据导致的写时拷贝,会直接拷贝"页"这个东西,意味着并不是拷贝单个数据,而是这个数据所在的一整片页,而"页表"也管理的是页,并不是对单个数据管理,这也意味着,修改单个数据,不会影响其他页和父进程的共享关系,这也是写时拷贝的一个重要意义,因为并不是所有数据都需要被修改,程序中很多数据都是只读的,这极大地节约了内存空间,同时,因为共享物理内存的关系,使得创建子进程时,只需要初始化task_struct,mm_struct,vm_area_struct,页表等内容,对于容量庞大的代码部分和数据部分发生的拷贝的开销很小,使得创建子进程的效率提高了不少

  • 对于"页"的概念,我们再放一放,后面还会提到的

6.2 进程终止

6.2.1 进程终止的不同形式
  • 一般父进程创建子进程,基本都是要求子进程完成某种任务,所以我们有僵尸状态来获取子进程的退出信息

  • 但子进程并不是保证一定可以完成任务,也可能会遇到完不成任务的情况,于是进程的退出就会分为几种状况:

    1. 子进程正常退出,任务正常完成: 代码本身没有问题,任务也正常完成了
    2. 子进程正常退出,任务没有正常完成: 代码本身并没有问题,但程序本身捕获到了错误,导致任务没有正常完成
    3. 子进程发生错误,报错,并被系统杀死: 代码出现问题,被系统强制杀死
  • 我们来看几个例子

  • 第一种情况:

#include<stdio.h>

int main()
{
    int i = 0;
    for(; i < 10; i++)
    {
        printf("oldking is handsome\n");
    }

    return 0;
}
  • 第二种情况:
#include<stdio.h>
#include<errno.h>

int main()
{
    FILE* filep = fopen("OK.txt", "r");
    if(filep == NULL) return errno; //注意!这里是将错误码当成退出码打印

    return 0;
}
  • 第三种情况:
#include<stdio.h>

int main()
{
    int arr[9] = {1,2,3,4,5,6,7,8,9};
    int i = 0;
    for(; i < 10; i++)
    {
        printf("%d\n", arr[i]);
    }

    return 0;
}

//这个程序会因为越界而报错
6.2.2 退出码与错误码
  • 我们知道,一个父进程创建了一个子进程要求它去完成任务,那么父进程需要知道子进程完成得怎么样,有没有正常完成

  • 于是退出码就诞生了,事实是这个退出码我们早就见过了,就是main()函数得返回值,一般我们判断一个程序有没有引发报错就是这个main()的返回值

  • 同时我们也知道,其实对于系统来说,一个程序的入口并不是main()函数,而是一个名字类似于_start()的函数,这个函数会帮助我们接收main()的返回值,并在结束时传递返回值给内核,然后内核帮助我们设置该进程的task_struct中的退出码,然后进程进入僵尸状态等待父进程获取退出信息

struct task_struct {
    //......

/* task state */
	struct linux_binfmt *binfmt;
	long exit_state;

    //exit_code就是退出码
	int exit_code, exit_signal;
	
    int pdeath_signal;  /*  The signal sent when the parent dies  */
	/* ??? */
	unsigned long personality;

    //......
  • 所以父进程需要从退出码获取子进程完成任务的情况

  • 在上一个小节中的第二个例子中,我们将错误码用作退出码返回

  • 错误码是C语言规定的错误代码,表示各种常见错误,这个我们在C语言阶段就已经了解过了

  • 我们可以看看错误码都可以表示些什么

#include<stdio.h>
#include<errno.h>
#include<unistd.h>
#include<string.h>

int main()
{
    int i = 0;
    for(; i < 200; i++)
    {
        //strerror可以用于解析错误码
        printf("%d:%s\n", i, strerror(i));
    }

    return 0;
}
$ ./test
0:Success
1:Operation not permitted
2:No such file or directory
3:No such process
4:Interrupted system call
5:Input/output error
6:No such device or address
7:Argument list too long
8:Exec format error
9:Bad file descriptor
10:No child processes
11:Resource temporarily unavailable
12:Cannot allocate memory
13:Permission denied
14:Bad address
15:Block device required
16:Device or resource busy
17:File exists
18:Invalid cross-device link
19:No such device
20:Not a directory
21:Is a directory
22:Invalid argument
23:Too many open files in system
24:Too many open files
25:Inappropriate ioctl for device
26:Text file busy
27:File too large
28:No space left on device
29:Illegal seek
30:Read-only file system
31:Too many links
32:Broken pipe
33:Numerical argument out of domain
34:Numerical result out of range
35:Resource deadlock avoided
36:File name too long
37:No locks available
38:Function not implemented
39:Directory not empty
40:Too many levels of symbolic links
41:Unknown error 41
42:No message of desired type
43:Identifier removed
44:Channel number out of range
45:Level 2 not synchronized
46:Level 3 halted
47:Level 3 reset
48:Link number out of range
49:Protocol driver not attached
50:No CSI structure available
51:Level 2 halted
52:Invalid exchange
53:Invalid request descriptor
54:Exchange full
55:No anode
56:Invalid request code
57:Invalid slot
58:Unknown error 58
59:Bad font file format
60:Device not a stream
61:No data available
62:Timer expired
63:Out of streams resources
64:Machine is not on the network
65:Package not installed
66:Object is remote
67:Link has been severed
68:Advertise error
69:Srmount error
70:Communication error on send
71:Protocol error
72:Multihop attempted
73:RFS specific error
74:Bad message
75:Value too large for defined data type
76:Name not unique on network
77:File descriptor in bad state
78:Remote address changed
79:Can not access a needed shared library
80:Accessing a corrupted shared library
81:.lib section in a.out corrupted
82:Attempting to link in too many shared libraries
83:Cannot exec a shared library directly
84:Invalid or incomplete multibyte or wide character
85:Interrupted system call should be restarted
86:Streams pipe error
87:Too many users
88:Socket operation on non-socket
89:Destination address required
90:Message too long
91:Protocol wrong type for socket
92:Protocol not available
93:Protocol not supported
94:Socket type not supported
95:Operation not supported
96:Protocol family not supported
97:Address family not supported by protocol
98:Address already in use
99:Cannot assign requested address
100:Network is down
101:Network is unreachable
102:Network dropped connection on reset
103:Software caused connection abort
104:Connection reset by peer
105:No buffer space available
106:Transport endpoint is already connected
107:Transport endpoint is not connected
108:Cannot send after transport endpoint shutdown
109:Too many references: cannot splice
110:Connection timed out
111:Connection refused
112:Host is down
113:No route to host
114:Operation already in progress
115:Operation now in progress
116:Stale file handle
117:Structure needs cleaning
118:Not a XENIX named type file
119:No XENIX semaphores available
120:Is a named type file
121:Remote I/O error
122:Disk quota exceeded
123:No medium found
124:Wrong medium type
125:Operation canceled
126:Required key not available
127:Key has expired
128:Key has been revoked
129:Key was rejected by service
130:Owner died
131:State not recoverable
132:Operation not possible due to RF-kill
133:Memory page has hardware error
134:Unknown error 134
135:Unknown error 135
136:Unknown error 136
137:Unknown error 137
//......
  • 不难看出,C语言只设计了134个错误码,对照错误码表,父进程就知道子进程有没有正常退出了

  • 当然我们也完全可以不按照错误码表来,完全可以使用自己制定的错误码表

  • 如果该进程的父进程是bash,那我们如何在shell中查看一个进程的退出码呢?

echo $?
  • 这个指令可以查看上一个退出的进程的退出码,我们可以理解为bash会帮助我们保存上一个退出的进程的退出码

  • 当然,还有一种情况我们没讲,即:如果进程是因为报错而被动让OS给杀死的,此时还有退出码吗?

  • 当然有

#include<stdio.h>

int main()
{
    int i = 10;
    i /= 0;

    return0;
}
$ ./test
Floating point exception

$ echo $?
136
  • 但因为是程序本身有问题,所以不论这个程序有没有完成任务,这个退出码都是没有任何意义的
6.2.3 进程退出的不同方式
  • 常见的退出方式,就是return 0,这个我们再熟悉不过了

  • 当然,我们还可以用C语言标准库给出的函数退出

exit(int status);
  • 区别于return,这个函数可以在程序的任何地方结束进程,同时,它唯一的参数就是需要传递给内核的退出码

  • 还有一种退出方式

_exit(int status)
  • 这种退出方式和exit()很像,但区别是_exit()不会刷新缓冲区

  • 同时,exit()是C语言标准库的函数,_exit()是系统调用接口,意味着exit()的底层包含有_exit()

6.3 进程等待

6.3.1 为什么需要进程等待
  • 我们需要解决一个之前的遗留问题

  • 即:如何处理僵尸进程的PCB,如何获取进程的退出信息,并防止发生因为僵尸进程而导致的内存泄漏

6.3.2 wait()
  • pid_t wait(int *status)

  • 这个接口用于获取子进程的退出信息

  • 这个接口是一个系统调用级接口,唯一的参数其实用作于返回数值,具体则是返回一个位图,子进程的退出信息都在这个位图里,这个位图的具体使用方式我们还会再谈的

  • 返回值很好说,就是返回子进程的pid

  • 这里我简单写了一个程序

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<errno.h>
#include<stdlib.h>

int main()
{
    pid_t pid_num = fork();
    if(pid_num == 0)
    {
        //child
        
        int option = 0; //选项,用于设定子进程的模式,方便查看我们想要的状态
        scanf("%d", &option);
        
        //当输入值为1时,子进程正常完成,且结果正确
        if(option == 1) // complete and result is right
        {
            int cnt = 10;
            for(; cnt > 0; cnt--)
            {
                printf("this is child, pid: %d, ppid: %d\n", getpid(), getppid());
                sleep(1);
            }
            exit(0);
        }
        //当输入值为0时,子进程正常完成,但结果不正确
        else if(option == 0) // complete but result is not right
        {
            FILE* pfile = fopen("oldkingnana.txt", "r");   
            //注意!这里不能用fclose关闭文件,因为文件不存在,关闭NULL会导致段错误
            if(pfile == NULL) 
            {
                return errno;
            }
            exit(122);
        }
        //当输入值为1时,子进程发生错误,被OS杀死
        else if(option == -1) // incomplete
        {
            int a = 10;
            a /= 0;
            exit(111);
        }
        //输入值错误
        else
        {
            printf("option error!\n");
            exit(0);
        }

    }
    else if(pid_num == -1) //fork错误
    {
        perror("fork error!\n");        
        exit(errno);
    }
    else 
    {
        //parent
        
        int status = 0;
        wait(&status);

        //关于status的具体用法,我们还会提到的,这里我们只需要暂时记住可以这么用就是了
        printf("child is over complete! exit_code: %d, exit_signal: %d\n", (status >> 8) & 0xFF, status & 0x7F);
    }

    return 0;
}
  • 我们先来看现象
$ ./test
1
this is child, pid: 24585, ppid: 24584
this is child, pid: 24585, ppid: 24584
this is child, pid: 24585, ppid: 24584
this is child, pid: 24585, ppid: 24584
this is child, pid: 24585, ppid: 24584
this is child, pid: 24585, ppid: 24584
this is child, pid: 24585, ppid: 24584
this is child, pid: 24585, ppid: 24584
this is child, pid: 24585, ppid: 24584
this is child, pid: 24585, ppid: 24584
child is over complete! exit_code: 0, exit_signal: 0

$ ./test
0
child is over complete! exit_code: 2, exit_signal: 0

$ ./test
-1
child is over complete! exit_code: 0, exit_signal: 8
  • 其中:exit_code就是退出码,这个我们已经了解过了

  • exit_signal则是退出信号,这个和kill -9 [pid]中,第9号信号是同类东西

  • 我们可以用kill -l查一下信号列表

$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX	
  • 不难发现,第三种情况下的退出信号是一个叫做SIGFPE的错误,即"Floating Point Exception","浮点异常"

  • 其实就是除零产生的问题

  • 现在我们来解释一下status究竟是怎么构成的

  • status是一个int类型的变量,意味着其中包含32个bit

  • 前16个bit全部弃用,只使用后面16个bit

退出情景第32~17位第16~9位第8位第7~1位
程序正常结束且结果正确弃用表示退出码全0全0
程序正常结束但结果不正确弃用表示退出码全0全0
程序未正常结束弃用退出码(全0)core dump标志(暂时不了解)退出信号
  • 所以我们做(status >> 8) & 0xFFstatus & 0x7F的位运算就是为了计算出退出码和退出信号

  • PS:如果不需要检查退出码的话,int *status这个参数可以直接填NULL

6.3.3 status的相关宏定义
  • 解析status的值其实并不需要手动进行位运算,其实还有两个宏定义可以帮我们快速解析status

  • WIFEXITED(status)是一个宏,如果返回值是true,则代表子进程正常结束,如果返回值是false,则代表子进程未正常结束

  • WEXITSTATUS(status)也是一个宏,用于解析status的退出码,返回值是退出码

  • 一般可以这样用

if(WIFEXITED(status))
    printf("child is over complete! exit_code: %d\n", WEXITSTATUS(status));
6.3.4 waitpid()
  • 还有另一个接口pid_t waitpid(pid_t pid, int *status, int options)

  • 这个接口一样可以完成获取子进程退出信息的任务,但比wait更加高级一些

  • 首先,用屁股都看得出来,这家伙可以指定等待的进程,用参数pid来设定指定进程

  • pid可以是以下值

pid<-1pid==-1pid==0pid>0
暂时不用了解等待所有子进程暂时不用了解等待pid指定的进程
  • 文档的解释:
The value of pid can be:
    
< -1   meaning wait for any child process whose  process  group  ID  is equal to the absolute value of pid.
    
-1     meaning wait for any child process.
    
0      meaning  wait  for  any  child process whose process group ID is equal to that of the calling process.
    
> 0    meaning wait for the child whose process  ID  is  equal  to  the value of pid.
  • 返回值和status我们已经知道了,用法和wait()一样

  • 至于options,这个可就非常有意思了,我们下一小节专门聊这个事情

6.3.5 等待机制,options,阻塞调用,非阻塞轮询
  • 首先,options这个形参用作控制阻塞

  • 但谈options之前,我们首先得了解等待的实际机制

  • 我们知道,进程与进程之间保持一定的独立性,所以父进程不能直接访问子进程的代码,数据,以及PCB,包括

  • wait()waitpid()这两个接口却能让父进程获取到子进程的信息,这是怎么做到的?

  • 因为,wait()waitpid()都是系统调用,所以父进程相当于靠他俩请求操作系统,靠操作系统帮忙取回子进程信息

  • 但如果父进程已经在等待了,但子进程还没有完成任务怎么办?

    • 选择1: 父进程一刻也不停歇地不停向操作系统请求子进程的退出信息
    • 选择2: 父进程可以干自己的活,隔一段时间向操作系统请求一次
  • 是个人都会选择后者,这种做法可以极大地提高父进程地效率,同时避免因为等待问题而导致的父进程卡死

  • 当我们调用某个接口时,如果该接口在没有完成任务之前,位于这个接口后面的代码无法运行,需要等待该接口完成任务,我们称这种调用方式为"阻塞调用"

  • 如果调用某个接口时,允许该接口不完成任务,程序可以执行接口之后的代码,代码逻辑采用循环隔一段时间询问完成情况,这种我们称为"非阻塞轮询",即"不阻塞的每轮询问一次"

  • 也就是说,我们在前面几个小节使用的方式都是阻塞调用,我们可以改一下之前的代码验证一下

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<errno.h>
#include<stdlib.h>

int main()
{
    // 设置子进程模式改到子进程创建之前
    int option = 0;
    scanf("%d", &option);
    
    pid_t pid_num = fork();
    if(pid_num == 0)
    {
        //child
        
        
        if(option == 1) // complete and result is right
        {
            int cnt = 10;
            for(; cnt > 0; cnt--)
            {
                printf("this is child, pid: %d, ppid: %d\n", getpid(), getppid());
                sleep(1);
            }
            exit(0);
        }
        else if(option == 0) // complete but result is not right
        {
            FILE* pfile = fopen("oldkingnana.txt", "r");   
            //fclose(pfile);
            if(pfile == NULL) 
            {
                return errno;
            }
            exit(122);
        }
        else if(option == -1) // incomplete
        {
            int a = 10;
            a /= 0;
            exit(111);
        }
        else
        {
            printf("option error!\n");
            exit(0);
        }

    }
    else if(pid_num == -1)
    {
        perror("fork error!\n");        
        exit(errno);
    }
    else 
    {
        //parent
        int i = 20;
        while(i--) //父进程延迟20s才开始等待子进程
        {
            sleep(1);
            printf("parent is in %dS sleep\n", i);
        }
        int status = 0;
        wait(&status);

        //printf("child is over complete! exit_code: %d, exit_signal: %d\n", (status >> 8) & 0xFF, status & 0x7F);
        if(WIFEXITED(status))
            printf("child is over complete! exit_code: %d\n", WEXITSTATUS(status));
    }
    return 0;
}
  • 如果没出错的话,父进程在停止sleep之后立马就会获取到子进程的信息
$ ./test
1
this is child, pid: 29132, ppid: 29131
parent is in 19S sleep
this is child, pid: 29132, ppid: 29131
parent is in 18S sleep
this is child, pid: 29132, ppid: 29131
parent is in 17S sleep
this is child, pid: 29132, ppid: 29131
parent is in 16S sleep
this is child, pid: 29132, ppid: 29131
parent is in 15S sleep
this is child, pid: 29132, ppid: 29131
this is child, pid: 29132, ppid: 29131
parent is in 14S sleep
this is child, pid: 29132, ppid: 29131
parent is in 13S sleep
parent is in 12S sleep
this is child, pid: 29132, ppid: 29131
parent is in 11S sleep
this is child, pid: 29132, ppid: 29131
parent is in 10S sleep
parent is in 9S sleep
parent is in 8S sleep
parent is in 7S sleep
parent is in 6S sleep
parent is in 5S sleep
parent is in 4S sleep
parent is in 3S sleep
parent is in 2S sleep
parent is in 1S sleep
parent is in 0S sleep
child is over complete! exit_code: 0
  • 关于非阻塞轮询,我们可以写个例子看看
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<errno.h>
#include<stdlib.h>

int main()
{
    pid_t pid_num = fork();
    if(pid_num == 0)
    {        
        int cnt = 10;
        for(; cnt > 0; cnt--)
        {
            printf("this is child, pid: %d, ppid: %d\n", getpid(), getppid());
            sleep(1);
        }
        exit(0);
    }
    else
    {
        while(1)
        {
            int status = 0;
            printf("parent is inquiring\n");
            pid_t ret_pid = waitpid(pid_num, &status, WNOHANG);  //这里的WNOHANG就是代表轮询,是一个宏定义,如果不用轮询而是阻塞调用的话直接填0就行
            if(ret_pid < 0) printf("waitpid error\n");

            if(ret_pid > 0 && WIFEXITED(status))
            {
                printf("child is over complete! exit_code: %d\n", WEXITSTATUS(status));
                exit(0);
            }
            int i = 1;
            while(i < 8)
            {
                printf("parent working time is %dS\n", i);
                sleep(1);
                i++;
            }
        }
    }
    return 0;
}
  • 子进程一共运行10s,父进程每7s询问一次,如果结果没错的话,子进程结束后,父进程应该还要再等几秒
$ ./test
parent is inquiring
parent working time is 1S
this is child, pid: 30588, ppid: 30587
parent working time is 2S
this is child, pid: 30588, ppid: 30587
parent working time is 3S
this is child, pid: 30588, ppid: 30587
parent working time is 4S
this is child, pid: 30588, ppid: 30587
parent working time is 5S
this is child, pid: 30588, ppid: 30587
parent working time is 6S
this is child, pid: 30588, ppid: 30587
parent working time is 7S
this is child, pid: 30588, ppid: 30587
parent is inquiring
this is child, pid: 30588, ppid: 30587
parent working time is 1S
parent working time is 2S
this is child, pid: 30588, ppid: 30587
parent working time is 3S
this is child, pid: 30588, ppid: 30587
parent working time is 4S
parent working time is 5S
parent working time is 6S
parent working time is 7S
parent is inquiring
child is over complete! exit_code: 0
  • 结果非常正确

6.4 进程程序替换

6.4.1 接口execl
  • 我们先来了解一个接口int execl(const char *path, const char *arg, ...);

  • 这个接口完成的任务很简单,一旦当前进程调用这个接口,就会让指定的程序替换当前进程

  • 所以:

    1. path:需要替换的程序的路径+程序自己
    2. arg以及...:是可变参数列表,传入的是可执行程序名和选项,这也是这个接口叫execl的原因,其中l代表list,即参数以类似于列表的形式传入
    3. 返回值:如果替换成功,则没有返回值,如果替换失败,返回值为-1(我们会在讲机制的小节再提一遍)
  • 使用示例

//test.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>

int main()
{
    printf("This process's pid is %d\n", getpid());

    execl("/usr/bin/ls", "ls", "-l", "-a", NULL); //使用的时候和直接用命令非常像,区别就是要以NULL结尾

    printf("execl error\n");

    return 0;
}
$ ./test
This process's pid is 5141
total 28
drwxrwxr-x  2 oldking oldking 4096 Feb  5 04:52 .
drwx------ 18 oldking oldking 4096 Jan 22 23:33 ..
-rw-rw-r--  1 oldking oldking  131 Feb  5 04:45 Makefile
-rwxrwxr-x  1 oldking oldking 8512 Feb  5 04:52 test
-rw-rw-r--  1 oldking oldking  496 Feb  5 04:52 test.c
  • 不难发现,我们这里试图让ls替换掉当前进程,并且成功了
  • 我们发现,一旦当前进程被替换掉了,execl()后面的代码就不执行了
  • 这里我们得好好谈一谈进程程序替换的机制
6.4.2 进程程序替换的机制与应用方式
  • 我们一旦调用这个接口,OS首先得知道用哪个程序来替换,即参数path,然后得知道替换的程序得是什么形式,用什么功能,即arg...
  • 然后OS会用需要替换的程序的数据和代码直接覆盖当前进程在内存中的数据部分和代码部分,对,你没听错,连代码都会被覆盖
  • 这意味着页表也会发生一些改变,主要是映射的物理地址的部分,但task_struct,mm_struct,vm_area_struct却几乎不变,这也得益于虚拟地址空间维护了一个虚拟的空间
  • 怎么验证其PCB不变?
  • 这里我们可以改替换的进程为我们自己写的程序
//other.cpp
#include<iostream>
#include<unistd.h>
#include<sys/types.h>

int main()
{
    std::cout << "this process's pid is " << getpid() << std::endl;

    return 0;
}
  • 然后将other.cpp编译链接形成可执行程序"other_cpp"

  • 修改test.c

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>

int main()
{
    printf("This process's pid is %d\n", getpid());
    execl("./other_cpp", "other_cpp", NULL);
    printf("execl error\n");

    return 0;
}
$ ./test
This process's pid is 7122
this process's pid is 7122
  • 此时我们惊人地发现,哪怕是进程替换之后,其pid依旧不变,这也应证了我们的结论

  • 因为连代码都被覆盖了,所以被替换的进程中还没有执行的代码就直接没了,再也执行不了了,除非替换失败(比方说文件名写错了这样,就会替换失败)

  • 并且,咱们有没有发现,这里我替换的程序是用CPP写的,这意味着替换的可执行程序可以是任意语言编写的,因为即便使用不同的语言编写,最后执行的都是可执行程序,全部都是机器码,然后创建PCB,拷贝资源到内存等等步骤都是一样的

  • 于是,更加大胆一点,我们是不是就可以实现一个进程启动另一个进程的功能呢?

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<sys/wait.h>

int main()
{
    pid_t pidnum = fork();
    
    if(pidnum == 0)
    {
        //child

        printf("This process's pid is %d, ppid is %d\n", getpid(), getppid());
        execl("./other_cpp", "other_cpp", NULL);
        printf("execl error\n");
        exit(1);
    }
    else if(pidnum < 0)
    {
        printf("foork error!\n");
        exit(1);
    }
    else
    {
        //parent
        printf("sleep 1S\n");        
        sleep(1);

        waitpid(pidnum, NULL, 0);

        printf("This is parent process, this process's pid is %d\n", getpid());
        exit(0);
    }

    return 0;
}
$ ./test
sleep 1S
This process's pid is 7982, ppid is 7981
this process's pid is 7982
This is parent process, this process's pid is 7981
  • 依旧也是没有问题

  • 其中的机制又是怎样的?

  • 我们知道,子进程刚刚被创建的时候,是和父进程共享数据和代码的,但此时数据和代码被直接覆盖了!这时候发生了什么???

  • 我们又知道,子进程修改数据会发生写时拷贝,那这里算不算修改数据?当然算!

  • 但不仅仅是修改数据,连代码都被修改了!

  • 是的!代码部分也可以被写时拷贝!!!

  • 所以我们可以完成一件非常神奇的事情

  • 要知道,Python这门语言挺适合用来写ui的,因为简单方便

  • 假设我们需要开发一款空气动力学模拟程序,这种程序对于CPU要求极高,因为需要模拟真实的物理世界

  • 所以在程序的内核的开发,我们使用CPP,因为它效率高

  • 当我们启动这个程序的时候,我们启动的是它的内核程序,作为父进程,它压根就没有前台窗口,只在后台运行

  • 但这个内核程序会创建一个子进程,子进程又会被相对路径下的另一个程序替换,另一个程序就是我们用Python写的ui窗口,并且可以与内核沟通资源(沟通资源我们后面就会了解到哦)

  • 用户使用软件的时候,只能看见前台的ui窗口和模拟画面,实际的计算任务全部交给后台的内核程序,此时系统中就有两个程序在跑,一个负责ui,一个负责主要任务的计算

  • 这种设计架构称为前后端分离架构

  • 我们常见的软件,例如著名游戏平台:"Steam",没错的话就是使用的这种架构,它一共拥有三个进程Steam,Steam Client Service,Steam Client WebHelper,其中只有最后面的是前台程序,其他的全部是后台程序,负责网络服务和文件管理等等(我猜的)

  • 像是"网易云音乐"的新版本中,就只有一个进程"NetEase Cloud Music",即前后端合并(前后端合并有很多种架构,这里不多提)(也是我猜的)

  • 这也许是你为什么有时候能在软件的安装目录看到很多个可执行程序的原因,因为他们都可以被内核当作子进程启动,以完成某些任务

6.4.3 其他接口与相关接口调用机制
6.4.3.1 int execv(const char *path, char *const argv[])
  • v即代表vector,意思是用数组的形式传选项
  • 我们修改一下test.c来验证一下
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<sys/wait.h>

int main()
{
    pid_t pidnum = fork();
    
    if(pidnum == 0)
    {
        //child

        printf("This process's pid is %d, ppid is %d\n", getpid(), getppid());
        
        char *const argv[] = {
            (char *const)"ls",
            (char *const)"-l",
            (char* const)"-a",
            NULL               //注意!这里一样要在末尾填NULL!!
        };

        execv("/usr/bin/ls", argv);
        
        printf("execl error\n");
        exit(1);
    }
    else if(pidnum < 0)
    {
        printf("foork error!\n");
        exit(1);
    }
    else
    {
        //parent
        printf("sleep 1S\n");        
        sleep(1);

        waitpid(pidnum, NULL, 0);

        printf("This is parent process, this process's pid is %d\n", getpid());
        exit(0);
    }

    return 0;
}
$ ./test
sleep 1S
This process's pid is 10266, ppid is 10265
total 44
drwxrwxr-x  2 oldking oldking 4096 Feb  5 06:30 .
drwx------ 18 oldking oldking 4096 Jan 22 23:33 ..
-rw-rw-r--  1 oldking oldking  131 Feb  5 04:45 Makefile
-rwxrwxr-x  1 oldking oldking 9080 Feb  5 05:29 other_cpp
-rw-rw-r--  1 oldking oldking  161 Feb  5 05:28 other.cpp
-rwxrwxr-x  1 oldking oldking 8768 Feb  5 06:30 test
-rw-rw-r--  1 oldking oldking 1064 Feb  5 06:30 test.c
This is parent process, this process's pid is 10265
6.4.3.2 int execlp(const char *file, const char *arg, ...)
  • 这里的p代表PATH,就是环境变量的意思,l表达的意思不变

  • 这个接口可以在进程本身自带的环境变量区找路径,从而使本身需要绝对路径的接口转变为只需要文件名的接口

  • 即,这个接口会自己在环境变量设定的路径里找可执行程序

  • 再修改test.c

$ cat test.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<sys/wait.h>


int main()
{
    pid_t pidnum = fork();
    
    if(pidnum == 0)
    {
        //child

        printf("This process's pid is %d, ppid is %d\n", getpid(), getppid());
        
        execlp("ls", "ls", "-l", "-a", NULL);
        
        printf("execl error\n");
        exit(1);
    }
    else if(pidnum < 0)
    {
        printf("foork error!\n");
        exit(1);
    }
    else
    {
        //parent
        printf("sleep 1S\n");        
        sleep(1);

        waitpid(pidnum, NULL, 0);

        printf("This is parent process, this process's pid is %d\n", getpid());
        exit(0);
    }

    return 0;
}
$ ./test
sleep 1S
This process's pid is 11787, ppid is 11786
total 44
drwxrwxr-x  2 oldking oldking 4096 Feb  5 06:40 .
drwx------ 18 oldking oldking 4096 Jan 22 23:33 ..
-rw-rw-r--  1 oldking oldking  131 Feb  5 04:45 Makefile
-rwxrwxr-x  1 oldking oldking 9080 Feb  5 05:29 other_cpp
-rw-rw-r--  1 oldking oldking  161 Feb  5 05:28 other.cpp
-rwxrwxr-x  1 oldking oldking 8768 Feb  5 06:40 test
-rw-rw-r--  1 oldking oldking 1124 Feb  5 06:40 test.c
This is parent process, this process's pid is 11786
6.4.3.3 int execvp(const char *file, char *const argv[])
  • 没啥好说的,同上
6.4.3.4 int execle(const char *path, const char *arg, ..., char * const envp[])
  • 这个就比较有意思了

  • e代表environment,即环境变量

  • 意味着这个接口允许我们自定义替换程序的环境变量

$ cat test.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<sys/wait.h>


int main()
{
    pid_t pidnum = fork();
    
    if(pidnum == 0)
    {
        //child

        printf("This process's pid is %d, ppid is %d\n", getpid(), getppid());
        
        char *const env[] = {
            (char *const)"PATH=/home/oldking/testdir",
            NULL
        };

        execle("./other_cpp", "other_cpp", NULL, env);

        printf("execl error\n");
        exit(1);
    }
    else if(pidnum < 0)
    {
        printf("foork error!\n");
        exit(1);
    }
    else
    {
        //parent
        printf("sleep 1S\n");        
        sleep(1);

        waitpid(pidnum, NULL, 0);

        printf("This is parent process, this process's pid is %d\n", getpid());
        exit(0);
    }

    return 0;
}
$ ./test
sleep 1S
This process's pid is 14571, ppid is 14570
this process's pid is 14571
This is parent process, this process's pid is 14570
6.4.3.5 int execvpe(const char *file, char *const argv[], char *const envp[])
  • 同上,没什么好说的
6.4.3.6 int execve(const char *filename, char *const argv[], char *const envp[])
  • 这个接口的使用方式也是同上,不过值得注意的是这个接口是正儿八经的系统调用,而其他的接口确实C语言标准库的接口
  • 具体我们下一个小节会细聊的
6.4.3.7 接口调用机制
  • 事实上,即便是某几个接口没有自定义环境变量,被替换的进程依然可以获取环境变量
  • 这是因为PCB并不会改变,这个我们提过了
  • 同时,其实除了execve()以外,其他的接口全都是在它的基础上封装的,所以事实是,即便我们不自定义传环境变量,因为封装的缘故,execve()也可以自动获得环境变量,也许是通过environ这个全局变量获得的
  • 同时,我们自定义的环境变量,实际上会直接覆盖先前的环境变量
  • 为什么这么说?
  • 我们知道程序启动的时候都需要传选项表和环境变量表,原本的环境变量表是自动传入的,直接用子进程没被替换之前的环境变量(也就是父进程的环境变量表),但这里却要求用全新的环境变量表,相当于这几个需要传环境变量的接口中自动传入环境变量表这个步骤直接没了,传入新程序的main()的环境变量表是用户自己定义的表!同时因为execve()是系统调用,他能直接修改mm_struct管理的环境变量区的内容!所以环境变量可以修改!
6.4.4 环境变量问题
  • 那如何解决因为自定义环境变量而导致原先的环境变量被替换的问题呢?

  • 我们需要用一个接口int putenv(char *string)

  • 这个接口用于为当前进程添加环境变量

  • 我们修改一下other.cpptest.c

//test.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<sys/wait.h>

int main()
{
    pid_t pidnum = fork();
    
    if(pidnum == 0)
    {
        //child

        printf("This process's pid is %d, ppid is %d\n", getpid(), getppid());       
        putenv((char *const)"AAA=111");             //添加环境变量
        execl("./other_cpp", "other_cpp", NULL);    //替换进程

        printf("execl error\n");
        exit(1);
    }
    else if(pidnum < 0)
    {
        printf("foork error!\n");
        exit(1);
    }
    else
    {
        //parent
        printf("sleep 1S\n");        
        sleep(1);

        waitpid(pidnum, NULL, 0);

        printf("This is parent process, this process's pid is %d\n", getpid());
        exit(0);
    }

    return 0;
}
other.cpp
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
#include<stdio.h>

int main(int argc, char *argv[], char *env[])
{
    (void)argc;
    std::cout << "this process's pid is " << getpid() << std::endl;

    for(int i = 0; argv[i]; i++)                //打印选项
    {
        printf("argv[%d]:%s\n", i, argv[i]);
    }

    printf("\n");

    for(int i = 0; env[i]; i++)                 //打印环境变量
    {
        printf("env[%d]:%s\n", i, env[i]);
    }

    return 0;
}
$ ./test
sleep 1S
This process's pid is 18690, ppid is 18689
this process's pid is 18690
argv[0]:other_cpp

env[0]:XDG_SESSION_ID=20332
env[1]:HOSTNAME=iZwz9b2bj2gor4d8h3rlx0Z
env[2]:TERM=xterm
env[3]:SHELL=/bin/bash
env[4]:HISTSIZE=1000
env[5]:SSH_CLIENT=36.148.182.93 24330 22
env[6]:SSH_TTY=/dev/pts/0
env[7]:USER=oldking
env[8]:LD_LIBRARY_PATH=:/home/oldking/.VimForCpp/vim/bundle/YCM.so/el7.x86_64
env[9]:LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=01;05;37;41:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=01;36:*.au=01;36:*.flac=01;36:*.mid=01;36:*.midi=01;36:*.mka=01;36:*.mp3=01;36:*.mpc=01;36:*.ogg=01;36:*.ra=01;36:*.wav=01;36:*.axa=01;36:*.oga=01;36:*.spx=01;36:*.xspf=01;36:
env[10]:MAIL=/var/spool/mail/oldking
env[11]:PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/oldking/.local/bin:/home/oldking/bin
env[12]:PWD=/home/oldking/testdir
env[13]:LANG=en_US.UTF-8
env[14]:HISTCONTROL=ignoredups
env[15]:SHLVL=1
env[16]:HOME=/home/oldking
env[17]:LOGNAME=oldking
env[18]:SSH_CONNECTION=36.148.182.93 24330 172.17.42.39 22
env[19]:LESSOPEN=||/usr/bin/lesspipe.sh %s
env[20]:XDG_RUNTIME_DIR=/run/user/1001
env[21]:_=./test
env[22]:OLDPWD=/home/oldking
env[23]:AAA=111
This is parent process, this process's pid is 18689
  • 不难发现,此时环境变量已经加上了

  • 如果直接替换就会变成这样

//test.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<sys/wait.h>


int main()
{
    pid_t pidnum = fork();
    
    if(pidnum == 0)
    {
        //child

        printf("This process's pid is %d, ppid is %d\n", getpid(), getppid());
        
        //新环境变量表
        char *const env[] = {
            (char *const)"AAA=111",
            NULL
        };      
        
        //替换环境变量表
        execle("/home/oldking/testdir/other_cpp", "other_cpp", NULL, env);

        printf("execl error\n");
        exit(1);
    }
    else if(pidnum < 0)
    {
        printf("foork error!\n");
        exit(1);
    }
    else
    {
        //parent
        printf("sleep 1S\n");        
        sleep(1);

        waitpid(pidnum, NULL, 0);

        printf("This is parent process, this process's pid is %d\n", getpid());
        exit(0);
    }

    return 0;
}
$ ./test
sleep 1S
This process's pid is 20403, ppid is 20402
this process's pid is 20403
argv[0]:other_cpp

env[0]:AAA=111
This is parent process, this process's pid is 20402
  • 环境变量就剩了自定义的那个了

  • 当然,你也可以直接传environ,也是没问题的

//test.c
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<stdlib.h>
#include<sys/wait.h>

int main()
{
    pid_t pidnum = fork();
    
    if(pidnum == 0)
    {
        //child
        printf("This process's pid is %d, ppid is %d\n", getpid(), getppid());
        
        putenv((char *const)"AAA=111");

        extern char** environ;
        execle("/home/oldking/testdir/other_cpp", "other_cpp", NULL, environ);

        printf("execl error\n");
        exit(1);
    }
    else if(pidnum < 0)
    {
        printf("foork error!\n");
        exit(1);
    }
    else
    {
        //parent
        printf("sleep 1S\n");        
        sleep(1);

        waitpid(pidnum, NULL, 0);

        printf("This is parent process, this process's pid is %d\n", getpid());
        exit(0);
    }

    return 0;
}
$ ./test
sleep 1S
This process's pid is 21424, ppid is 21423
this process's pid is 21424
argv[0]:other_cpp

env[0]:XDG_SESSION_ID=20332
env[1]:HOSTNAME=iZwz9b2bj2gor4d8h3rlx0Z
env[2]:TERM=xterm
env[3]:SHELL=/bin/bash
env[4]:HISTSIZE=1000
env[5]:SSH_CLIENT=36.148.182.93 24330 22
env[6]:SSH_TTY=/dev/pts/0
env[7]:USER=oldking
env[8]:LD_LIBRARY_PATH=:/home/oldking/.VimForCpp/vim/bundle/YCM.so/el7.x86_64
env[9]:LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=01;05;37;41:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.axv=01;35:*.anx=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=01;36:*.au=01;36:*.flac=01;36:*.mid=01;36:*.midi=01;36:*.mka=01;36:*.mp3=01;36:*.mpc=01;36:*.ogg=01;36:*.ra=01;36:*.wav=01;36:*.axa=01;36:*.oga=01;36:*.spx=01;36:*.xspf=01;36:
env[10]:MAIL=/var/spool/mail/oldking
env[11]:PATH=/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/oldking/.local/bin:/home/oldking/bin
env[12]:PWD=/home/oldking/testdir
env[13]:LANG=en_US.UTF-8
env[14]:HISTCONTROL=ignoredups
env[15]:SHLVL=1
env[16]:HOME=/home/oldking
env[17]:LOGNAME=oldking
env[18]:SSH_CONNECTION=36.148.182.93 24330 172.17.42.39 22
env[19]:LESSOPEN=||/usr/bin/lesspipe.sh %s
env[20]:XDG_RUNTIME_DIR=/run/user/1001
env[21]:_=./test
env[22]:OLDPWD=/home/oldking
env[23]:AAA=111
This is parent process, this process's pid is 21423

  • 如有问题或者有想分享的见解欢迎留言讨论