HeLei Blog

进程&线程

进程

Linux创建进程采用fork()和exec()

  • fork: 采用复制当前进程的方式来创建子进程,此时子进程与父进程的区别仅在于pid, ppid以及资源统计量(比如挂起的信号)
  • exec:读取可执行文件并载入地址空间执行;一般称之为exec函数族,有一系列exec开头的函数,比如execl, execve等

进程创建

  • Linux进程创建: 通过fork()系统调用创建进程
  • Linux用户级线程创建:通过pthread库中的pthread_create()创建线程
  • Linux内核线程创建: 通过kthread_create()
    Linux线程,也并非”轻量级进程”,在Linux看来线程是一种进程间共享资源的方式,线程可看做是跟其他进程共享资源的进程。

fork, vfork, clone根据不同参数调用do_fork:

  • pthread_create: flags参数为 CLONE_VM, CLONE_FS, CLONE_FILES, CLONE_SIGHAND
  • fork: flags参数为 SIGCHLD
  • vfork: flags参数为 CLONE_VFORK, CLONE_VM, SIGCHLD

fork的具体流程

进程/线程创建的方法fork(),pthread_create(), 万物归一,最终在linux都是调用do_fork方法。 当然还有vfork其实也是一样的, 通过系统调用到sys_vfork,然后再调用do_fork方法,该方法 现在很少使用,所以下图省略该方法。

img01

fork执行流程:

  1. 用户空间调用fork()方法;
  2. 经过syscall陷入内核空间, 内核根据系统调用号找到相应的sys_fork系统调用;
  3. sys_fork()过程会在调用do_fork(), 该方法参数有一个flags很重要, 代表的是父子进程之间需要共享的资源; 对于进程创建flags=SIGCHLD, 即当子进程退出时向父进程发送SIGCHLD信号;
  4. do_fork(),会进行一些check过程,之后便是进入核心方法copy_process.

进程复制

使用fork函数得到的子进程从父进程的继承了整个进程的地址空间,包括:进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设置、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端等。

子进程与父进程的区别在于:

  1. 父进程设置的锁,子进程不继承(因为如果是排它锁,被继承的话,矛盾了)
  2. 各自的进程ID和父进程ID不同
  3. 子进程的未决告警被清除;
  4. 子进程的未决信号集设置为空集。

写时复制(copy on write)

linux系统为了提高系统性能和资源利用率,在fork出一个新进程时,系统并没有真正复制一个副本。

如果多个进程要读取它们自己的那部分资源的副本,那么复制是不必要的。每个进程只要保存一个指向这个资源的指针就可以了。

如果一个进程要修改自己的那份资源的“副本”,那么就会复制那份资源。这就是写时复制的含义

fork 和vfork:
在fork还没实现copy on write之前。Unix设计者很关心fork之后立刻执行exec所造成的地址空间浪费,所以引入了vfork系统调用。

vfork有个限制,子进程必须立刻执行_exit或者exec函数。

即使fork实现了copy on write,效率也没有vfork高,但是我们不推荐使用vfork,因为几乎每一个vfork的实现,都或多或少存在一定的问题。

孤儿进程&僵尸进程

fork系统调用之后,父子进程将交替执行,执行顺序不定。

  • 孤儿进程:如果父进程先退出,子进程还没退出那么子进程的父进程将变为init进程(托孤给了init进程)。(注:任何一个进程都必须有父进程)
  • 僵尸进程:如果子进程先退出,父进程还没退出,那么子进程必须等到父进程捕获到了子进程的退出状态才真正结束,否则这个时候子进程就成为僵进程(僵尸进程:只保留一些退出信息供父进程查询)

    僵尸进程的影响:
    比如进程采用exit()退出的时候,操作系统会进行一些列的处理工作,包括关闭打开的文件描述符、占用的内存等等,但是,操作系统也会为该进程保留少量的信息,以供父进程使用。例如进程的ID号、进程的退出状态、进程运行的CPU时间等,因而占用了系统的资源。
    在一种极端的情况下,档僵尸进程过多的时候,占用了大量的进程ID,系统将无法产生新的进程,相当于系统的资源被耗尽。
    
  • 代码例子-孤儿进程

    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 <sys/types.h>
    #include <sys/wait.h>
    #include <cstdio>
    #include <cstdlib>
    #include <iostream>
    using namespace std;
    int main(int argc, char const *argv[])
    {
    int childpid = 0;
    int i;
    pid_t pid = fork();
    if (pid == 0) {
    //child process
    sleep(20);
    printf("child:%d, parent: %d\n", getpid(), getppid());
    // char * execv_str[] = {"echo", "executed by execv",NULL};
    // if (execv("/bin/echo",execv_str) <0 ){
    // perror("error on exec");
    // exit(0);
    // }
    printf("child process end\n\n");
    } else if (pid > 0) {
    //parent process
    printf("parent: %d\n", getpid());
    //sleep(20);
    //wait(NULL);
    printf("parent process end\n\n");
    } else {
    perror("fork");
    return -1;
    }
    return 0;
    }

例子中先让父进程退出,子进程在fork后sleep一段时间,用ps命令查看进程信息,可以看到5083的父进程是1

1
2
3
$ ps -eF |grep exec_test
chris 5083 1 0 3180 172 3 17:01 pts/1 00:00:00 ./exec_test
chris 5095 4391 0 24366 980 3 17:01 pts/2 00:00:00 grep --color=auto exec_test

  • 代码例子-僵尸进程
    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 <sys/types.h>
    #include <sys/wait.h>
    #include <cstdio>
    #include <cstdlib>
    #include <iostream>
    using namespace std;
    int main(int argc, char const *argv[])
    {
    int childpid = 0;
    int i;
    pid_t pid = fork();
    if (pid == 0) {
    //child process
    //sleep(20);
    printf("child:%d, parent: %d\n", getpid(), getppid());
    // char * execv_str[] = {"echo", "executed by execv",NULL};
    // if (execv("/bin/echo",execv_str) <0 ){
    // perror("error on exec");
    // exit(0);
    // }
    printf("child process end\n\n");
    } else if (pid > 0) {
    //parent process
    printf("parent: %d\n", getpid());
    sleep(30);
    //wait(NULL);
    printf("parent process end\n\n");
    } else {
    perror("fork");
    return -1;
    }
    return 0;
    }

父进程不调用wait等待子进程结束,使用ps命令查看进程状态,defunct表示僵尸进程。

1
2
3
$ ps -eF |grep exec_test
chris 5225 3922 0 3181 1116 2 17:17 pts/1 00:00:00 ./exec_test
chris 5226 5225 0 0 0 3 17:17 pts/1 00:00:00 [exec_test] <defunct>

也可以这这样查询:

1
ps -e -o stat,ppid,pid,cmd|egrep '^[Zz]'

如何避免僵尸进程

我们可以使用如下几种方法避免僵尸进程的产生:

  1. 在fork后调用wait/waitpid函数取得子进程退出状态。
  2. 调用fork两次(第一次调用产生一个子进程,第二次调用fork是在第一个子进程中调用,同时将父进程退出(第一个子进程退出),此时的第二个子进程的父进程id为init进程id。
  3. 在程序中显示忽略SIGCHLD信号(子进程退出时会产生一个SIGCHLD信号,我们显示忽略此信号即可)。
  4. 捕获SIGCHLD信号并在捕获程序中调用wait/waitpid函数。

参考文献

坚持原创技术分享,您的支持将鼓励我继续创作!