进程状态
- 进程状态的简要介绍
- 运行状态
- 进程排队
- 阻塞状态
- 挂起状态
- Linux中的进程状态
进程状态的简要介绍
进程状态指的是一个操作系统中正在运行的进程当前所处的状态。根据不同的操作系统,进程状态可能会有一些细微的差别,但最主要的是以下三种状态
- 运行状态
- 阻塞状态
- 挂起状态
下面我会对这三个状态做一个详细的介绍
运行状态
谈到运行状态我们常常会有一个误区,那就是认为一个进程在运行中才能叫做运行状态,其实不然,只要是进程在运行中或者是在运行队列中都可以被称作当前进程处于运行状态
那么此时又有了一个新的概念:运行队列,什么是运行队列?简单来说,运行队列就是操作系统中用来存储和管理进程的数据结构,通过该队类可以充分利用 CPU 资源并提高系统的响应速度。也可以这么认为,这么运行队列和我们平常认识的队列特殊就在于运行队列中装的都是进程,并且这个运行队列是专门为CPU所服务的。
我们想要运行进程,就需要CPU去参与运算,但是进程有很多,CPU却只很少,大部分的电脑上只有一个CPU,所以为了高效率管理,操作系统就会帮CPU维护一个运行队列,当我们想要加载一个进程时,首先操作系统就会让这个进程的pcb对象去CPU的运行队列中排队,这个时候,有牵扯出了一个新的概念:进程排队
进程排队
通过进程的初步认识一和进程的初步认识二我们知道,所谓的pcb对象,实际上就是一个结构体对象,所以,进程排队,我们就可以简单理解为将一个结构体对象push到队列中。
了解到了以上之后,我们再来看看这个pcb
对象是如何push到队列中的
可以看到,在pcb
中嵌套了一个struct listnode
类型的结构体对象,这个结构体对象的next
指针指向的就是下一个结构体对象中的类型为struct listnode
的结构体对象,并且一个pcb
对象中包含了不止一个struct listnode
的结构体对象,这样就确保了该pcb
对象可以同时被联入不同的数据结构中。
补充
虽然这样的结构设计可以确保pcb同时在操作系统中的不同的数据结构中,但是next
指针和prev
指针始终只能指向下一个pcb
对象中的struct listnode
类型的结构体对象,我们找到pcb
对象的主要目的就是访问pcb对象的数据,通过这种方法却只能访问pcb
对象中的某个成员变量,那要怎么去访问整个pcb
对象呢?
我们可以简化以下这个问题
struct A
{char x;int c;
};int main()
{struct A s;return 0;
}
以该结构体为例,当我们知道了s的成员变量c的地址,要想知道结构体对象s的地址,通过结构体的内存对齐规则可以得出&c=&s+偏移量
,现在拿到了c的地址,要知道s的地址,也就是&s=&c-偏移量
,我们只需要知道偏移量即可
要知道偏移量,我们可以对零地址(地址为0)进行强转成struct A
类型,然后对拿到的成员变量n进行取地址操作,由于结构体对象的地址为0,所以变量n的地址就是n相对于结构体对象地址的偏移量
同理,当我们知道了pcb
对象中的struct listnode
类型的结构体对象的地址,我们也可以进行如上操作,即&s=(task_struct*)(&c-&((task_struct*)0->c))
阻塞状态
首先我们要知道,进程不是一直都在运行的,有时候进程需要等待某种硬件资源,比如在C语言程序中,我们会用到printf函数和scanf函数,printf函数就需要用到屏幕,而scanf就需要键盘,如果说进程没有等到对应的硬件资源时,它就不会继续运行。而是会做两件事
- 将当前进程状态由运行状态改为阻塞状态
- 将pcb对象连到对应资源的等待队列中
通过以上我们可以知道,不是只有CPU才有队列,其他的设备也有自己的队列。
当一个程序中有scanf函数需要用到键盘,CPU此时无法继续执行下去,操作系统就会将该程序对应进程pcb从CPU的运行队列拿到键盘的等待队列中去,当我们通过键盘按下一个按键的时候,操作系统会将该数据拿到并交给对应的进程,然后再将pcb从键盘的等待队列中拿到CPU的运行队列中去
挂起状态
挂起状态的前提就是当前计算机的资源(主要是内存资源)已经十分吃紧了,而有个进程还在等待队列中(阻塞状态)
也就是说当前进程无法继续执行下去并且该进程对应的可执行程序仍然会占用内存空间,操作系统为了节省资源,就会将该进程的对应的程序和数据写入磁盘中,这个时候该进程就是挂起状态。等到该进程要再次被调度的时候操作系统再将对应的数据和程序加载到内存
补充
操作系统一般会将进程所对应的代码和数据放到磁盘中的一块固定的区域,这块区域叫做swap分区,这个区域是操作系统在资源紧张的时候和磁盘进行数据交换的地方
一般来说,swap分区不会太大,通常是内存大小的一般或者是等于内存大小,最多不会超过内存大小的两倍,因为唤入和唤出其实是一个访问外设的过程(将数据进行拷贝),这是比较慢的,用效率换取系统的稳定性,如果将swap分区设置的比较大,那么操作系统就会比较依赖这个swap分区最终带来的结果就是整个系统和swap分区IO交互的频率变高,使得系统的效率就变低了
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 */
};
以上为状态在kernel原代码中的定义。
R 运行状态:
运行状态并不代表着进程正在运行中,它表明进程要么是在运行中要么在运行队列里随时做好了被调度的准备
补充:前台进程和后台进程
Linux中,前台进程和后台进程是指进程与终端(Terminal)之间的关系。
前台进程:前台进程是指当前正在终端上活跃运行的进程,它会接收终端输入并向终端输出信息。用户可以直接与前台进程进行交互,而且前台进程通常会占用终端的显示区域。当一个进程在前台运行时,用户可以通过键盘输入命令来控制该进程的行为。
后台进程:后台进程是指在终端上运行,但不会占用终端显示区域或接收终端输入的进程。用户可以将一个前台进程转为后台进程,从而让其在后台静默运行,不影响用户在终端上的其他操作。后台进程通常用于执行一些需要长时间运行的任务,或者不需要用户直接交互的程序。
T在Linux中,可以使用特定的命令和符号来控制进程的前台和后台运行。例如,使用 & 符号可以将一个命令放到后台运行;使用 fg 命令可以将一个后台进程切换到前台运行。这些功能使得用户能够更灵活地管理和控制系统中的进程。
S 睡眠状态:
睡眠状态意味着在等待某个事件的完成(这个睡眠也叫做可中断睡眠),当我们在C语言文件中加入sleep函数也可以使进程变为睡眠状态
在以上代码中,因为进程一直在等待从键盘中拿数据,所以该进程处于键盘的等待队列中,此时的状态就是直接讲到的阻塞状态,在Linux中,将其之为睡眠状态
D 磁盘休眠状态
有时候也叫不可中断睡眠状态,在这个状态下的进程通常会等待IO的结束(操作系统在内存严重不足时会杀死进程,但是不会中断状态为D的进程)
T 停止状态
T停止状态(stopped): 可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
注意:一旦将一个进程停止,这个进程就变成了后台进程
X死亡状态
这个状态只是一个返回状态,你不会在任务列表里看到这个状态