每天学五分钟 Liunx 0101 | 服务篇:创建进程


创建子进程

上一节说过创建子进程的三种方式:
1. fork 复制进程:fork 会复制当前进程的副本,产生一个新的子进程,父子进程是完全独立的两个进程,他们掌握的资源(环境变量和普通变量)是一样的。
2. exec:exec 方式不会产生子进程,它会加载新的程序从而取代当前进程,当前进程的变量是被初始化了。exec 加载的程序执行完毕后会退出当前 exec 所在的 shell。
3. clone:用来实现 Liunx 中的线程。
 
同时也介绍了通过开启子 shell 来产生特殊的子 bash 进程。
 
这里在加深下印象,通过 exec 方式实现在子 bash 进程中执行 ls 命令:
[root@lianhua process]$ cat exec.sh
#!/bin/bash
ps -lf
echo $BASHPID
exec ls
 
[root@lianhua process]$ /bin/bash exec.sh
F S UID        PID  PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
4 S root     36259 36250  0  80   0 - 28754 do_wai 13:45 pts/15   00:00:00 -bash
0 S root     53778 36259  0  80   0 - 28296 do_wai 17:29 pts/15   00:00:00 /bin/bash exec.sh
0 R root     53779 53778  0  80   0 - 38356 -      17:29 pts/15   00:00:00 ps -lf
53778
exec.sh
 
执行 bash 脚本开启了子 shell,它的进程 id 是 53778,父进程是 bash 进程 36259,同时调用 exec 方法执行了 ls 命令。完整的流程示意图如下:
 

fork

如前所述,可以通过调用 fork 产生子进程。不同于其它函数调用方式,fork 函数被调用一次会返回两次,两次返回的区别是,对于子进程的返回值是 0,对于父进程的返回值是创建的子进程的 PID 号。
fork 出的子进程是父进程的副本,子进程获得父进程数据空间,堆和栈的副本,它们不共享存储空间。
写时复制(Copy-On-Write, COW)技术使得父子进程可以共享存储空间的内容,如果父子进程有一个需要修改存储空间的区域,内核会给修改区域的内存做副本。
 
fork 有两种用法:
1.父进程希望复制自己,使父子进程同时执行不同的代码段。在网络服务中比较常见,父进程等待客户端的服务请求,当请求到达时,父进程 fork 自己,给子进程处理该请求,同时父进程继续等待下一个请求。
2. 一个进程要执行不同的程序,常见于 shell 脚本,子进程 fork 后立即调用 exec 执行程序。
 
使用 fork 函数创建一个子进程:
[root@lianhua process]$ cat process1.cpp
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
 
int main()
{
    pid_t pid;
    pid = fork();
    if (pid < 0)
    {
        perror("fork error:");
        exit(1);
    }
    if (pid == 0)
    {
        printf("I am the child process.\n");
        printf("pid: %d\tppid:%d\n",getpid(),getppid());
        printf("I will sleep five seconds.\n");
        sleep(5);
        printf("pid: %d\tppid:%d\n",getpid(),getppid());
        printf("child process is exited.\n");
    }
    else
    {
        printf("I am father process.\n");
        printf("father process is  exited.\n");
    }
    return 0;
}
 
[root@lianhua process]$ ./process1.out
I am father process.
pid: 2365       ppid:36259
father process is  exited.
I am the child process.
pid: 2366       ppid:2365
I will sleep five seconds.
[root@lianhua process]$ ps -lf
F S UID        PID  PPID  C PRI  NI ADDR SZ WCHAN  STIME TTY          TIME CMD
1 S root      2366     1  0  80   0 -  1056 hrtime 20:53 pts/15   00:00:00 ./process1.out
0 R root      3799 36259  0  80   0 - 38356 -      20:53 pts/15   00:00:00 ps -lf
4 S root     36259 36250  0  80   0 - 28754 do_wai 13:45 pts/15   00:00:00 -bash
[root@lianhua process]$ pid: 2366    ppid:1
child process is exited.
 
从上述代码可以看出:
1. 
调用 fork 函数产生了子进程 2366,它的父进程是 2365,2365 的父进程是 bash 进程 36259。
2. 
fork 之后先执行父进程还是子进程是不确定的,取决于内核使用的调度算法,如果要求进程间相互同步,需要使用进程间通信。
3.
这里我们使用了比较简单的 sleep 方法让子进程 sleep 5 秒来确保父进程先结束,当然这不是保险的方式,保险的方式可以使用:
while(getppid() != 1)
    sleep(1);
这种形式的循环叫做轮询(polling),它的缺点是浪费 CPU 时间,需要等待和做条件测试。可以使用给进程发送信号的信号机制来避免竞争和轮询。
4.
当子进程 sleep 时,父进程执行结束,进程退出,这时候 bash 进程处于运行状态,可以执行 ps -lf 命令,子进程 2366 处于 sleep 状态。由于父进程先结束了,系统进程 systemd(PID=1) 会接管子进程成为它的父进程。
 
 
前面代码展示了父进程先结束,子进程成为孤儿进程被 systemd 接管的情况。那么,如果子进程先结束,父进程没结束会怎么样呢?

僵尸进程

fork 出的子进程和父进程的执行是异步的,子进程和父进程都会运行。父进程先结束的话,子进程成为孤儿进程被系统进程 systemd 接管。子进程先结束的话,内核会释放进程所使用的所有存储区,关闭进程打开的所有文件。同时,保存进程的少量信息,包括进程 ID,进程的终止状态以及进程使用 CPU 时间总量等信息。并且,内核会向父进程发送 SIGCHLD 信号,正常情况下接收到该信号的父进程会对子进程做善后工作(子进程退出到父进程接管的这段时间,子进程处于僵尸态,只是时间短,难以被捕捉到),包括获取终止子进程的信息,释放它所占有的资源。但是,如果因为父进程繁忙或者没有收到该信号(系统默认是忽略该信号的),那么子进程将成为僵死态,也就是僵尸进程(zombie)。
 
看一个产生僵尸进程的例子:
[root@lianhua process]$ cat process3.cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
 
int main()
{
    pid_t  pid;
    while(1)
    {
        pid = fork();
        if (pid < 0)
        {
            perror("fork error:");
            exit(1);
        }
        else if (pid == 0)
        {
            printf("I am a child process PID=%d, PPID=%d.\nI am exiting.\n", getpid(), getppid());
            exit(0);
        }
        else
        {
            printf("pid: %d\tppid:%d\n",getpid(),getppid());
            sleep(5);
            continue;
        }
    }
    return 0;
}
 
[root@lianhua process]$ ./process3.out
pid: 6345       ppid:36259
I am a child process PID=6346, PPID=6345.
I am exiting.
pid: 6345       ppid:36259
I am a child process PID=6367, PPID=6345.
I am exiting.
pid: 6345       ppid:36259
I am a child process PID=6535, PPID=6345.
I am exiting.
^C
 
[root@lianhua ~]$ ps -lA | grep defunct
1 Z     0  6346  6345  0  80   0 -     0 do_exi pts/15   00:00:00 process3.out <defunct>
1 Z     0  6367  6345  0  80   0 -     0 do_exi pts/15   00:00:00 process3.out <defunct>
1 Z     0  6535  6345  0  80   0 -     0 do_exi pts/15   00:00:00 process3.out <defunct>
 
可以看出,父进程 6345 每隔 5 秒 fork 一个子进程,当子进程退出的时候,父进程没有替它收尸导致它成为了僵尸进程。
僵尸进程会占用进程表和少量资源,并且它已经不工作了,是不好的,要将它杀掉,直接 kill 僵尸进程是 kill 不掉的,因为它已经退出了,接收不到信号,可以 kill 它的父进程来给它陪葬,父进程杀掉了,内核就会清理父子进程的资源。
 
kill 掉父进程 6345:
[root@lianhua ~]$ kill -9 6345
[root@lianhua process]$ ./process3.out
...
pid: 6345       ppid:36259
I am a child process PID=6656, PPID=6345.
I am exiting.
Killed
[root@lianhua process]$
 
 
在代码实现上,可以通过调用信号处理函数和 fork 两次的方法来避免僵尸进程的产生:

1. 调用信号处理函数

父进程是默认忽略 SIGCHLD 信号的,所以可以提供一个当该信号发生即被调用执行的信号处理函数来接收该信号,该函数会取得子进程的状态并立即返回。函数 wait 和 waitpid 即是这种信号处理函数,调用它可能会发生以下三种情况:
  • 如果所有子进程运行,则阻塞;
  • 如果一个子进程终止,正等待父进程获取其终止状态,则取得该子进程的终止状态并立即返回;
  • 如果没有任何子进程,则立即出错返回;
 
使用 wait 函数等待子进程结束:
[root@lianhua process]$ cat process3.1.cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <wait.h>
 
int main()
{
    pid_t  pid;
    while(1)
    {
        pid = fork();
        if (pid < 0)
        {
            perror("fork error:");
            exit(1);
        }
        else if (pid == 0)
        {
            printf("I am a child process PID=%d, PPID=%d.\nI am exiting.\n", getpid(), getppid());
            exit(0);
        }
        else
        {
            printf("pid: %d\tppid:%d\n",getpid(),getppid());
            wait(NULL);
            sleep(5);
            continue;
        }
    }
    return 0;
}
 
[root@lianhua process]$ ./process3.1.out
pid: 9207       ppid:36259
I am a child process PID=9208, PPID=9207.
I am exiting.
pid: 9207       ppid:36259
I am a child process PID=9297, PPID=9207.
I am exiting.
pid: 9207       ppid:36259
I am a child process PID=9306, PPID=9207.
I am exiting.
^C
 
[root@lianhua ~]$ ps -lA | grep defunct
[root@lianhua ~]$ ps -lA | grep defunct
通过调用 wait 函数处理子进程结束发出来的信号,避免了僵尸进程的产生。
 

2. fork 两次

fork 两次也可以避免僵尸进程的产生,举例:
[root@lianhua process]$ cat process5.cpp
#include <unistd.h>
#include <stdio.h>
 
int main(void)
{
    int i=0;
    printf("i child/father ppid pid fpid\n");
    for(i=0; i<2; i++)
    {
        pid_t fpid=fork();
        if(fpid == 0)
            printf("%d child %4d %4d %4d\n", i, getppid(), getpid(), fpid);
        else
            printf("%d father %4d %4d %4d\n", i, getppid(), getpid(), fpid);
    }
    return 0;
}
 
[root@lianhua process]$ ./process5.out
i child/father ppid pid fpid
0 father 36259 45119 45120
0 child 45119 45120    0
1 father 36259 45119 45121
1 father 45119 45120 45122
1 child 45120 45122    0
1 child    1 45121    0
 
根据代码执行结果,画出进程执行示意图,如下:
 
 
从图中可以看出,进程 45119 退出之后,它所产生的子进程被 systemd 接管,成为 systemd 的子进程。
在 systemd 进程下,无论何时只要有一个子进程终止,systemd 就会调用 wait 函数获得该进程的终止状态,从而被 systemd 接管的子进程是不会产生僵尸进程的。
 
那么,子进程提前结束会怎么样呢?不会产生僵尸进程码?
看代码:
[root@lianhua process]$ cat process5.cpp
#include <unistd.h>
#include <stdio.h>
 
int main(void)
{
    int i=0;
    printf("i child/father ppid pid fpid\n");
    for(i=0; i<2; i++)
    {
        pid_t fpid=fork();
        if(fpid == 0)
            printf("%d child %4d %4d %4d\n", i, getppid(), getpid(), fpid);
        else
        {
            printf("%d father %4d %4d %4d\n", i, getppid(), getpid(), fpid);
            sleep(20);
        }
    }
    return 0;
}
 
[root@lianhua process]$ ./process5.out
i child/father ppid pid fpid
0 father 36259 10739 10740
0 child 10739 10740    0
1 father 10739 10740 10741
1 child 10740 10741    0
1 father 36259 10739 10764
1 child 10739 10764    0
 
[root@lianhua ~]$ ps -lA | grep defunct
0 Z     0 10500  1720  0  80   0 -     0 do_exi ?        00:00:00 fluentd-report <defunct>
1 Z     0 10740 10739  0  80   0 -     0 do_exi pts/15   00:00:00 process5.out <defunct>
1 Z     0 10764 10739  0  80   0 -     0 do_exi pts/15   00:00:00 process5.out <defunct>
[root@lianhua ~]$ ps -lA | grep defunct
0 Z     0 10500  1720  0  80   0 -     0 do_exi ?        00:00:00 fluentd-report <defunct>
 
画进程执行示意图,如下:
 
从图中可以看出,父进程在 sleep 时子进程退出,成为僵尸进程,然后父进程运行,退出,它下面的僵尸进程也随之结束,内核将释放父进程和子进程的资源。
 
综上所述, fork 两次可以避免僵尸进程的产生。
 
 
 
(完)
 
posted @ 2020-05-04 23:37  lubanseven  阅读(201)  评论(0编辑  收藏  举报