Learn Linux Process

概述

进程可以理解为应用程序执行的一个实例,它包括可执行程序及其相关系统资源。 简单地来说,应用程序运行起来就是个进程。 在进程方面,Linux 和 Windows 是有区别的,Linux 的进程变现地更像 Windows 的线程。 每一个进程都有一个唯一的 ID 号。操作系统有一个初始进程 init,它的 ID 是1。 每个进程都可以通过 fork 产生子进程,每个进程都是 init 进程的子进程。
每个进程的 task_struct 结构体中有一个 state 字段(可以通过 ps aux 命令看到),用来表示当前进程的状态。 进程一共有以下五种状态:
  • R:running~运行
  • S:sleeping~休眠
  • D:uninterruptible sleep~不可中断
  • Z:zombie~僵尸进程
  • T:traced~停止

如何创建进程

依靠 fork() 函数可以随时产生新的进程,产生新的进程以后,当前进程被分为两个进程,父进程和子进程,两者拥有相同的代码段,不相同的数据段。通过 fork() 函数的返回值来区分是父进程还是子进程。返回小于0说明创建失败,等于0代表子进程在执行,大于0是子进程的 ID 号,可以通过分支来控制父子进程分别走哪个代码段。
看下面这个程序:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>

int main()
{
    pid_t pid;
    printf("before\n");
    int num = 0;
    pid = fork();

    if(pid < 0) {
        printf("create failed: %s\n",strerror(errno));
    }
    
    if(pid == 0) {
        printf("child process is running\n");
    }
    
    if(pid > 0) {
        printf("parent process is running \n");
    }
    
    num--;
    printf("%d\n",num);
    
    return 0;
}
看完它想必就会理解上面的话。这个程序的输出顺序不会是固定的(因为父子进程拥有相同的优先级),子进程和父进程哪一个先开始哪个先结束,都是不一定的。num 最后输出的值都是-1,也就是父子进程虽然共享代码段,但是数据是不共享的。
是不是比 Windows 简单多了?
关于 fork() 还有个有意思的点,比如在程序一开始有如下代码段,一共会有几个进程? fork(); fork();
答案是四个。 父进程在第一次 fork 产生了一个子进程;第二次 fork,子进程和父进程又分别产生了一个进程,所以就会像满二叉树一样, fork() n 次,就会出现 2^n 个进程。

两种特殊进程

僵尸进程

什么是僵尸进程?通常子进程在结束(死掉)以后,需要父进程负责回收操作系统没有回收掉的小部分资源和 ID 等。但是父进程被困住了(比如在死循环里),子进程死掉之后,父亲没有机会为他收尸(比如调用 wait() 或者 waitpid() )。于是这个子进程就变成了僵尸进程。
进程虽死,但是还占着 ID 和小部分资源,是不是跟僵尸很像? 看一段代码:
int main()
{
    pid_t pid;

    if( (pid = fork() ) < 0) {
        printf("create failed %s\n",strerror(errno));
    }

    if(pid == 0) {
        printf("child process is running\n");
        exit(0);
    }

    if(pid > 0) {
        printf("parent process is running \n");
        while(1) {
        }
        wait(NULL);
    }

    return 0;
}
可以看到父进程深陷死循环,根本没有机会为死掉的子进程收尸,子进程就变成僵尸咯(注意,这个时候子进程是有父亲的)。 少量的僵尸进程还 OK,但是系统的进程 ID 是有限的,大量的僵尸进程只会拖累系统,导致无法产生新的进程。 感兴趣的可以看一下fork 炸弹,十几个字符就可以一摧毁一台计算机。
服务器通常是7*24小时开启的,如果程序编写不当,是一定会产生僵尸进程,那么该如何避免僵尸进程呢?
有两种方法,第一种,通过信号的方法:
#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <sys/types.h>#include <errno.h>#include <signal.h>void handler(){    wait(NULL);}int main(){    pid_t pid;    signal(SIGCHLD,handler);    if( (pid = fork()) < 0) {        printf("create failed %s\n",strerror(errno));    }    if(pid == 0) {        printf("child process is running\n");        exit(0);    }    if(pid > 0) {        printf("parent process is running \n");        while(1) {        }        wait(NULL);    }    return 0;}
signal() 用来监视信号,两个参数分别是信号和信号处理函数。子进程退出会发出 SIGCHLD 信号,处理函数调用 wait() 把子进程的尸体回收。
第二种方法涉及孤儿进程的概念,随后道来。

孤儿进程

孤儿进程,顾名思义就是那些父进程已经退出了,但它还没有退出。操作系统怎么会让孤儿进程 没了父亲呢?于是 init 进程会把该进程自动接收为子进程(收养),由 init 负责回收。 是不是很有爱? 孤儿进程的存在有什么意义呢?还记得防止僵尸进程的第二个方法么? 先看代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <signal.h>

void handler()
{
    wait(NULL);
}

int main()
{
    pid_t pid;
    signal(SIGCHLD,handler);  

    if( (pid = fork()) < 0) {
        printf("create failed %s\n",strerror(errno));
    }

    if(pid == 0) {
        printf("child process is running\n");
        exit(0);
    }

    if(pid > 0) {
        printf("parent process is running \n");
        while(1) {
        }
        wait(NULL);
    }

    return 0;
}
在父进程无脑循环的时候,子进程又创建了一个进程,并且自己先退出了,我们把子进程创建的进程称为孙子进程,他的父亲退出了,便成为了孤儿进程,这时 init 负责收养,并在孙子进程退出的时候为他收尸。 说实话,不知是我代码的问题,还是怎样,这种方法并不如信号那样奏效。而且还难于理解,所以我并不推荐。
对了,关于僵尸进程的测试方法我一般是这样做的:
  • 执行程序的时候,放到后台:./a.out &
  • 查看进程,看有没有僵尸进程: ps aux | grep Z
  • 有的话说明错误了,这时候把后台程序拉回来:fg
  • 结束程序:CTRL + C
 

© Xinyu 2014 - 2024