一操作系统的进程状态我们今天所讲的Linux进程状态和下图的操作系统进程状态的关系是特殊性与普遍性的关系二Linux中的进程状态2.1进程状态有哪些下面的状态在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 */ };R运行状态running并不以为着进程一定在运行中它表明进程要么在运行中要么在运行队列里面S睡眠状态sleeping意味着进程在等待时间完成睡眠状态有时也叫做可中断睡眠D磁盘休眠状态Disk sleep有时候也叫不可中断睡眠状态在这个状态的进程通常会等待IO的结束T停止状态stopped可以通过发送SIGSTOP信号给进程来停止进程这个被暂停的进程可以通过发送SIGCONT信号来让进程继续运行X死亡状态dead这个状态只是一个返回状态正常情况下不会在任务列表看到这个状态2.2调度队列FIFO算法进程运行状态在上一篇中我们知道了进程是由PCB和代码与数据构成的进程的PCB里面有一个指针指向下一个PCB于是就形成了“全局进程链表”这里的队列和全局链表非常相似为什么这个既是队列又是链表后文会提及结论进程在调度队列里面进程处于运行状态通过进程的调度队列我们也能知道进程的FIFO算法进程按照队列规则先进先出依次被CPU调用执行2.3设备树和进程的阻塞状态2.3.1什么是设备树我们的计算机摄像头麦克风鼠标键盘显示器以及磁盘网卡一系列的硬件他们都是操作系统通过硬件驱动调用的然而硬件驱动不是直接访问硬件本身实际上其内部也经历了先描述再组织的过程操作系统将他们的数据封装成一个结构体然后将他们组织在一起于是就形成了设备树2.3.2什么是阻塞状态设备树上的每一个节点都有一个等待队列进程在等待队列中的时候就是阻塞状态那么什么是阻塞当一个进程正在运行但它发起的某个请求暂时无法得到满足或者必须等待某个事件发送如磁盘读取网卡数据到达操作系统就会剥夺该进程的CPU使用权然后将其从运行队列中移出然后改进程就会被链入对应设备的等待队列中等待设备就绪运行队列是可运行的任务竞争CPU的地方等待队列是暂时不能运行的任务正在等待事件的地方。运行队列里面的任务不一定正在运行但是一定可以运行等待队列里面的任务通常正在阻塞直到被唤醒后重新进入运行队列什么是就绪很简单我们键盘键入硬盘读取这些都是设备就绪2.4交换分区与进程挂起关于挂起首先我们要明白这是一种极端情况一般情况下不会发生只有当我们操作系统认识到内存资源严重不足的时候这里是物理内存不是虚拟内存虚拟内存后面再提2.4.1交换分区定义关于交换分区我们首先啊需要了解它是磁盘的中的一个概念在磁盘中有一块临时区域用来临时存放从内存中交换过来的代码2.4.2挂起与优先挂起当程序挂起时就是操作系统将程序代码和数据从内存中提取出来挂在这个交换分区当中以此腾出空间给其他进程使用缓减内存压力这个过程中PCB不会动那么谁会被优先带走呢答案是正在阻塞的进程他们不着急使用CPU资源所以先挂起在极端情况中的极端情况有可能运行队列中一些靠后的一些进程的代码和数据也会被挂起来一般情况下阻塞状态下是在物理内存中只有极端情况下才会挂起存放到交换分区中2.5进程的状态变化从运行到阻塞从阻塞到挂起从运行到挂起以及所有反方向这里有一张总结图三理解内核链表相关话题3.1内核链表不是传统链表Linux中的一个个PCB他们被链起来形成一个链表但是Linux内核中石油的链表不是传统的链表关于传统的链表应该如下一个带头的循环双向链表每一个节点里面都会有两个struct Node*指针指向上一个或下一个结点那么Linux的内核链表呢把前后指针独立封装一下让其和我们的数据解耦这就是Linux内核链表想表达的想法struct list_head { struct list_head *next, *prev; }; struct task_struct{ int x, y, z; struct list_head links; ... };也就是说现在这个链表本身依赖的不是指针而是这样一个连接器结构体它来帮助前后PCB建立联系传统链表是“数据绑着指针”Linux内核链表是“指针独立插进数据里面”接下来我来进行详细解释struct Student{ int id; char name[20]; strcut Student* next; struct Student* prev; }有注意到什么吗传统链表是数据绑定指针构成了结构体而Linux内核是把前后指针独立封装成一个结构体就可以实现我想要什么数据就写什么数据我需要做的就是把这个指针结构体插入就行了3.2为什么内核链表要这么设计3.2.1更方便的计算偏移量---offset宏现在如果我手里只有一个指向links成员的指针想要拿到整个task_struct的首地址你必须知道links在这个结构体里往后挪了多少距离结构体首地址 成员地址 - 偏移量有一个非常通用的计算方式offset ((struct task_struct*)0) -links)这种计算方式的优势在于1.首先人为设置起点将0强制转换为结构体指针相当于在内存地址0的位置虚拟出一个结构体2.算位置访问成员links并获取地址编译器胡根据内存对齐规则算出其相对于起点地址0的距离3.空间即偏移因为起点是0所以得到的成员地址在数值上就精准等于它在结构体里面的偏移量C自带一个宏可以直接获取这个偏移量上面讲的是它的底层首先包含头文件#includestddef.h第一个参数填结构体类型第二个填成员名size_t offset offsetof(struct task_struct, links);3.2.2彻底理清“流动”的本质多链表的动态解绑与重组在探讨如何在各种队列流动之前我们需要先明白一点设备树≠等待队列上文将设备树和等待队列混为一谈认为进程是阻塞在设备树上面这是极其不准确的设备树和等待队列没有关系设备树是系统启动时内核用来清点初始化硬件的点名册不参与运行时的进程调度等待队列是驱动程序内部维护的运行时动态链表专门用来组织正在等待该设备的进程3.2.2.1物理上他们是很近的在物理内存中一个进程有且仅有一个task_struct(进程控制PCB)实例这个结构体非常庞大包含一个进程的所有核心信息PID状态优先级它就像一个巨大的多功能插板身上长满了各种不同的接口--这些接口就是内嵌在其中的struct list-head成员变量3.2.2.2逻辑上他们属于完全不同的部分虽然所以的插口都长在同一个PCB上但每个插口链接的都是不同的互相独立的没有交集的独立链表这就是著名的用绳子穿过实体的设计全局链表task list:像一根红绳横穿每个PCB的tasks插口只要进程存活这条线就永不解绑内核通过它来遍历系统中的所有进程运行队列Runqueue像一根橙绳它只穿过那些状态为“就绪Ready”或”运行Running“的PCB的run_list插口调度器只在这条线上挑选进程去CPU上面执行某一条等待队列设备有很多等待队列也有很多每个硬件设备网卡键盘磁盘等都有自己专属的等待队列3.2.2.3流动是如何发生的当一个进程从运行态变成阻塞状态时内核绝对不会去搬动或复制这个巨大的PCB它实际上只做了两件事情1.解绑断开旧线调用list-del把该PCB负责“运行队列”的run_list的钩子从橙线上面取下来2.重连挂上新线调用list_add把该PCB负责“等待队列”的wait_list钩子挂到该磁盘设备特有的等待队列上面3.2.2.4核心总结实体唯一内存中只有task_struct一个实体静止不动状态改变关系解绑与重组进程所谓的状态流动本质上是不同的链表指针在PCB不同插口上进行解绑和重连的过程高效率的秘诀这种设计让进程状态切换变成了极其高效的指针操作不需要任何内存拷贝完美体现了Linux内核“扁平且高效”的管理哲学四进程状态的查看4.1回顾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 */ };进程状态的本质就是一个在PCB中的整形是一个状态数组的下标4.2运行R与睡眠S状态Linux的Sleep就相当于操作系统的阻塞状态#include iostream #include unistd.h int main() { std::cout std::unitbuf; int count 0; pid_t pid getpid(); while (true) { std::cout 我是后台进程, PID: pid , 循环次数: count std::endl; sleep(1); } return 0; }还有一点为社么我查到的进程状态都是S因为我们的进程是一个循环打印假设执行一次是1ms在这1ms的百分之九十九的时间是在等待显示屏就绪百分之一的时间是拿来打印于是我们运气不好查不出来#include iostream #include unistd.h int main() { // 打印当前的 PID方便你后续去查状态或杀进程 std::cout 进程已启动, PID: getpid() 请在另一个终端观察其状态... std::endl; // 纯 CPU 计算的死循环没有任何 I/O 打印也没有 sleep while (true) {} return 0; }禁止其IO查出来的全是R如果程序在前台那么状态前面就有一个号否则没有加上表示当前进程在后台启动[whbbite-alicloud lesson13]$ ./myprocess 4.3停止T与追踪停止状态tt表示进程处于“正在被追踪而停止”的状态虽然其也属于停止状态但是它和大写T有细微语义区别T(stopped):通常是用户主动发送信号让进程停下来ttracing stop专门用于debugger调试器环境当进程被系统追踪当我在使用gdb调试时进程等待调试器发送下一个指令的期间状态会显示为t关于19号信号需要补充的是在Linux信号机制中19号信号是SIGSTOPSIGSTOP 的唯一作用就是强制停止挂起当进程接受到这个信号时它的状态会立即转变为TStopped按下ctrlZ就是传递这个信号给进程4.4深度睡眠D4.4.1对比一般睡眠状态浅睡眠S比如有一个scanf在等待阻塞然后我们可以直接通过crtlC杀死这个进程或者执行kill -9 PID杀死但是深度睡眠D状态如何都不会被杀死如何杀死1.等待其自己醒来 2.物理办法4.4.2深度睡眠的意义场景一个进程正在向磁盘写入数据但是磁盘写入数据的速度超级慢相对于进程而言于是进程就要去等待磁盘进入S状态。等待过程中可能发生一种非常极端的内存不足情况这个时候OS就要开始杀没有用的进程。这个时候可能会误杀这个正在“摆烂”的进程。于是这个时候磁盘还没有读完数据但是进程已经被干掉了磁盘拿着数据不知道该怎么办于是就只能把数据丢弃了。这个时候用户还不知道于是就丢失了数据。 如果这个数据是医院里几千个病人的病历或者是银行的转账信息这是非常危险的。为了防止操作系统发生上述情况。于是一种新的状态被设计出来。D状态不可中断。