进程调度 Linux 进程调度 操作系统要实现多进程,进程调度必不可少。进程调度是对TASK_RUNNING状态的进程进行调度 (参见《linux进程状态浅析》) 。如果进程不可执行 (正在睡眠或其他) ,那么它跟进程调度没多大关系。
所以,如果你的系统负载非常低,盼星星盼月亮才出现一个可执行状态的进程。那么进程调度也就不会太重要。哪个进程可执行,就让它执行去,没有什么需要多考虑的。
反之,如果系统负载非常高,时时刻刻都有N多个进程处于可执行状态,等待被调度运行。那么进程调度程序为了协调这N个进程的执行,必定得做很多工作。协调得不好,系统的性能就会大打折扣。这个时候,进程调度就是非常重要的。
尽管我们平常接触的很多计算机 (如桌面系统、网络服务器、等) 负载都比较低,但是linux作为一个通用操作系统,不能假设系统负载低,必须为应付高负载下的进程调度做精心的设计。
当然,这些设计对于低负载 (且没有什么实时性要求) 的环境,没多大用。极端情况下,如果CPU的负载始终保持0或1 (永远都只有一个进程或没有进程需要在CPU上运行) ,那么这些设计基本上都是徒劳的。
优先级 现在的操作系统为了协调多个进程的"同时"运行,最基本的手段就是给进程定义优先级。定义了进程的优先级,如果有多个进程同时处于可执行状态,那么谁优先级高谁就去执行,没有什么好纠结的了。
那么,进程的优先级该如何确定呢?有两种方式: 由用户程序指定、由内核的调度程序动态调整。 (下面会说到) linux内核将进程分成两个级别: 普通进程和实时进程。实时进程的优先级都高于普通进程,除此之外,它们的调度策略也有所不同。
实时进程的调度 实时,原本的涵义是"给定的操作一定要在确定的时间内完成"。重点并不在于操作一定要处理得多快,而是时间要可控 (在最坏情况下也不能突破给定的时间) 。
这样的"实时"称为"硬实时",多用于很精密的系统之中 (比如什么火箭、导弹之类的) 。一般来说,硬实时的系统是相对比较专用的。
像linux这样的通用操作系统显然没法满足这样的要求,中断处理、虚拟内存、等机制的存在给处理时间带来了很大的不确定性。硬件的cache、磁盘寻道、总线争用、也会带来不确定性。
比如考虑"i++;“这么一句C代码。绝大多数情况下,它执行得很快。但是极端情况下还是有这样的可能:
i的内存空间未分配,CPU触发缺页异常。而linux在缺页异常的处理代码中试图分配内存时,又可能由于系统内存紧缺而分配失败,导致进程进入睡眠;
代码执行过程中硬件产生中断,linux进入中断处理程序而搁置当前进程。而中断处理程序的处理过程中又可能发生新的硬件中断,中断永远嵌套不止……;等等……
而像linux这样号称实现了"实时"的通用操作系统,其实只是实现了"软实时”,即尽可能地满足进程的实时需求。
如果一个进程有实时需求 (它是一个实时进程) ,则只要它是可执行状态的,内核就一直让它执行,以尽可能地满足它对CPU的需要,直到它完成所需要做的事情,然后睡眠或退出 (变为非可执行状态) 。
而如果有多个实时进程都处于可执行状态,则内核会先满足优先级最高的实时进程对CPU的需要,直到它变为非可执行状态。于是,只要高优先级的实时进程一直处于可执行状态,低优先级的实时进程就一直不能得到CPU;只要一直有实时进程处于可执行状态,普通进程就一直不能得到CPU。
(后来,内核添加了/proc/sys/kernel/sched_rt_runtime_us和/proc/sys/kernel/sched_rt_period_us两个参数,限定了在以sched_rt_period_us为周期的时间内,实时进程最多只能运行sched_rt_runtime_us这么多时间。这样就在一直有实时进程处于可执行状态的情况下,给普通进程留了一点点能够得到执行的机会。参阅《linux组调度浅析》。)
那么,如果多个相同优先级的实时进程都处于可执行状态呢?这时就有两种调度策略可供选择:
SCHED_FIFO: 先进先出。直到先被执行的进程变为非可执行状态,后来的进程才被调度执行。在这种策略下,先来的进程可以行sched_yield系统调用,自愿放弃CPU,以让权给后来的进程;
SCHED_RR: 轮转调度。内核为实时进程分配时间片,在时间片用完时,让下一个进程使用CPU;
强调一下,这两种调度策略仅仅针对于相同优先级的多个实时进程同时处于可执行状态的情况。
在linux下,用户程序可以通过sched_setscheduler系统调用来设置进程的调度策略以及相关调度参数;sched_setparam系统调用则只用于设置调度参数。这两个系统调用要求用户进程具有设置进程优先级的能力 (CAP_SYS_NICE,一般来说需要root权限) (参阅capability相关的文章) 。
通过将进程的策略设为SCHED_FIFO或SCHED_RR,使得进程变为实时进程。而进程的优先级则是通过以上两个系统调用在设置调度参数时指定的。
对于实时进程,内核不会试图调整其优先级。因为进程实时与否?有多实时?这些问题都是跟用户程序的应用场景相关,只有用户能够回答,内核不能臆断。
综上所述,实时进程的调度是非常简单的。进程的优先级和调度策略都由用户定死了,内核只需要总是选择优先级最高的实时进程来调度执行即可。唯一稍微麻烦一点的只是在选择具有相同优先级的实时进程时,要考虑两种调度策略。
普通进程的调度 实时进程调度的中心思想是,让处于可执行状态的最高优先级的实时进程尽可能地占有CPU,因为它有实时需求;而普通进程则被认为是没有实时需求的进程,于是调度程序力图让各个处于可执行状态的普通进程和平共处地分享CPU,从而让用户觉得这些进程是同时运行的。
与实时进程相比,普通进程的调度要复杂得多。内核需要考虑两件麻烦事:
一、动态调整进程的优先级
按进程的行为特征,可以将进程分为"交互式进程"和"批处理进程":
交互式进程 (如桌面程序、服务器、等) 主要的任务是与外界交互。这样的进程应该具有较高的优先级,它们总是睡眠等待外界的输入。而在输入到来,内核将其唤醒时,它们又应该很快被调度执行,以做出响应。比如一个桌面程序,如果鼠标点击后半秒种还没反应,用户就会感觉系统"卡"了;
批处理进程 (如编译程序) 主要的任务是做持续的运算,因而它们会持续处于可执行状态。这样的进程一般不需要高优先级,比如编译程序多运行了几秒种,用户多半不会太在意;
如果用户能够明确知道进程应该有怎样的优先级,可以通过nice、setpriority (非实时进程优先级的设置) 系统调用来对优先级进行设置。 (如果要提高进程的优先级,要求用户进程具有CAP_SYS_NICE能力。
然而应用程序未必就像桌面程序、编译程序这样典型。程序的行为可能五花八门,可能一会儿像交互式进程,一会儿又像批处理进程。以致于用户难以给它设置一个合适的优先级。再者,即使用户明确知道一个进程是交互式还是批处理,也多半碍于权限或因为偷懒而不去设置进程的优先级。 (你又是否为某个程序设置过优先级呢?)
...