Linux内核设计与实现(4)第四章:进程的调度

1. 调度

1.1 什么是调度

决定哪些进程运行,哪些进程等待;决定每个进程运行多长时间

1.1.1 背景

现在的操作系统都是多任务的,为了能让更多的任务能同时在系统上更好的运行,需要一个管理程序来管理计算机上同时运行的各个任务(也就是进程)。

1.1.1 调度是一个平衡的过程

总之,调度是一个平衡的过程
一方面,它要保证各个运行的进程能够最大限度的使用CPU(即尽量少的切换进程,进程切换过多,CPU的时间会浪费在切换上);
另一方面,保证各个进程能公平的使用CPU(即防止一个进程长时间独占CPU的情况)。

此外,为了获得更好的用户体验,运行中的进程还可以立即被其他更紧急的进程打断。

1.2 进程调度程序

进程调度程序是多任务系统在可运行态进程之间分配有限的处理器时间资源的内核子系统。

1.3 调度策略(cpu密集型和IO密集型)

cpu密集型:大部分时间执行代码
IO密集型:大部分时间提交io和等待io,经常可运行,但运行时间极短
从系统响应速度考虑,linux调度策略更倾向于优先调度IO密集型进程

2. 进程的优先级

调度算法中最基本的一类:基于优先级调度,根据进程的价值和其对处理器时间的需求分级的思想

2.1 动态优先级的调度算法

inux实现了一种基于动态优先级的调度算法。
一开始设置基本优先级,然后根据需要动态加,减优先级。

例子:
如果一个进程IO等待时间大于运行时间,它属于IO密集型,会提高优先级;
相反,如果进程时间片一下就别耗尽,属于cpu密集型,会降低优先级

cpu密集型:大部分时间执行代码
IO密集型:大部分时间提交io和等待io,经常可运行,但运行时间极短

2.2 两种度量方法:nice值和实时优先级

进程的优先级有2种度量方法,一种是nice值,一种是实时优先级
nice值的范围是-20~+19,值越小优先级越高,也就是说nice值为-20的进程优先级最大。
实时优先级的范围是0~99,与nice值的定义相反,实时优先级是值越大优先级越高。

实时优先级高于nice值;一个进程不可能有2个优先级

实时优先级的范围是 0~MAX_RT_PRIO-1 MAX_RT_PRIO的定义参见

include/linux/sched.h
#define MAX_USER_RT_PRIO        100
#define MAX_RT_PRIO             MAX_USER_RT_PRIO

nice值在内核中的范围是 MAX_RT_PRIO ~ MAX_RT_PRIO+40 即 MAX_RT_PRIO~MAX_PRIO

#define MAX_PRIO                (MAX_RT_PRIO + 40)

2.2.1 例子 ps 看优先级

[root@localhost home]# ps -eo state,uid,pid,ppid,rtprio,ni,time,comm
S   UID    PID   PPID RTPRIO  NI     TIME COMMAND
S     0      1      0      -   0 00:01:11 systemd
S     0      2      0      -   0 00:00:00 kthreadd
S     0      3      2      -   0 00:00:05 ksoftirqd/0
S     0      5      2      - -20 00:00:00 kworker/0:0H
S     0      6      2      -   0 00:00:12 kworker/u256:0
S     0      7      2     99   - 00:00:00 migration/0
S     0      8      2      -   0 00:00:00 rcu_bh
R     0      9      2      -   0 00:00:15 rcu_sched
S     0     10      2      - -20 00:00:00 lru-add-drain
S     0     11      2     99   - 00:00:04 watchdog/0
…… ……

RTPRIO是实时优先级,NI是Nice值

3. 时间片

时间片是一个数值,表示一个进程被抢占前能持续运行的时间。

3.1 时间片背景:

有了优先级,可以决定谁先运行了。但是对于调度程序来说,并不是运行一次就结束了,还必须知道间隔多久进行下次调度。
于是就有了时间片的概念。

3.2 默认时间片(10ms)

默认的时间片一般是10ms;
因为设大了,系统响应变慢(调度周期长);
设小了,会明显增大进程频繁切换带来的处理器消耗;

备注:
进程不一定要一次用完时间片,可分多次使用,尽可能长时间保证可运行

3.3 调度实现简单过程(基于优先级和时间片)

1.确定每个进程能占用多少CPU时间
	(这里确定CPU时间的算法有很多,根据不同的需求会不一样)
	
2.占用CPU时间多的先运行

3.运行完后,扣除运行进程的CPU时间,再回到 1)

4. 完全公平调度算法 CFS

Linux上的调度算法是不断发展的,在2.6.23内核以后,采用了“完全公平调度算法”,简称CFS

4.1 通俗说:

CFS算法在分配每个进程的CPU时间时,不是分配给它们一个绝对的CPU时间,而是根据进程的优先级分配给它们一个占用CPU时间的百分比。

4.2 定义:

Linux自2.6.23内核版本开始使用称为完全公平调度算法的进程调度算法.

该算法不直接分配时间片到进程,而是将处理器的使用比划分给进程,因此进程所获得的处理器时间和系统的负载密切相关。

4.3 CFS nice 值变化:

nice值(-20到+19)不再像标准Unix系统那样用来表示优先级,而是作为权重来影响进程的处理器使用比例,具有高nice值的进程将被赋予低权重,从而丧失一小部分处理器使用比。

4.4 进程抢占时机:

Linux系统是抢占式的,其抢占时机取决于新的可运行程序消耗了多少处理器使用比。
如果消耗的使用比比当前进程小,则新进程立刻投入运行,抢占当前进程。否则将推迟其运行。

4.5 时间记账:

CFS不再有时间片的概念,但是也必须维护每个进程运行时间的记账,以此确保每个进程只在公平分配给它的处理器时间内运行。

4.5.1 struct sched_entity 与 task_struct

CFS使用调度器实体结构struct sched_entity来跟踪进程运行并记账。

sched_entity在task_struct中以一个名为se的成员变量被嵌入
struct task_struct {
	……
	struct sched_entity		se;
	……
}

4.5.2 虚拟实时 vruntime

在 sched_entity 中有一个名为vruntime的变量用来存放进程的虚拟运行时间,该运行时间称为虚拟实时。
其值的计算是经过了所有可运行进程总数的标准化,即被加权的,单位为ns,因此和定时器的节拍不相关。

CFS使用vruntime变量来记录一个程序到底运行了多长时间以及它还应该再运行多久。

4.6 CFS调度算法的核心:进程选择(选择最小vruntime进程)

CFS试图利用一个简单规则去均衡进程的虚拟运行时间,当要选择下一个运行进程时,它会挑一个具有最小vruntime的进程,这也是CFS调度算法的核心。

CFS使用红黑树(rbtree)来组织可运行的进程队列,并利用其找到最小vruntime值的进程。

4.7 CFS 简单例子说明

比如ProcessA(NI=1),ProcessB(NI=3),ProcessC(NI=6),在CFS算法中,分别占用CPU的百分比为:ProcessA(10%),ProcessB(30%),ProcessC(60%)
因为总共是100%,ProcessB的优先级是ProcessA的3倍,ProcessC的优先级是ProcessA的6倍。

Linux上的CFS算法主要有以下步骤:(还是以ProcessA(10%),ProcessB(30%),ProcessC(60%)为例)

1)计算每个进程的vruntime(注1),通过update_curr()函数更新进程的vruntime。

2)选择具有最小vruntime的进程投入运行。(注2)

3)进程运行完后,更新进程的vruntime,转入步骤2) (注3)

5. 睡眠和唤醒

睡眠即被阻塞,此时进程处于一个特殊的不可执行的状态,进程把自己标志成睡眠状态,从可执行红黑树中移除,放入等待队列,然后调用schedule()选择和执行一个其他进程。

唤醒的过程则相反,进程被设置为可执行状态,然后再从等待队列中移到可执行二叉树中。

6. 上下文切换

6.1 上下文切换定义

上下文切换,即从一个可执行进程切换到另一个可执行进程。
由 context_switch()函数负责处理。

6.2 上下文切换过程

每当一个新的进程被选出来准备投入运行的时候, schedule()就会调用该函数,它完成两项基本的工作

6.2.1 切换虚拟内存

调用 switch_mm(),把虚拟内存从上一个进程映射切换到新进程中

6.2.2 切换处理器

调用 switch_to(),从上一个进程的处理器状态切换到新进程的处理器状态。
包括保存、恢复栈信息和寄存器信息,以及其他任何与体系结构相关的状态信息。

6.3 抢占时机

每个进程都包含一个 need_resched 标志,用来表明是否需要重新执行一次调度。

当某个进程应该被抢占时, scheduler_tick()会设置这个标志;
当一个优先级高的进程进入可执行状态时, try_to_wake_up()也会设置这个标志。
内核会检查该标志(如返回用户空间以及从中断返回的时候),确认其被设置,
然会调用 schedule()来切换到一个新的进程。

6.4 用户抢占

6.4.1 用户抢占定义:

在从系统调用或者中断处理程序返回用户空间时,如果 need_resched 标志被设置,会导致 schedule()被调用,内核会选择一个其他(更合适的)进程投入运行,即发生用户抢占。

6.4.2 用户抢占时机:

即用户抢占发生在: 从系统调用返回用户空间时; 从中断处理程序返回用户空间时

6.5 内核抢占

6.5.1 内核抢占前提–是调度安全的(进程没有锁)

Linux支持内核抢占,前提是重新调度是安全的。只要进程没有持有锁,就是安全的。

具体实现就是

在每个进程的 thread_info 中引入 preempt_count 计数器,初试为0。
每当使用锁的时候+1,释放锁的时候-1,当数值为0时,内核就可以抢占。

6.5.2 内核抢占发生时机

从中断返回内核空间时,内核会检查need_resched和preempt_count的值,以决定是调用调度程序(内核抢占)还是返回执行进程。如果内核中的进程被阻塞了,或者它显式地调用了schedule(),内核抢占也会显式地发生。

即内核抢占会发生在:

中断处理程序正在执行,且返回内核空间之前; 
内核代码再一次具有可抢占性的时候; 
内核中的任务显式调用schedule(); 
内核中的任务阻塞(这同样也会导致调用schedule());

Linux:上下文,进程上下文和中断上下文概念,上下文切换
https://blog.csdn.net/lqy971966/article/details/119103989

7. 实时调度策略

7.1 两种实时调度策略:SCHED_FIFO、SCHED_RR

Linux提供了两种实时调度策略:SCHED_FIFO、SCHED_RR
而普通的、非实时的调度策略是SCHED_NORMAL

7.2 实时调度器

实时调度策略不使用CFS来管理,而是被一个特殊的实时调度器管理。

SCHED_FIFO实现了一种简单的先入先出的调度算法,不使用时间片,处于可运行态的SCHED_FIFO进程会比任何SCHED_NORMAL级的进程都先得到调度,一旦一个SCHED_FIFO级进程处于可执行状态,就会一直执行,直到它自己受阻或者显式地释放处理器。 SCHED_RR与SCHED_FIFO大体相同,区别在于SCHED_RR带有时间片,在执行完预先分配的时间后就不能再继续执行了。

8. 调度相关的系统调用

1.与调度策略和进程优先级相关 (就是上面的提到的各种参数,优先级,时间片等等) - 下表中的前8个
2.与处理器相关 - 下表中的最后3个

系统调用				 描述
nice()					 设置进程的nice值
sched_setscheduler()	 设置进程的调度策略,即设置进程采取何种调度算法
sched_getscheduler()	 获取进程的调度算法
sched_setparam()		 设置进程的实时优先级
sched_getparam()		 获取进程的实时优先级
sched_get_priority_max() 获取实时优先级的最大值,
						 由于用户权限的问题,非root用户并不能设置实时优先级为99
sched_get_priority_min() 获取实时优先级的最小值,理由与上面类似
sched_rr_get_interval()	 获取进程的时间片
sched_getaffinity()		 获取进程的处理亲和力
sched_yield()			 暂时让出处理器
sched_setaffinity()		 设置进程的处理亲和力,
						 其实就是保存在task_struct中的cpu_allowed这个掩码标志。
						 该掩码的每一位对应一个系统中可用的处理器,
						 默认所有位都被设置,即该进程可以再系统中所有处理器上执行。
						 用户可以通过此函数设置不同的掩码,
						 使得进程只能在系统中某一个或某几个处理器上运行

猜你喜欢

转载自blog.csdn.net/lqy971966/article/details/119539577