Linux 学习笔记——第二章 进程管理和调度(6)
《深入 Linux 内核架构》阅读笔记。书籍参考的内核版本较老,文章参考的 Linux 内核版本为 5.4.103,并根据新版内核调整了一些代码片段
处理优先级
优先级的内核表示
在用户空间可以通过 nice 命令设置进程的静态优先级,这在内部会调用 nice 系统调用。进程的 nice 值在 -20 和 +19 之间(包含)。值越低,表明优先级越高。内核使用一个简单些的数值范围,从 0 到 139(包含),用来表示内部优先级。同样是值越低,优先级越高。从 0 到 99 的范围专供实时进程使用。nice 值 [-20, +19] 映射到范围 [100, 139],如图所示。实时进程的优先级总是比普通进程更高。DL 调度器负责的进程优先级为负值,即拥有最高的优先级。
下列宏用于在各种不同表示形式之间转换:
// include/linux/sched/prio.h
#define MAX_NICE 19
#define MIN_NICE -20
#define NICE_WIDTH (MAX_NICE - MIN_NICE + 1)
#define MAX_USER_RT_PRIO 100
#define MAX_RT_PRIO MAX_USER_RT_PRIO
#define MAX_PRIO (MAX_RT_PRIO + NICE_WIDTH)
#define DEFAULT_PRIO (MAX_RT_PRIO + NICE_WIDTH / 2)
#define NICE_TO_PRIO(nice) ((nice) + DEFAULT_PRIO)
#define PRIO_TO_NICE(prio) ((prio) - DEFAULT_PRIO)
// include/linux/sched/deadline.h
#define MAX_DL_PRIO 0
计算优先级
只考虑进程的静态优先级(task_struct->static_prio)是不够的,还必须考虑动态优先级(task_struct->prio)和普通优先级(task_struct->normal_prio)。
static_prio 是计算的起点。假定它已经设置好,而内核现在想要计算其他优先级。一行代码即可:
p->prio = effective_prio(p);
// kernel/sched/core.c
static int effective_prio(struct task_struct *p)
{
p->normal_prio = normal_prio(p);
/*
* 如果是实时进程或已经提高到实时优先级,则保持优先级不变。否则,返回普通优先级
*/
if (!rt_prio(p->prio))
return p->normal_prio;
return p->prio;
}
这里首先计算了普通优先级,并保存在 normal_priority。辅助函数 rt_prio,会检测普通优先级是否在实时范围中,即是否小于 MAX_RT_PRIO。
// kernel/sched/core.c
static inline int normal_prio(struct task_struct *p)
{
int prio;
if (task_has_dl_policy(p)) // 由 DL 调度器负责的实时进程
prio = MAX_DL_PRIO-1;
else if (task_has_rt_policy(p)) // 由 RT 调度器负责的实时进程
prio = MAX_RT_PRIO-1 - p->rt_priority;
else // 普通进程
prio = __normal_prio(p);
return prio;
}
static inline int __normal_prio(struct task_struct *p)
{
return p->static_prio;
}
普通优先级需要根据普通进程和实时进程进行不同的计算。由于更高的 rt_priority 值表示更高的实时优先级,内核内部优先级的表示刚好相反,越低的值表示的优先级越高。因此,对于实时进程在内核内部的优先级数值,正确的算法是用减法。与 effective_prio
相比,实时进程的检测不再基于优先级数值,而是通过 task_struct 中设置的调度策略来检测。
内核在 effective_prio
中检测实时进程是基于优先级数值。对于临时提高至实时优先级的非实时进程来说,这是必要的,这种情况可能发生在使用实时互斥量(RT-Mutex)时。
在进程分支出子进程时,子进程的静态优先级继承自父进程。子进程的动态优先级,即 task_struct->prio,则设置为父进程的普通优先级。这确保了实时互斥量引起的优先级提高不会传递到子进程。
计算负荷权重
进程的重要性不仅是由优先级指定的,而且还需要考虑保存在 task_struct->se.load 的负荷权重。set_load_weight 负责根据进程类型及其静态优先级计算负荷权重。负荷权重包含在数据结构 load_weight 中:
// include/linux/sched.h
struct load_weight {
unsigned long weight;
u32 inv_weight;
};
内核不仅维护了负荷权重自身,而且还有另一个数值用于计算被负荷权重除的结果(2^32/x)。
进程每降低一个 nice 值,则多获得 10% 的 CPU 时间,每升高一个 nice 值,则放弃 10% 的 CPU 时间。为执行该策略,内核将优先级转换为权重值。看一下转换表,各数组之间的乘数因子是 1.25:
// kernel/sched/core.c
const int sched_prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};
实时进程的权重是普通进程的两倍,SCHED_IDLE 进程的权重总是非常小。
// kernel/sched/sched.h
#define WEIGHT_IDLEPRIO 3
#define WMULT_IDLEPRIO 1431655765
// kernel/sched/core.c
static void set_load_weight(struct task_struct *p, bool update_load)
{
int prio = p->static_prio - MAX_RT_PRIO;
struct load_weight *load = &p->se.load;
// SCHED_IDLE tasks get minimal weight:
if (task_has_idle_policy(p)) {
load->weight = scale_load(WEIGHT_IDLEPRIO);
load->inv_weight = WMULT_IDLEPRIO;
p->se.runnable_weight = load->weight;
return;
}
// SCHED_OTHER tasks have to update their load when changing theirweight
if (update_load && p->sched_class == &fair_sched_class) {
reweight_task(p, prio);
} else {
load->weight = scale_load(sched_prio_to_weight[prio]);
load->inv_weight = sched_prio_to_wmult[prio];
p->se.runnable_weight = load->weight;
}
}
核心调度器
如前所述,调度器的实现基于两个函数:周期性调度器函数和主调度器函数。
周期性调度器
周期性调度器在 scheduler_tick 中实现。如果系统正在活动中,内核会按照频率 HZ 自动调用该函数。该函数有下面两个主要任务。
- 管理内核中与整个系统和各个进程的调度相关的统计量。
- 激活负责当前进程的调度类的周期性调度方法。
// kernel/sched/core.c
void scheduler_tick(void)
{
int cpu = smp_processor_id();
struct rq *rq = cpu_rq(cpu);
struct task_struct *curr = rq->curr;
struct rq_flags rf;
sched_clock_tick();
rq_lock(rq, &rf);
update_rq_clock(rq);
curr->sched_class->task_tick(rq, curr, 0);
calc_global_load_tick(rq);
psi_task_tick(rq);
rq_unlock(rq, &rf);
perf_event_task_tick();
}
该函数的第一部分处理就绪队列时钟的更新。该职责委托给 update_rq_clock
完成,本质上就是增加 struct rq 当前实例的时钟时间戳。由于调度器的模块化结构,主要的工作可以委托给特定调度器类的方法 task_tick
,它的实现方式取决于底层的调度器类。
主调度器
在内核中的许多地方,如果要将 CPU 分配给与当前活动进程不同的另一个进程,都会直接调用主调度器函数(schedule)。在从系统调用返回之后,内核也会检查当前进程是否设置了重调度标志 TIF_NEED_RESCHED。如果是这样,则内核会调用 schedule 。该函数假定当前活动进程一定会被另一个进程取代。
// kernel/sched/core.c
asmlinkage __visible void __sched schedule(void)
{
struct task_struct *tsk = current;
sched_submit_work(tsk);
do {
preempt_disable();
__schedule(false);
sched_preempt_enable_no_resched();
} while (need_resched());
sched_update_worker(tsk);
}
EXPORT_SYMBOL(schedule);
static void __sched notrace __schedule(bool preempt)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq_flags rf;
struct rq *rq;
int cpu;
// 确定当前就绪队列,并在 prev 中保存一个指向(仍然)活动进程的 task_struct 的指针
cpu = smp_processor_id();
rq = cpu_rq(cpu);
prev = rq->curr;
// ...
// 更新就绪队列的时钟
rq->clock_update_flags <<= 1;
update_rq_clock(rq);
switch_count = &prev->nivcsw;
if (!preempt && prev->state) {
if (signal_pending_state(prev->state, prev)) {
prev->state = TASK_RUNNING;
} else {
// 用相应调度器类的方法使进程停止活动
deactivate_task(rq, prev, DEQUEUE_SLEEP | DEQUEUE_NOCLOCK);
if (prev->in_iowait) {
atomic_inc(&rq->nr_iowait);
delayacct_blkio_start();
}
}
switch_count = &prev->nvcsw;
}
// 选择下一个应该执行的进程
next = pick_next_task(rq, prev, &rf);
// 清除当前运行进程的重调度标志 TIF_NEED_RESCHED
clear_tsk_need_resched(prev);
clear_preempt_need_resched();
if (likely(prev != next)) {
rq->nr_switches++;
// ...
trace_sched_switch(preempt, prev, next);
// 执行上下文切换
rq = context_switch(rq, prev, next, &rf);
} else {
rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
rq_unlock_irq(rq, &rf);
}
}
与 fork 的交互
每当使用 fork 系统调用或其变体之一建立新进程时,调度器有机会用 sched_fork 函数挂钩到该进程。在单处理器系统上,该函数实质上执行 3 个操作:初始化新进程与调度相关的字段、建立数据结构、确定进程的动态优先级。
// kernel/sched/core.c
int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
// 初始化数据结构
__sched_fork(clone_flags, p);
p->state = TASK_NEW;
p->prio = current->normal_prio;
// ...
if (dl_prio(p->prio))
return -EAGAIN;
else if (rt_prio(p->prio))
p->sched_class = &rt_sched_class;
else
p->sched_class = &fair_sched_class;
// ...
}
通过使用父进程的普通优先级作为子进程的动态优先级,内核确保父进程优先级的临时提高不会被子进程继承。