进程状态
当一个可执行程序,被载入内存,获得自己的PCB
,那么其就可以变成一个进程。也许你学习过一些进程状态的相关知识,其中最常见的就是以下图片:
我们把进程状态分为了运行,阻塞,挂起三个状态。但是这个图片过于笼统了,相信你学习的时候,也不是很能理解这三个状态到底是啥。本博客将深入讲解Linux中的进程状态,把每一个状态都深入展开讲述。
在Linux中,进程状态的本质其实就是一个整型变量,Linux通过管理进程的PCB结构体,来管理进程,而PCB内部就有一个整型变量来表示进程。
我们不妨看看Linux源码怎么说:
/* * 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 */ };
其中包含了R
,S
,D
等等状态,后面都标识了其对应的整型变量。
第一行中,用bitmap
来描述这个数组,也就是一个位图,你会发现每个状态,都是2的次幂,也就是说只有一位是1
,当全0
就是R
运行状态。
比如S
状态对应的位图就是00000001
,T
状态对应的位图就是00000100
。
所以在源码中,进程状态就是用一个整型来描述的。
那么进程的状态有什么意义呢?
比如说,当你敲完一天的代码,你突然对别人说:”我饿了!“,其中饿了
就是你当前的状态,这个状态意味着,你即将要去吃饭。
因此,进程状态标识着这个进程的后续动作!
比如一个简单的程序:
#include <stdio.h> int main() { int a; scanf("%d", &a); return 0; }
当这个程序运行到一半,就会暂停运行,等待用户输入一个数字,此时进程就进入了一个等待
的状态,其标识着这个程序在等待用户输入。
运行状态
也许你听说过,进程是可以排队的,最常听的莫过于运行队列
,那么进程是如何排队的呢?
进程排队的本质,是进程的PCB
在排队,一个PCB
可以在多个队列中。
比如下图:
这就是一个运行队列的视图,操作系统给CPU
维护了一个运行队列,当有进程要进入运行队列时,就把PCB
连入这个链表中,然后CPU
遍历链表,一个一个执行。
不过通过上图可以看出,PCB进入链表中,其实不是把整个PCB都连入链表,而是PCB中有多个链表节点的成员,把这个链表节点的成员连入运行队列中。这个链表节点成员是一个简单的链表节点指针List Node*,然后该指针指向下一个节点。
这样操作的好处在于,一个PCB可以连入多个链表,处于多个不同队列中。而想要通过这个小的链表节点找回整个PCB,只需要调用C语言提供的宏offsetof即可。
每个软硬件都有自己的等待队列,比如CPU的等待队列就可以叫做运行队列,各个进程在等待CPU执行它们。比如键盘也有等待队列,各个进程在等待从键盘中提取数据。
当一个进程需要运行,就把它链接到CPU的等待队列中,当一个进程需要网络请求,就把它链接到网卡的等待队列中。对于PCB的状态改变,以及把PCB放到哪一个队列中,都是由OS来执行的。
R状态
R
状态就是运行状态running
,只要一个进程处于CPU
的运行队列中,它就是R
状态。
也就是说,运行状态不一定是当前进程正在使用CPU
,也许CPU
还没有运行当前进程,只是这个进程在CPU
的运行队列中,马上要被执行了。
我们看到以下程序test.exe
:
#include <stdio.h> #include <unistd.h> #include <stdbool.h> int main() { while(true) { printf("hello world!\n"); } return 0; }
这是一个死循环的程序,其会一直输出hello world
字符串,现在我们让其运行起来,然后使用执行ps ajx
来查看这个进程的状态。
先执行程序:./tets.exe
:
由于是一个死循环,而且没有sleep,会一直高速刷屏,我们再开一个窗口输入ps ajx | head -1 && ps ajx | grep test.exe | grep -v grep:
我先简单说明该指令,ps ajx用于列出所有进程相关数据,其第一行用于表示各个数据的含义,因此我们先用ps ajx | head -1保留第一行数据。
由于系统中进程太多了,直接ps ajx
会大量刷屏,我们只查找进程test.exe
,所以用grep test.exe
筛选。
由于grep
本身也是一个进程,而且输入了选项test.exe
,如果筛选test.exe
,会把grep
自己也筛选出来,所以我们要把grep
自己删掉:grep -v grep
。
输出结果:
STAT栏表示进程状态,我们这个test.exe明明一直在执行,最后的状态不是R,而是一个S+,这是为什么?
因为我们的进程中,有一个printf语句,其需要向屏幕输出,而CPU计算的速度远大于向屏幕输出的速度,因此这个进程99%的时间都在显示屏的等待队列中,只有很小一段时间在CPU的运行队列中,所以我们很难看到R状态。
现在我们删掉printf
,再看看:
#include <stdio.h> #include <unistd.h> #include <stdbool.h> int main() { while(true) {} return 0; }
执行结果:
此时光标一直卡顿,说明这个进程开始执行,陷入死循环了。
再用ps ajx | head -1 && ps ajx | grep test.exe | grep -v grep
查看:
此时进程test.exe
就进入R+
的运行状态了!现在我们成功证明,并且观察到了R
状态的存在,那么R+
的+
是什么意思呢?
进程状态后面的+
代表这个进程是一个前台运行的进程,我们这个进程一旦运行,我们就不可以在这个窗口再输入命令了,这就算一个前台进程。
如果想让一个进程在后台运行,只需要在指令的最后面加上一个&
符号即可,./test.exe &
:
可以看到,现在这个指令一执行完,给我们返回了一个PID
,我们就可以再次使用命令行了,现在这个test.exe
的进程就在后台运行了。
再用ps ajx | head -1 && ps ajx | grep test.exe | grep -v grep
查看:
现在test.exe
的状态就是R
状态,而不是R+
了,说明这是一个后台运行的进程。
那么我们要如何关闭这个进程呢?
- 对于前台进程,可以通过
ctrl + c
或者输入指令kill -9 PID
来关闭- 对于后台进程,只能通过
kill -9 PID
来关闭
对于刚刚的后台进程,我们就要输入kill -9 15738
来关闭进程。
阻塞状态
S状态
S
状态,即sleep
休眠状态,属于阻塞状态,其一般处于等待资源的状态,比如等待scanf
输入,printf
输出等。
比如我们刚刚的死循环程序:
#include <stdio.h> #include <unistd.h> #include <stdbool.h> int main() { while(true) { printf("hello world!\n"); } return 0; }
此时就可以观察到S
状态了:
此处的printf
一直在等待显示器的输出,也就是等待显示器资源,所以会处于S
状态。
这个S
状态,也可以叫做可中断睡眠
或者浅度睡眠
状态,我们可以在其等待资源的时候,通过ctrl + c
直接中断程序。
D状态
D
状态,disk sleep
,属于阻塞状态,也叫做不可中断睡眠
或者深度睡眠
状态。
当OS过于繁忙的时候,内存中可能会有大量进程,此时OS就会选择直接杀掉某些进程。比如你使用低配置的手机打高能耗的游戏的时候,程序就有可能会直接闪退,这个过程就是OS直接杀掉程序的过程。因为该进程已经让操作系统过于繁忙了,操作系统会直接将其杀掉,防止崩溃。
而D状态,是一个免死金牌,如果某些进程正在处理很重要的内容,我们不希望OS主动把它杀掉,此时给这个进程一个D状态,操作系统就不会主动杀掉该进程。
由于出现D
状态时,操作系统已经处于很危险的状态了,所以其不好观察,这里就不观察了。
T状态
T
状态,属于阻塞状态,即进程处于暂停状态。
想要出现T
状态,我们可以通过kill
给进程发送信号,其中19
号状态,就是SIGSTOP
暂停的信号。
现在我们再次运行刚刚的./test.exe
死循环:
现在其处于R+
状态,也就是前台运行的状态,然后我们给其发信号kill -19 18280
:
现在test.exe
显示了一个stopped
,即被暂停了,我们再通过ps ajx
观察:
此时进程就处于T
状态了,而且会被从前台转到后台运行。
如果想要从T
状态恢复,可以使用kill -18
信号,其代表SIGCONT
,即继续进程。
不过就算从T
状态恢复了,其也依然是一个后台进程。
t状态
t
状态也是一个暂停状态,但是其是一种被追踪的暂停状态,属于阻塞状态。
什么叫做被追踪呢?其实就是只有收到某个命令后,进程才会继续执行。
比如说使用调试程序的时候,当进程到某个断点处停止了,此时进程就属于t
状态。
现在我们将代码编译为debug
版本,命名为test-debug.exe
,然后用gdb
调试,gdb test-debug.exe
:
随便打一个断点后,让程序运行到断点时停止,然后使用指令ps ajx | head -1 && ps ajx | grep test-debug.exe | grep -v grep
:
输出结果:
此时可以看到,出现了两个进程,一个是gdb
调试器进程,另外一个就是test-debug.exe
,该进程的状态就是t
状态了。
挂起状态
挂起状态就是当内存资源不足时,OS
将数据和代码交换到磁盘中挂起,此时内存中只有PCB
。
当内存吃紧了,此时如果有进程处于阻塞状态,OS检测到该进程暂时不会得到数据,于是把该进程的代码和数据放到磁盘的swap分区中,这个过程叫做换出,于是就有内存空出来放其它的内容了。
这个过程中,PCB一直保留在内存中,因为PCB要去排队,比如这个进程在等待网卡资源,那么PCB就会一直处于网卡的等待队列,直到获得资源,再把磁盘中的代码和数据拷贝回来,这个过程叫做换入,然后再运行程序。
当代码和数据处于磁盘中,而PCB还在队列中等待资源,这就算一个挂起状态。因此挂起状态是一个特殊的阻塞状态。
举个例子,我们在下载软件时,下载这个进程就会被加载到内存中,此时如果突然断网了,就会阻塞(缺少网卡资源),由于这里进程暂时不会被运行,资源放在内存中就会浪费空间,OS就会将代码和数据(进程)暂时存放到磁盘中,等到有网了再从磁盘中拿取,这个过程就是挂起状态
僵尸进程 & 孤儿进程
X状态
X
状态表示死亡状态,此时进程已经死亡了,但是该状态只保留非常非常短暂的时间,因为很快该进程的PCB
就被释放了。所以该进程很难观察到,我们只需要知道确实有该状态存在即可。
Z状态
当一个进程退出时,其代码和数据会被立马释放,但是其PCB
会被保留,因为我们需要通过这个进程的PCB
来判断这个进程的执行情况。
就好像法医需要对死者验尸,来确定该死者的伤亡情况。进程在执行结束后,也要进行一个”验尸“的操作。
当一个进程执行完毕,PCB
还被保留,等待别人读取的时候,该进程就处于Z
僵尸状态(zombie)。所有进程结束后,都必须经过Z
状态。
这个读取PCB
的任务,是父进程执行的。
我们看到这样一个程序:
#include <stdio.h> #include <unistd.h> #include <stdbool.h> #include <sys/types.h> #include <stdlib.h> int main() { pid_t id = fork(); if(id == 0)//子进程 { int cnt = 3; while(cnt--) { printf("I am child, pid = %d, ppid = %d\n", getpid(), getppid()); sleep(1); } exit(0); } while(true) { printf("I am father, pid = %d, ppid = %d\n", getpid(), getppid()); sleep(1); } return 0; }
该进程通过fork
创建了一个子进程,子进程三秒后exit
退出,而父进程一直运行。此时子进程死亡,但是父进程依然运行还没有读取子进程的PCB
,子进程就处于Z
状态了。
执行程序:
三秒已过,此时查看子进程状态,ps ajx | head -1 && ps ajx | grep 20152 | grep -v grep
:
子进程就是Z+
状态了,也就是僵尸进程。子进程此时在等待父进程结束后,回收其PCB
,此时进程才算真正退出。
孤儿进程
如果一对父子进程,父进程先退出了,子进程后退出,那么子进程的PCB
就无法被父进程回收,因为父进程已经先退出了。
对于这种父进程先退出的进程,叫做孤儿进程
。
而只要僵尸进程的PCB
不被回收,其就会一直留在操作系统中等待回收。那么就到造成内存泄漏的问题,Linux
是如何解决这个问题的?
我们看到以下代码:
#include <stdio.h> #include <unistd.h> #include <stdbool.h> #include <sys/types.h> #include <stdlib.h> int main() { pid_t id = fork(); if(id == 0)//子进程 { while(true) { printf("I am child, pid = %d, ppid = %d\n", getpid(), getppid()); sleep(1); } } int cnt = 10; while(cnt--) { printf("I am father, pid = %d, ppid = %d\n", getpid(), getppid()); sleep(1); } return 0; }
该程序中,父进程执行十秒后退出,而子进程一直死循环,那么此时子进程就失去了父进程,这该怎么办?
输出:
此时子进程的PID = 21350
,我们前十秒观察该PID
的子进程:
此时PPID = 21349
,也就是进程test.exe
十秒后再观察子进程:
此时PPID = 1
,Linux中1
号进程代表OS
操作系统。
也就是说:孤儿进程会被1
号进程操作系统认领,随后PCB
由操作系统亲自回收。