【Linux】八.进程概念--进程的切换,上下文数据,进程的状态,进程的优先级,以及Linux内核进程的调度队列 📅 2026/7/5 15:26:28 一.进程的切换进程在CPU上面运行每个进程在运行的时候都有自己的一个运行时间单位叫做时间片。时间片从进程开始运行到被别的进程抢占的时间当代计算机都是分时操作系统没有进程都有它合适的时间⽚(其实就是⼀个计数器)。时间⽚到达进程就被操作系统从CPU中剥离下来。举个例子一个进程只能运行100ms即使进程A没有运行完毕但它的时间片耗尽了必须从CPU中抽离开换成下一个进程运行进程切换的几种情况1优先级比之前的高前提OS支持可以抢占2时间片的时间用完了一般来说操作系统可以允许一次性同时运行多个进程。但单独一个CPU对应一个进程你所看到的多个进程同时运行实则是通过进程快速切换的方式在一段时间内让所有代码都可以得到推进这叫做并发。但是时间片的时间很多ms甚我们用户感受不到而已。对于多的CPU也就意味着允许多个进程同时运行这叫做并行。注意(1)大多数情况下操作系统都是并行和并发同时进行(2)进程切换最重要的一步就是上下文数据的保存补充概念-竞争、独立、并行、并发• 竞争性: 系统进程数⽬众多⽽CPU资源只有少量甚⾄1个所以进程之间是具有竞争属性的。为了⾼效完成任务更合理竞争相关资源便具有了优先级• 独⽴性: 多进程运⾏需要独享各种资源多进程运⾏期间互不⼲扰• 并⾏: 多个进程在多个CPU下分别同时进⾏运⾏这称之为并⾏• 并发: 多个进程在⼀个CPU下采⽤进程切换的⽅式在⼀段时间之内让多个进程都得以推进称之为并发二.上下文数据进程在CPU中开始运行的时候CPU的寄存器上面会保存许多临时的数据当发生进程的切换的时候这些数据要肯定被保存的这些数据被称为进程的上下文数据。可以理解为CPU内寄存的数据成为进程的上下文数据举个例子说明正在上大一的张三中途想去当兵但为了不让自己的学业受到影响他申请保留学籍休学两年这才能正常离开学校。当兵结束后给学校申请恢复学籍才能正常完成学业有了上下文数据它是如何恢复和保存的呢上下文数据的保存当一个进程在运行的时候时间片耗尽了因此需要暂停让出CPU给其他进程此时进程要保护好自己的数据也就是当前进程的上下文数据到PCB中方便下次的恢复上下文数据的恢复当这个进程被又被重新切换回来或者切换到下一个新的进程运行的时候当前进程 A 的时间片用完了系统决定运行另一个进程 B这个 B 可能是之前暂停的也可能是新创建的只需要把此进程PCB上下文数据重新写入到CPU的寄存器中就可以继续恢复运行。运行队列假设在当前的操作系统有5个进程在处于可以运行状态中操作系统就会形成可以与运行队列每一个进程用链表连接起来其中有的进程处于可运行状态有的进程处于可运行队列当中CPU在执行任务的时候就只需要从这个可运行的队列中进行寻找即可。一个进程因为状态变成“可运行”所以被放入“可运行队列”只要在队列里它的状态就是“可运行”。在操作系统中寻找一个新的进程在CPU当中运行应该选择当前可以运行的状态的进程如果你整个扫描链表的话效率会很低因此引入了双向链表来提高可运行状态进程的效率也称为运行队列(runqueue)运行队列通过task_struct结构体中的两个指针run_list链表来维持。队列的两个标志空进程idle_task队列的长度。操作系统为每个进程状态管理各种类型的队列与进程相关的 PCB 也存储在相同状态的队列中。如果进程从一种状态转移到另一种状态则其 PCB 也从相应的队列中断开并被添加到进行转换的另一个状态队列中。PCB 是可以被列入多种数据结构内的。比如 PCB 在被调度的时候以及在等待某种资源的时候会被从调度队列移入或移出包括等待某种资源的等待队列。【补充】进程有三种常见状态运行态正在 CPU 上执行。就绪态可运行在就绪队列中。阻塞态在某个等待队列中。PCB 永远只出现在就绪队列一个或多个取决于调度策略等待队列可以有多个例如等待键盘、等待硬盘、等待锁不会有一个 PCB 同时存在于就绪队列和等待队列因为一个进程不可能既“可运行”又“在等某件事”。三.进程状态3-1查看进程状态ps aux / ps axja显⽰⼀个终端所有的进程包括其他⽤⼾的进程。x显⽰没有控制终端的进程例如后台运⾏的守护进程。j显⽰进程归属的进程组ID、会话ID、⽗进程ID以及与作业控制相关的信息u以⽤⼾为中⼼的格式显⽰进程信息提供进程的详细信息如⽤⼾、CPU和内存使⽤情况等一个进程的生命周期划分为一种状态进程的状态即体现一个进程的生命状态注意操作系统所说明的进程的状态一般是宏观上面的描述3-2Linux内核源代码的进程状态为了弄明⽩正在运⾏的进程是什么意思我们需要知道进程的不同状态。⼀个进程可以有⼏个状态在Linux内核⾥进程有时候也叫做任务。下⾯的状态在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):意味着进程在等待事件完成这⾥的睡眠有时候也叫做可中断睡眠interruptible sleep。D磁盘休眠状态Disk sleep有时候也叫不可中断睡眠状态uninterruptible sleep在这个状态的进程通常会等待IO的结束。T停⽌状态stopped可以通过发送 SIGSTOP 信号给进程来停⽌T进程。这个被暂停的进程可以通过发送 SIGCONT 信号让进程继续运⾏。X死亡状态dead这个状态只是⼀个返回状态你不会在任务列表⾥看到这个状态。僵尸状态zombie)3-3状态解释3-3-1R状态(running)定义可运行状态。准备就绪可以被调度进程是R状态不一定是在CPU上运行的进程在运行队列也是R状态。但如果一个进程想被CPU运行就必须是R状态才行#includestdio.h 2 #includeunistd.h 3 int main() 4 { 5 while(1) 6 { 7 printf(Im a running\n); 8 sleep(1); 9 } 10 return 0; 11 }解答该进程大部分时间都在休眠sleep(1)因为 printf 是往显示器上打印涉及到 IO后面会讲是为啥效率比较低该进程需要等待操作系统把数据刷新到显示器中。因此该进程绝大多数时间都在休眠只有极少数的时间在运行所以很难看到该进程处在 R 状态。那要件进程R的状态可以使用这个代码int main({while1{} }就可以看见R的显示。3-3-2S(sleep状态和Ddisk sleep)磁盘休眠状态S休眠状态sleeping浅度休眠大部分情况S状态进程虽然是一种休眠状态但随时可以接受外部的信号处理外部的请求被唤醒一叫就回应开始执行任务。D磁盘休眠状态disk sleep深度休眠属于叫不醒给你信号没反应kill -9都杀不掉 D 状态的进程只能等着 I/O 完成或重启系统。举例子说明比如进程 A 想要把些数据写入磁盘中因为 一些原因进程 A 需要等待。但因为内存资源不足在等待期间进程 A 被操作系统 kill 掉而此时磁盘也因为空间不足数据写入磁盘失败却不能把情况汇报给进程 A那这些数据应该怎么办这样很可能导致这些数据被丢失操作系统 kill 掉进程 A 导致了此次事故的发生。所以诞生了D 状态进程A不可以被杀掉即便是操作系统。只能等待 D 状态自动醒来或者是关机重启。其实S状态和D状态从本质上来说都是一种等待状态只是各自的触发条件没有被满足。【补充】请问S和S两个状态的区别在哪里S 表示前台进程。前台进程一旦运行bash 就无法进行命令行解释使用 CtrlC 可以终止前台进程S 表示后台进程。后台进程在运行时bash 可以进行命令行解释使用 CtrlC 无法终止后台进程3-3-3T状态stopped)kill 指令可以向目标进程发信号我们给进程发 19 号信号 SIGSTOP可以让进程进入 T 停止状态。停止运行。我们给进程发 18 号信号 SIGCONT可以让进程停止 T 停止状态。恢复运行。之前的循环写入这个指令又开始运行3-3-4僵尸状态--僵尸进程Zombie进程创建的目的目的是完成任务和工作当它结束的时候我们要看他任务和工作完成的怎么样所以我们得知道它是正常还是异常退出进程的假设进程正常退出那么交给进程的任务有没有正常完成进程退出时会自动将自己的退出的信息保存到PCB当中让OS或者父进程进行读取1如果进程退出了但是父进程是没有读取此时的进程处于僵尸状态。2假如读取成功后该进程才是真正的死亡状态也就是变成X状态。僵尸进程不属于可运行队列也不属于等待队列它是一个孤立的 PCB 残骸。基本定义僵尸状态是当子进程退出时并且父进程没有读取到子进程退出时返回的数据和代码就会产生僵尸状态父进程使用系统调用让wait回收子进程。僵尸状态的保持在进程表中一直会等到父进程读取退出代码说白了就是子进程退出但是父进程没有读取到子进程的状态子进程就会进入Z状态。举例说明#includestdio.h 2 #includestdlib.h 3 #includesys/types.h 4 #includeunistd.h 5 6 int main() 7 { 8 for(int i 0;i5;i) 9 { 10 pid_t ret fork(); 11 if(ret0) 12 { 13 printf(子进程%d,pid:%u,ppid %u\n,i,getpid(),getppid()); 14 sleep(1); 15 exit(1);//子进程退出 16 17 } 18 } 19 getchar();//父进程无法退出无法回收子进程 20 return 0; 21 }结果是五个进程创建成功但是这里被卡住了没有自动退出同时我们也可以看见子进程的打印顺序并不是按照创建顺序这是因为操作系统调度进程的执行没有固定的顺序进程的执行顺序可能会因为多种因素如调度策略、系统负载等而有所不同。这展示了进程调度的非确定性特性。注意父进程通过调用 getchar() 函数来等待用户输入这样做可以防止父进程过早退出在大多数情况下这也意味着父进程不会立即回收结束的子进程资源。父进程通过 getchar() 等待但这并不是处理僵尸进程的正确做法。在实际应用中父进程一般使用 wait / waitpid 函数来等待子进程结束并回收它们的数据和代码以避免僵尸进程的产生。getchar()就像你站在路口发呆等朋友但你没有接他的电话他到了也只能在路边干等僵尸。wait()就像你一直盯着到达口他一来你就把他领走。僵尸进程的危害如果一个父进程创建了很多子进程但就是不回收会造成内存资源的浪费。因为数据结构要占用内存定义一个 task_struct(PCB) 结构体变量要在内存的某个位置开辟空间僵尸进程会导致内存泄漏。进程的退出状态必须被维持下去因为他要告诉关心它父进程你交给我的任务办的怎么样。假如可父进程如果一直不读取那子进程就会一直处于 Z 状态。维护退出状态本身就是要用数据维护也属于进程的基本信息所以保存在task_struct(PCB) 中换句话说Z 状态一直不退出PCB 就要一直维护。僵尸进程的危害不是消耗CPU或内存而是像“进程ID的漏洞”——只占位置不干活攒多了系统就“生不出新孩子”了一般来说避免内存泄漏的方法是进程等待【补充】Z状态不能被kill杀掉因为赭红进程已经没了。3-3-5孤儿进程父进程先退出子进程就被称为孤儿进程。一般被1号系统进程领养孤儿进程退出时1号系统进程进行回收。举例说明1#includestdio.h 2 3 #includesys/types.h 4 #includeunistd.h 5 6 int main() 7 { 8 pid_t id fork(); 9 if(id0) 10 { 11 while(1) 12 { 13 printf(我是一个子进程,pid: %d,ppid %d\n,getpid(),getppid()); 14 } 15 } 16 else 17 { 18 int cnt 5; 19 while(cnt) 20 { 21 printf(我是一个父进程,pid %d,ppid:%d\n,getpid(),getppid()); 22 cnt--; 23 sleep(1); 24 } 25 } 26 return 0; 27 }总结就绪 / 运行R 状态阻塞S / D / T 状态退出Z / X 状态四.进程的优先级4-1基本概念cpu资源分配的先后顺序就是指进程的优先权priority。优先权⾼的进程有优先执⾏权利。配置进程优先权对多任务环境的linux很有⽤可以改善系统性能。还可以把进程运⾏到指定的CPU上这样⼀来把不重要的进程安排到某个CPU可以⼤⼤改善系统整体性能进程权限对比进程优先级优先级在资源有限的前提下确立多个进程中谁先访问资源谁后访问资源。权限决定能不能得到某种资源。最大的区别前者有资源后者是否可以得到资源4-2查看系统进程在 Linux 或者 Unix 系统中使用命令ps -al /ps -l查看当前系统进程的信息UID : 代表执⾏者的⾝份在 Linux 中标识一个用户不是通过用户名来标识的而是通过用户的UID。UID 是给计算机看的UID 对应的用户名是方便给人看的。比如 QQ 可以随意更改昵称那就说明昵称不是唯一标识这个 QQ 用户的而是通过 QQ 账号因为账号是不变的给计算机看的。PID : 代表这个进程的代号PPID 代表这个进程是由哪个进程发展衍⽣⽽来的亦即⽗进程的代号PRI 代表这个进程可被执⾏的优先级其值越⼩越早被执⾏其值越小优先级越高越早被执行。其值越大优先级越低越晚被执行。NI 代表这个进程的nice值nice 值表示进程可被执行的优先级的修正范围数值[-20, 19]。进程新的优先级PRI(new) PRI(old, 默认都是 80) nice补充ps -a默认排除了会话首进程如 bash而ps -l显示当前终端的所有进程4-3查看进程优先级的命令top 命令类似于 Windows 的任务管理器更改已存在进程的 nice执行 top 命令后按 r 键输入进程的 PID输入 nice 值。每次输入 nice 值调整进程优先级都是默认从 PRI 80 开始调整的80作为一个基准值方便调整在设计上实现比较简单。输入的 nice 值如果超过 [-20, 19] 这个范围默认是按照最左/最右范围来取的。在这个范围是一种可控状态保证了进程的优先级始终在 [60, 99] 这个范围内保证了 OS 调度器的公平性。但不是绝对公平。根据每个进程的特性尽可能公平的去调度它们而不是指每个进程的调度时间必须完全一样。普通优先级100139我们都是普通的优先级想想 nice 值的取值范围可与之对应实时优先级099不关心其他调整优先级的命令nicerenice五. Linux2.6内核进程O(1)调度队列方便理解画的数据结构图一个 CPU 拥有一个 runqueue如果有多个 CPU 就要考虑进程个数的负载均衡问题。5-1 活动队列调度流程工作原理CPU 每次只从活动队列中挑选进程运行。选中的依据优先级O(1) 调度器维护140 个优先级队列。进程运行时消耗时间片时间片未用完 → 放回活动队列可能调整优先级时间片用完 → 移到过期队列当活动队列为空时交换指针active expired; expired active原来的过期队列变成新的活动队列开始新的一轮【补充】时间⽚还没有结束的所有进程都按照优先级放在该队列nr_active: 总共有多少个运⾏状态的进程queue[140]: ⼀个元素就是⼀个进程队列相同优先级的进程按照FIFO规则进⾏排队调度,所以数组下标就是优先级从该结构中选择⼀个最合适的进程过程是怎么的呢1. 从0下表开始遍历queue[140]2. 找到第⼀个⾮空队列该队列必定为优先级最⾼的队列3. 拿到选中队列的第⼀个进程开始运⾏调度完成4. 遍历queue[140]时间复杂度是常数但还是太低效了bitmap[5]:⼀共140个优先级⼀共140个进程队列为了提⾼查找⾮空队列的效率就可以⽤5*32个⽐特位表⽰队列是否为空这样便可以⼤⼤提⾼查找效率5-2过期队列同活动队列长得一模一样工作原理进程创建或唤醒后先进入活动队列并获得一个时间片。CPU 每次从活动队列中选最高优先级的进程运行。当进程的时间片用完如果还有剩余时间片→ 放回活动队列优先级可能调整如果彻底用完→ 从活动队列摘下放入过期队列当活动队列为空时交换指针active expired; expired active原来的过期队列变成新的活动队列开始新一轮调度active指针和expired指针active指针永远指向活动队列expired指针永远指向过期队列可是活动队列上的进程会越来越少过期队列上的进程会越来越多因为进程时间⽚到期时⼀直都存在的。没关系在合适的时候只要能够交换active指针和expired指针的内容就相当于有具有了⼀批新的活动进程【补充】活动队列Active Queue存放还有时间片未用完、可以被调度的进程过期队列Expired Queue存放时间片已用完、需要等待下一轮调度的进程简单理解活动队列 有票的人过期队列 票用完、等下一轮发票的人。过期队列是 O(1) 调度器中存放“时间片已用完但还可运行”的进程的地方通过定期与活动队列交换实现公平调度。总结:在系统当中查找一个最合适调度的进程的时间复杂度是一个常数不随着进程增多而导致时间成本增加我们称之为进程调度 O(1) 算法。O(1) 调度算法 给每个优先级准备一个队列 用一个 140 位的位图快速定位“哪个优先级有进程”查找过程永远只需要固定几操作所以不管进程有多少找下一个运行进程的速度都不变。