linux内核时间片调度实现详解(基于ARM处理器)

(本文基于linux-4.5.3对内核公平调度之时间片相关函数主要流程做了详细介绍,作者水平有限,难免有理解不到位、甚至不对之处,一切以实际为准,其他细节请参考其他书籍或源码。)

1、内核相关函数调用栈

1.1、时间片计数

(时间片计数主要用于对当前运行进程的时间进行统计;公平调度算法使进程都能够公平得到cpu运行时间,调度是总是选择运行时间最短的就绪进程来运行。以下栈是从定时器中断到当前进程运行时间更新)

#0  update_curr (cfs_rq=0x87ec1db8) at kernel/sched/fair.c:702
#1  0x8004e10c in entity_tick (queued=<optimized out>, curr=<optimized out>, cfs_rq=<optimized out>) at kernel/sched/fair.c:3395
#2  task_tick_fair (rq=<optimized out>, curr=0x87840000, queued=<optimized out>) at kernel/sched/fair.c:8026
#3  0x8004630c in scheduler_tick () at kernel/sched/core.c:2972
#4  0x8006f9a0 in update_process_times (user_tick=0) at kernel/time/timer.c:1425
#5  0x8007c334 in tick_periodic (cpu=<optimized out>) at kernel/time/tick-common.c:92
#6  0x8007c4f0 in tick_handle_periodic (dev=0x87ec4880) at kernel/time/tick-common.c:104
#7  0x80015e28 in twd_handler (irq=<optimized out>, dev_id=<optimized out>) at arch/arm/kernel/smp_twd.c:238
#8  0x80064890 in handle_percpu_devid_irq (desc=0x87805c00) at kernel/irq/chip.c:726
#9  0x80060690 in generic_handle_irq_desc (desc=<optimized out>) at include/linux/irqdesc.h:146
#10 generic_handle_irq (irq=<optimized out>) at kernel/irq/irqdesc.c:363
#11 0x80060940 in __handle_domain_irq (domain=0x87802400, hwirq=16, lookup=<optimized out>, regs=<optimized out>) at kernel/irq/irqdesc.c:400
#12 0x80009458 in handle_domain_irq (regs=<optimized out>, hwirq=<optimized out>, domain=<optimized out>) at include/linux/irqdesc.h:164
#13 gic_handle_irq (regs=0x87ec1db8) at drivers/irqchip/irq-gic.c:339
#14 0x80013b14 in __irq_svc () at arch/arm/kernel/entry-armv.S:213

1.2、设置重新调度标志

(当前进程时间片更新后会判断是否有运行时间更短的进程需要调度,以及当前进程时间片是否达到最大值等。以下栈是从定时器中断到设置重新调度标识,中断退出的时候会判断进程重新调度标识,有设置的话,会进行进程切换。)

#0  resched_curr (rq=0x87ec1d80) at kernel/sched/core.c:576
#1  0x8004e9c8 in check_preempt_tick (curr=<optimized out>, cfs_rq=<optimized out>) at kernel/sched/fair.c:3245
#2  entity_tick (queued=<optimized out>, curr=<optimized out>, cfs_rq=<optimized out>) at kernel/sched/fair.c:3421
#3  task_tick_fair (rq=<optimized out>, curr=0x87840000, queued=<optimized out>) at kernel/sched/fair.c:8026
#4  0x8004630c in scheduler_tick () at kernel/sched/core.c:2972
#5  0x8006f9a0 in update_process_times (user_tick=0) at kernel/time/timer.c:1425
#6  0x8007c334 in tick_periodic (cpu=<optimized out>) at kernel/time/tick-common.c:92
#7  0x8007c4f0 in tick_handle_periodic (dev=0x87ec4880) at kernel/time/tick-common.c:104
#8  0x80015e28 in twd_handler (irq=<optimized out>, dev_id=<optimized out>) at arch/arm/kernel/smp_twd.c:238
#9  0x80064890 in handle_percpu_devid_irq (desc=0x87805c00) at kernel/irq/chip.c:726
#10 0x80060690 in generic_handle_irq_desc (desc=<optimized out>) at include/linux/irqdesc.h:146
#11 generic_handle_irq (irq=<optimized out>) at kernel/irq/irqdesc.c:363
#12 0x80060940 in __handle_domain_irq (domain=0x87802400, hwirq=16, lookup=<optimized out>, regs=<optimized out>) at kernel/irq/irqdesc.c:400
#13 0x80009458 in handle_domain_irq (regs=<optimized out>, hwirq=<optimized out>, domain=<optimized out>) at include/linux/irqdesc.h:164
#14 gic_handle_irq (regs=0x87ec1d80) at drivers/irqchip/irq-gic.c:339
#15 0x80013b14 in __irq_svc () at arch/arm/kernel/entry-armv.S:213

1.3、进程重新调度

(用户进程被定时器中断,调用__irq_usr函数,__irq_usr调用定时器中断处理函数更新当前进程的时间片,检查当前进程是否需要被抢占,设置抢占标识,中断退出;退出中断之前,先判断_TIF_WORK_MASK标识是否被设置,该标识包括重新调度标识,有设置则调用do_work_pending函数,该函数检查重新调度标识,然后调用schedule进行进程切换。)

#0  schedule () at kernel/sched/core.c:3306
#1  0x80012bf8 in do_work_pending (regs=0x873d9fb0, thread_flags=<optimized out>, syscall=0) at arch/arm/kernel/signal.c:576
#2  0x8000f614 in slow_work_pending () at arch/arm/kernel/entry-common.S:78
#3  0x8000f630 in ret_to_user_from_irq () at arch/arm/kernel/entry-common.S:98
#4  0x80013e1c in __irq_usr () at arch/arm/kernel/entry-armv.S:98

2、时间片相关函数

(以下介绍是以64位无符号数为例的,最低位即个位索引为0,最高位索引为63;下标b代表二进制;有符号数最高位代表符号位,1为负数,0为正数。)

2.1、无符号数溢出

就如tcp报文的序号一样,序号不断递增,但是呢序号位数是固定的,随着数据的不断增加,序号会溢出,从零开始,这个时候我们需要判断是0大呢,还是0xffffffff大,单从数字看0xffffffff肯定比0大,时间情况应该是0更大,否则对报文解析的时候,顺序就会出错。

linux内核对进程运行时间也是不断累计的,总会出现溢出情况,对运行时间比较是基于公平调度前提下的,即所有进程运行时间相差不大,假设进程1的运行时间为t1、进程2的运行时间为t2,运行时间相差不大的意思是|t1 - t2|足够小(小到什么程度后面会介绍)。

计算机中,对于n位无符号数运算有如下公式成立(前边是表达式,结果是以n位表示的,代码中只关注0~(n-1)位;后边是成立条件):

(t1<t2时,需要向第n位借位,相当于t1 + 2^n - t2)

2.1.1 都未溢出的情况

当t1 > t2时:

公平调度会使得t1、t2之间的差值很小,使得t1-t2远远小于2^(n-1),举个十进制的例子来说,距离大概是1万和9千9百多的差距吧,使得他们之间的距离用万位、千位、百位都为0,距离只有几十;

t1-t2有如下表达式成立

当我们把结果当有符号数看待时,最高位为0,结果为正数,即可得到t1>t2,这与实际情况是一致的。

t2-t1有如下表达式成立

把结果当作有符号数看待,最高位为1,结果为负数,同样可以得到t2<t1

2.1.2、部分溢出的情况

假设t1是已经溢出,t2未溢出(都未溢出的情况2.1.1已经说明,都溢出的情况,计算比较方法与2.1.1是完全一样的),数值上有t1<t2,因为t1已经溢出了,就比如最大值为1000,然后999加上10之后溢出了,就变为9了,9的数值自然比999小,但是9所代表的时间却比999大,下面是我们要证明的。

t1与t2之间的时间差应该是

也是说t2需要经过2^n-t2时间才能溢出,溢出后需要在经过t1的时间,才能赶上t1,不是很明白的可以画图演示,此次就不提供图形描述了。另外,t1与t2间的时间间隔应该是很小的,否则就不公平了。因此有如下公式成立

t1-t2有如下公式成立

把最高位当符号位看待,结果为正数,则有t1>t2(虽然数值上t2>t1,但是计算结果确是t1>t2,这也正是我们所需要的)。

t2-t2有如下公式成立

把最高位当作符号位,结果为负数,则有t2<t1(t2数值比t1大,但是计算结果却是t2<t1,与运行时间递增是一致的)。

2.1.3、全部溢出的情况

全部溢出的情况与2.1.1计算方法一致,做减法运算时,相当于都先加2^n-1再做减法,例如(1+1000)-(2+1000)与1-2计算是一样的。

3、时间片调度相关函数解释

3.1、update_curr

前面函数调用栈已经有介绍了,怎么触发更新当前进程的时间片,根据函数调用栈就可以分析出来,此次不做介绍,相关结构体变量参考源码及其他书籍。

/*
 * Update the current task's runtime statistics.
 */
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr; // 获取当前进程的sched_entity结构体指针
u64 now = rq_clock_task(rq_of(cfs_rq)); // 获取当前时间(cpu时间?)
u64 delta_exec;


if (unlikely(!curr))
return;


delta_exec = now - curr->exec_start; // 当前时间-上一次计算时的开始时间,获取当前进程已经在cpu上连续运行了多少时间
if (unlikely((s64)delta_exec <= 0)) // 什么时候会为负数?在此先不考虑,比较方法还是通过无符号运算,然后比较最高位,即将结果当作有符号数看待
return;

curr->exec_start = now; // 重置计算开始时间

schedstat_set(curr->statistics.exec_max,
     max(delta_exec, curr->statistics.exec_max));

curr->sum_exec_runtime += delta_exec; // 总的运行时间加上本次运行时间间隔
schedstat_add(cfs_rq, exec_clock, delta_exec);

curr->vruntime += calc_delta_fair(delta_exec, curr); // 虚拟运行时间加上本次运行时间(根据权重计算的,非物理时间,不同权重计算结果不一样,同样的物理时间,vruntime增加得越慢,表示该进程能运行的时间得到越多)
update_min_vruntime(cfs_rq); // 更新min_vruntime,函数里面也是采用了第2节中的无符号数比较方法,更新时比较当前进程虚拟运行时间、rb树最左端叶子节点、上次记录的最小虚拟运行时间,取所有进程中的最小虚拟运行时间与上次记录的最小虚拟运行时间中的较大者为新的最小虚拟运行时间;所有进程中的最新虚拟运行时间,只需要比较当前进程虚拟运行时间与rb最左端叶子节点即可;如果上次记录的最小虚拟运行时间小于所有进程的虚拟运行时间,取两者中较大的,是合情合理的;如果上次记录的最小虚拟运行时间大于所有进程的最小虚拟运行时间,保持最小虚拟运行时间不变,这种情况是存在的,因为并不是所有进程都一直是就绪状态,可能被临时挂起,或者新创建进程,他们的运行时间并不是跟随就绪进程变化的,阻塞状态变就绪状态时,该进程记录的虚拟运行时间可能与其他进程运行时间相差很远,有可能误认为运行了很长时间,有可能被插入到最末尾,最后才得到调度,为了尽快公平得到调度,会根据当前记录的最小虚拟运行时间做调整,使之不会偏离最小运行时间太大,而最小虚拟运行时间通常介于所有进程虚拟运行之间,调整之后,就使得新的进程可能插入到就绪进程中间某个位置,而不是最末尾,这样就有可能得到更快的调度;具体代码可以搜索min_vruntime,看看新进程创建已经进程唤醒时的运行时间调整。

if (entity_is_task(curr)) {
struct task_struct *curtask = task_of(curr);

trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
cpuacct_charge(curtask, delta_exec);
account_group_exec_runtime(curtask, delta_exec);
}

account_cfs_rq_runtime(cfs_rq, delta_exec);
}

3.2、check_preempt_tick

(检查当前进程是否需要被抢占;当前进程运行时间是否够长,当前进程是否可以被下一个进程抢占...)
/*
 * Preempt the current task with a newly woken task if needed:
 */
static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
unsigned long ideal_runtime, delta_exec;
struct sched_entity *se;
s64 delta;

ideal_runtime = sched_slice(cfs_rq, curr); // 计算当前进程的理想运行时间
delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime; // 计算当前调度已经运行的时间(在进程被调度时会记录prev_sum_exec_runtime)
if (delta_exec > ideal_runtime) { // 当前进程的运行时间已经超过了理想的运行时间,强制设置重新调度标志
resched_curr(rq_of(cfs_rq)); // 设置重新调度标志
/*
* 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 (delta_exec < sysctl_sched_min_granularity) // 这个应该是为了保证进程调度时间不会太短,太短频繁切换会降低cpu利用率,因为切换会引起其他一些开销,例如内存切换、缓存失效以及进程切换本身需要消耗cpu等等。
return;

se = __pick_first_entity(cfs_rq); // 取下一个调度的进程(rb左节点),该进程的虚拟运行时间最短,下一个进程只可能是该节点
delta = curr->vruntime - se->vruntime; // 表达式右边是无符号数运算,左边是有符号数,比较大小方法在第2节讲过,本次实际有两个作用,一是比较大小,二是计算差值

if (delta < 0) // 符号位为1,表示当前进程虚拟运行时间小于下一个可调度进程虚拟运行时间
return;

if (delta > ideal_runtime) // 两个进程之间虚拟运行时间大于理想运行时间,重新调度当前进程;delta>0时,delta表示的是当前进程与下一进程虚拟运行时间之差
resched_curr(rq_of(cfs_rq)); // 设置重新调度标志
}

3.3、ret_to_user_from_irq

上面的假设是在用户态执行时发生的中断,设置进程被抢占、重新调度是在中断里面处理的,在返回到用户态时,会检查当前进程的抢占标志是否被设置(如何获取当前进程thread_info在之前文章中有介绍),以确定是否需要重新调度;至于在内核态发生中断,原理类似。
 
__irq_usr:
usr_entry // 中断相关上下文保存
kuser_cmpxchg_check
irq_handler // 中断处理(在这里逐级调用到定时器函数,更新当前进程的虚拟运行时间,设置重新调度标志...)
get_thread_info tsk // 获取当前进程thread_info(里面有进程重新调度标志等)
mov why, #0
b ret_to_user_from_irq // 跳转到返回用户态的函数


ENTRY(ret_to_user)
ret_slow_syscall:
disable_irq_notrace @ disable interrupts
ENTRY(ret_to_user_from_irq)
ldr r1, [tsk, #TI_FLAGS]
tst r1, #_TIF_WORK_MASK // 检查任务标志(包含进程重新调度标志在内)
bne slow_work_pending // 有设置,则跳转到挂起任务处理函数(该函数包括进程重新调度处理、软中断处理等)
no_work_pending:
asm_trace_hardirqs_on save = 0


/* perform architecture specific actions before user return */
arch_ret_to_user r1, lr
ct_user_enter save = 0


restore_user_regs fast = 0, offset = 0 // 不需要重新调度,则恢复用户寄存器,返回到用户态等等
ENDPROC(ret_to_user_from_irq)
ENDPROC(ret_to_user)


asmlinkage int
do_work_pending(struct pt_regs *regs, unsigned int thread_flags, int syscall)
{
	/*
	* The assembly code enters us with IRQs off, but it hasn't
	* informed the tracing code of that for efficiency reasons.
	* Update the trace code with the current status.
	*/
	trace_hardirqs_off();
	do {
		if (likely(thread_flags & _TIF_NEED_RESCHED)) {
			schedule(); // 设置了重新调度标志,进行进程调度,大概记录了新进程的开始运行时间等等,然后进行进程上下文切换...... 该函数在后续文章中介绍......
		} else {
			if (unlikely(!user_mode(regs)))
				return 0;
			local_irq_enable();
			if (thread_flags & _TIF_SIGPENDING) {
				int restart = do_signal(regs, syscall); // 处理挂起的信号
				if (unlikely(restart)) {
					/*
					* Restart without handlers.
					* Deal with it without leaving
					* the kernel space.
					*/
					return restart;
				}
				syscall = 0;
			} else if (thread_flags & _TIF_UPROBE) {
				uprobe_notify_resume(regs);
			} else {
				clear_thread_flag(TIF_NOTIFY_RESUME);
				tracehook_notify_resume(regs);
			}
		}
		local_irq_disable();
		thread_flags = current_thread_info()->flags;
	} while (thread_flags & _TIF_WORK_MASK);
	return 0;
}

  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值