Linux 进程调度详解

4,049 阅读3分钟

1. PCB 进程控制器 task_struct

对于 Linux 操作系统而言,每一个进程都对应一个 task_struct 结构体(Linux 中的线程也是由 task_struct 控制的,本文主要讨论进程调动的内容,就不展开说明了,在内存管理章节会介绍)。

在 task_struct 中维护了进程的 PID,进程状态以及堆栈信息等进程的几乎所有信息。

我们常常提到的“僵尸进程”,就是进程的堆栈信息被回收了,但是其 task_struct 却依旧存活在操作系统中。 “僵尸进程”往往是因为父进程处理子进程的销毁信号错误导致的。也是因为 task_struct 留存在操作系统中,所以“僵尸进程”是会消耗操作系统资源的。

与“僵尸进程”常常一起提及的是“孤儿进程”,“孤儿进程”是父进程先于子进程销毁导致的,不过“孤儿进程”会进行“寻找养父”的过程,先找“亲生父亲”的进程组中的兄弟进程,找不到的话就会认“0”号进程为父亲,所以“孤儿进程”的销毁并不会受阻,不会消耗操作系统过多的资源。

2. 进程调度算法

Linux 标准内核有两类调度类:CFS 调度算法的默认调度类和实时调度类。下面我们分类来讨论这两种不同的调度算法。

2.1 完全公平调度算法 CFS

一句话总结,CFS 会根据权重为各进程尽量“公平”的分配时间片。虽然叫做完全公平,但 CFS 算法并不是完全相同的未每个进程分配同样的时间片来运行,导致其不公平的原因有以下两点:

  • CFS 是根据权重为每个进程分配时间片的(体现在其具体实现的红黑树上就是vruntime)。

  • 为了避免进程的过度频繁切换,时间片的分配有一个最小值。

CFS 算法的底层实现是靠维护一棵进程vruntime的红黑树实现的,每一次选取vruntime最小的进程占据CPU。

下面分析一下 CFS 调度程序是如何工作的。假设有两个任务,它们具有相同的友好值。一个任务是 I/O 密集型而另一个为 CPU 密集型。通常,I/O 密集型任务在运行很短时间后就会阻塞以便等待更多的 I/O;而 CPU 密集型任务只要有在处理器上运行的机会,就会用完它的时间片。

因此,I/O 密集型任务的虚拟运行时间最终将会小于 CPU 密集型任务的,从而使得 I/O 密集型任务具有更高的优先级。这时,如果 CPU 密集型任务在运行,而 I/O 密集型任务变得有资格可以运行(如该任务所等待的 I/O 已成为可用),那么 I/O 密集型任务就会抢占 CPU 密集型任务。

2.2 实时进程调度算法

Linux 底层实现了SCHED_FIFO 或 SCHED_RR 两种实时进程调度策略。

  • SCHED_FIFO 先到先服务,所有进程排队,先到的进程先执行,执行完到下一个进程。

  • SCHED_RR 时间片轮转,所有进程排队,但是一次只能领取一定的时间片,如果时间片用完,进程任务还没执行完,就会到队尾排队。

2.3 调度算法的选择

Linux 采用两个单独的优先级范围,一个用于实时任务,另一个用于正常任务。实时任务分配的静态优先级为 0〜99,而正常任务分配的优先级为 100〜139。

这两个值域合并成为一个全局的优先级方案,其中较低数值表明较高的优先级。正常任务,根据它们的友好值,分配一个优先级;这里 -20 的友好值映射到优先级 100,而 +19 的友好值映射到 139。