Linux 学习笔记——第二章 进程管理和调度(6)

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;
	// ...
}

通过使用父进程的普通优先级作为子进程的动态优先级,内核确保父进程优先级的临时提高不会被子进程继承。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值