进程原语fork exceve waitpid

frok

一个运行的进程,它调用了fork()函数,然后就产生了子进程,原来的进程叫父进程。这个子进程也是进程,但凡是进程,都有自己的虚拟地址空间(让每个进程自己看起来是独占内存,通过段页内存管理映射到不同的物理地址空间)。虚拟地址空间是从0到4G的大小,其中3-4G是属于内核的(32位系统)。创建完子进程后,父进程继续运行程序(即原来的进程)的代码,刚创建出来的子进程拥有和父进程完全一样的代码段,数据段,也就是说完完全全拷贝了一份父进程,和父进程完全一样。即clone父进程0-3G的内容,而3-4G的kernel只需要重新映射一下到物理地址的kernel即可。但是操作系统要如何区分这两个进程呢?答案就是进程ID即pid。pid是存储在PCB当中的类似身份证的东西. kernel会创建子进程自己的PCB,然后clone父进程的PCB(task_struct)的绝大部分信息,如内存映射(mm_struct,采用 COW 机制copy on write),文件描述符表,调度信息(优先级、CPU 时间等),但某些关键字段会被修改,如pid,ppid等.

kernel会设置 fork() 的返回值.在子进程的 task_struct 中,kernel会预先设置 eax/rax 寄存器(存储返回值)为 0,因此子进程看到的 fork() 返回 0。父进程的 fork() 返回子进程的 pid。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int q_val = 100;

int main() {
    printf("father is running, pid: %d, ppid: %d\n", getpid(), getppid());
    sleep(2);

    pid_t id = fork();
    int cnt = 3;

    while(1) {
        if(id == 0) {
            // Child process
            printf("I am child process, pid: %d, ppid: %d, q_val: %d, &q_val: %p\n", 
                  getpid(), getppid(), q_val, &q_val);
            sleep(1);
            cnt--;
            if(cnt == 0) {
                q_val = 300;
                printf("I am child, q_val is changed, 100 -> 300\n");
            }
        } else {
            // Parent process
            printf("I am father process, pid: %d, ppid: %d, q_val: %d, &q_val: %p\n", 
                  getpid(), getppid(), q_val, &q_val);
            sleep(1);
        }
    }

    return 0;
}

fork底层是调用了内核的函数来实现fork的功能的,即先create()先创建进程,此时进程内容为空,然后clone()复制父进程的内容到子进程中,此时子进程就诞生了,接着父进程就return返回了。而子进程诞生后,是直接运行return返回的,然后接着执行后面的程序,这里注意:子进程是不会执行前面父进程已经执行过的程序了得,因为PCB中记录了当前进程运行到哪里,而子进程又是完全拷贝过来的,所以PCB的程序计数器也是和父进程相同的,所以是从fork()后面的程序继续执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
father is running, pid: 2193928, ppid: 784260
I am father process, pid: 2193928, ppid: 784260, q_val: 100, &q_val: 0x55e878c7f010
I am child process, pid: 2193930, ppid: 2193928, q_val: 100, &q_val: 0x55e878c7f010
I am father process, pid: 2193928, ppid: 784260, q_val: 100, &q_val: 0x55e878c7f010
I am child process, pid: 2193930, ppid: 2193928, q_val: 100, &q_val: 0x55e878c7f010
I am father process, pid: 2193928, ppid: 784260, q_val: 100, &q_val: 0x55e878c7f010
I am child process, pid: 2193930, ppid: 2193928, q_val: 100, &q_val: 0x55e878c7f010
I am father process, pid: 2193928, ppid: 784260, q_val: 100, &q_val: 0x55e878c7f010
I am child process, pid: 2193930, ppid: 2193928, q_val: 100, &q_val: 0x55e878c7f010
I am father process, pid: 2193928, ppid: 784260, q_val: 100, &q_val: 0x55e878c7f010
I am child, q_val is changed, 100 -> 300
I am child process, pid: 2193930, ppid: 2193928, q_val: 300, &q_val: 0x55e878c7f010
I am father process, pid: 2193928, ppid: 784260, q_val: 100, &q_val: 0x55e878c7f010
I am child process, pid: 2193930, ppid: 2193928, q_val: 300, &q_val: 0x55e878c7f010
I am father process, pid: 2193928, ppid: 784260, q_val: 100, &q_val: 0x55e878c7f010
I am child process, pid: 2193930, ppid: 2193928, q_val: 300, &q_val: 0x55e878c7f010
I am father process, pid: 2193928, ppid: 784260, q_val: 100, &q_val: 0x55e878c7f010
I am child process, pid

exec族

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支), 子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的 用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建 新进程,所以调用exec前后该进程的id并未改变

1
2
3
4
5
6
7
#include 
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错 则返回-1,所以exec函数只有出错的返回值而没有成功的返回值

wait/waitpid

僵尸进程: 子进程退出,父进程没有回收子进程资源(PCB),则子进程变成僵尸进程 孤儿进程: 父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为1号 进程init进程,称为init进程领养孤儿进程

一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还 保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止 则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这 些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变 量$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状 态同时彻底清除掉这个进程。但是,如果父进程先于子进程结束,则子进程成为孤儿进程。孤儿进程将被 init 进程(进程号为1)领养,并由 init 进程对孤儿进程完成状态收集工作。而如果子进程先于父进程退出,同时父进程太忙了,无瑕回收子进程的资源,子进程残留资源(PCB)存放于内核中,变成僵尸。任何进程在刚终止时都是僵尸进程,正常情况下,僵 尸进程都立刻被父进程清理了.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <unistd.h>
#include <stdlib.h>
int main(void)  
{  
    pid_t pid;  
    pid = fork();  
    if (pid == 0) {  
            printf("I am child, my parent= %d, going to sleep 3s\n", getppid());  
            sleep(3);  
            printf("-------------child die--------------\n");  
    } else if (pid > 0) {  
            printf("I am parent, pid = %d, myson = %d, going to sleep 5s\n", getpid(), pid);  
            sleep(5);  
            system("ps -o pid,ppid,state,tty,command");  
    } else {  
        perror("fork");  
        return 1;  
    }  

    return 0;  
} 

在这个程序里,父进程创建子进程之后,就休眠 5 秒钟。而子进程只休眠 3 秒钟就退出,在它退出之后,父进程还未苏醒,因此没人给子进程「收尸」,所以它就变成了僵尸进程。

僵尸进程其实已经就是退出的进程,因此无法再利用kill命令杀死僵尸进程。僵尸进程的罪魁祸首是父进程没有回收它的资源,那我们可以想办法它其它进程去回收僵尸进程的资源,这个进程就是 init 进程。因此,我们可以直接杀死父进程,init 进程就会很善良地把那些僵尸进程领养过来,并合理的回收它们的资源,那些僵尸进程就得到了妥善的处理了

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计