操作系统的进程状态
当一个进程被执行时,内存中会载入它的PCB
和代码和数据。而PCB
中会有数据记录进程的状态,一般有运行状态、阻塞状态、挂起状态。
操作系统根据进程的状态,选择将进程的PCB
放在不同的队列中。
所有处于运行状态的进程的PCB
,都会放在运行队列中,CPU
会根据程序运行状态和时间片来挨个运算每一个处于运行状态的进程。
如果一个进程需要输入或输出,而又迟迟得不到输入或输出,比如我们程序中有scanf
函数,该进程希望能从键盘中得到输入,如果我们一直不输入,该程序就会处于阻塞状态,阻塞状态的PCB
会被链接在对应的硬件的结构体对象中,比如上面的例子中,进程的PCB
会链接在键盘结构体对象中,等待键盘输入。
Linux中的进程状态
那么具体在Linux中,进程有哪些状态呢?
如下是进程状态在kernel源代码里的定义:
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
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 */
};
注释的大意是,进程的状态是由一个位图存储的,运行状态是全0,其他组合代表不同的状态。
进程的状态是存储在PCB
中的,因为操作系统通过管理PCB
来管理进程。
查看进程状态
我们可以通过:
ps aux / ps axj 命令
来查看目前进程的运行状态。
运行状态
当我们创建一个进程,该进程死循环运行,我们可以查看它的状态信息。
当我们运行该程序后,命令行不会再被调出来,因为该进程一直未结束,这时我们登录另一个账户来查看进程信息。
此时我们可以看到,该程序处于运行状态R
,后面的+
表示该程序在前台运行,所以该程序如果不运行结束,bash
命令行就不会继续执行(bash也是进程)。如果想让test
后台执行,需要在调用test
的时候加&
。
此时后台运行的进程我们无法用ctrl + C
来结束进程,只能使用kill -9 PID
。
睡眠状态
上面的程序中,如果我们取消注释:
程序会循环输出
此时我们在另一个账户查看他的状态,就变成了S
睡眠状态
这是符合我们预期的,因为我们程序中有sleep
函数,但是如果我们注释掉sleep
函数呢?
可以看到,程序的状态还是S
,这是因为CPU
的运行效率很高,而打印的效率很低,所以在这个进程大部分时间都在等待输出,而不是进行计算,所以这就符合我们一开始描述的阻塞状态,没错,睡眠状态就是一种阻塞状态。
深度睡眠状态
深度睡眠状态D
也是阻塞状态的一种,也叫做磁盘休眠状态,或不可中断睡眠状态。
在这种状态下的程序无法被操作系统杀死,只能等待IO结束。
暂停状态
暂停状态T
也是阻塞状态的一种,可以通过发送SIGSTOP
信号给进程来停止(T)进程。这个被暂停的进程可 以通过发送SIGCONT
信号让进程继续运行。
我们注释掉打印和sleep
,再次运行上面的死循环,然后用kill -19 PID
来让他变为暂停状态:
想要使其恢复运行状态,可以使用kill -18 PID
需要注意的是:即使是前台运行的进程,在变成暂停状态后也会变成后台进程,而且即使再恢复,也还是后台进程。
暂停状态还有另一种t
,这是一种出现在调试时的状态。
如果我们使用gdb
调试一个debug
版的进程,而且该进程现在停在断点处,那么该进程就会处于t
状态。
挂起状态
挂起状态也是一种阻塞状态。
当内存不足时,操作系统会将处于睡眠状态的进程的代码和数据存储在磁盘中,只留下PCB
“排队”,这样就可以省出内存空间。
当进程从进程状态转为运行状态时,再将代码和数据从磁盘中写入内存中。
死亡状态
当进程运行结束,并完成资源释放后,程序就会处于死亡状态X
。这个状态只是一个返回状态,用户不会在任务列表里看到这个状态。
僵尸状态
僵死状态(Zombies)是一个比较特殊的状态。当进程退出并且父进程没有读取到子进程退出的返回代码时就会产生僵尸进程。僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态代码。 所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
来一个创建僵尸进程例子:
编译,5s后查看进程状态:
30s后查看进程状态:
可以看到,子进程结束后,父进程没有释放子进程的资源,所以子进程就成了僵尸进程。而父进程结束后,自动释放自己和子进程的资源。
孤儿进程
还是上面的例子,如果父进程先退出了,子进程还在运行,那么该子进程就被称为孤儿进程。
父进程运行结束后,自己先退出了,而后子进程才运行结束,此时如果不对子进程进行资源释放,子进程就会一直维持僵尸状态,从而造成内存泄漏。
编译,前5s查看进程状态:
5s后查看进程状态:
可以看到,父进程推出后,子进程还在运行,而且它的PPID
变成了1,PPID
为1的进程就是操作系统。
也就是说,如果父进程比子进程先退出,该子进程会被操作系统“收养”,由操作系统完成子进程的资源清理。