一、课本上的说法:一个结点可以在多种数据结构中,进程可以在双链表,可以在队列(本文)。

运行&&阻塞&&挂起
1、在Linux里,进程被创建,一般就要放在运行队列里等待被调度,可以把“创建”、“就绪”、“运行”理解成“运行”,

总的来说:运行不仅在cpu上被调度就是运行,只要在调度队列都叫运行
2、阻塞:进程调度 scanf等待用户输入,其实是设备输入
操作系统里有一个数据结构是用来管理设备的,假设叫struct device,里面有各种属性、id、设备状态,包括struct task_struct* waitquene。当cpu在调度一个进程需要访问硬件设备如scanf时,os需要查看对应设备如键盘的状态比如是否活跃,否则os会把当前进程从CPU或者调度队列拿下放到os里的struct device* devices 的设备等待队列waitqueue(可以有多个进程在等待键盘输入/就绪),此时这个进程就是阻塞。我们可以得出以下结论:
- 运行到阻塞:把pcb链入到不同结构(从运行队列移动到等待队列)
- 阻塞到运行:在等待队列重新找到PCB,再把它链入到调度队列。 当键盘读取到数据时,等待队列的进程并不知道键盘读取到数据,但是操作系统知道,会把键盘这个设备的status设置为激活状态,然后检查wait_queue是否为空,不为空,这个task_struct就重新被链入调度队列,等到CPU调度到这个进程时,才从scanf开始,此时才将从键盘读取的数据写入到进程的上下文,再执行后续的代码。
进程状态的变化,表现之一:在不同的队列中进行流动,本质就是数据结构的增删查改,就好比简历:你的简历在被老板筛选(运行),老板觉得你不是最优但是还可以当作备用放到人才池(阻塞)。
3、阻塞挂起:把阻塞队列代码和数据挂到外设。内存资源有限,极端情况下如果内存资源严重不足,操作系统会在磁盘创建一个swap分区,此时OS会去检查哪些进程是闲置的,就比如键盘未就绪,对应的wait_queue的进程不急着执行,那么操作系统就不会让它们“占着茅坑不拉屎”,会把它们的代码和数据交换到swap交换分区,只保留对应的task_struct,这个过程叫swap交换分区的“唤出”。此时这个进程就处于阻塞挂起状态,它们余下了空间给其他正在调度需要的进程使用。
当键盘准备就绪了,还是操作系统先知道,把之前交换到swap交换分区的代码和数据重新加载到内存,移动到阻塞挂起的task_struct(在waitqueue),让里面的指针重新指向代码和数据,重新建立映射关系,这个过程叫swap交换分区的“唤入”。然后再放入运行队列,等CPU调度时,将从键盘读取的数据写入到进程的上下文,再执行后续的代码。
4、运行挂起:把运行队列的代码和数据放到磁盘。当内存资源相当吃紧,即使是阻塞挂起,内存空间也不足的时候,操作系统就会打运行调度队列的task_struct的主意,它会把当前不急着在cpu上运行(不管是单CPU还是多CPU,都不可能同时运行调度队列的所有进程)的进程的代码和数据唤出到磁盘的swap交换分区,等轮到被移除的task_struct被CPU调度时,操作系统再把它们的代码和数据再“唤入”到运行队列。
不管是阻塞挂起还是运行挂起,都是因为内存资源不足时磁盘的swap交换分区的“唤出”与“唤入”。

一个PCB在Linux内核数据中只有一份,但是可以同时隶属于多种数据结构,例如全局链表、运行队列、阻塞队列等。这是因为内核使用了list_head结构体来管理这些链接关系,每个list_head结构体可以独立地将进程链接到不同的链表中。怎么更好地理解呢?
假设task_struct里定义了一个list_head对象links,观察list_head的结构,我们可以根据links的地址通过 (task_struct*) ( (links->next) - &((struct task_struct*)0 ->links) ) 找到struct task_struct的地址,访问task_struct的所有成员变量。因此,一个task_struct对象里有多个struct list_head,操作系统用list_head既可以通过双链表将各个task_struct链接起来也可以通过队列链接,甚至还可以用二叉树等各种各样的数据结构将这些task_struct链接起来,所以才说一个task_sturct可以在全局双链表也可以在运行队列,只要是结点。反正都能通过list_head找到对应的task_struct。
这跟我们之前学的c语言的双链表不同,我们之前学的是整个结点通过next指针将所有结点串联,操作系统内核是通过task_struct里的list_head用各种各样的数据结构对进程进行串联。
所以我们就可以理解为什么进程可以从一个进程既在运行队列又在全局链表,移动到阻塞队列后,还是可以在全局链表里找到它。

二、为了弄明白正在运行的进程是什么意思,我们需要知道进程的不同状态。⼀个进程可以有几个状 态(在Linux内核里,进程有时候也叫做任务)
下面的状态在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,状态是整数变量,通过下标可以找到对应状态。
在vim写入一个代码块

运行代码发现进程状态是阻塞状态,原因:
printf 导致 S 状态:通常是因为缓冲区刷新触发了系统调用 write,而 I/O 设备未及时响应。假设进程运行需要1s,那么1ns的时间执行代码,剩下的时间等待I/O 设备响应。要想显示为运行状态R+,就去掉printf和sleep;"+"代表进程在前台也就是指令界面运行,编译的时候在后面加 & 代表在后台运行,查看进程只出现R。
再如当代码运行到scanf时,进程被移动到键盘设备的等待队列,进程不被调度,处于阻塞状态,在Linux叫S(sleeping)。
t : 当debug进程时,设置断点,运行到断点时停下来,此时进程会处于 t 状态。
T:代码可能有问题,操作系统停下来让用户检查(用Ctrl+Z / kill -19 pid 暂停进程)。
D:前面将的S属于可中断睡眠,这里的D则属于不可中断睡眠,都属于阻塞状态,怎么理解呢?例如,要向磁盘写入100MB当内存资源严重不足,即使挂起很多进程也不足,操作系统看到有进程在等待磁盘工作完毕给进程反馈的过程中可能会杀死进程,可能会导致重要信息丢失。因此当进程中进行重要的高io时,访问磁盘重要文件时,进程处于会D状态,也就是深度睡眠状态,此时进程不需要对OS的“杀死”操作作出响应动作 ,防止数据丢失。
X : 进程结束。
Z :linux操作系统中,每一个进程都是是一个子进程,子进程是为了帮父进程完成某件事的,那么完成的结果是需要反馈给父进程的。在子进程退出时到父进程获取子进程退出信息的这段时间,子进程处于僵尸状态,就是Z状态(zombie)。
a. 如何模拟Z状态呢?在vim中运行这段代码,当子进程退出时,父进程不对子进程进行回收结束信息。
我们发现此时子进程处于Z状态,这个defunct是不存在的意思,这里指子进程的代码和数据被释放,但是它的task_struct还在(因为pid没有消失);如果父进程一直不管,不获取子进程的退出信息,那么Z会一直存在,task_struct在内存中会占用内存空间,此时会存在资源泄漏问题。所以父进程不仅需要获取子进程退出信息还要回收task_struct。操作系统不能主动回收task_sturct,需要交给父进程去处理,所以后续需要用户自己对它进行回收

孤儿状态:父子进程关系中,如果父进程先退出,子进程要被1号进程领养,这个被领养的进程(子进程)叫做孤儿进程。此时它会变成后台进程,需要用kill -9才能结束这个进程。这里的1号进程可以理解为操作系统,如果操作系统不领养这个子进程,子进程的结束信息没人获取,没人回收这个子进程的PCB,就会存在内存泄漏。


被折叠的 条评论
为什么被折叠?



