Linux 进程状态

2017-02-16 kernel linux

Linux 中的进程在不同的阶段会通过其状态显示,一般来说会有 7 种,相当于一个状态机运行。

这里简单介绍,以及一些常见的特殊状态。

进程状态

Linux是一个多用户,多任务的系统,可以同时运行多个用户的多个程序,就必然会产生很多的进程,而每个进程会有不同的状态,可以参考 task_state_array[]@fs/proc/array.c 中的内容。

static const char * const task_state_array[] = {
    "R (running)",      /*   0 */
    "S (sleeping)",     /*   1 */
    "D (disk sleep)",   /*   2 */
    "T (stopped)",      /*   4 */
    "t (tracing stop)", /*   8 */
    "X (dead)",         /*  16 */
    "Z (zombie)",       /*  32 */
};

其中进程的转换过程如下所示。

process

R (TASK_RUNNING,可执行状态)

只有在该状态的进程才可能在 CPU 上运行,这些进程的 struct task_struct 会被放入对应 CPU 的可执行队列中,进程调度器会从可执行队列中分别选择一个进程在该 CPU 上运行。

S (TASK_INTERRUPTIBLE,可中断的睡眠状态)

在等待某事件的发生 (例如 socket 连接、信号量等) 而被挂起,对应的 TCB 被放入对应事件的等待队列中,当事件发生时 (由外部中断触发或由其它进程触发),对应的等待队列中的一个或多个进程将被唤醒。

D (TASK_UNINTERRUPTIBLE,不可中断的睡眠状态)

进程同样处于睡眠状态,但该进程是不可中断的,也就是进程不响应异步信号,即使使用 kill -9 信号。

T (TASK_STOPPED or TASK_TRACED,暂停状态或跟踪状态)

向进程发送一个 SIGSTOP 信号,它就会因响应该信号而进入该状态,其中 SIGSTOP 与 SIGKILL 信号一样,是强制的,不允许用户进程通过 signal 系列的系统调用重新设置对应的信号处理函数。

特殊状态

子进程是通过父进程创建的,子进程的结束和父进程的运行是一个异步过程,父进程永远无法预测子进程到底什么时候结束。当一个进程完成它的工作终止之后,它的父进程需要调用 wait() 或者 waitpid() 取得子进程的终止状态。

当父子进程在不同时间点退出时,那么就可能会进入到异常状态。

孤儿进程

一个父进程退出,相应的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。

孤儿进程将被 init 进程所收养,并由 init 进程收集它们的完成状态,也就是说,孤儿进程没有危害,最终仍然回被 init 回收。

僵尸进程

一个进程使用 fork 创建子进程,如果子进程退出后父进程没有调用 waitwaitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,仍然占用进程表,显示为 defunct 状态。

可以通过重启或者杀死父进程解决。

示例

在 Linux 中,进程退出时,内核释放该进程所有的部分资源,包括打开的文件、占用的内存等。但仍为其保留一定的信息,包括进程号 PID、退出的状态、运行时间等,直到父进程通过 wait()waitpid() 来获取时才释放。

如果父进程一直存在,那么该进程的进程号就会一直被占用,而系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。

如下是两个示例,分别为孤儿进程和僵尸进程。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    pid_t pid = fork();
    if (fpid &lt; 0) {
        printf("error in fork!");
        exit(1);
    }
    if (pid == 0) { // child process.
        printf("child process create, pid: %d\tppid:%d\n",getpid(),getppid());
        sleep(5);   // sleep for 5s until father process exit.
        printf("child process exit, pid: %d\tppid:%d\n",getpid(),getppid());
    } else {
        printf("father process create\n");
        sleep(1);
        printf("father process exit\n");
    }
    return 0;
}
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

//static void sig_child(int signo)
//{
//     pid_t        pid;
//     int        stat;
//     while ((pid = waitpid(-1, &stat, WNOHANG)) &gt; 0)
//            printf("child %d terminated.\n", pid);
//}
int main ()
{
    pid_t fpid;
    // signal(SIGCHLD, sig_child);
    fpid = fork();
    if (fpid &lt; 0) {
        printf("error in fork!");
        exit(1);
    }
    if (fpid == 0) {
        printf("child process(%d)\n", getpid());
        exit(0);
    }
    printf("father process\n");
    sleep(2);
    system("ps -o pid,ppid,state,tty,command | grep defunct | grep -v grep");
    return 0;
}

第一个是孤儿进程,第二次输出时其父进程 PID 变成了 init(PID=1);第二个是僵尸进程,进程退出时会产生 SIGCHLD 信号,父进程可以通过捕获该信号进行处理。

TASK_STOPPED

TASK_STOPPED,进程终止,通常是由于向进程发送了 SIGSTOPSIGTSTPSIGTTINSIGTTOU 信号,此时可以通过 kill -9(SIGKILL) pid 尝试杀死进程,如果不起作用则 kill -18 pid ,也就是发个 SIGCONT 信号过去。

孤儿进程接管

如上所述,所谓的孤儿进程是指,当父进程被 kill 掉,其子进程就会成为孤儿进程 Orphaned Process,并被 init(PID=1) 所接管。

那么,孤儿进程如何被接管的?

在 Linux 内核中,有如下的代码 Kernel find_new_reaper() ,其开头的注释摘抄如下:

/*
 * When we die, we re-parent all our children, and try to:
 * 1. give them to another thread in our thread group, if such a member exists
 * 2. give it to the first ancestor process which prctl'd itself as a
 *    child_subreaper for its children (like a service manager)
 * 3. give it to the init process (PID 1) in our pid namespace
 */

也就是说,接管分三步:A) 找到相同线程组里其他可用的线程;B) 如果没有找到则进行第二步;C) 最后交由 PID=1 的进程管理。

SubReaper

当一个进程被标记为 SubReaper 后,这个进程所创建的所有子进程,包括子进程的子进程,都将被标记拥有一个 SubReaper

当某个进程成为孤儿进程时,会沿着它的进程树向祖先进程找一个最近的是 SubReaper 且运行着的进程,这个进程将会接管这个孤儿进程。

tinit

其功能类似于 init 进程,实际上就是模拟 init 进程的僵尸进程回收,一般用于容器中,用于回收容器中退出的进程。

Uninterruptable

Linux 中有一个 uninterruptable 状态,此时的进程不接受任何的信号,包括了 kill -9 ,通常是在等待 IO,比如磁盘、网络、其它外设等。如果 IO 设备出现了问题,或者 IO 响应慢,那么就会有很多进程处于 D 状态。

当出现了这类的进程后,要么等待 IO 设备满足请求,要么重启系统。

所以,为什么会出现这一状态?为什么内核不能正常回收?

正常 Sleep

一般来说,当一个进程在系统调用中正常休眠时,它会收到异步的信号,例如 SIGINT,此时会做如下的处理:

  1. 系统调用立即返回,并返回 -EINTR 错误码;
  2. 设置的信号回调函数被调用;
  3. 如果进程仍然在运行,那么会获取到系统调用返回的错误码,并决定是否继续运行。

也就是说,正常的 Sleep 允许进程收到信号后做一些清理操作。

何时出现

进程会通过系统调用来调用 IO 设备,正常来说这一过程很快,用户几乎不会察觉,但是当 IO 设备异常或者设备驱动有 bug ,就可能会出现上述的状态。

例如通过 read() 系统调用读取磁盘上的数据,如果是机械磁盘,那么磁盘需要寻道、移动读针、读取数据,然后才会返回结果,那么在读取数据的过程中就处于 uninterruptable 状态。

正常这一过程很快,用户几乎无法感知。

为什么

这主要是因为,IO 请求比较特殊,它必须按照固定的顺序执行,甚至有些时序的要求,如果操作不是原子性的,那么就可能导致 IO 设备异常,可能会无法响应下次请求,可能会被死锁,这都跟具体的设备有关。

一个好的设备驱动需要处理这些异常场景,除非出现了 bug ,也就是说只有在一些极端场景下才会出现。

KillAble

在 2.6.25 版本中引入了新的 TASK_KILLABLE 状态,会屏蔽普通信号,但可以响应强制信号,不过这个同样要依赖设备驱动的实现,详细可以参考 TASK_KILLABLE