进程控制函数

孤儿进程和僵尸进程

  • 经验:

很多时候 我们会让子进程先退出,然后再退出父进程

如果父进程先死亡, 那么 子进程就是孤儿进程【很容易理解】

  • 僵尸进程
    • 僵尸进程:子进程退出了,但是父进程没有用wait或waitpid去获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称为僵死进程。

 僵尸进程危害场景:

  例如有个进程,它定期的产 生一个子进程,这个子进程需要做的事情很少,做完它该做的事情之后就退出了,因此这个子进程的生命周期很短,但是,父进程只管生成新的子进程,至于子进程 退出之后的事情,则一概不闻不问,这样,系统运行上一段时间之后,系统中就会存在很多的僵死进程,倘若用ps命令查看的话,就会看到很多状态为Z的进程。 严格地来说,僵死进程并不是问题的根源,罪魁祸首是产生出大量僵死进程的那个父进程。因此,当我们寻求如何消灭系统中大量的僵死进程时,答案就是把产生大 量僵死进程的那个元凶枪毙掉(也就是通过kill发送SIGTERM或者SIGKILL信号啦)。枪毙了元凶进程之后,它产生的僵死进程就变成了孤儿进 程,这些孤儿进程会被init进程接管,init进程会wait()这些孤儿进程,释放它们占用的系统进程表中的资源,这样,这些已经僵死的孤儿进程 就能瞑目而去了。

父进程有义务回收 自己创建的子进程

  1. 解决方法:
    1. 调用 wait方法回收子进程

一个进程在终止时会关闭所有文件描述符释放在用户空间分配的内存,但它的 PCB 还保留着,内核在其中保留了一些信息。如果是正常终止,则保存退出状态;如果是异常终,则保存着导致该进程终止的信号是哪一个。这个进程的父进程可以调用 wait 或 waitpid 获取这些信息 ,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在 Shell 中用特殊变量 $? 查看,因为 Shell 是它的父进程,当它终止时 Shell 调用 wait 或者 waitpid 得到它的退出状态同时彻底清除掉这个进程。

MMU 原理

fork学习视频

说到子进程只是一个额外的流程,那他跟父进程的联系和区别是什么呢?

我很想建议你看看linux内核的注解(有兴趣可以看看,那里才有本质上的了解),总之,fork后,子进程会复制父进程的task_struct结构,并为子进程的堆栈分配物理页。理论上来说,子进程应该完整地复制父进程的堆,栈以及数据空间,但是2者共享正文段。

关于写时复制:由于一般 fork后面都接着exec,所以,现在的 fork都在用写时复制的技术,顾名思意,就是,数据段,堆,栈,一开始并不复制,由父,子进程共享,并将这些内存设置为只读。直到父,子进程一方尝试写这些区域,则内核才为需要修改的那片内存拷贝副本。这样做可以提高 fork的效率。

三.多线程

线程是可执行代码的可分派单元。这个名称来源于“执行的线索”的概念。在基于线程的多任务的环境中,所有进程有至少一个线程,但是它们可以具有多个任务。这意味着单个程序可以并发执行两个或者多个任务。

简而言之,线程就是把一个进程分为很多片,每一片都可以是一个独立的流程。这已经明显不同于多进程了,进程是一个拷贝的流程,而线程只是把一条河流截成很多条小溪。它没有拷贝这些额外的开销,但是仅仅是现存的一条河流,就被多线程技术几乎无开销地转成很多条小流程,它的伟大就在于它少之又少的系统开销。(当然伟大的后面又引发了重入性等种种问题,这个后面慢慢比较)。

还是先看linux提供的多线程的系统调用:

int pthread_create(pthread_t *restrict tidp, const pthread_attr_t *restrict attr, void *(*start_rtn)(void), void *restrict arg);
Returns: 0 if OK, error number on failure

第一个参数为指向线程标识符的指针。 第二个参数用来设置线程属性。 第三个参数是线程运行函数的起始地址。 最后一个参数是运行函数的参数。

fork函数

  1. 创建一个子进程:
    1. pid_t fork(void) , 失败返回 -1, 成功返回 子进程的ID(非负)
    2. man fork 可以查看 文档
    3. fork 函数返回值有2个:
      1. 返回子进程的 pid
      2. 返回 0 表示成功
1
2
3
4
5
6
7
8
9
lyr@DESKTOP-FSVN6C0:~$ g++ main.cpp
lyr@DESKTOP-FSVN6C0:~$ ./a.out
:<<EOF
fork 子进程 224
fork成功: 父进程获得 子进程id:= 224
fork 子进程 0
这个是子进程 := 0

EOF
 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
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cerrno>

using namespace std;

int main(void) {
    pid_t pid = fork();

    printf("fork 子进程 %d\n", pid);
    if (pid == -1) {
        perror("fork进程失败");
        exit(1);
    }
    if (pid == 0 ){
        printf("这个是子进程 := %d\n", pid);
    }
    else {
        printf("fork成功: 父进程获得 子进程id:= %d\n", pid);
    }



    return 0;
}
 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
35
36
37
38
39
40

#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cerrno>



using namespace std;

int main(void) {
    pid_t pid = fork();
    // printf("fork 子进程 %d\n", pid);
    if (pid == -1) {
        perror("fork进程失败");
        exit(1);
    }
    if (pid == 0 ){
        printf("父进程id: = %d 这个是子进程 := %d\n", getpid() , pid);
    }
    else {
        printf("fork成功:父进程ID:=%d  父进程获得 子进程id:= %d\n",getpid(), pid);
    }



    return 0;
}

/*
lyr@DESKTOP-FSVN6C0:~$ g++ main.cpp && ./a.out

fork成功:父进程ID:=264  父进程获得 子进程id:= 265
父进程id: = 265 这个是子进程 := 0

getpid() 表示获得当前进程的 id ,proccess_id



*/

循环 fork 出现的问题

image-20210925141339448

 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
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cerrno>

using namespace std;

int main(void) {
    // printf("fork 子进程 %d\n", pid);
    for(int i=0;i<5;++i ) {
        pid_t pid = fork();
        if( pid == -1) {
            perror("fork error");
            exit(1);
        }
        else if( pid == 0 ) {
            printf("这个是子进程 := %d\n",pid);
        }else {
            printf("这个是父亲进程 := %d, 子进程:=%d\n", getpid(), pid);
            sleep(1);
        }

    }
    return 0;
}

解决的方法

 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
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cerrno>



using namespace std;

int main(void) {
    // printf("fork 子进程 %d\n", pid);
    for(int i=0;i<5;++i ) {
        pid_t pid = fork();
        if( pid == -1) {
            perror("fork error");
            exit(1);
        }
        else if( pid == 0 ) {
            //这样就不会继续 fork下去了
            break;
            //printf("这个是子进程 := %d\n",pid);
        }else {
            printf("这个是父亲进程 := %d, 子进程:=%d\n", getpid(), pid);
            sleep(1);
        }

    }


    return 0;
}

exec 原理

 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
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cerrno>



using namespace std;

int main(void) {
    // printf("fork 子进程 %d\n", pid);
    pid_t pid;
    pid = fork();
    if (pid == -1 ) {
        exit(1);
    }
    if (pid == 0 ) {
        //第2个参数随便写,不会被用
       execlp("ls","ls" ,"-l" , "-d" ,NULL);
    } else {
        printf("parent\n" );
        sleep(2);
    }


    return 0;
}

进程的复制

进程的创建,其实就是对 linux 0号进程或者当前进程的复制,堆栈的复制

参考文章

其他笔记

[[post/03.基础学科/01.操作系统/7.面试问题整理 |面试问题]]

fork原理

linux 内核视频教程

父子进程共享文件描述符

子进程继承的父进程属性有哪些