【Linux 系统】进程状态
文章目录
- 前言
- 1. 操作系统的进程状态
- 1.1 运行状态
- 1.2 阻塞状态
- 1.3 挂起状态
- 2. Linux下的进程状态
- 2.1 R状态
- 2.2 S状态
- 2.3 D状态
- 2.4 T状态
- 2.5 X和Z状态
- 2.5.1 孤儿进程
- 2.5.2 小结
前言
Linux操作系统中,一个进程有许多状态。一个进程可能在等待资源就绪,一个进程可能在被CPU调度。
进程状态 | 进程状态的描述 |
---|---|
R | 运行状态(Running) |
S | 阻塞(Sleeping) |
D | 深度睡眠(Dick Sleeping) |
T | 暂停(Stopped) |
t | 进程因调试(gdb等)而暂停(本文不做介绍) |
X | 进程终止(dead) |
Z | 僵尸状态(进程退出之后等待父进程回收的状态) |
小编在这篇文章会和大家介绍这些进程的状态。
注意下面的测试场景我们都是再开一个终端会话。因为我们启动的进程都是一个前台进程,会让我们无法再执行命令,所以我们先再开一个会话。点击标签然后复制会话即可。
1. 操作系统的进程状态
在整个操作系统上,我们主要谈到几个状态:
-
运行状态
-
阻塞状态
-
挂起状态
1.1 运行状态
-
在操作系统中为了管理维护需要被调用进程,其中会对运行状态进行描述:
思考:难道只有在CPU中运行的进程才是运行状态吗?
未必!
-
在操作系统中,会为进程维护一个运行队列。其中队列连接的可以是进程的PCB:
对于进程来说,只要它进入了运行队列,进程就在运行队列中随时准备被CPU调度。可是我们同样面临一个问题:一个进程在CPU中将其代码运行完成才会被切换吗?
- 显然:这是不可能的。要是是这样的话,要是我给一个程序打个
while(1);
的死循环那别的进程怎么被调度呢? - 实际上:每一个进程都有一个时间片的概念,每一个进程都有一个在CPU上的规定的运行时间,只要时间到了,那么进程就会在CPU上被切换下来。那么也就意味着:在一段特定的时间内,在运行队列中的所有进程都会被CPU执行。在单核CPU中:宏观上,这就是并发执行。但是实际上,还是进程在被交替进行!!!
- 显然:这是不可能的。要是是这样的话,要是我给一个程序打个
1.2 阻塞状态
-
思考一个问题:如果当前一个进程需要等待用户用键盘输入数据,那么操作系统还会允许这个进程处于运行状态吗?
这是不会的。键盘输入就是等待资源就绪,一个进程需要等待资源就绪,说明它现在就是不能向下执行了,它现在需要等待资源!基于这样的考虑,操作系统不会让一个需要资源的进程在运行状态,而是在阻塞状态!
- 阻塞状态:进程因等待某个事件完成(如 I/O 操作、信号量、定时器等)而暂时无法继续执行,此时会进入阻塞状态,主动放弃 CPU 资源。
-
特点:
-
被动性:进程自身无法继续执行(必须等待外部事件),因此主动让出 CPU。
-
内存驻留:阻塞状态的进程仍驻留在内存中,未被换出。这样主要在这样的情形下,一旦资源准备就绪了,就可以直接进入运行队列进行调度!!!
-
状态转换:当等待的事件完成后/资源就绪之后,操作系统会将其从阻塞状态转为运行状态,重新加入运行队列等待 CPU 调度。
-
1.3 挂起状态
-
思考:如果现在你手头的钱包非常紧张了,此时如果有一个人还要来找你要钱,你会给他吗?
我想就是不会的!对于操作系统来说也是这样的,现在手头的内存资源十分紧张了,甚至自身难保了,所以操作系统会选择性地将一些不活跃的进程从内存中换出到磁盘中的一个特殊区域(如果是一个普通的区域,这和那些没有加载到内存中的程序有什么区别?),从此这个进程就不占用内存资源。进入了一个挂起状态。
- 挂起状态:进程因系统管理需要(而非等待事件)被暂时从内存中换出到磁盘的交换区(Swap Area),此时进程无法被 CPU 调度执行,直到被重新换入内存。
-
特点:
-
主动性(系统层面):挂起是操作系统或用户主动触发的行为,与进程自身是否等待事件无关。
-
外存存放:挂起状态的进程被移至磁盘(外存),不再占用宝贵的内存资源。
-
状态转换:挂起状态的进程需先被操作系统换入内存,转为就绪状态或阻塞状态(取决于进程挂起前的状态),才能再次被 CPU 调度。
-
- 对比阻塞状态和挂起状态。
阻塞状态(Blocked) | 挂起状态(Suspended) | |
---|---|---|
根本原因 | 等待外部事件(如 I/O、资源) | 系统内存管理或用户干预 |
内存位置 | 驻留在内存中 | 被换出到磁盘(外存) |
能否直接调度 | 不能(需事件完成后转为就绪) | 不能(需先换入内存转为就绪 / 阻塞) |
与 CPU 的关系 | 主动放弃 CPU(因无法继续执行) | 被系统强制剥夺 CPU(即使进程可执行) |
典型场景 | 等待磁盘读写、网络响应 | 内存不足时换出进程、手动暂停程序(例如: gdb调试) |
- 注意:上面我们谈到的进程状态是从操作系统设计角度来说。对于具体的操作系统实现是由差异的!
2. Linux下的进程状态
2.1 R状态
来看下面一个测试代码:
#include <iostream>
using namespace std;int main()
{while(1); //死循环,让代码一致运行return 0;
}
利用makefile工具进行编译:
然后我们通过ps
指令对进程进行查看我们刚刚启动的进程:
- 注意:这里的’
+
'代表这这个进程是在前台运行的(在守护进程的时候我们会介绍),这里简单理解成,现在这个进程占用着bash命令行,标准输入只能输入给这个进程就可以了。我们不能再命令行上启动其它的进程/命令了。
2.2 S状态
对于S状态,我们可以很容易得到:S状态,不就是得到资源就绪嘛,显示器资源也是一个资源,所以我们可以向显示器上打印看到S状态。
while(1) //死循环,让代码一致运行
{printf("我正在运行了\n");
}
重新编译和运行一下,我们来看结果:
-
原因:
printf
函数是向显示器中写入,显示器是一个外设。软件和硬件打交道是需要IO消耗的。这是和外设进行交互,所以是需要等待外设资源就绪的!!同时,IO交互的速度是远远不及CPU的运行速度的,所以我们的进程大部分时间都是在等待外设的资源准备就绪。也就是处于S状态! -
'
+
'也是代表这个进程是一个前台进程。 -
处于S状态的进程是可以被操作系统唤醒的,操作系统可以
kill
处于S状态的进程!
2.3 D状态
-
刚刚谈到了,处于S状态的进程是可以被操作系统
kill
掉的。D状态是深度休眠状态,什么是深度休眠呢?为什么需要一个深度休眠状态呢?我们来看下面一个场景我们现在有一个进程正在向磁盘中写入大量的数据,可是不幸的是现在操作系统检测到现在的内存空间严重不足,所以操作系统为了维护整个系统的安全和管理,必须选择性的kill掉一些进程,恰好如果一个进程处于S状态,那么它很有可能就被操作系统强制下线!但是我们的这个进程确实是在和磁盘进行交互,可是进行交互的时候如果被进程kill了那么进程向磁盘中写入的数据是否就是丢失了呢?是的!原因是:磁盘无法将是否成功写入的结果返回给进程了(进程已经被kill了),一般情况下,磁盘就会丢掉一部分数据。
- 所以:基于上面的这个场景,我们必须保证一个正在向磁盘中写入的进程不能被kill,所以就有了一个D状态。处于D状态的进程是无法被操作系统给kill的!
2.4 T状态
-
T状态,是一个进程现在处于暂停状态,我们可以通过
kill
命令给进程发送信号,让一个进程暂停:
下面给出我们的测试(还是刚刚的那份代码):
(小编重新运行了,所以PID不同)
现在,我们让这个进程继续运行:
该进程就继续运行了,这就是T状态。
- 细节:一个从T状态的进程受到信号
kill -18
变成了一个后台进程。所以我们想要终止这个进程现在想用ctrl c
这样的方式是不可以的了,我们可以通过kill -9 PID
的方式将这个进程终止了。
2.5 X和Z状态
这两个状态是十分重要的。对于我们理解一些概念是很有帮助的
- X状态:终止状态,即进程死亡。
来看下面一段测试代码:
// 已经正确包含头文件
int main()
{pid_t pid = fork();if(pid == 0){//子进程int cnt = 5;while(cnt--){printf("I am a child, pid: %d, ppid: %d\n", getpid(), getppid()); //注意要带'\n'刷新用户级别缓存sleep(1);}exit(0); //5s之后,让子进程退出}else{//父进程while(true){printf("I am a father, pid: %d\n", getpid());sleep(1);}//父进程不退出}return 0;
}
本身我们这个代码就是不完整的,我们为了后面的讲解,所以给出了不完整的代码。
-
这里为了方面我们动态观察整个变化情况,我们决定使用一个循环执行指令:
while true; do ps ajx | head -1 && ps ajx | grep procstates | grep -v 'grep'; sleep 1; done
来看运行结果:
-
现象:
- 父子进程同时在运行,5s之后,子进程退出,子进程处于了一个Z状态而不是我们预料的进程退出之后处于一个X状态(其实我们也看不到……)
-
为什么呢?
- 一个进程退出时,资源不会立马释放,需要维持一段状态,这个状态我们就称其为僵尸状态,也就是Z状态。为什么需要维持这一段状态呢?很显然,子进程一定是父进程创建出来的,那么父进程为什么要创建一个子进程呢?一定是给子进程安排了任务,需要子进程执行。那么子进程的运行的结果父进程需要知道吗?一般情况下当然需要!(特殊情形下不需要知道)所以,维持这段Z状态,就是等待父进程来接受子进程的返回结果
-
结论:
-
Z状态的存在就是为了让父进程能够对子进程的退出状态进行响应(回收),否则子进程就会一直维持Z状态。
-
如果一个进程一直维持Z状态,那么Linux内核为其创建的
struct task_struct
等结构PCB就不会被释放,就会一直占用内存资源(内存泄露)。 -
解决方案:
waitpid()
(进程控制章节详细介绍)
-
2.5.1 孤儿进程
- 那么我们就留下来了一个问题:如果父进程先退出呢?
来看下面一个测试代码:
//头文件已经正确包含
int main()
{pid_t pid = fork();if(pid == 0){//子进程while(true){printf("I am a child, pid: %d, ppid: %d\n", getpid(), getppid());sleep(1);}exit(0); //5s之后,让子进程退出}else{//父进程int cnt = 5;while(cnt--){printf("I am a father, pid: %d\n", getpid());sleep(1);}//父进程不退出}return 0;
}
注意上面的代码子进程在父进程退出之后并没有退出。
-
运行结果:
-
现象:
-
父进程退出之后,子进程还在运行,但是子进程的PPID变成了1。
-
父进程退出之后,子进程是一个后台进程,可以使用
kill -9
结束
-
PID为1的进程是哪个进程呢?
-
1号进程就是操作系统!!!
-
结论:
-
在Linux中,进程只关心父子关系。所以当一个父进程创建了一个子进程之后,父进程优先退出,那么子进程就会被操作系统这个进程领养。
(凡是被PPID==1的进程,我们称之为:孤儿进程)
一个进程为什么需要被领养?因为资源需要释放!
-
成为孤儿进程之后,一旦孤儿进程退出,操作系统会自动回收其资源。
-
成为孤儿进程并不是一件坏事,在一些场景之下,我们常常会使用到孤儿进程。例如,守护进程。
-
2.5.2 小结
- 僵尸进程、孤儿进程和守护进程(精灵进程)对比
守护进程后面会谈,这里给出总结
类型 | 定义 | 特点 | 影响 |
---|---|---|---|
僵尸进程 | 已经终止的进程但是仍然占有资源 | 不占用CPU资源和内存。但是保留内核描述PCB | 消耗内存,造成内存泄露 |
孤儿进程 | 父进程终止之后,仍然运行的子进程 | 被init进程接管。资源会被系统回收 | 由操作系统自动管理,不会造成资源泄露 |
守护进程 | 后台运行的长时间的生命周期的进程 | 没有控制终端,独立于用户的交互操作,父进程通常是init | 提供系统服务,例如日志、监控等。不与用户直接交互 |
说明:
- 僵尸进程:僵尸进程是程序编写不当造成的,特别是父进程没有正确管理子进程的退出。这些进程已经完成了父进程的任务,但是没能被父进程即使回收资源。
- 孤儿进程:孤儿进程不会对系统性能造成负面影响,因为操作系统会自动来接管这些进程并在合适的时候对资源进行回收。操作系统会定期性的清理已经终止的子进程的资源。
- 守护进程:守护进程通常是在系统的引导时启动。设计的目的是在没有登录的后台进行运行
完。希望这篇文章能够帮助你!