3. 进程状态
目录一、教材中经典的进程状态模型二、调度 运行 阻塞 挂起2.1. 画图 例子2.2. 阻塞与挂起的区别2.3. 上述例子的流程图三、Linux 内核链表 的底层工作原理和内存布局图3.1. 结构定义3.2. 如何通过链表节点找到宿主结构体3.3. 图示可视化四、Linux 的进程状态4.1. Linux内核源代码怎么说4.2. 进程状态查看4.3. 用代码展示不同的进程状态4.3.1. R 运行状态4.3.2. S 睡眠状态 (sleeping)4.3.3. t 状态跟踪停止4.3.4. T 停止状态 (stopped)4.3.5. D 磁盘休眠状态 (Disk sleep)4.3.6. 僵死状态 (Zombies)五、孤儿进程一、教材中经典的进程状态模型这张图详细描述了进程在生命周期中可能经历的各种状态以及导致状态发生转换的条件。图中的主要状态包括创建 (New)进程正在被创建。执行 (Running)进程正在 CPU 上运行图上方有 CPU 指向它。终止 (Terminated)进程执行结束正在被系统回收。活动就绪 (Active Ready)进程准备好运行等待 CPU 调度。静止就绪 (Suspended Ready)进程准备好运行但被挂起换出到外存即使有 CPU 也无法运行需要先“激活”。活动阻塞 (Active Blocked)进程因等待某事件如 I/O而暂停且驻留在内存中。静止阻塞 (Suspended Blocked)进程因等待某事件而暂停且被挂起换出到外存。关键的状态转换过程调度活动就绪 → 执行系统分配 CPU。时间片完执行 → 活动就绪时间片用完被切换下来。事件发生活动/静止阻塞 → 活动/静止就绪等待的事件满足了。挂起 (Suspend)执行/活动就绪/活动阻塞 → 静止就绪/静止阻塞系统把进程调出到外存以释放内存。激活 (Activate)静止就绪/静止阻塞 → 活动就绪/活动阻塞系统把进程从外存调入内存。简单来说这张图展示了一个进程从创建到终止的过程中是如何被操作系统调度、等待资源以及被挂起和激活的。上述的内容是 教材中经典的进程状态模型其中箭头指向是不同状态之间转换的事件发生但是这个过于复杂并且不好理解我们主要讲解的是从内核、数据结构的视角对 不同状态进行理解。二、调度 运行 阻塞 挂起2.1. 画图 例子我们在第一章 --- 初步了解操作系统中我们聊过管理在 内核中是通过双向链表 进行管理的其中上面是一个示意图。进程的调度就是CPU 按照一定的顺序选取调度队列中一个 task_struct 执行。小问题之前不是说 PCB 是由 双向链表 来管理的怎么现在又扯到了 队列这个是设计的原因把PCB设计成了既可以成为一个 全局的双向链表又可以把相关进程的放到一个队列当中换而言之一个 PCB 节点可以是 A 的数据结构又可以是 B 的数据结构。运行进程在调度队列中进程的状态都是 running阻塞等待某种设备或者资源就绪例如键盘、显示器、网卡、磁盘等。运行的时候是存在一个 运行队列阻塞的时候也会存在一个队列是等待队列。这些硬件设备的伪代码可以是这样的struct device{ ind id; in vender; int staurs void*data; struct device*next; struct device*prev; int type; struct task_struct* wait_queue; }里面会有一个 等待队列。我们下面以 按住键盘 这个事件为例进行展开当代码运行到 cin 或者 scanf 的时候会阻塞等待键盘硬件就绪。当我们没有按住键盘的时候称为键盘文件没有就绪。阻塞的话调度队列就会查询为什么阻塞把相应的 PCB节点放到对应硬件设施的PCB的wait_queue 中。按住键盘属于硬件就绪操作系统作为硬件的管理者就会查询对应硬件设备的节点将运行状态设置为activate并且检查等待队列发现指针不为空就把对应的节点的状态改为 运行状态把该进程重新链接回去到 运行队列 里。此时这个进程还没有被调度键盘中的数据没有被读取。在 CPU 的调度之下在运行scanf把数据从设备中去读取到进程的上下文当中然后被我们拿到。还会发生这个情况当我们的内存资源严重不足了有些进程不会被访问但是还站着内存。例如在阻塞队列中的代码和数据就不会被执行。此时磁盘就会把 阻塞队列中的代码和数据放到 swap 交换分区只将 PCB 存下来。此时就是挂起状态内存资源严重不足操作系统做一些页面置换的算法把一些不会被调度的进程或者内存块交换到对应的磁盘上此时进程只有PCB的成为挂起进程。等到有资源了输入内容了操作系统知道了就会把之前把之前的数据和代码 换入到 原来的PCB上2.2. 阻塞与挂起的区别状态是否在运行队列PCB 是否在内存代码/数据是否在内存原因阻塞❌ 不在✅ 在✅ 在等待设备/资源就绪挂起❌ 不在通常✅ 在部分❌ 被换出到交换分区swap内存资源严重不足阻塞 → 挂起当内存不足时操作系统可能将阻塞队列中的进程的代码和数据交换到磁盘swap只保留 PCB此时进程进入挂起阻塞状态。唤醒时需要先换入代码和数据从 swap 读回内存再进入就绪队列。2.3. 上述例子的流程图运行队列 (runqueue)│├─ 进程A (运行) → scanf → 阻塞│ ↓│ 从运行队列移除│ ↓└─ 进程B (运行) 键盘等待队列 (wait_queue)│用户按键 (中断)↓状态 → 就绪↓重新放入运行队列↓调度器选中 → 继续执行三、Linux 内核链表的底层工作原理和内存布局图3.1. 结构定义传统链表结构体 Node 包含 data数据以及next/prev(指针。这种方式导致链表只能存放特定类型的数据通用性差。内核链表单独定义一个通用的 struct list_head只包含 next 和 prev 指针。实际使用你想让哪个结构体变成链表节点就把 struct list_head 作为一个成员嵌入进去如图中间的 struct task_struct 嵌入了 list_head list。3.2. 如何通过链表节点找到宿主结构体因为链表里存储的只是 list_head 的地址当你拿到一个 list 节点的指针时如何反向找到它所属的那个大的 task_struct 结构体呢offsetof 宏和 container_of 宏的底层原理求偏移量利用 (struct task_struct*)0 - links 假设结构体在 0 地址直接访问成员 links 的地址就得到了 links 在 task_struct 中的偏移量偏移地址。反向推导利用 (char*)list - 偏移量用当前 list 的内存地址减去该偏移量就能算出宿主结构体task_struct的起始内存地址。转换类型最后将算出的地址强转为struct task_struct*)就拿到了完整的结构体指针。3.3. 图示可视化红色大框代表一个完整的 task_struct 结构体。蓝色小框代表嵌入其中的 list_head 节点。箭头表示链表前后的指针指向。通过这种方式你可以把任意类型的结构体进程控制块、文件描述符、内存页等用同一套 list_head 逻辑串起来极大提高了代码复用率。四、Linux 的进程状态4.1. Linux内核源代码怎么说static const char *task_state_array[] { R (running), /* 0 */ S (sleeping), /* 1 */ D (disk sleep), /* 2 */ T (stopped), /* 4 */ T (tracing stop), /* 8 */ Z (zombie), /* 16 */ X (dead) /* 32 */ };R 运行状态 (running)并不意味着进程一定在运行中它表明进程要么是在运行中要么在运行队列里。S 睡眠状态 (sleeping)意味着进程在等待事件完成这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep。D 磁盘休眠状态 (Disk sleep)有时候也叫不可中断睡眠状态 (uninterruptible sleep)在这个状态的进程通常会等待IO的结束。T 停止状态 (stopped)可以通过发送 SIGSTOP 信号给进程来停止T进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。X 死亡状态 (dead)这个状态只是一个返回状态你不会在任务列表里看到这个状态。僵死状态 (Zombies)是一个比较特殊的状态。当进程退出并且父进程使用wait()系统调用后面讲没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。t 状态跟踪停止当进程被 ptrace系统调用所跟踪时产生。最典型的场景是被调试器如 gdb 或者系统调用追踪器如 strace挂起。当调试器在断点处命中、或者进行单步调试时被调试的进程就会进入这个t状态4.2. 进程状态查看ps aux / ps axj 命令a显示一个终端所有的进程包括其他用户的进程。x显示没有控制终端的进程例如后台运行的守护进程。j显示进程归属的进程组ID、会话ID、父进程ID以及与作业控制相关的信息u以用户为中心的格式显示进程信息提供进程的详细信息如用户、CPU和内存使用情况等4.3. 用代码展示不同的进程状态4.3.1. R 运行状态R 运行状态 (running)并不意味着进程一定在运行中它表明进程要么是在运行中要么在运行队列里。#include iostream #include cstdio #include unistd.h using namespace std; int main() { int i 0; while (true) { i; } return 0; }在另一个终端输入以下命令用于监视while :; do ps ajx | head -1; ps ajx | grep myprocess; sleep 1; done在这代码中会发现它的状态永远是 R 因为你时刻都在用户态跑 CPU内核永远不会主动让它睡眠不会进入到 内核中如果你是在 循环中使用打印操作如下面的代码就会出现 R 和 S 交替进行。#include iostream #include cstdio #include unistd.h using namespace std; int main() { while (true) { printf(hello linux\n); } return 0; }此时我们发现他的绝大部分时间都是在 S并不是 R。首先需要确定的是这个任务一定是有 R 的状态毋庸置疑但是为什么绝大多数情况下都是 S呢粗略的解释一下这个 printf 操作其实是封装了 系统调用 函数的相当于 会把数据 放入到 内核中如果终端此时正在处理之前的数据、缓冲区已满或者硬件来不及消费write系统调用就会把进程挂起进入 S 状态直到终端驱动程序把数据取走腾出空间。因此会出现 S 的情况。4.3.2.S 睡眠状态 (sleeping)S 睡眠状态 (sleeping)意味着进程在等待事件完成这里的睡眠有时候也叫做可中断睡眠 (interruptible sleep。可以中断浅睡眠#include iostream #include cstdio #include unistd.h using namespace std; int main() { while (true) { sleep(1); } return 0; }4.3.3.t 状态跟踪停止t 状态跟踪停止当进程被 ptrace系统调用所跟踪时产生。最典型的场景是被调试器如 gdb 或者系统调用追踪器如 strace挂起。当调试器在断点处命中、或者进行单步调试时被调试的进程就会进入这个t状态操作方式在 scanf 中打入断点然后使用 gdb 进行调试#include iostream #include cstdio #include unistd.h using namespace std; int main() { printf(我是一个进程,pid:%d\n, getpid()); int x; scanf(%d, x); printf(%d\n, x); return 0; }在 scanf(%d,x); 打上断点此时我们看一下进程状态变化4.3.4.T 停止状态 (stopped)T 停止状态 (stopped)可以通过发送 SIGSTOP 信号给进程来停止T进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。操作方式运行一个进程后按住 ctrl z 即可#include iostream #include cstdio #include unistd.h using namespace std; int main() { while (true) { sleep(1); } return 0; }如果想要杀死这个 myprocess 进程可以使用这个命令kill -9 pid4.3.5.D 磁盘休眠状态 (Disk sleep)D 磁盘休眠状态 (Disk sleep)有时候也叫不可中断睡眠状态 (uninterruptible sleep)在这个状态的进程通常会等待IO的结束。现在有个场景就是 os 中的某个进程要想磁盘写入 100 MB但是呢内存资源太紧张了一直在 sleep 中然后 os 发现了这个进程为节约资源就把这个进程给杀了。万一写入这个100MB的数据是非常关键的呢例如金钱的转入流水信息等如果直接删除了那么谁都不知道这个问题就会造成一系列的后果。所以这类进程是不能删除的哪怕资源很紧张因此这种进程的状态称之为D(disk sleep)不可中断睡眠深度睡眠输入这个命令dd if/dev/zero of~/test.txt bs4096 count10000 oflagdsync命令的含义用全零数据生成一个 40MB 的文件但写入方式极其“较真”——每写 4KB 数据都必须等硬盘物理确认“收到并存好”后才继续写下一批用下面的的命令进行检测while :; do ps ajx | head -1; ps ajx | grep dd | grep -v grep; sleep 1; done4.3.6.僵死状态 (Zombies)僵死状态 (Zombies)是一个比较特殊的状态。当进程退出并且父进程使用wait()系统调用后面讲没有读取到子进程退出的返回代码时就会产生僵死(尸)进程。意思就是子进程已经进行完毕但是父进程一直不管。就会出现僵尸进程代码#include iostream #include cstdio #include unistd.h using namespace std; int main() { pid_t id fork(); if (id 0) { cerr 创建子进程失败 endl; return 1; } else if (id 0) { // child int count 5; while (count--) { sleep(1); cout 我是子进程 当前 pid: getpid() , 父进程id: getppid() count: count endl; } } else { // father while (true) { sleep(1); cout 我是父进程 当前 pid: getpid() , 父进程id: getppid() endl; } } return 0; }实验效果如果父进程一直不管不收回不孩子进程的退出信息那么Z就会一直存在会造成什么问题严重的内存泄漏问题如果进程退出了内存泄露还存在不不存在因为在学习c语言malloc申请空间的时候就算申请空间大退出程序该进程就销毁了就不会出现内存泄漏的时候。那么什么样的进程具有内存泄漏的问题是比较的麻烦的。常驻内存的进程 ---- 长期运行内存泄漏非常危险因为泄漏会不断累积最终导致系统内存耗尽。五、孤儿进程孤儿进程不属于 Linux 进程状态如 R、S、D、Z、T 等它属于一种“进程分类”或“进程场景”。孤儿进程的定义当父进程先于子进程退出被杀死或正常结束了而子进程还在运行那么这个子进程就变成了“孤儿进程”。父进程如果提前退出那么子进程后退出进入Z之后那该如何处理呢父进程先退出子进程就称之为“孤儿进程”孤儿进程被1号init进程领养当然要有init进程回收喽。#include iostream #include cstdio #include unistd.h using namespace std; int main() { pid_t id fork(); if (id 0) { cerr 创建子进程失败 endl; return 1; } else if (id 0) { // child while (true) { sleep(1); cout 我是子进程 当前 pid: getpid() , 父进程id: getppid() endl; } } else { // father int count 5; while (count--) { sleep(1); cout 我是父进程 当前 pid: getpid() , 父进程id: getppid() count: count endl; } } return 0; }使用 下面的命令进行监听while :; do ps ajx | head -1; ps ajx | grep myprocess; sleep 1; done看看结果图在父进程走之后子进程 就会被 1 号进程给 “领养”此时 终端仍然会打印这个语句“我是子进程当前 pid: 789879 , 父进程id: 1” 需要输入 这个命令才可以杀死kill pid那么 1 号进程是什么呢输入以下命令就知道了ps ajx | head -1 ps ajx | grep 1 | head -5 ls -l /sbin/init父子进程关系中如果父进程先退出子进程要被1号进程可以理解成操作系统领养这个被领养的进程子进程叫做孤儿进程。为什么要领养这是因为在父进程结束之后子进程无人看管然后为了防止出现内存泄漏问题就交给系统pid1管着完
