进程状态
在学习操作系统这一课程中,相信一定见过类似下面这样的图;
上图中提到了很多进程状态:就绪、运行、阻塞、就绪挂起、阻塞挂起;
那这些状态表示的到底是什么呢?
运行
这简单描述一下就绪状态,就绪状态就是当前进程已经准备好被
CPU
调度执行了;也就是说CPU
可以随时进行调度的进程。
那运行态呢?简单来说,正在占用CPU
的进程就处于运行态。
换一种说法就是:进程占有处理器正在运行的状态;进程已获得CPU,其程序正在执行。
在上一篇文章中,我们了解到,操作系对于加载到内存的程序要进行管理;描述就是进程
PCB
;组织就是使用数据结构将这些PCB
管理起来。那现在
CPU
下想要调度一个进程,只需要找到这个进程的PCB
就可以进行调度了。
每一个CPU
它都有一个运行队列runqueue
(这个运行队列中存放着这个CPU
即将运行进程的PCB
);这样我们的进程调度器就从CPU
的运行队列中选择一个进程在CPU
中运行。
在Linux
操作系统中,运行态并不一定是占有CPU
资源的进程,处于CPU
的运行队列runqueue
中的进程都是处于运行态。
阻塞
阻塞、通过观察操作系统进程状态图,我们可以发现,阻塞态的进程都是在等待某种事件/资源;那阻塞态的进程到底是在等待什么呢?
最常见的就是I/O
、当前进程在等待I/O
;当然也存在等待其他硬件响应的。
简单来说就是,我们写了一个代码,让它运行起来;现在我们要从键盘中读取一个数据,但是键盘现在没有就绪(响应),当我们通过键盘输入数据之后,代码才能继续运行;
像这样等待硬件就绪的进程就处于阻塞态。
我们知道,操作系统不仅要进行进程管理,还要管理硬件资源,如何管理呢?(先描述、后组织)
那我们是不是可以理解为在内存中存在对描述硬件信息的struct
(struct device
),那还存在一中数据结构将每一个硬件的信息存储起来divices
。
当我们进程等待某一个硬件资源响应时,从运行态 -> 阻塞态;
从本质上将,这个进程就从
CPU
的运行队列变到了某个硬件资源的等待队列(wait_queue
)。
挂起
挂起又分为就绪挂起和阻塞挂起;那这到底是什么呢?
挂起进程可以理解为暂时被淘汰出内存的进程。
我们知道内存是有限的,当内存中存在特别多的进程时,内存中的可用空间非常少;此时操作系统就会对内存空间进行调整,将一部分进程放入到磁盘中,从而来释放内存空间。
- 阻塞挂起:操作系统将处于阻塞态的进程放入到内存中,该进程就变成了阻塞挂起状态。
- 就绪挂起:操作系统将处于就绪态的进程放入到内存中,该进程就变成了就绪挂起状态。
Linux
中的进程状态
上述进程概念运行
、阻塞
、挂起
那都是系统的概念,那在Linux
中我们进程状态是什么呢?
在Linux
中,进程状态其实就是task_struct
中的一个整数。
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 */
};
我们可以看到,Linux
中进程状态有很多:R
、S
、D
等等。
那这些状态到底是什么呢?
运行状态
R
运行状态,和上述中一样,位于CPU
的运行队列中的进程。
只有该状态的进程才可能在
CPU
上运行,同一个时刻可能有多个进程处于运行态,这些进程的task_struct
都位于CPU
的运行队列中。
睡眠/休眠状态
睡眠S
状态、休眠D
状态,对应的就是我们进程状态中的阻塞状态,那为什么有两个呢?
处于
S
状态的进程,是因为进程在等待某个设备就绪或者等待其他资源,而被阻塞;这些进程的task_struct
被放入到了对应的等待队列中;当等待事件发生,处于S
状态的设备就被唤醒。
这里通过查看所有进行属性可以发现,大部分的进程都是处于S
状态的;毕竟我们的CPU
资源有限,如果所有的进程都处于运行态,那CPU
响应不过来啊。
这里演示一下
S
状态:
#include <stdio.h>
int main()
{int x;scanf("%d",&x);return 0;
}
这里留下一个疑问,我们正在运行中的程序,为什么我们查看时还是处于S
状态?
休眠D
状态,也称为磁盘休眠状态;
在我们内存资源紧张时,我们的进程可能会被挂起,而如果挂起以后资源还是非常紧张,我们的操作系统是会杀死处于
S
状态的进程的;但是如果某些进程正在等待磁盘设备写入数据的结果反馈,我们操作系统此时因为内存资源极度不足,然后将此类进程杀死导致数据丢失;
操作系统不想要数据丢失,为了避免将这类进程杀死,
Linux
就设置了状态D
,此状态的进程处于深度休眠状态;操作系统无法杀死处于D
状态的进程,只能等待进程自己醒来。
当出现了D
状态的进程时,操作系统就快要崩溃了。
暂停状态
暂停状态也分为两种T
和t
;
当我们某些进程执行时存在一些危险的操作行为时,这时候操作系统就先暂停此进程;此时该进程就处于T
状态;
我们也可以理解为操作系统层面暂停的进程都是处于
T
状态的;这里我们也可以通过发送信号,来暂停程序,也是处于
T
状态的。
kill -l
:可以查看所有kill指令的参数选项与对应信号效果kill -19 [pid]
:暂停对应进程kill -18 [pid]
:让对应进程继续运行
t
状态,当我们使用调试工具时打断点/逐行指向代码时,我们的进程是处于暂停状态t
的;
死亡状态
当进程运行结束,进程的状态就是死亡状态X
;这里我们是没办法在任务列表中看到这个X
状态的。
X
状态是一个返回状态。
僵尸状态
Z
状态表示僵尸状态,也被称为僵尸进程;什么意思呢?
简单来说就是,子进程在运行结束之后,不会直接进入死亡状态X
,而是处于僵尸状态Z
;直到父进程对子进程的退出(返回)进行处理(调用wait()
或者waitpid()
),子进程才会真正意义上的结束。
说的直白一些就是,父进程通过
fork
创建子进程之后,子进程先运行结束了,子进程不会直接挂掉,而是等待父进程获取它的返回信息,直到父进程获取返回信息之后,子进程才会挂掉。处于僵尸进程的状态,它并没有被完全销毁,操作系统只是释放了进程的所有资源,并没有销毁进程的
task_struct
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{int id = fork();if(id < 0){perror("fork");return 1;}else if(id == 0){printf("子进程 pid : %d, ppid : %d\n",getpid(),getppid());} else{ while(1){ printf("父进程 pid : %d, ppid : %d\n",getpid(),getppid()); sleep(1);} }return 0;
}
僵尸进程的危害:
- 占用进程号资源:耗尽PID池,导致新进程无法创建。
- 消耗系统资源:虽不占用CPU和内存,但长期堆积会影响系统性能。
- 干扰进程管理:使进程监控和调试变得困难,可能引发程序异常。
孤儿进程
子进程比父进程先结束,子进程会变成孤儿进程;
那如果父进程比子进程先结束呢?子进程又是什么状态呢?
如果父进程比子进程优先结束,此时的子进程被称为孤儿进程;
这时候孤儿进程的父进程就变成了操作系统。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{int id = fork();if(id < 0){perror("fork");return 1;}else if(id == 0){while(1){printf("子进程 pid : %d, ppid : %d\n",getpid(),getppid());sleep(1);}} else{ int x = 3;while(x--){ printf("父进程 pid : %d, ppid : %d\n",getpid(),getppid()); sleep(1);} }return 0;
}
父进程先运行结束,子进程就变成了孤儿进程。
父进程退出后,子进程的父进程就变成了
1
;(1
号进程可以理解为操作系统)
孤儿进程的危害:
- 资源浪费
若孤儿进程长期运行(如死循环),会持续占用CPU、内存等资源,且难以通过父进程直接管理。- 依赖性问题
若孤儿进程依赖父进程的某些资源(如共享文件句柄或网络连接),可能导致未预期的行为(如文件未关闭、端口未释放)。- 管理困难
孤儿进程被init/systemd
接管后,需通过系统级工具(如kill
)手动终止,增加运维复杂度。
现在我们来看一下问题:我们正在运行中的程序,为什么我们查看时还是处于S
状态?
进程的运行是动态的
进程状态是动态变化的,R
(运行/就绪)和 S
(可中断睡眠)可能频繁切换:
- R状态:进程正在CPU上运行,或在就绪队列中等待调度。
- S状态:进程在等待事件(如磁盘I/O、用户输入、网络响应等)。
即使程序逻辑上是“运行中”,如果它在大部分时间内等待I/O(例如读写文件、网络请求),则会被标记为 S
,仅在获得CPU时间片时短暂进入 R
。观察工具(如 ps
、top
)可能只捕捉到了它处于等待的瞬间。
后台进程
我们在查看进程状态时看到了R
状态,同时我们也看到了R+
状态,那+
表示什么意思呢?
这里简单来说,
+
表示当前进程属于前台进程组;
R+
:进程处于可运行状态,并且当前进程是前台进程;R
:进程属于可执行状态,但是当前进程是后台进程。
一个task_struct
如何实现既在全局队列中,也在运行队列/等待队列中?
什么意思呢?
我们知道,在内存中操作系统要将所有进程的
task_struct
管理起来,存在一个全局队列,每一个进程的task_struct
都在这一个全局队列中;但是我们在上述中提到了,进程运行态就是进程的
task_struct
处于CPU
的运行队列中,而阻塞态就是进程的task_struct
处于硬件资源中的等待队列中;那我们的
task_struct
是如何实现既在全局队列中,又在运行队列/等待队列中呢?
在我们之前对于队列的了解,队列的每一个节点中存放了前驱和后置节点的指针;那如果使用这样的结构去实现这个数据结构能达到我们想要的效果吗?
很显然是不能的,我们这种结构是不能实现的
而我们Linux
是可以的,在Linux
中它并没有像我们之前那样将前驱节点指针prve
和后置节点指针next
直接存放在task_struct
中,而是将其包装起来:
struct list_head{struct list_head* prve, *next;
}
这样task_struct
中list_head
中prve
和next
分别指向对应的前驱和后继task_struct
中的list_head
;
现在问题来了,我们如何通过list_head
的地址,获取task_struct
的地址从而能够方法task_struct
中的所有数据
还记得在C语言学习
struct
结构体时,我们了解一个宏offset
,这宏就用来计算结构体中的一个数据的偏移量;那我们知道
task_struct
中list_head
的地址,通过offset
再求出list_head
成员的偏移量,那不就求出来了task_struct
的地址吗,有了地址我们就可以方式task_struct
中的所有数据了。
当然只是包装起来,是无法是达到预期效果的,在task_struct
中不止存在一个struct list_head
;存在多个struct list_head
,那这样我们一个task_struct
就可以同时属于多个数据结构;一个task_struct
就可以既在全局队列中,又在运行队列/等待队列中。
到这里本篇文章就结束了,简单总结:
进程的状态:运行、阻塞、挂起。
Linux
中的进程状态:R
、S
、D
、T
、t
、x
、Z
等。后台进程。
一个
task_struct
在多个队列的原理。