Linux 进程调度管理

来源:互联网 发布:淘宝新卖家如何推广 编辑:程序博客网 时间:2024/05/16 17:32

若要深入理解信号,首先必须稍微深入的理解一下 Linux的进程调度管理,这是必不可少的。

Linux 进程调度的目标

为什么需要进程调度,因为操作系统是一个多任务多用户的操作系统,保证所有的进程都公平的得到CPU的资源。

进程调度的目标:

  1. 高效。 高效意味着在相同的时间内要完成更多的任务。调度程序会被频繁的执行,所以调度程序需要尽可能的高效。
  2. 交互性。在系统具有相当的负载下, 也要保证系统的响应时间。
  3. 保证公平性,避免饥饿。
  4. SMP 调度: 调度程序必须支持 多处理器 系统。
  5. 软实时调度: 系统必须有效的调度实时进程,但是不保证一定满足其要求。

Linux 进程优先级

进程提供了两种优先级:
1. 普通进程优先级: SCHED_NORMAL 调度策略
2. 实时进程优先级: SCHED_FIFO 或者 SCHED_RR 调度策略

任何时候,实时进程的优先级都高于普通进程的优先级
实时进程只会被高优先级的实时进程抢占
同优先级实时进程,之间是按照 FIFO(一次机会做完) 或者 RR(多次轮转)的规则调度。

实时进程的调度

  1. 实时进程 只具有 “静态优先级”, 内核不会根据休眠等因素来对 其 静态优先级作调整。
  2. “静态优先级”的范围在 0~MAX_RT_PRIO-1 之间。默认 MAX_RT_PRIO-1 = 100,也就是说默认的实时优先级范围是 “0~99”。
  3. nice 值 影响的是 优先级在 MAX_RT_PRIO ~ MAX_RT_PRIO+40 之间的进程。 对实时优先级的进程无影响。

不同于普通的进程,系统调度的时候,实时优先级高的进程总是先于优先级低的进程执行。(实时优先级低的进程或得不到执行机会,知道实时优先级高的进程执行完毕 或者其无法继续执行。普通进程中低优先级的进程还是可以获得执行机会的)

实时进程总是被认为处于活动状态(什么是活动状态)。 假设当前CPU运行的实时进程A的优先级是 a, 此时出现了一个优先级为 b 的实时进程 B 进入到了可运行状态,那么只要 b < a,系统将中断 A 的执行,而优先执行B,知道B无法执行为止。(无论A,B为何种实时进程)。

如果有数个优先级相同的实时进程,那么系统就会按照进程出现在队列上的顺序进行选择。
对于优先级相同的实时进程,具有以下几种调度策略:
1. FIFO: 意味着只有当前进程执行完毕,才会轮到其他的进程执行。 相当霸道
2. RR: 轮转,一旦时间片消耗完毕,则会将该进程置于队列的末尾,然后运行其他的优先级相同的进程。如果没有其他的相同优先级的进程,那么该进程会继续执行。

总之,对于实时进程,高优先级的进程就是大爷。 它执行到没法执行了,才轮到低优先级的进程执行。等级制度森严。

非实时进程(普通进程)的调度

#将当前目录下面的 documents 目录打包,但是不希望 tar 命令 占用太多的CPU。$: nice -19 tar zcf pack.tar.gz documents## "-19" 中的 "-"表示参数前缀,所以如果希望赋予 tar 进程最高的优先级,则:$: nice --19 tar zcf pack.tar.gz documents.## 修改已经存在的进程的优先级:将 PID 为1799的进程优先级设置为最低$: renice 19 1799 (为什么是19呢?)renice 命令直接以优先级的值作为参数。 nice 命令则不是。

Linux 对于普通的进程,可动态的调整优先级。 动态优先级是由静态优先级(static_prio)调整而来。在Linux下,静态优先级对于用户时是不可见的,隐藏在内核中。而内核提供给用户一个可以影响静态优先级的接口,就是 nice 值,二者关系如下:

static_prio = MAX_RT_PRIO + nice + 20

nice的值是 -20~19,因而静态优先级的范围在 100~139之间。nice 数值越大—>static_prio 越大 —-> 优先级越低。

ps -el 命令的执行结构: NI 显示每个进程的 nice 值, PRI 则是进程的优先级(如果是实时进程,那么就是静态优先级,如果是非实时进程,就是动态优先级)

而进程的时间片(这个是什么意思?),就是完全依赖于 static_prio 而定制的。见下图,来自于 <深入理解Linux 内核>

进程的时间片

我们前面也说了,系统调度的时候也会考虑到其他的因素,计算出一个动态优先级的东西,根据动态优先级来实时调度。 因为,不仅仅要考虑静态优先级,而且也要考虑进程的属性。
1. 如果进程属于交互进程,可以适当的提高他的优先级,使得界面反应更加迅速,从而是用户得到更好的体验。
2. Linux 2.6 认为,交互式进程可以从平均睡眠时间这样一个 measurement 来进行判断。 如果进程过去睡眠的时间越多,那么这个进程就越有可能属于交互式进程。则系统调度的时候,会给该进程更多的奖励(bonus),使得该进程能有更多的机会来执行,奖励从(bonus) 0 到10 不等。

操作系统 会严格按照 动态优先级高低的顺序来安排进程执行: 动态优先级高的进程进入非运行状态, 或者 时间片消耗完毕才会轮到动态优先级较低的进程执行。 (时间片消耗完毕是什么意思?)

动态优先级的计算: 两个因素,静态优先级,进程的平均睡眠时间bonus。

dynamic_prio = max(100, min(static_prio - bonus + 5, 139))

在进行调度的时候,Linux2.6使用了一个小小的 tric, 就是算法中经典的空间换时间的思想。使得计算最优的进程能够在 O(1) 的时间内完成。
(有关是如何调度 普通进程 还是不是很清楚)

为什么根据睡眠和运行时间确定 “奖惩分数” 是合理的

  1. 睡眠 和 CPU 耗时反映了 IO 密集型和CPU密集型 两大瞬时特点。
  2. 不同时期一个进程可能处于不同的角色。
  3. 对于表现为IO密集型的进程,应该经常运行,但是每次时间片不要太长。
  4. 对于表现为CPU密集的进程,不应该进程运行,但是每次运行时间片要长。

交互进程为例: 如果之前其大部分时间都在等待 CPU,这个时候为了提高相应的速度,就需要增加奖励分,让其优先级增高。 另一方面,如果此进程每次总是耗费尽分给他的时间片,为了对其它进程公平,就要增加这个进程的惩罚分数。
(可以参考 CFS 的 virtutime 机制)

现代方法 CFS

不在单纯依靠进程优先级绝对值,而是参考其绝对值,综合考虑所有进程的时间,给出当前调度时间单位内其应有的权重。 也就是,每个进程的权重 * 单位时间 = 应获得到CPU时间。

这个应得到 cpu 时间不应该太小(假设阈值为1ms),否则会因为切换而得不偿失。 但是当进程足够多的时候,肯定有很多不同的权重的进程获得相同的时间 —– 最低阈值1ms。 所以 CFS 只是近似完全公平。


Linux 进程 状态机。

Linux进程状态机的转换

从上图中好好观察,可以看到很多有关进程的有趣的事情。

  1. 进程通过 fork 系列的系统调用(fork, clone, vfork)创建的。
  2. 内核模块 可以通过 kernle_thread函数创建进程。

这些创建子进程函数本质上都完成了相同的事情 — 将调用进程 复制一份,得到子进程。(通过参数选项,决定各种资源是否共享,还是私有)

  1. 调用进程处于 TASK_RUNNING 状态, 那么子进程默认也处于 TASK_RUNNING 状态。
  2. 系统调用 clonekernel_thread 也接受 CLONE_STOPPED 选项,从而将子进程 的初始状态 置为 “TASK_STOPPED” 状态。

进程创建之后,状态可能发生一系列的变换,直到进程退出。 尽管进程状态有好几种,但是进程状态的变迁只有两个方向:

TASK_RUNNING->非TASK_RUNNING
非TASK_RUNNING->TASK_RUNNING

TASK_RUNNING 是必经之路,而不可能在两个 非 TASK_RUNNING 状态之间转换。

也就是说,如果给一个 TASK_INTERRUPTIBLE(可中断) 状态的进程发送 SIGKILL信号,那么这个进程将先会被唤醒(进入TASK_RUNNING状态),然后再去响应 SIGKILL信号(在用户态响应)而退出(变为 TASK_DEAD状态)。并不会从TASK_INTERRUPTIBLE状态直接退出。

进程从 非TASK-RUNNING状态 变为 TASK_RUNNING 状态,是由别的进程(也可能是中断处理程序:比如网络资源来了)执行唤醒操作来实现的。执行唤醒的进程设置被唤醒进程的状态为 TASK_RUNNING,然后将其 task_struct结构体加入到某个CPU的可执行队列中,于是被唤醒的进程将有机会被调度。

那么,进程从 TASK_RUNNING 状态变为 非TASK_RUNNING状态,则有两种途径:

  1. 响应信号 而进入 “TASK_STOPED” 状态,或者 TASK_DEAD 状态
  2. 执行系统调用主动进入 TASK_INTERRUPIBLE 状态(比如 nanosleep系统调用),或者TASK_DEAD状态(如exit系统调用);或者由于系统调用需要的资源不满足,而进入 TASK_INTERRUUPTIBLE 或者 TASK_UNINTERRUPTIBLE状态(比如,select系统调用)。
    显然,这两种情况都只能发生在 进程 正在 CPU上执行的情况下。

通过 ps 命令,我们可以查看 系统中存在的进程,以及它们的状态。

(R == TASK_RUNNING), 可执行状态。

只有在该状态下的进程才可能在CPU上运行,同一时刻可能有多个进程处于可执行状态,这些进程的 task_struct结构体(进程控制块)被放入到对应CPU的可执行的队列中(一个进程最多只能出现在一个队列中)。

只要可执行队列不为空,那么CPU就不能偷懒,就要执行其中的某个进程。一般称此 CPU “忙碌”,对应的 CPU空闲是指可执行队列为空,CPU无事可做。

有人问,为什么 死循环 会导致 CPU占用率过高呢? 原因是死循环 程序 基本上总是处于 TASK_RUNNING状态(进程处于可执行队列中)。除非是一些非常极端的情况(比如系统内存严重紧缺,导致进程某些需要使用的页面被换出,并且在页面需要换入的时候无法分配到内存),否则这个进程不会 睡眠。所以CPU的可执行队列总是不为空(因为有这个进程的存在)。CPU 也就不会空闲。

很多操作系统教科书上 将正在 CPU上执行的继承定义为 “RUNNING”状态,而将可执行的但是尚未被调度执行的进程定义为 “READY”状态。 这两种状态在 Linux下统一为 TASK_RUNNING状态。

S (TASK_INTERRUPTIBLE), 可中断的睡眠状态。

处于这个状态的进程: 因为等待某个事情的发生(等待socket连接,等待信号量),而被挂起。 这些进程的 task_struct结构被放入对应事件的等待队列中。
当这些事件发生的时候(由外部中断出发, 由别的进程触发),对应的等待队列中的一个或者多个进程将会被唤醒。

通过 ps 命令,可以看到,一般情况下, 进程列表中的绝大多数进程都处于 TASK_INTERRUTPIBLE 状态(除非机器的负载比较高)。毕竟CPU就这么一两个,进程 一般会有几百个, 如果不是绝大多数进程都在 “睡眠”,CPU又怎么相应的过来。

D(TASK_UNINTERRUPTIBLE),不可中断的睡眠状态。

进程处于睡眠状态,但是 此刻进程是不可中断的。
不可中断: 不是 进程不响应外部硬件的中断。
是进程不响应异步信号的中断。

绝大多数情况下,进程处于“可中断的睡眠状态”,可以响应异步信号。 否则你将惊奇的发现, kill -9 竟然杀不死一个正在睡眠的进程!!。 于是我们可以知道,为什么 ps 命令看到的绝大多数进程几乎不会出现 TASK_UNINTERRUPTIBLE状态,而总是 TASK_INTERRUPTIBLE状态。

TAKS_INTERRUPTIBLE状态存在的意义是:内核的某些处理流程是不能被打断的。 如果相应异步信号,程序的执行流程中就会被插入 一段用于 处理异步信号 的流程 (这个插入的流程可能存在于内核态, 也可能延伸到用户态),于是原有的流程就被中断了。

在进程对某些硬件进行操作时(比如进程调用read系统调用对某个设备文件进行读操作,而read系统调用最终执行到对应设备驱动的代码,并与对应的物理设备进行交互),可能需要使用TASK_UNINTERRUPTIBLE状态对进程进行保护,以避免进程与设备交互的过程被打断,造成设备陷入不可控的状态。(比如read系统调用触发了一次磁盘到用户空间的内存的DMA,如果DMA进行过程中,进程由于响应信号而退出了,那么DMA正在访问的内存可能就要被释放了。)这种情况下的TASK_UNINTERRUPTIBLE状态总是非常短暂的,通过ps命令基本上不可能捕捉到。

(中断嵌套,信号处理函数嵌套,中断嵌套信号处理函数,信号处理函数嵌套中断)。

在 Linux 系统中,也存在容易捕捉的 TASK_UNINTERRUPTIBLE 状态。 进程执行完 vfork 之后,父进程将进入到 TASK_UNINTERRUPTIBLE状态,直到子进程调用 exit 或者 exec。 通过下面的就可以得到处于 TASK_INTERRUUPTIBLE状态的进程。

#include <unistd.h>void main(){    if(!vfork()) sleep(100);}

编译运行, ps 一下:

$: ps -ax | grep a.out4371 pts/0 D+ 0:00 ./a.out4372 pts/0 S+ 0:00 ./a.out4374 pts/1 S+ 0:00 grep a.out

然后我们可以尝试一下 TASK_UNINTERRUPTIBLE状态的威力,无论是 kill 或者是 kill -9,这个 TASK_UNINTERRUPTIBLE状态的父进程,依旧屹立不倒。

T (TASK_STOPPED or TASK_TRACED) 暂停状态,或者跟踪状态。

  1. 向一个进程发送 SIGSTOP信号, 这个进程会响应该信号,从而进入 TASK_STOPPED 状态(除非该进程本身 处于 TASK_UNINTERRUPTABLE状态而不能相应该信号)
  2. SIGSTOP信号和SIGKILL信号一样都是非常强制性的,不允许用户通过 signal系列的系统调用重新设置对应的信号处理函数。
  3. 向这个进程发送一个 SIGCONT,可以让该进程从 TASK_STOPPED状态恢复到 TASK_RUNNING状态。

所以说 信号 是 进程控制(controll the process)的一个基本方法。

当进程正在被跟踪时,它处于TASK_TRACED这个特殊的状态。“正在被跟踪”指的是进程暂停下来,等待跟踪它的进程对它进行操作。比如在gdb中对被跟踪的进程下一个断点,进程在断点处停下来的时候就处于TASK_TRACED状态。而在其他时候,被跟踪的进程还是处于前面提到的那些状态。
对于进程本身来说,TASK_STOPPEDTASK_TRACED状态很类似,都是表示进程暂停下来。
TASK_TRACED状态相当于在TASK_STOPPED之上多了一层保护,处于TASK_TRACED状态的进程不能响应SIGCONT信号而被唤醒。只能等到调试进程通过ptrace系统调用执行PTRACE_CONT、PTRACE_DETACH等操作(通过ptrace系统调用的参数指定操作),或调试进程退出,被调试的进程才能恢复TASK_RUNNING状态。

Z(TAKS_DEAD 或者 EXIT_ZOMBIE),退出状态成为僵尸进程。

进程在退出的过程中,处于 TASK_DEAD状态。

在这个退出的过程中,进程所有的资源都将会被回收,除了 task_struct结构(以及少数资源)以外。 就是进程就只剩下task_struct这个空壳,称为僵尸。

之所以保留 task_struct 是因为,task_struct保存了进程的退出码、以及一些统计信息。而父进程可能会关心这些信息,比如在 shell中,$? 变量就保存了最后一个退出的前台进程的退出码,而这个退出码往往被作为 if 语句的判断条件。

当然,内核也可以将这些信息保存在别的地方,释放掉 task_struct结构体,从而节省出一些空间。 但是使用 task_struct结构会更加的方便,因为在内核中已经建立了从 pid 到 task_struct的查找关系,还有父子进程之间的关系。 释放掉 task_struct,则需要建立一些新的数据结构,以便让父进程找到它的子进程的退出信息。

父进程可以使用 wait 系列的系统调用 (例如 wait4, waitid)来等待某个或者某些子进程的退出(称为进程间的同步),并且获取它们的退出信息。 然后wait系列的函数会数遍将子进程的尸体(task_struct)也释放掉。(只有 wait系列的函数可以释放task_struct吗?)

子进程在退出的过程中,内核会给父进程发送一个信号,通知父进程来“收尸”。这个信号默认是 SIGCHLD,但是在通过 clone系统调用创建子进程的时候,可以设置这个信号。

通过下面的代码,制造一个 EXIT_ZOMBIE状态的进程:

#include <unistd.h>void main(){    if(fork())        while(1) sleep(100);}

编译运行,然后 ps 一下:

$: ps -ax | grep a\.out (需要一个转义符号)10401  pts/0 S+ 0:00 ./a.out10411  pts/0 Z+ 0:00 [a.out] <defunct>10413  pts/1 S+ 0:00  grep a.out

只要父进程不退出,这个僵尸状态的子进程就会一直存在,如果父进程推出了呢? 谁又来给子进程“收尸”?

当一个进程退出的时候,这个进程的所有子进程 将会托管为 init 进程( PID = 1)。
Linux系统启动之后,第一个被创建的用户态进程就是 init 进程,它有两个使命:
1. 执行系统初始化脚本,创建一系列的进程(它们都是 init 进程的子孙)
2. 在一个死循环中 等待其他子进程的退出事件,并且调用 waitpid 系统调用来完成收尸工作。 (有些不理解…)

X (TASK_DEAD 或者 EXIT_DEAD) 退出状态,进程即将被销毁

进程被彻底销毁。 所以 EXIT_DEAD状态是非常短暂的, 几乎不可能通过 ps 命令捕捉到。


一些重要的杂项

调度程序的效率

“优先级”明确了哪个进程应该被调度执行,而调度程序还必须要关心效率问题。调度程序跟内核中的很多过程一样会频繁被执行,如果效率不济就会浪费很多CPU时间,导致系统性能下降。

在linux 2.4时,可执行状态的进程被挂在一个链表中。每次调度,调度程序需要扫描整个链表,以找出最优的那个进程来运行。复杂度为O(n);

在linux 2.6早期,可执行状态的进程被挂在N(N=140)个链表中,每一个链表代表一个优先级,系统中支持多少个优先级就有多少个链表。每次调度,调度程序只需要从第一个不为空的链表中取出位于链表头的进程即可。这样就大大提高了调度程序的效率,复杂度为O(1);

在linux 2.6近期的版本中,可执行状态的进程按照优先级顺序被挂在一个红黑树(可以想象成平衡二叉树)中。每次调度,调度程序需要从树中找出优先级最高的进程。复杂度为O(logN)。

那么,为什么从linux 2.6早期到近期linux 2.6版本,调度程序选择进程时的复杂度反而增加了呢?

这是因为,与此同时,调度程序对公平性的实现从上面提到的第一种思路改变为第二种思路(通过动态调整优先级实现)。而O(1)的算法是基于一组数目不大的链表来实现的,按我的理解,这使得优先级的取值范围很小(区分度很低),不能满足公平性的需求。而使用红黑树则对优先级的取值没有限制(可以用32位、64位、或更多位来表示优先级的值),并且O(logN)的复杂度也还是很高效的。

调度触发的时机

调度的触发主要有如下几种情况:

1、当前进程(正在CPU上运行的进程)状态变为非可执行状态。
进程执行系统调用主动变为非可执行状态。比如执行nanosleep进入睡眠、执行exit退出、等等;

进程请求的资源得不到满足而被迫进入睡眠状态。比如执行read系统调用时,磁盘高速缓存里没有所需要的数据,从而睡眠等待磁盘IO;

进程响应信号而变为非可执行状态。比如响应SIGSTOP进入暂停状态、响应SIGKILL退出、等等;

2、抢占。进程运行时,非预期地被剥夺CPU的使用权。这又分两种情况:进程用完了时间片、或出现了优先级更高的进程。

优先级更高的进程受正在CPU上运行的进程的影响而被唤醒。如发送信号主动唤醒,或因为释放互斥对象(如释放锁)而被唤醒;

内核在响应时钟中断的过程中,发现当前进程的时间片用完;

内核在响应中断的过程中,发现优先级更高的进程所等待的外部资源的变为可用,从而将其唤醒。比如CPU收到网卡中断,内核处理该中断,发现某个socket可读,于是唤醒正在等待读这个socket的进程;再比如内核在处理时钟中断的过程中,触发了定时器,从而唤醒对应的正在nanosleep系统调用中睡眠的进程;

内核抢占

理想情况下,只要满足“出现了优先级更高的进程”这个条件,当前进程就应该被立刻抢占。但是,就像多线程程序需要用锁来保护临界区资源一样,内核中也存在很多这样的临界区,不大可能随时随地都能接收抢占。

linux 2.4时的设计就非常简单,内核不支持抢占。进程运行在内核态时(比如正在执行系统调用、正处于异常处理函数中),是不允许抢占的。必须等到返回用户态时才会触发调度(确切的说,是在返回用户态之前,内核会专门检查一下是否需要调度);

linux 2.6则实现了内核抢占,但是在很多地方还是为了保护临界区资源而需要临时性的禁用内核抢占。

也有一些地方是出于效率考虑而禁用抢占,比较典型的是spin_lock。spin_lock是这样一种锁,如果请求加锁得不到满足(锁已被别的进程占有),则当前进程在一个死循环中不断检测锁的状态,直到锁被释放。

为什么要这样忙等待呢?因为临界区很小,比如只保护“i+=j++;”这么一句。如果因为加锁失败而形成“睡眠-唤醒”这么个过程,就有些得不偿失了。

那么既然当前进程忙等待(不睡眠),谁又来释放锁呢?其实已得到锁的进程是运行在另一个CPU上的,并且是禁用了内核抢占的。这个进程不会被其他进程抢占,所以等待锁的进程只有可能运行在别的CPU上。(如果只有一个CPU呢?那么就不可能存在等待锁的进程了。)

而如果不禁用内核抢占呢?那么得到锁的进程将可能被抢占,于是可能很久都不会释放锁。于是,等待锁的进程可能就不知何年何月得偿所望了。

对于一些实时性要求更高的系统,则不能容忍spin_lock这样的东西。宁可改用更费劲的“睡眠-唤醒”过程,也不能因为禁用抢占而让更高优先级的进程等待。比如,嵌入式实时linux montavista就是这么干的。

由此可见,实时并不代表高效。很多时候为了实现“实时”,还是需要对性能做一定让步的。

多处理器下的负载均衡

前面我们并没有专门讨论多处理器对调度程序的影响,其实也没有什么特别的,就是在同一时刻能有多个进程并行地运行而已。那么,为什么会有“多处理器负载均衡”这个事情呢?

如果系统中只有一个可执行队列,哪个CPU空闲了就去队列中找一个最合适的进程来执行。这样不是很好很均衡吗?

的确如此,但是多处理器共用一个可执行队列会有一些问题。显然,每个CPU在执行调度程序时都需要把队列锁起来,这会使得调度程序难以并行,可能导致系统性能下降。而如果每个CPU对应一个可执行队列则不存在这样的问题。

另外,多个可执行队列还有一个好处。这使得一个进程在一段时间内总是在同一个CPU上执行,那么很可能这个CPU的各级cache中都缓存着这个进程的数据,很有利于系统性能的提升。

所以,在linux下,每个CPU都有着对应的可执行队列,而一个可执行状态的进程在同一时刻只能处于一个可执行队列中。

于是,“多处理器负载均衡”这个麻烦事情就来了。内核需要关注各个CPU可执行队列中的进程数目,在数目不均衡时做出适当调整。什么时候需要调整,以多大力度进程调整,这些都是内核需要关心的。当然,尽量不要调整最好,毕竟调整起来又要耗CPU、又要锁可执行队列,代价还是不小的。

另外,内核还得关心各个CPU的关系。两个CPU之间,可能是相互独立的、可能是共享cache的、甚至可能是由同一个物理CPU通过超线程技术虚拟出来的……CPU之间的关系也是实现负载均衡的重要依据。关系越紧密,进程在它们之间迁移的代价就越小。

优先级继承

由于互斥,一个进程(设为A)可能因为等待进入临界区而睡眠。直到正在占有资源的进程(B)退出临界区,进程A才能被唤醒。

可能存在这样的情况: A的优先级非常高,B的优先级非常低。 B进入了临界区,但是却被其他的优先级较高的进程(设为C)抢占了,而得不到运行。也就是无法退出临界区,于是A也无法被唤醒。

A有很高的优先级,但是现在却沦落到跟B一起,被优先级并不太高的C抢占,导致执行被推迟。这种现象就叫做优先级反转。

出现这种现象是很不合理的,比较好的解决方式是: 当A开始等待B退出临界区的时候,B临时得到A的优先级(还是假设A的优先级高于B),以便顺利完成处理过程,退出临界区。之后B的优先级恢复。 这就是优先级继承。

中断处理线程化

在 Linux 下,中断程序运行在一个不可调度的上下文中。 从CPU 响应 硬件中断自动跳转到内核设定的 中断处理程序去执行,到中断处理程序退出,整个过程是不能被抢占的。(这里抢占指的是,有一个更高优先级的进程来了,想要占领CPU)

一个进程如果被抢占了,可以通过保存在它的 进程控制块 (task_struct)中的信息,在之后的某个时间内,恢复它的运行。 而中断上下文没有 task_struct,被抢占了就没有办法恢复了。 (但是中断处理程序,可以嵌套。 所以,嵌套和抢占的区别是什么,这也回答了我看书的时候的一些疑惑)

中断处理程序不能被抢占,也就意味着,中断处理程序的 “优先级” 比任何进程都要高。(必须等中断处理程序完成了,进程才能被执行)

但是在实际的应用场景中,可能某些实时进程应该得到比中断处理程序更高的优先级。 于是一些实时性要求更高的系统就给中断处理程序赋予了task_struct 以及优先级,使得他们在必要的时候能够被更高的优先级进程抢占。但是显然,做这些工作是会给系统造成一定的开销,这也是为了实现 “实时”而对性能做出的一定的让步。

0 0