本篇主要讲解进程的运行状态和退出状态。为了方便对调度进程进行管理,linux对进程不同的运行状态和退出状态进行了划分和标识。
- 进程的运行状态保存在task_struct结构体中的state成员中
- 进程的退出状态保存在task_struct结构体中的exit_state成员中
进程运行状态
linux主要共划分了6种运行状态:TASK_RUNNING、TASK_INTERRUPTIBLE、TASK_UNINTERRUPTIBLE、__TASK_STOPPED、__TASK_TRACED和TASK_DEAD。
TASK_RUNNING(R)
当进程处于运行态或者就绪态等待调度运行时,该进程的运行状态即为TASK_RUNNING。在ps aux命令中用R(Running)标识进程处于TASK_RUNNING状态。
注意,TASK_RUNNING状态不只是指正在cpu上运行的进程状态,还指处于就绪态的进程,此时该进程在等待cpu的调度执行。cpu的运行队列上保存的进程都是处于TASK_RUNNING状态的进程,调度时也是针对这些进程,每次从运行队列上筛选出一个最优的进程调度运行。
TASK_INTERRUPTIBLE(S)
可中断的睡眠状态,又称为浅睡眠状态。表示进程正在睡眠等待,并且睡眠过程中可以被信号唤醒,这是和TASK_UNINTERRUPTIBLE状态最大的不同。在ps aux命令中使用S(Sleep)标识进程处于TASK_INTERRUPTIBLE状态。
TASK_UNINTERRUPTIBLE(D)
不可中断睡眠,又称为深度睡眠状态。表示进程正在睡眠等待,并且睡眠的过程中不可以被信号唤醒。通常这个进程在等待I/O的情况。由于深度睡眠状态的进程不可被信号唤醒,因此我们不能使用信号将其杀死,包括SIGKILL(9)也不行。该状态通常用于内核中一些不能打断的流程中,如read一些设备文件和物理设备时。
在ps aux命令中使用D标识进程处于TASK_INTERRUPTIBLE状态。
__TASK_STOPPED(T)
终止状态。该状态下的进程停止运行,如进程收到SIGSTOP(19,暂停信号)、SIGTTIN、SIGTSTP或者SIGTTP等信号时,进入该状态。
在ps aux命令中使用T(stop)标识进程处于__TASK_STOPPED状态。
__TASK_TRACED(T)
进程正在被跟踪状态。注意,“正在被跟踪”是指在进程暂停下来,进程等待跟踪它的进程下发命令操作。如gdb对被跟踪的进程打了一个断点,该进程运行到断点处停下来了,此时该进程即处于__TASK_TRACED状态。而其他时候,该被跟踪进程的状态还是处于上面提到的那些状态,即和正常进程一样。
在ps aux命令中使用T(stop)标识进程处于__TASK_TRACED状态,和__TASK_STOPPED标识相同。
TASK_DEAD
进程在退出时对应的状态。在进程退出的过程中,进程占有的所有资源(包括内存空间、可执行代码等)都会被回收,除了task_struct这个结构体。task_struct这个结构体中保存了进程的退出状态(task_struct中的exit_state),这样方便父进程能获取子进程的退出状态。
进程退出状态
linux主要划分了3种退出状态:EXIT_ZOMBIE、EXIT_DEAD和EXIT_TRACE(EXIT_ZOMBIE | EIXT_DEAD)。
EXIT_ZOMBIE(Z)
僵尸态。如果一个进程退出后,其父进程没有将其内核中的task_struct结构体进行回收,那么该进程即变为僵尸进程,自身处于僵尸态。这里面将两个概念:僵尸进程和孤儿进程。
- 僵尸进程:如果子进程退出后,其父进程还存在,但没有将其内核中的task_struct结构体进行回收,那么该子进程将变为僵尸进程。
- 孤儿进程:父进程退出后,它的子进程将由init进程托管,init进程成为它们的父进程,这些子进程成为孤儿进程。
注意,孤儿进程不会对系统有什么危害,但僵尸进程对系统有危害。僵尸进程由于其内核中的task_struct结构体还未释放,继续占用进程表和内存空间。若产生了大量的僵尸进程将消耗大量的系统资源,影响系统稳定性。
在ps aux命令中使用Z(Zombie)标识进程退出时处于EXIT_ZOMBIE状态。
EXIT_DEAD(X)
进程的最终退出状态即为EXIT_DEAD,进程即将被销毁。进程的资源(task_struct结构)被父进程回收后,进程的退出状态会由EXIT_ZOMBIE变为EXIT_DEAD。
进程状态变化
通过上述介绍,我们已经了解到进程有很多状态。在linux运行过程中,进程在一定条件下可以由一种状态变化为另一种状态。
在介绍之前,我们首先要树立一个观念,即进程状态的变化只有两个方向:
- 从运行态/就绪态TASK_RUNNING变为其他状态。
- 从其他状态变为运行态/就绪态TASK_RUNNING。
任何两个非TASK_RUNNING状态都不能直接转变,它们都需要先转变为TASK_RUNNING状态再变为其他,也即必须通过TASK_RUNNING状态(唤醒)过度。
这个其实也很好理解,进程只有先处于TASK_RUNNING才能得到执行,在执行的过程中再改变其状态。各个运行状态的转化图如下:
各个状态间的转化分析如下:
- 我们在通过fork创建子进程时,子进程会继承父进程的状态。由于父进程肯定在执行,因此创建的子进程状态一般也为TASK_RUNNING运行态。处于运行态/就绪态的子进程将被放到cpu的运行队列上。
- TASK_RUNNING包括两类进程:处于cpu运行队列上就绪态的进程和正在cpu上执行的进程。调度器通过schedule函数将cpu运行队列上处于就绪态的进程放到cpu上运行; 当正在cpu上执行的进程被抢占时,该进程将被被放回cpu的运行队列上。
- 若正在cpu上运行的进程若调用sleep函数、等待特定事件阻塞等主动放弃cpu,自身进入睡眠状态,此时进程状态将变为TASK_INTERRUPTIBLE或者TASK_UNINTERRUPTIBLE。 举两个例子: 1. 若进程是调用锁lock或者sleep进入休眠状态,则进程进入的是TASK_UNINTERRUPTIBLE状态。(相关于有活干,知识缺乏料) 2. 若进程是调用epoll_wait进入休眠状态,则进程进入的是TASK_INTERRUPTIBLE状态。(相当于没活儿干,信号可以唤醒它)
- 当处于睡眠状态的进程(TASK_INTERRUPTIBLE/TASK_UNINTERRUPTIBLE)等待的事件已经发生,任务被唤醒,进程状态将变为TASK_RUNNING,进程将被添加到cpu的运行队列上。
- 若正在cpu上运行的进程若调用do_exit函数,进程状态将由TASK_RUNNING变为TASK_DEAD,进程被终止。(若调用exit函数时进程不处于TASK_RUNNING状态,则先等待进程状态变为TASK_RUNNING再变为TASK_DEAD)
- 若正在cpu上运行的进程收到了SIGSTOP、SIGTTIN、SIGTSTP等信号,进程状态将由TASK_RUNNING变为__TASK_STOPPED。(若收到SIGSTOP、SIGTTIN、SIGTSTP等信号时,进程并不处于TASK_RUNNING状态,则先等待进程状态变为TASK_RUNNING再变为__TASK_STOPPED)
- 若正在cpu上运行的进程被gdb调试运行到了断点处停下来了,等待下发新的命令,则进程的状态将由TASK_RUNNING变为__TASK_TRACED。
进程的运行状态、退出状态总结如下: