进程(二)
文章目录
- 进程(二)
-
- [一、 进程状态](#一、 进程状态)
-
- 1、进程的基础状态:运行、阻塞、就绪
- 2、队列决定状态
- [3、 什么是阻塞?](#3、 什么是阻塞?)
- 4、挂起状态
- [5、OOM Killer(内存溢出杀手)](#5、OOM Killer(内存溢出杀手))
- 二、Linux下进程状态的具体实现
-
- 1、进程核心状态
- 2、SDT的对比
-
- [(1)S态(可中断睡眠)vs D态(不可中断睡眠)](#(1)S态(可中断睡眠)vs D态(不可中断睡眠))
- [(2)T态(停止态)vs S/D态(阻塞态)](#(2)T态(停止态)vs S/D态(阻塞态))
- 3、僵尸态(Z态)深度解析
- 4、前台进程与后台进程
-
- [(1) 前台进程](#(1) 前台进程)
- [(2) 后台进程和T态展示](#(2) 后台进程和T态展示)
- (3)T态第二种展示
- 5、孤儿进程
- [6、进程查看指令:ps -l](#6、进程查看指令:ps -l)
- 三、进程优先级
一、 进程状态
1、进程的基础状态:运行、阻塞、就绪
进程的基础状态分为三种,这是所有状态模型的根基:
- 运行态(Running) :进程正在CPU上执行指令,占用CPU时间片
- 就绪态(Ready) :进程已具备运行条件,等待CPU调度,位于系统的调度队列(runqueue)中
- 阻塞态(Blocked,也叫等待态) :进程因等待某事件(如键盘输入、磁盘读写)而暂停执行,不占用CPU,也不参与调度
2、队列决定状态
凡是task_struct(PCB)在CPU调度队列(runqueue)中的进程,均属于运行状态 ------不是只有正在CPU上执行的才叫"运行相关",只要排队等着抢占CPU,就属于可调度的运行类状态,等待CPU分配时间片即可
所以进程状态可以再核心区分为两类,本质就看你的task_struct在谁提供的队列中:
- 运行态 :task_struct在CPU调度队列(runqueue),参与CPU时间片轮转调度
- 阻塞态 :task_struct在硬件设备的等待队列(wait_queue),脱离CPU调度,不参与时间片竞争
小知识:每一个硬件设备 (键盘、磁盘、网卡、鼠标等),操作系统都会为其维护一个独立的等待队列 。进程需要等待某个硬件完成操作(比如键盘输入、磁盘读写),就会被操作系统移到对应硬件的等待队列中,直到事件完成,再被移回CPU调度队列
3、 什么是阻塞?
(1)现象层面
我们写代码时,最常见的阻塞场景就是scanf输入

运行这段代码后,你会发现程序 "卡住不动" ------不再往下执行,终端只显示提示信息,直到你输入一个整数并按下回车,程序才会继续运行 。这个"卡住不动、等待外部事件"的现象,就是阻塞

(2)阻塞的本质流程
从操作系统底层来看,阻塞的完整流程的是:
- 进程执行到需要等待外部事件的代码(如scanf等待键盘输入、读取文件等待磁盘IO)
- 操作系统 检测到这个需求,主动将该进程的task_struct从CPU调度队列中移除
- 将该task_struct放入对应硬件的等待队列(比如键盘等待队列、磁盘等待队列)
- 进程暂时失去CPU使用权,不再参与时间片轮转,处于"暂停执行"状态
- 当等待的事件完成 (如按下回车输入完毕、磁盘读写完成),操作系统再将该进程的task_struct从硬件等待队列移回CPU调度队列,进程变为运行态,等待CPU调度执行
打个比方:
就像你正在书桌前写作业(运行态),突然要等外卖(等待外部事件),你会主动离开书桌(移出CPU队列),站到门口的等待区(硬件等待队列),书桌(CPU)可以让给别人用;等外卖到了(事件完成),你再回到书桌旁排队(CPU调度队列),等待轮到你继续写作业(运行态)
4、挂起状态
当系统中进程过多(比如大量垃圾软件开机自启),内存被占满了 ,操作系统没法承载所有进程的代码和数据同时驻留内存,就会采用挂起(Suspend)机制 ,来缓解内存压力,挂起有两种:
- 运行态挂起 :进程原本处于运行态 (在CPU调度队列),但因为长期不活跃 ,被操作系统换到磁盘的Swap分区 ,把内存留给活跃的进程使用,当该进程需要运行时 ,再从Swap分区转移回内存,再次回到运行态
- 阻塞挂起态 :进程原本处于阻塞态 (在硬盘等待队列),由于内存压力过大 ,被操作系统换到磁盘的Swap分区 。如果等待事件完成了 ,该进程就会被换回内存,变成运行态,等待CPU调度
挂起其实是用磁盘空间换内存空间 ,暂时缓解了内存不足的问题。代价是磁盘的读写速度远慢于内存 (内存是纳秒级,磁盘是毫秒级,相差上万倍),过度的Swap换入换出会导致系统响应变慢
我舍友电脑就是这样卡的,严重到打开word都要加载一分钟,后面才发觉原来是垃圾软件自动开启把内存给占满了。大量垃圾软件开机自启→进程数量过多→物理内存被占满→系统频繁进行Swap换入换出→磁盘I/O过载+CPU调度压力大→系统响应迟缓、卡顿。 所以要记得清理垃圾软件
5、OOM Killer(内存溢出杀手)
如果挂起机制(Swap换出)仍然无法解决内存不足的问题,严重到内存和Swap都被占满。这时操作系统为了避免自身彻底崩溃,会触发OOM Killer(内存溢出杀手)机制 ------选择性杀死部分进程来释放内存,优先杀死占用内存大、优先级低的进程(比如后台无关的垃圾软件),保住系统核心进程(如桌面、终端)正常运行。
这也是为什么有时候电脑卡顿到极致,会自动关闭一些软件------本质是操作系统在自救

二、Linux下进程状态的具体实现
1、进程核心状态
- R------运行态,只要task_struct在CPU调度队列(runqueue),状态就是R,无论是否正在CPU上执行
- S------可中断睡眠(浅度阻塞),因等待事件阻塞(如scanf、sleep),可被信号唤醒(如Ctrl+C),能被kill命令终止
- D------不可中断睡眠(深度睡眠),因等待磁盘I/O阻塞,不响应任何信号,kill -9也无法杀死,避免数据损坏。简单来说正在与磁盘交互的进程不能被打扰
- T------停止态,被手动暂停(Ctrl+z)但是能通过SIGCONT信号恢复运行,后台进程读键盘就会进入此状态
- t------追踪暂停态,被GDB等调试工具暂停,处于被追踪状态,调试结束后可恢复(就是暂停执行的意思)
- Z------僵尸态,进程已退出,代码停止运行,但是task_struct未被回收,保留退出信息,不释放内存
- X------死亡态,进程完全退出,task_struct被系统或父进程回收,不占用任何资源,无法通过ps命令查看
Kill命令是给进程发送信号告诉它应该做什么
- kill 进程PID ,发送默认信号15,礼貌请进程自己退出,进程可以保存数据释放资源,甚至可以不理会这个信号
- kill -9进程PID ,强制杀死进程,进程不能拒绝,也没办法保存数据,直接被内核销毁
- kill -18 进程PID ,发送恢复信号SIGCONT ,当你按下Ctrl+Z,进程被暂停进入 T 态,给它发SIGCONT 信号,它就从 T 态 变回正常运行
其他的很多概念我们还都不清楚,后面回慢慢解释的
2、SDT的对比
(1)S态(可中断睡眠)vs D态(不可中断睡眠)
- 相同点: 都属于阻塞态,task_struct都在硬件等待队列,不参与CPU调度
- 不同点: S态是"浅度阻塞" ,等待的是普通事件(如键盘输入、sleep),可以被信号唤醒、被kill终止 ;D态是"深度阻塞",等待的是磁盘I/O(如保存文件、读取硬盘数据),绝对不允许被杀死
- 为什么D态不能被杀死?
就像你正在往U盘里拷贝重要的毕业论文,拷贝到一半时,有人强行拔掉U盘------会导致文件损坏、数据丢失。D态进程正在和磁盘进行关键交互(读写数据),如果强行用kill -9杀死,会导致磁盘数据写入不完整、文件损坏,甚至系统崩溃。因此,Linux内核会保护D态进程,不响应任何终止信号,直到磁盘I/O完
(2)T态(停止态)vs S/D态(阻塞态)
- S/D态: 阻塞是"被动暂停",因为等待外部事件(输入、磁盘IO),自己无法继续执行
- T态: 停止是"主动/被动暂停",要么是手动按Ctrl+Z暂停,要么是后台进程尝试读取键盘输入(违法操作)被系统暂停,和"等待外部事件"无关,恢复后能继续执行
3、僵尸态(Z态)深度解析
(1)僵尸的本质
- 僵尸态(Z态) :进程的代码已经执行完了,所有运行资源(内存、CPU)都已释放,但唯独保留了task_struct结构体, 目的是保存进程的退出信息,供父进程或操作系统读取
- 简单说:Z态进程就是"死了但没完全消失",只留下一个"身份证(task_struct)",记录自己的"死亡原因"(退出信息)
- 退出信息是啥?
进程退出时,会产生退出码和退出信号 (就像main函数return 0),如果进程正常退出,退出码就是0 ,如果进程被kill杀死,退出信号就是对应的kill信号(比如kill -9的信号是9) - 退出信号存在哪里?
所有退出信息(退出码、退出信号),都会保存在该进程的task_struct结构体中,不会立即释放
(2)回收僵尸进程
回收僵尸进程,本质就是释放该进程的task_struct结构体,清空保存的退出信息,彻底释放该进程占用的最后一点内存(task_struct本身占用的内存)
- 谁来回收?
这里简单提一下,主要由父进程回收 ,父进程通过wait()或waitpid()系统调用,读取子进程的退出信息,然后释放子进程的task_struct;如果父进程先退出,子进程会变成"孤儿进程",由操作系统(init进程或systemd进程)负责回收 - 不回收僵尸进程的危害:内存泄漏
子进程必须被回收 ,如果刻意不回收(比如父进程无限循环,不调用wait()),子进程会永远处于Z态,其task_struct无法被释放,会一直占用内存,导致内存泄漏
对于一次性程序 (比如运行完就退出的小程序),即使有僵尸进程,程序结束后,操作系统会自动回收所有相关资源,内存泄漏问题会随之消失 ;但对于常驻程序、死循环服务 (如服务器程序),如果长期产生僵尸进程且不回收,会导致内存被持续占用,最终内存耗尽,系统卡顿、崩溃------这也是内存泄漏最严重的场景
(3)Z态进程演示

编译运行代码,此时显示了父进程和子进程的PID

另外打开一个终端执行ps aux | grep Z (过滤只剩下Z)
进程PID23191就是代码中创建的子进程
状态Z+,Z代表僵尸态,+代表它是一个前台进程 。这说明子进程已经退出,但父进程还没回收它的资源,所以它成了僵尸进程

执行kill -9 父进程PID ,终止父进程 。父进程退出后,子进程会被回收,Z态消失
此时再看Z态,已经没有PID为23191的进程了

回到第一个终端也显示,父进程被杀死了

4、前台进程与后台进程
Linux中,进程分为前台进程和后台进程,两者的运行机制不同
(1) 前台进程
直接在终端执行命令(比如./test)就是进入前台进程
- 特点 :终端会被该进程独占,此时输入其他指令都不会接收,只能等该进程结束 。
但可以Ctrl+c直接中止
谁能从键盘读取输入,谁就是前台进程
任意时刻只能有一个前台进程
(2) 后台进程和T态展示
执行命令时,末尾加& (比如./test &),就能进入后台进程
- 特点:不占用终端输入,终端可继续执行其他命令
后台进程禁止读取键盘输入
后台进程可以有无数个
如果后台进程读取键盘会变成什么状态?
如果后台进程尝试用scanf等函数读取键盘输入,属于"违法操作" ------后台进程没有终端输入权限,操作系统会直接将该进程暂停,使其进入T态(停止态),无法继续运行

写一个有scanf的代码,并且编译运行它,运行成功,此时该程序是在前台运行

执行./test2 &,在命令末尾加了 &,表示后台运行
终端立刻返回了 [3] 8249。 3是终端给它分配的后台任务编号 ,8249是它的进程PID 。程序在后台运行到 scanf 时,需要读取键盘输入,但后台进程没有终端输入权限,被系统直接暂停了

执行 ps aux | grep 8249 ,可以看到PID 8249 对应的 STAT 列是 T (暂停态)
这验证说明了后台进程读取终端输入时,会被系统自动暂停,进入 T 态

fg 3(3 是它的任务编号),把这个暂停的后台进程拉回前台。
终端重新显示 ./test2,程序回到前台运行

(3)T态第二种展示

前台运行:./t_state
按Ctrl+Z,进程被暂停,终端提示"已停止"

执行ps aux | grep 进程PID,看到进程状态为T

5、孤儿进程
(1)什么是孤儿进程?
如果是父进程先退出,子进程还在运行,这个子进程就叫孤儿进程
因为任何进程退出,都必须被父进程回收资源,否则会变成僵尸进程 ,造成内存泄漏。但是如果父进程提前死掉,子进程没人收尸,系统就必须指认一个进程来负责子进程退出后的善后工作。Linux下默认是1号进程来领养孤儿进程
孤儿进程还会自动变成后台进程
(2)代码演示

从图中可以看到,父进程退出前,子进程 8335 的 PPID 为父进程 8334 。父进程退出后,子进程的 PPID 变为 1 ,子进程被系统 1 号进程领养,成为孤儿进程 。同时进程状态变为不带 + 的 S,证明其已转为后台进程

6、进程查看指令:ps -l

- UID:用户ID,表示这个进程是由哪个用户启动的
- PID:进程ID
- PPID:父进程ID
- PRI :进程优先级 ,系统默认的优先级,初始值一般是 80,值越小,优先级越高,越先抢占 CPU
- NI :nice 值(优先级修正值),用来调整进程的优先级,默认值是 0
三、进程优先级
1、优先级
- 权限解决能不能使用资源的问题,而优先级解决能使用资源,但谁先谁后的排队问题
- 为什么需要优先级?
CPU、内存等系统资源是有限的,如果有很多进程,必须通过优先级规则决定进程获取CPU资源的先后顺序,避免资源争抢混乱
2、PRI与NI
Linux通过两个整型变量管理进程优先级 :PRI(基础优先级)、NI(修正优先级/nice值)
- 公式:新PRI = 旧PRI + NICE
- 优先级规则:数值越小,优先级越高,越优先抢占CPU
- nice值固定范围:-20 ~ 19,共40个优先级等级
- 在Linux操作系统当中,PRI(old)默认为80,即PRI = 80 + NI
- 注意:普通用户只能输入0~19不能输入负数(比如 -1),会提示权限不足;只有 root 用户可以输入 -20~19 之间的任意值
为什么优先级的变化范围是有限的?
因为Linux系统大体是分时操作系统,给进程分配时间片要求相对公平公正,较为均衡的让不同的进程都能得到一段时间的CPU资源。所以优先级不能太夸张。CPU如何实现同时运行多个进程
除此之外还有实时操作系统,智能驾驶里会用到,大家想了解更多可以查查资料哦
3、查看进程优先级信息
先写一个可以一直运行的代码,编译运行它

当我们的进程创建后,可以使用ps -al命令查看该进程优先级信息
在Linux下,初始进程一般优先级默认为80,NI默认为0

用top命令修改已存在进程的nice值
top命令 相当于Windows的任务管理器,能够实时动态显示系统中进程的资源占用情况
打开top后按r键,终端提示"PID to renice: ",输入你要修改的进程 PID,再按下回车

接着提示 "Renice PID 24040 to value: ",输入要修改的 nice 值,再按回车,按q退出top页面

再ps -al查看修改结果,可以发现PRI已经变成90(旧PRI+10),NI变成了10 ,说明修改成功
