hello~ 很高兴见到大家! 这次带来的是Linux系统中关于进程这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页 : 默|笙

文章目录
- 一、理论上的操作系统状态
-
-
- [1.1 运行状态](#1.1 运行状态)
- [1.2 阻塞状态](#1.2 阻塞状态)
- [1.3 挂起状态](#1.3 挂起状态)
-
- 二、Linux操作系统状态说明
- 三、僵尸进程Z
-
-
- [3.1 介绍](#3.1 介绍)
- [3.2 僵尸进程危害](#3.2 僵尸进程危害)
-
- 四、孤儿进程
一、理论上的操作系统状态
- 观察下图,能够看到不同的操作系统状态以及它们直接的关系,比如状态是如何改变的。接下来会对这些状态做一个基本的讲解。

1.1 运行状态
- cpu是如何处理一个又一个进程的呢?是通过调度队列,一个cpu一个调度队列。比如先进先出FIFO(first in first out)调度队列:链表的起始位置直接指向最先进入就绪态的 struct task_struct(进程控制块PCB),而这个struct task_struct则会链接到后面进入的struct task_struct结构体,如此形成一条队列。

- 放入运行队列里面的我们统一都认为是进入了运行状态,如果细分的话(对应上面的图),正在被cpu执行的就是运行状态,而在队伍里排队但还没有被cpu执行的进程就是就绪状态。
1.2 阻塞状态
-
当代码里有scanf()这样需要从键盘里读取设备的函数,进程状态会变得怎么样呢?执行到scanf所在代码时,如果我们迟迟不在窗口里面输入数据,那么就会一直卡在这个阶段,不会执行接下来的代码。这是在等待硬件键盘的输入,而硬件就绪好之后操作系统一定会是最先知道的。操作系统毕竟是硬件的管理者,而它如何进行管理?类似于PCB,先描述(硬件的各种属性)再组织(构成链表),之后直接对链表进行操作就能完成管理。
-
这时候会发生的事情如下图,这个执行到scanf的进程会从调度队列里面撤下来,进入键盘设备对应的等待队列里面,等待键盘的就绪。这个等待队列每一个控制硬件的struct device里面都会有一个。

-
只有当键盘就绪即数据输入完成之后,这个进程就会离开这个struct_device的等待序列,重新回到运行队列中去。因为是FIFO,不考虑优先级的话,它会回到队尾。
-
cpu在这个进程离开的时间里也不会闲着,它会执行没有发生阻塞状态的下一个进程。
-
可以说,阻塞和运行的本质就是看某个进程task_struct在谁的队列里面。
1.3 挂起状态
- 要知道,无论是运行还是阻塞状态它们的test_task和代码数据可是一直都在内存里面存着,而内存的空间有限,如果进程太多,内存满了该怎么办?
- 这个时候,就得提到磁盘里面会存在的一块区域swap分区,当内存满了的时候,操作系统就会把长期处于阻塞状态的进程的代码数据丢到swap分区里面,等硬件就绪的时候,再把这些代码和数据拿回来;如果还是不够,就会把长期处于就绪状态的进程的代码数据丢到swap分区里面,同样也是等到要执行的时候再把它们拿回来。前者叫做阻塞挂起状态,后者就叫做运行挂起状态
- swap分区的大小一般跟内存大小差不多或者是1.5倍左右,它不会太大。这是因为不能太依赖swap分区,代码数据进出swap分区是有时间损耗的,如果进出swap分区次数太多,效率就会下降,导致系统变慢。本质是利用时间换空间。

- 操作系统在内存不足的时候,会先尝试挂起,如果尽可能的挂起进程结果内存还是不够,那么操作系统就会直接杀掉特定的进程节省空间。
二、Linux操作系统状态说明
以下是Linux系统里对于状态的定义
cpp
static const char * const task_state_array[] = {
"R (running)" //运行
"S (sleeping)" //休眠
"D (disk sleep)" //深度休眠
"T (stopped)" //暂停
"t (tracing stop)"//追踪暂停
"X (dead)" //死亡
"Z (zombie)", //僵尸
};
2.1 查看进程状态 ps axj / ps aux
- -a: 显示⼀个终端所有的进程,包括其他用户的进程。
- -x: 显示无控制终端的进程,比如后台进程。
- -j: 显示进程归属的进程组ID、会话ID、父进程ID,以及与作业控制相关的信息。
- -u: 以 "用户导向" 的格式输出,含资源占用。
2.2 初识前后台进程和运行状态R
- 只要是放在运行队列里的进程,都是运行状态,它不一定正在运行,也可能在等待。
- 进程也分前后台进程,若在启动命令后面加上符号&,进程就会在后端运行,否则会进入前台。
cpp
//代码如下:
#include <stdio.h>
int main()
{
while(1)
{}
return 0;
}


- 可以看到,如果加&放在后台的进程状态后面是不会有+号的,只有放在前台的进程状态后面会有+号。
2.3 休眠状态S和深度休眠状态D
- S和D状态其实都是对于上面提到过的阻塞状态的定义。它们都是在等待事件的完成。
- S和D状态的不同之处在于:S状态是可以打断的,可以发送信号kill -9也可以ctrl+c结束进程。而D状态是无法被打断的,它会等待IO信号输入的完成,在此期间不会相应任何外部信号。
S状态
- 对于只有输入输出的代码,它的状态大部分时间会是S,而不是R,这是因为cpu的速度非常快(纳秒级),而外设的速度相对很慢(毫秒/秒级),大部分的时间都是进程在等待输入和输出的数据,这个时候就是休眠状态,也就是对应的S状态。
输出:
等待显示屏的就绪,task_struct在显示屏struct device的等待队列里等待。
输入:
等待键盘的就绪,task_struct在键盘struct device的等待队列里等待。
D状态
- 为什么会有深度休眠状态D?设想一个场景,一个进程将在对磁盘做输出,而要输出的文件非常大,这需要时间,在这期间task_struct只能在内存里面等待,等待磁盘输出完成的信号。而此时内存又非常紧张,操作系统已经挂起了很多进程的代码数据了,操作系统这个时候看到没有运行的task_struct就在那里什么事都不干,很有可能就会发信号一下子把这个进程干掉。而假设磁盘读取过程中出现问题,想要告诉task_struct,结果根本找不着,之后就只能将这份未传输成功的数据删除掉。而万一这份数据它很重要,比如是银行一个小时的转账记录,那么就会有很大的损失。
- 但在这个过程中,操作系统、进程和磁盘的行为都是没有问题的。操作系统要清理内存,进程只能等磁盘,磁盘在卖力输入。为了不让某些进程被草率的杀掉,就有了这个深度休眠状态D,它不会被外部的信号影响到,也就不会被干掉。
2.4 暂停T和追踪暂停状态t
- 可以发送信号来使进程进入停止状态,也可以继续发送信号让进程恢复之前状态。T和t状态没有本质的区别,只是t状态是调试时打断点后运行到断点处会出现的状态。
kill -19 进程PID 暂停进程
kill -18 进程PID 恢复进程之前状态
kill - 9 进程PID 干掉进程
用的也是一个简单无限循环代码。
- 放在前台运行,这个时候是R+状态,我们发送暂停信号之后,会变成T状态,而且会把这个进程从前台给它放到后台中去,即便是发送恢复信号也不能从后台移动到前台了。
- 停止状态并不属于阻塞状态,阻塞状态的S和D是停下来等待资源的输入输出没有办法运行,而停止状态则是被信号叫停。
- 若把有scanf输入的进程放在后台,执行到scanf时,进程会进入暂停状态。
- 这是因为后台进程无法从键盘读取数据,系统就强制给进入暂停状态。我们无法用ctrl+c去终止一个后台进程也是这个原因,因为它根本就收不到。只能用发送信号来完成。
- 这也变相指明了前后台进程的区别:能够从键盘里读取数据的就是前台进程,否则就是后台进程。我们只有一个键盘,也就是说只能有一个前台进程能够读取数据。这也说明同时只能有一个前台进程,而后台则可以有多个。
当运行到断点的时候,状态就会变成ts+,s是首个会话进程,+是前台进程。
不只是t的原因:ts+:因为我用了 CGDB 调试(终端前台运行),GDB 为 test 进程创建了独立会话,且占用终端前台 → 三个标记同时触发。
2.5 死亡X
- 死亡状态我们是无法通过任务列表看到的,这是因为死亡的进程是不占用系统资源的,会被操作系统进行回收。
三、僵尸进程Z
3.1 介绍
- 进程执行完之后退出并不会直接进入死亡状态,而是进入僵尸状态Z,在这个状态里面,代码和数据会离开内存,但是进程对应的部分task_struct(退出信息)会等待父进程的回收,在此之前会一直留在内存。
- 为什么要有这个Z状态?这是因为,所有关于进程的信息都是被保存在task_struct里面的。当一个进程结束后,我们想要知道这个进程结束的如何就必须想办法获取task_struct里面的信息。如果直接进入死亡状态X,那么代码数据还有task_struct一个都不会留下,也就没有办法获得进程的退出信息。
- 由于直接在终端创建可执行文件会是bash的子进程,执行完之后会自动被bash回收。这里我们在代码里创建一个子进程来进行测试。这个父进程不会回收子进程,函数waitpid才是用来回收子进程的。
c
#include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 int main()
5 {
6 pid_t p = fork();
7 if (p == 0)
8 {
9 while(1)
10 {
11 printf("我是子进程,PID:%d, PPID: %d\n", getpid(), getppid ());
12 sleep(1);
13 }
14 }
15 else
16 {
17 while(1)
18 {
19 printf("我是父进程,PID:%d\n", getpid());
20 sleep(1);
21 }
22 }
23 return 0;
24 }

- 可以看到在发送信号给27556之后,这个子进程并没有直接消失,他依旧可以显示在任务列表,是Z状态。
3.2 僵尸进程危害
- 僵尸进程的危害之一是内存占用:这些僵尸进程不能被回收的话,它那拥有退出信息的部分task_struct就会一直留在内存里面,没有办法清理掉。
- 也会造成PID资源资源泄漏,因为PID的数量是有限的。如果僵尸进程数量太多,那么PID可能就会被消耗光,这会导致新进程无法创建。
- 如果退出程序,内存泄漏自然也就没有了,比如像qq这样的常驻进程,如果变得一卡一卡的,退出重进之后就会流畅很多。
四、孤儿进程
- 僵尸进程是子进程被干掉,父进程还不进行回收的情况。如果保留子进程,让其正常运行,转而把它的父进程干掉,那么这个子进程就会变成孤儿进程。之后这个孤儿进程会被系统给领养。

- 系统为什么要领养孤儿进程?这是因为孤儿进程没有了父进程,也就没有对其进行回收的进程。当孤儿进程执行完毕的时候就会变成僵尸进程,会有危害。而把这些孤儿进程交给系统,当孤儿进程退出的时候系统就可以将其回收。系统领养孤儿进程的目的就是:避免孤儿进程终止后成为 "无人回收的僵尸进程"。
今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!








