IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> Linux内核对比学习系列(3)——进程调度 -> 正文阅读

[系统运维]Linux内核对比学习系列(3)——进程调度

前言

linux0.12与linux2.6在进程调度的实现上有很大的不同,在此进行记录

Linux 0.12

该版本对于进程调度算法的实现十分简单,具体实现看schedule()即可,该函数为调度入口
kernel/sched.c

void schedule(void)
{
	int i,next,c;
	struct task_struct ** p;
    // ----------------省略检查相关代码------------------

/* this is the scheduler proper: */

	while (1) {
		c = -1;
		next = 0;
		i = NR_TASKS;
		p = &task[NR_TASKS];
		while (--i) {
			if (!*--p)
				continue;
			if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
				c = (*p)->counter, next = i;
		}
		if (c) break;
		for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
			if (*p)
				(*p)->counter = ((*p)->counter >> 1) +
						(*p)->priority;
	}
	switch_to(next);
}

由上可知晓该版本调度全过程,无非遍历 task_struct 指针数组(task),判断是否有进程的 counter(允许运行时间)大于0,若有则找出 counter 最大的,否则对所有进程根据优先级(priority)重新分配 counter ,再遍历找出 counter 最大的进行切换(switch_to)

说到调度,除了如何执行调度还需要了解何时调度。根据之前文章,我们知道时钟中断对应的中断执行程序为timer_interrupt,其触发do_timer 进行当前时间片的检查,对于该版本而言,do_timer()先判断当前进程counter是否耗尽,才会执行schedule()。这意味着,该版本的进程是不可抢占的。(倘若分配的时间片很多,则其他进程将久久得不到相应)

void do_timer(long cpl)
{
	//-----省略无关代码---------
	if (next_timer) {
		next_timer->jiffies--;
		while (next_timer && next_timer->jiffies <= 0) {
			void (*fn)(void);
			
			fn = next_timer->fn;
			next_timer->fn = NULL;
			next_timer = next_timer->next;
			(fn)();
		}
	}
	if (current_DOR & 0xf0)
		do_floppy_timer();
	if ((--current->counter)>0) return;
	current->counter=0;
	if (!cpl) return;
	schedule();
}

关于0.12版本的进程调度核心内容只有这么多,对于何种调度算法最优在此不进行讨论,只是比较两个版本的实现有何区别

Linux 2.6

在该版本中,为了实现进程调度,相较于0.12版本设计了很多新的概念。笔者进行学习的过程中,参考了 https://www.cnblogs.com/ck1020/p/6089970.html。而《Linux内核设计与实现》中对于该知识点的梳理十分跳跃,对于初学者难以把握精髓。为此,先从原理进行梳理,在进入代码与0.12版本进行对比。

如何调度

相关概念

关于进程调度的一些结构体:

  1. 就绪队列 rq
  2. 调度器 sched_class
  3. 调度实体 sched_entity

调度器结构

与0.12不同,该版本内核并不是直接操作进程的task_struct,在获取到最终需要执行的task_struct之前,还涉及就绪队列,调度器等相关逻辑。首先,如下图所示,主调度器(可以理解为schedule())会先选择调度器类,再由调度器类选择下一个要执行的进程。每个进程只会属于一个调度器类,而调度器类之间会有优先级,主调度器调用时会参考优先级从高到底选择调度器。由于每个cpu上执行的进程不同,为了在多个cpu环境下执行任务,设计就绪队列rq,每个CPU关联一个rq,该rq上记录可以被当前cpu执行的调度器类及其进程。
在这里插入图片描述
因此,schedule()主调度器要做的事情是:

  1. 获取当前就绪队列rq
  2. 找到rq上关联的调度器类
  3. 根据调度器类优先级,按顺序从调度器类中选择进程
  4. 至于每个调度器类会执行怎样的进程选择策略交由调度器类自己定义
  5. 切换任务

如此一来,内核开发人员可以自定义调度器类,使得内核的调度算法变得可扩展。这与dubbo设计loadbanlance接口是类似的。代码如下(省略了非重点代码)
kernel/sched.c

asmlinkage void __sched schedule(void)
{
	struct task_struct *prev, *next;
	unsigned long *switch_count;
	struct rq *rq;
	int cpu;

	// ---------省略无关代码--------
	rq = cpu_rq(cpu); // 1. 获取当前就绪队列rq
	pre_schedule(rq, prev);

	if (unlikely(!rq->nr_running))
		idle_balance(cpu, rq);

	put_prev_task(rq, prev);
	next = pick_next_task(rq); // 2. 找到rq上关联的调度器类。3. 根据调度器类优先级,按顺序从调度器类中选择进程

	if (likely(prev != next)) {
		sched_info_switch(prev, next);
		perf_event_task_sched_out(prev, next);

		rq->nr_switches++;
		rq->curr = next;
		++*switch_count;

		context_switch(rq, prev, next); // 4. 切换任务
		/*
		 * the context switch might have flipped the stack from under
		 * us, hence refresh the local variables.
		 */
		cpu = smp_processor_id();
		rq = cpu_rq(cpu);
	} else
		raw_spin_unlock_irq(&rq->lock);

	// ---------省略无关代码--------	
}

进一步地跟踪pick_next_task()。根据变量及函数名我们大致能够猜测该函数完成了几件事

  1. 判断就绪队列rq上就绪态的进程数nr_running是否等于调度器cfs的就绪态进程数,如果相等,说明进程都在调度器cfs中,直接执行调度器cfs的pick方法选择进程
  2. 否则,从优先级最高的调度器类sched_class_highest开始遍历,调用其pick方法,如果当前调度器找不到进程,则去后一个调度器里找
    kernel/sched.c
static inline struct task_struct *
pick_next_task(struct rq *rq)
{
	const struct sched_class *class;
	struct task_struct *p;

	/*
	 * Optimization: we know that if all tasks are in
	 * the fair class we can call that function directly:
	 */
	if (likely(rq->nr_running == rq->cfs.nr_running)) {
		p = fair_sched_class.pick_next_task(rq);
		if (likely(p))
			return p;
	}

	class = sched_class_highest;
	for ( ; ; ) {
		p = class->pick_next_task(rq);
		if (p)
			return p;
		/*
		 * Will never be NULL as the idle class always
		 * returns a non-NULL p:
		 */
		class = class->next;
	}
}

具体地,以cfs调度器类为例,看看其pick方法如何找到一个待执行的进程。根据该调度器类的定义,我们知道知道其pick_next_task方法对应的是pick_next_task_fair

kernel\sched_fair.c

static const struct sched_class fair_sched_class = {
	.next			= &idle_sched_class,
	.enqueue_task		= enqueue_task_fair,
	.dequeue_task		= dequeue_task_fair,
	.yield_task		= yield_task_fair,

	.check_preempt_curr	= check_preempt_wakeup,

	.pick_next_task		= pick_next_task_fair,
	.put_prev_task		= put_prev_task_fair,
	.task_tick		= task_tick_fair,
	.task_fork		= task_fork_fair,
};

可以看到,该方法先是通过rq获取cfs_rq,再调用该类的pick_next_entity方法从cfs_rq上获得一个sched_entity,其中sched_entity对应一个进程或者一个进程组,是一个调度单位。由此我们也知道了,实际上,所有的进程或者进程组都被封装为一个调度实体sched_entity并关联着rq。而每个调度器类只是提供了在rq上找出目标调度实体的方法而已,它并不关联和存储任何调度实体内容

kernel\sched_fair.c

static struct task_struct *pick_next_task_fair(struct rq *rq)
{
	struct task_struct *p;
	struct cfs_rq *cfs_rq = &rq->cfs;
	struct sched_entity *se;

	if (!cfs_rq->nr_running)
		return NULL;

	do {
		se = pick_next_entity(cfs_rq);
		set_next_entity(cfs_rq, se);
		cfs_rq = group_cfs_rq(se);
	} while (cfs_rq);

	p = task_of(se);
	hrtick_start_fair(rq, p);

	return p;
}

在这里,由于sched_entity是task_struct的属性,因此通过task_of(se)调用container_of可以由sched_entity找到task的位置。container_of是内核十分常见的函数了,也是C语言魅力的体现。

更进一步地,pick_next_entity()就是具体地CFS调度算法了,这部分内容可以回到《Linux内核设计与实现》中找到答案。在梳理清楚调度的整个流程后,我们进一步地去学习每个CFS调度算法才能更得心应手。这也是本篇文章的目的

何时调度

前半部分我们了解了schedule()执行一次进程调度的大致流程。那么该版本何时会触发schedule()进行调度呢?即类似0.12版本的时钟中断,timer_interrupt和do_timer的功能由哪些函数来实现?

本节内容参考了几个博文

直接说结论:

  1. 主动调度:直接显式调用schedule()进行调度
  2. 被动调度(延时调度):系统调用返回或中断返回时,会检查进程的thread_info中的flag是否标记为TIF_NEED_RESCHED,若被设置,则意味着需要被调度,执行schedule()主调度器。而TIF_NEED_RESCHED可以由用户定义程序进行设置。

该版本的调度时机判断相较于0.12版本复杂了不少,需要通过标志位flag进行判断。在0.12版本中,进程被动调度一定要在当前进程时间片耗尽才能触发,这样导致其他进程无法抢占,实时交互需求无法满足。在2.6版本中,是否需要调度,只与当前进程标志位flag有关,因此可以自定义flag的设置算法,满足具体需求,来实现合理的调度策略。

设置重调度标志

最简单地,我们希望每次时钟中断都判断一下当前进程是否满足被调度条件,若满足,则把flag设置为TIF_NEED_RESCHED。下面,我们跟踪一下具体流程,可以看到update_process_times的注释提到其与timer interrupt关联,具体地再往前跟踪就涉及时钟管理内容,因此本文打算直接从该函数开始往后跟踪

kernel\timer.c

/*
 * Called from the timer interrupt handler to charge one tick to the current
 * process.  user_tick is 1 if the tick is user time, 0 for system.
 */
void update_process_times(int user_tick)
{
	struct task_struct *p = current;
	int cpu = smp_processor_id();

	/* Note: this timer irq context must be accounted for as well. */
	account_process_tick(p, user_tick);
	run_local_timers();
	rcu_check_callbacks(cpu, user_tick);
	printk_tick();
	perf_event_do_pending();
	scheduler_tick();
	run_posix_cpu_timers(p);
}

进一步地,该函数调用了scheduler_tick()。该函数调用当前进程所在调度器的task_tick方法

kernel\sched.c

void scheduler_tick(void)
{
	int cpu = smp_processor_id();
	struct rq *rq = cpu_rq(cpu);
	struct task_struct *curr = rq->curr;

	sched_clock_tick();

	raw_spin_lock(&rq->lock);
	update_rq_clock(rq);
	update_cpu_load(rq);
	curr->sched_class->task_tick(rq, curr, 0);
	raw_spin_unlock(&rq->lock);

	perf_event_task_tick(curr);

#ifdef CONFIG_SMP
	rq->idle_at_tick = idle_cpu(cpu);
	trigger_load_balance(rq, cpu);
#endif
}

我们以CFS公平调度器类为例,其task_ticktask_tick_fair(由上述fair_sched_class定义可知)

kernel\sched_fair.c

static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
{
	struct cfs_rq *cfs_rq;
	struct sched_entity *se = &curr->se;

	for_each_sched_entity(se) {
		cfs_rq = cfs_rq_of(se);
		entity_tick(cfs_rq, se, queued);
	}
}

该函数会进一步执行 entity_tick-->check_preempt_tick。在该函数中,设置了一些与CFS有关的条件,若满足则通过resched_task设置重调度标志位

kernel\sched_fair.c

//kernel\sched.c
static void resched_task(struct task_struct *p)
{
	assert_raw_spin_locked(&task_rq(p)->lock);
	set_tsk_need_resched(p);
}

static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
	unsigned long ideal_runtime, delta_exec;

	ideal_runtime = sched_slice(cfs_rq, curr);
	delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
	if (delta_exec > ideal_runtime) {
		resched_task(rq_of(cfs_rq)->curr);
		/*
		 * The current task ran long enough, ensure it doesn't get
		 * re-elected due to buddy favours.
		 */
		clear_buddies(cfs_rq, curr);
		return;
	}

	/*
	 * Ensure that a task that missed wakeup preemption by a
	 * narrow margin doesn't have to wait for a full slice.
	 * This also mitigates buddy induced latencies under load.
	 */
	if (!sched_feat(WAKEUP_PREEMPT))
		return;

	if (delta_exec < sysctl_sched_min_granularity)
		return;

	if (cfs_rq->nr_running > 1) {
		struct sched_entity *se = __pick_next_entity(cfs_rq);
		s64 delta = curr->vruntime - se->vruntime;

		if (delta > ideal_runtime)
			resched_task(rq_of(cfs_rq)->curr);
	}
}

至此,我们大概清楚了通过时钟中断设置进程重调度标志的流程

  1. 时钟中断执行程序调用update_process_times
  2. update_process_times通过scheduler_tick调用当前进程所在调度器类的task_tick方法
  3. 在task_tick方法中,定义与调度有关的条件,判断是否满足条件来进行flag的设置

中断返回执行调度

另一方面,我们还要验证一下如中断返回时是否会根据flag标志执行schedule()
我们知道中断返回会调用ret_from_intr,其进一步调用resume_kernel

arch\x86\kernel\entry_32.S

ret_from_intr:
	GET_THREAD_INFO(%ebp)
check_userspace:
	movl PT_EFLAGS(%esp), %eax	# mix EFLAGS and CS
	movb PT_CS(%esp), %al
	andl $(X86_EFLAGS_VM | SEGMENT_RPL_MASK), %eax
	cmpl $USER_RPL, %eax
	jb resume_kernel		# not returning to v8086 or userspace

ENTRY(resume_kernel)
	DISABLE_INTERRUPTS(CLBR_ANY)
	cmpl $0,TI_preempt_count(%ebp)	# non-zero preempt_count ?
	jnz restore_all
need_resched:
	movl TI_flags(%ebp), %ecx	# need_resched set ?
	testb $_TIF_NEED_RESCHED, %cl
	jz restore_all
	testl $X86_EFLAGS_IF,PT_EFLAGS(%esp)	# interrupts off (exception path) ?
	jz restore_all
	call preempt_schedule_irq
	jmp need_resched
END(resume_kernel)

resume_kernel则调用preempt_schedule_irq,可以看到,该函数会根据need_resched()执行schedule()。至此,验证完毕

kernel\sched.c

asmlinkage void __sched preempt_schedule_irq(void)
{
	struct thread_info *ti = current_thread_info();

	/* Catch callers which need to be fixed */
	BUG_ON(ti->preempt_count || !irqs_disabled());

	do {
		add_preempt_count(PREEMPT_ACTIVE);
		local_irq_enable();
		schedule();
		local_irq_disable();
		sub_preempt_count(PREEMPT_ACTIVE);

		/*
		 * Check again in case we missed a preemption opportunity
		 * between schedule and now.
		 */
		barrier();
	} while (need_resched());
}
  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2022-04-06 16:28:46  更:2022-04-06 16:31:02 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/15 21:32:17-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码