Linux 进程(三) 从进程状态到僵尸进程、孤儿进程
目录
一. 调度队列

内存为方便cpu调度,会让多个PCB组成调度队列,cpu就会按照顺序选择PCB来调度执行。
二. 运行&&阻塞&&挂起
运行状态
当进程处于调度队列中就可以认为进程处于运行状态:也就是说进程被cpu调度执行或者已经准备好被调度都可以认为处于运行状态。
阻塞状态
等待某种设备或者资源就绪
假设我们运行一个程序,CPU 正在调度执行对应的进程,当执行到
scanf()时,需要从键盘读取用户输入。我们之前说过,操作系统管理软硬件的方式可以理解为先描述再组织,管理硬件也是如此:操作系统会通过相应的数据结构描述硬件的属性信息,再通过一定的数据结构将其组织起来,从而实现对硬件的管理。回到我们的例子,如果此时我们没有按下任何按键,键盘没有提供可读取的输入数据,那么进程就无法继续执行,操作系统便会将该进程的状态设置为阻塞,并将其放入相应的等待队列中。当我们按下键盘并按下回车后,操作系统就能感知到输入事件,得知进程等待的条件已经满足,随后唤醒该进程,将其状态改为就绪状态,并重新放入调度队列中,等待 CPU 再次调度执行。

挂起状态
运行挂起和阻塞挂起
我们先来理解阻塞挂起,假设当前内存的空间已经不足,那么操作系统会将等待队列中进程的代码数据交换到磁盘中的一个名为swap交换分区的地方来腾出空间,等到操作系统内存恢复时再换入到内存中,重新构建指针映射;运行挂起可以理解为是内存空间严重不足时,会将调度队列中末端进程的代码数据交换到磁盘中的swap交换分区,等到要被调度执行的时候再换入内存。
三. 理解内核链表
我们之前说过内核中对进程的组织形式是双链表,但是我们又聊到内核中又存在调度队列和等待队列,同一个进程是如何同时服务于多种数据结构的呢?
我们会发现:内核中的双链表和普通双链表最大的不同是prev和next被额外封装到一个单独的结构体里面。


但是还有一个问题,我们通过结构体 list_head 来实现链表的遍历,但是我们想要访问PCB里面的其他成员变量就很麻烦了
解决这个问题之前我们要了解几个知识点:结构体首成员的地址与结构体起始地址相同,其他成员按照声明顺序依次存储,但成员之间可能存在内存对齐产生的填充字节(这些知识点我们在C语言讲结构体的时候讲过,忘记的可以回到之前的博客查看)。
那么我们可以利用这个偏移量来得到结构体的地址:(struct task_struct)(next/list-&((struct task_struct*)0->links)),但是在实践中会使用宏 container_of 解决
#include <stddef.h> // 提供 offsetof 宏 /* * ptr :结构体中某个成员的地址 * type :成员所属的结构体类型 * member :成员在结构体中的名称 * * 实现原理: * 1. 将成员地址转换为 char*,便于按字节计算。 * 2. 使用 offsetof 获取成员相对于结构体起始地址的偏移量。 * 3. 成员地址减去偏移量,得到结构体的起始地址。 * 4. 将结果转换为对应的结构体指针。 */ #define container_of(ptr, type, member) \ ((type *)((char *)(ptr) - offsetof(type, member)))
那么通过对prev和next的封装,我们可以让一个PCB同时存在于多个数据结构中:

四. Linux的进程状态

- R运行状态(running):并不意味着进程一定在运行中,它表明进程要么是在运行中要么在运行队列里。
- S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))。
- D磁盘休眠状态(Disk sleep):有时候也叫不可中断睡眠状态(uninterruptible sleep),在这个状态的进程通常会等待IO的结束。
- T停止状态(stopped):可以通过发送 SIGSTOP 信号给进程来停止(T)进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运行。
- X死亡状态(dead):这个状态只是一个返回状态,你不会在任务列表里看到这个状态。
4.1 代码示例:R / S状态


但是我们发现当我执行./myprocess的时候,发现./myprocess的状态是S不是R,为什么呢?
虽然程序一直在执行
printf()输出内容,但输出操作也涉及 I/O。当程序连续向终端输出数据时,如果终端暂时来不及处理,进程就可能需要等待输出操作完成,从而进入 S(可中断睡眠)状态。因此,我们使用ps查看进程时,可能恰好捕捉到它处于 S 状态的瞬间。


但是只要我们把 printf 注释掉,就不存在进程阻塞的问题,./myprocess的状态也就变成 R了。
4.1.1 R+
我们会发现进程状态后面常常会有一个+,这表示该指令在前台进行也就是在命令行中进行;但是只要我们在命令后面加上&就可以让命令在后台进行,+也就消失了。

4.2 代码示例: t / T(暂停)状态
t(追踪状态)


这里我们在myprocess.cpp的第七行打上断点,然后运行 myprocess,程序在第七行停止了。此时进程被暂停了,处于 t (追踪)状态。
T(暂停状态)
t 与 T 都表示进程处于停止状态,但 t 更多出现在使用调试工具跟踪进程时,例如遇到断点或进行单步调试;而 T 通常表示进程被普通暂停机制停止。


ctrl+z
ctrl+ z 用于暂停当前前台运行的进程;我们前面也是使用ctrl+z,让进程处于T状态。
4.3 代码示例:D状态
D 和我们之前学的 S 状态很像,S 状态在linux下称为可中断休眠、浅睡眠状态,在操作系统下就是我们熟知的阻塞状态;D 在linux下称为不可中断休眠、深度睡眠状态。那么我们该去怎么理解呢
首先我们之前说过,当内存空间不足时OS会将进程设置为挂起状态来释放空间,但是如果遇到极端情况:已经将能挂起的进程都挂起了还是内存不足时OS就可能会杀掉进程;但是贸然的杀掉进程必然导致严重的后果,所以就诞生了D状态,D状态和S状态最大的不同就是它很难被杀,使用 ctrl+c 或者 kill 通常都不能直接杀死。
4.4 代码示例: X / Z状态
Z 状态又称为僵尸状态,我们该怎么去理解呢:
我们之前说过,绝大多数进程都是另一个进程的子进程,而我们创建子进程的目的就是完成某样任务,所以当一个进程结束后我们得知道这个任务完成的怎么样,换句话说就是当一个进程退出时它的代码数据可以被立即释放掉,但是它的PCB数据不能立即释放掉,得让父进程读取信息。那么这个进程结束后维持PCB信息的状态就称为 Z 状态。
也就是当一个进程结束后,会先经历Z状态再到X状态


上述代码就很好地展现了僵尸状态。通过代码我们会发现子进程比父进程先结束,那么子进程的PCB部分就等待着父进程的回收,但是父进程一直在打印内容并没有对子进程做任何管理,那么此时子进程处于僵尸状态,是僵尸进程。
五. 僵尸进程
5.1 内存泄漏
在4.4中我们已经了解过了僵尸进程,这一小节我们来谈谈僵尸进程造成的后果——内存泄漏
首先我们来想想僵尸进程为什么会造成内存泄漏呢:我们都知道僵尸进程无非就是子进程的PCB部分没有及时的释放,那么这就意味如果父进程不去管理PCB就会一直占用着内存无法释放,这不就是内存泄漏问题吗
其他小知识点:进程退出了,内存泄漏问题还在不在?
答案是不存在了,打个比方:我动态开辟内存后忘记free造成了内存泄漏,但是程序一旦退出操作系统依旧会帮我们释放掉;同样的只要我们关机或者重启操作系统僵尸进程一样会消失。那么我们也就明白了什么进程内存泄漏最麻烦呢?就是像操作系统一样的常驻进程!
其他小知识点:内核结构申请
我们都知道,创建进程会创建结构体(PCB),进程退出后PCB会在父进程读取完信息后即使释放。那么我们其实不用急着释放PCB而是把不用的PCB存起来,等新进程创建时更改数据复用即可,这种技术在Linux内核中叫做slab
六. 孤儿进程
我们之前了解到如果子进程先于父进程退出,父进程不做管理的话那么子进程会变为僵尸进程;那如果父进程先于子进程退出呢?子进程就会成为孤儿进程。
代码示例:


我们会发现,子进程的父进程由原来的1043687变为了1。那么我们就可以得出结论:父子进程关系中,如果父进程先退出子进程要被1号进程领养,这个被领养的进程叫做孤儿进程。
6.1 1号进程
1号进程我们可以理解为是操作系统或者操作系统的一部分。

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)