一.进程状态

在操作系统中,进程状态通常通过整型(int)或枚举类型(底层也是整型)表示。不同的数值对应不同的状态,便于内核高效管理和调度进程。
在linux中进程状态就是task_struct里的一个整数,在task_struct定义一个整形变量来表示进程状态。
其实在Linux中可以把创建状态和就绪状态和运行状态都看作是运行状态
二.运行&&阻塞&&挂起

1.
首先一个点cpu去选择进程运行不是直接选择进程的代码和数据运行,而是找到进程的pcb,pcb(linux中task_struct)内部有内存指针直接或间接的找到代码和数据。这一点在前面已经谈到过
2.一个cpu一个调度队列(runqueue)
其类型就是task_struct*指针,能够帮我们找到要运行的所有进程。
但问题是我们在前面不是知道task_struct不是双链表结构吗?为什么现在又是队列了呢?
这个问题放到后面详细说明,现在我们就简单理解即在Linux中对pcb维护的做法是让一个task_struct既可以属于一个全局的双链表,又可以把相关进程放到一个队列中。即可以属于两个数据结构
3.调度算法之一:FIFO(先进先出)
在队列头部优先级高,尾部优先级低,所以cpu调度就是在队列中按优先级顺序选一个pcb(Linux中task_struct)调度
4.运行
只要该进程处在调度队列时就是运行状态,所以running状态有两层含义
(1)cpu已经在调度该进程,正在运行
(2)处在调度队列,已经准备好了,随时可以被调度
5.阻塞:等待某种设备或者资源就绪
eg:scanf时真正对待的不是用户输入而是等待输入时键盘硬件的响应就绪。
各种硬件比如键盘,显示器,摄像头等等在前面我们已经知道os管理这些硬件,就是通过先描述在组织的方式。

6.今天我们就在os的角度下理解阻塞
在每一个struct device都有一个等待队列,例如如果cpu正在运行某一个进程执行这个进程的代码时,发现这个进程要scanf则os就会去检查键盘的活跃状态,如果发现键盘没有按键按下,即不是活跃状态,则os就知道该进程不能正常的从键盘读取数据,不能正常执行。紧接着os就会把这个进程从cpu拿下来,从调度队列移走,转而把它的pcb链入到键盘的对待队列当中,则此时该进程就不会被调度处于阻塞状态。
阻塞->运行:当键盘被按下时,os会第一时间知道,然后就转而去查看对应就绪设备的struct device,将里面的状态改为活跃状态,并检查等待队列(struct task _struct*wait_queue),发现里面的指针不为空就把该指针对应的进程状态改为运行状态,然后将该进程的pcb重新链回调度队列,等到被cpu执行的时候就会运行scanf代码获取到用户输入到键盘的数据
综上:进程状态的变化表现之一就是在不同的队列中进行流动,本质都是对数据结构的增删查改
7.挂起

当内存空间严重不足的时候os就会把处于阻塞状态(更严重甚至把调度队列)的进程的代码和数据直接swap唤出到磁盘的swap分区,只保留其pcb。此时这种状态就是挂起状态
等到硬件设备就绪,os知道后就把对应进程曾经唤出到磁盘分区的代码和数据,重新唤入到内存,形成完整的进程,然后就是阻塞状态到运行状态的步骤了。
三.理解内核链表的话题

我们普通的一个双链表结构

每一个Node节点都有一个next指针指向下一个节点的开始位置,已经一个prev指针指向前一个结点的开始位置。
但内核的双链表不一样,它单独把next和prev封装成一个结构(struct list_head)
在task_struct用list_head links表示

可现在的问题是每一个task_struct里的links都是指向的下一个节点的links,而不是开始位置,那我们怎么知道其余属性的地址在哪里?怎么才能访问到呢?

在c语言我们知道结构体的地址和结构体第一个成员变量的地址一样
所以可以先想在0地址处有一个task_struct然后访问它的links再&取地址,这样就拿到了links相比较0位置地址的偏移量

然后再用现在的地址减去偏移量不就让指针指向头部地址了吗,又结构体向下变量的地址依次增大,最后再转成(struct task_struct*)就可以访问到所有属性的地址,进而访问所有属性了

再回归到前面的理解因为内核是把next和prev单独封装成一个结构体,所有我们可以在task_struct内部定义多个links,而每两个task_struct的links之间都可以有不同的连接方式,比如下面的第1行的links都是以双链表的方式连接,而第2行的links又可以以队列的方式连接。所以每一组links对象都可以形成以节点为类型的各种数据结构。
即在内核数据结构很可能不是单一的而是呈现网状结构。

上面我们谈的是在操作系统里理论上进程的各种运行状态,下面我们研究具体在Linux下的进程状态
四.Linux的进程状态


1.理解为什么进程开始运行看到的是S+不是R+以及+号表示什么意思?
myprocess.c里面代码

编译运行后,在另外一台机器查看进程发现

myprocess这个进程是S+状态但为什么是这个状态呢?它在运行不应该是 R运行状态吗?
这是因为真正执行printf代码的时间非常非常短,剩余的时间进程都是在等待。即这个进程不断的在运行队列和阻塞队列来回切换运气好的话一直查能够查到进程刚好执行printf代码即处于r状态。
如果注释掉printf后,再去查看进程就会发现其处于运行状态了

同时R+ S+这个+号表示是放在前台运行,在后台运行不会影响当前我们继续在命令行进行命令输入, 有+号表示在前台(命令行)运行,会阻塞命令行,不能再在命令行输入命令
通过在再加一个&的方式让其在后台运行
所以此时看到就已经是R状态了,表示在后台运行

2.理解S+
阻塞状态在Linux的具体其中之一叫S(休眠状态)

执行到scanf时进程在对待我们从键盘输入,此时就是处于S(休眠状态)

3.T和t
(1)先来理解一下t状态
-g让其编译后处于debug模式,再gdb

现在我们再去查看进程可以看到

启动了一个新进程,这个进程不是myprocess而是gdb,myprocess还没有运行(./myprocess才运行,或者在gdb时输入r指令)

再第9行打一个断点再r起来,再去查看进程时就发现有一个/home/ysm/lesson14/myprocess运行起来了,但又因为有断点在第9行所以所以此时的状态为t(追踪状态)

所以一个进程要被debug,加了断点后程序停下来是该进程被暂停了
T状态

运行起来后,如果我们手动输入CTRL+z让它暂停此时再去查看进程时就为T状态

所以进程不是通过gdb以断点这种追踪形式暂停,而是用户手动让其暂停,这种状态为T状态
4.暂停状态和阻塞状态之有什么不一样的吗?
阻塞状态是是因为在对待某种设备就绪,而暂停则是因为某种条件不具备,或者进程做了非法操作
例如后面会讲的守护进程它不允许向显示器上打印只允许往文件里面写入,如果非要这样做的话则操作系统就会认为当前进程出现错误,会暂停进程及时止损,交给用户由用户来决定要不要继续。
5.D和S
D状态
S状态为可中断休眠,浅睡眠。即如果处于S状态我们可以直接杀掉,会响应杀掉的动作
D状态深度睡眠,不可中断睡眠。不能被直接杀掉
例如当某个进程要向磁盘写入100MB大小的内容时,则在等待磁盘将这100MB内容在自己内部找到空间并放入的时候,该进程就处于S状态(等待磁盘就绪),成功写入的话该磁盘就会返回结果给进程,然后进程再显示给用户,但如果现在内存的空间资源非常有限,OS在发现该进程后就会直接杀掉该进程以节省空间资源,但如果磁盘发现自己的空间资源也不够那么这100MB数据内容就没有成功写入,可是等它去找该进程返回结果时,就找不到该进程(以已经被杀了),那么这100MB数据内容就丢失了。
所以涉及到对磁盘这种关键存储数据设备的访问,或者进程进行高IO操作时,进程的状态就不能设为S而是应该设为D,防止该进程被杀进而数据丢失。
6.暂停的kill指令和重新运行
kill -19 pid 和ctrl+z一样也是暂停的指令
暂停后再运行kill -18 pid
所以现在阻塞状态在linux下有T,t和S,D
7.X和Z
X--死亡状态 Z--僵尸状态
(1)理解僵尸状态
我们创建子进程的目的无非就是为了让它完成某种任务,结果完成的相关信息,父进程就必须知道所以一个进程退出不是一下就把其pcb和代码和数据全部清理掉,而是os会把代码和数据清理掉,因为其不会再被调度执行,但它的pcb不能被释放因为进程退出的相关信息就保存在pcb里面,父进程需要从中获取到。所以就把在子进程退出后,父进程获取到子进程的退出信息之前这段状态叫进程处于僵尸状态
(2)模拟实现
前提得有父子进程,子进程如果要退出,父进程什么都不管不获取子进程的退出信息,那子进程就一直处于僵尸状态
eg:子进程跑5s结束,父进程什么都不做

等待5s后就会看到子进程已经处于Z状态

如果一个进程一直处于Z状态,内存一直被占用不释放就会引发内存泄漏的问题
(3)X
X状态我们看不到因为等父进程获取到了子进程的退出相关信息,完成回收了,进程就从Z直接变为X**,而一旦处于X操作系统就直接把这个子进程给全部释放掉了**
8.内存泄漏问题
只要进程退出了内存泄漏就不存在了,如果自己的代码里面有内存泄漏比如new/malloc出来的空间,没有被用户手动释放掉,只要进程退出了,这些空间就会被系统自动回收。这是在语言层面
而僵尸进程这样引发的内存泄漏是在系统层面
用户态程序内存泄漏
- 进程运行时未释放动态分配的内存(如malloc/new)
- 进程终止后操作系统回收整个地址空间
- 不会影响其他进程或系统稳定性
内核态内存泄漏
- 驱动程序或内核模块未释放分配的内存
- 即使相关进程退出,泄漏仍然存在
- 可能导致系统内存逐渐耗尽
9.关于内核结构的申请
创建pcb:(1)申请空间(2)初始化
正常情况下在僵尸状态到了死亡状态后,就直接把对应的task_struct给free掉下一次就需要重新malloc新的空间,再初始化。但可以在系统内部维护一个unuse链表把本来应该free掉的task_struct放到unuse链表里,后面如果要重新创建task_struct就直接用unuse里的,只需要重新初始化里面的字段即可,这样不就实现复用了吗?
形成了类似于内核数据结构对象的缓存,加速创建进程和释放进程的速度,这种技术具体的在Linux中叫slab