[linux][调度] 内核抢占入门 —— 高优先级线程被唤醒时会立即抢占当前线程吗 ?

在支持抢占的内核中,如果高优先级的线程被唤醒的时候,这个时候 cpu 被其它线程占用着,并且正在运行的这个线程的优先级比刚被唤醒的这个线程优先级低。

这个时候,刚唤醒的线程,能直接抢占正在运行的线程吗 ?

不能

在内核抢占中,有两种类型的点,一个是检查点,一个是抢占点。在检查点的地方会做检查,如果需要抢占,那么会设置一个需要抢占的标志,但是在检查点的时候不做真正的抢占;真正的抢占是在抢占点,抢占点会判断检查点中设置的标志,如果需要抢占并且允许抢占的话,那么就会进行抢占调度。

1 两个标志

两个标志分别是抢占计数和重新调度标志,前者表示能不能抢占,后者表示是不是需要抢占。能不能抢占指的是当前正在运行的线程能不能被抢占;需不需要抢占说的是在运行队列中等待的队列是不是优先级更高,如果有优先级更高的线程在等待,那么说明需要抢占。只有两个标志都满足的情况下,也就是说需要抢占并且允许抢占,才会做抢占调度。

1.1 抢占计数 preempt count

抢占计数用来表示当前运行的任务能不能被抢占。抢占计数保存在 struct thread_info 里,linux 中的,一个线程除了 struct task_struct 这样一个进程控制块来维护之外,每个线程都还有一个 struct thread_info。struct thread_info 的定义和 cpu 架构有关系,并不是内核统一的,如下是在 linux 源码中搜索 struct thread_info 结构体的定义,可以看出来,每种 cpu 架构都有自己的定义。

在 struct thread_info 中有一个成员是 preempt_count,对 preempt_count  进行操作的函数是是 __preempt_count_add() 和 __preempt_count_sub()。preempt_count 大于 0,禁止抢占;等于 0 的时候,允许抢占。

include/asm-generic/preempt.h

static __always_inline void __preempt_count_add(int val)
{
	*preempt_count_ptr() += val;
}

static __always_inline void __preempt_count_sub(int val)
{
	*preempt_count_ptr() -= val;
}

preempt count 分成了 4 段来使用。

bit0 ~ bit7: preempt,表示抢占计数

bit8 ~ bit15: 表示软中断计数

bit16 ~ bit23: 表示硬中断计数

bit24 ~ bit27: 表示有没有 nmi 中断

只要 preempt count 不是 0,那么就不能抢占。所以当前 cpu 在处理软中断、硬中断、nmi 中断的时候,也是不能抢占的。

对 preempt count 的使用,在加自旋锁的时候会关闭抢占,自旋锁解锁的时候会开抢占;其它在显式调用 preempt_disable() 的地方会关闭抢占,显式调用 preempt_enable() 的时候会开抢占。

内核中定义了几个宏,可以判断当前 cpu 处于什么状态。

in_irq(): cpu 正在处理硬中断

in_softirq(): cpu 正在处理软中断

in_interrupt(): 正在处理硬中断,或者软中断,或者 nmi 中断

in_nmi(): cpu 正在处理 nmi 中断

int_task: cpu 当前处在线程上下文

/*
 * Are we doing bottom half or hardware interrupt processing?
 *
 * in_irq()       - We're in (hard) IRQ context
 * in_softirq()   - We have BH disabled, or are processing softirqs
 * in_interrupt() - We're in NMI,IRQ,SoftIRQ context or have BH disabled
 * in_serving_softirq() - We're in softirq context
 * in_nmi()       - We're in NMI context
 * in_task()	  - We're in task context
 *
 * Note: due to the BH disabled confusion: in_softirq(),in_interrupt() really
 *       should not be used in new code.
 */
#define in_irq()		(hardirq_count())
#define in_softirq()		(softirq_count())
#define in_interrupt()		(irq_count())
#define in_serving_softirq()	(softirq_count() & SOFTIRQ_OFFSET)
#define in_nmi()		(preempt_count() & NMI_MASK)
#define in_task()		(!(preempt_count() & \
				   (NMI_MASK | HARDIRQ_MASK | SOFTIRQ_OFFSET)))

1.2 重新调度标志 need resched

重新调度标志也是保存在线程的 struct thread_info 中,保存在 flags 字段,标志为 TIF_NEED_RESCHED。如果需要重新调度,那么这个标志是设置到当前正在运行的这个线程的 struct thread_info 中的。

相关的操作函数如下,设置标志,清除标志,判断当前是不是设置了标志。

static inline void set_tsk_need_resched(struct task_struct *tsk)
{
	set_tsk_thread_flag(tsk,TIF_NEED_RESCHED);
}

static inline void clear_tsk_need_resched(struct task_struct *tsk)
{
	clear_tsk_thread_flag(tsk,TIF_NEED_RESCHED);
}

static inline int test_tsk_need_resched(struct task_struct *tsk)
{
	return unlikely(test_tsk_thread_flag(tsk,TIF_NEED_RESCHED));
}

2 检查点

2.1 线程被唤醒时

当现成被唤醒的时候,这个时候需要检查刚唤醒的这个线程是不是比正在运行的这个线程优先级高,如果是的话,则设置重新调度标志;否则不设置。

唤醒线程的函数是 wake_up_process(),我们可以跟踪这个函数。

wake_up_process()

调用

try_to_wake_up()

调用

ttwu_runnable()

调用

ttwu_do_wakeup()

调用

check_preempt_curr()

在函数 check_preempt_curr() 将刚被唤醒的线程和正在运行的线程进行对比,如果两者属于同一个调度策略,那么调用本策略内的检查函数;如果前者比后者的调度策略优先级高,比如前者是 SCHED_FIFO 的调度策略,后者是 SCHED_NORMAL 的调度策略,这种情况下是需要抢占调度的,那么就会直接设置需要抢占标志。

void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags)
{
	if (p->sched_class == rq->curr->sched_class)
		rq->curr->sched_class->check_preempt_curr(rq, p, flags);
	else if (p->sched_class > rq->curr->sched_class)
		resched_curr(rq);

	/*
	 * A queue event has occurred, and we're going to schedule.  In
	 * this case, we can save a useless back to back clock update.
	 */
	if (task_on_rq_queued(rq->curr) && test_tsk_need_resched(rq->curr))
		rq_clock_skip_update(rq);
}

rt 调度策略的 check_preempt_curr() 对应的函数是 check_preempt_curr_rt()。

(1)如果刚唤醒的线程优先级比正在运行的线程优先级高,那么直接设置抢占标志,在下一次调度的时候便会进行抢占调度

(2)如果在多核机器上,并且两者的优先级相等,并且刚唤醒的线程是不能迁移的,正在运行的线程是可以迁移的,那么会将正在运行的线程迁移到其它核上运行,在当前核上运行刚唤醒的线程

static void check_preempt_curr_rt(struct rq *rq, struct task_struct *p, int flags)
{
	if (p->prio < rq->curr->prio) {
		resched_curr(rq);
		return;
	}

#ifdef CONFIG_SMP
	/*
	 * If:
	 *
	 * - the newly woken task is of equal priority to the current task
	 * - the newly woken task is non-migratable while current is migratable
	 * - current will be preempted on the next reschedule
	 *
	 * we should check to see if current can readily move to a different
	 * cpu.  If so, we will reschedule to allow the push logic to try
	 * to move current somewhere else, making room for our non-migratable
	 * task.
	 */
	if (p->prio == rq->curr->prio && !test_tsk_need_resched(rq->curr))
		check_preempt_equal_prio(rq, p);
#endif
}

SCHED_NORMAL 调度策略的检查函数是 check_preempt_wakeup()。

2.2 tick

每种调度策略都要实现一个函数 task_tick(),这个函数是定时触发。在 task_tick 函数中也会检查,当前任务是不是需要抢占。rt 调度策略的 tick 函数是 task_tick_rt(),普通调度策略的 tick 函数是 task_tick_fair()。

task_tick_rt():

static void task_tick_rt(struct rq *rq, struct task_struct *p, int queued)
{
	struct sched_rt_entity *rt_se = &p->rt;

	update_curr_rt(rq);
	update_rt_rq_load_avg(rq_clock_pelt(rq), rq, 1);

	watchdog(rq, p);

	/*
	 * RR tasks need a special form of timeslice management.
	 * FIFO tasks have no timeslices.
	 */
    // 如果是 SCHED_FIFO,直接返回
	if (p->policy != SCHED_RR)
		return;
    
    // 如果 SCHED_RR 的时间片还没用完,直接返回
    // 时间片用完了,才会设置抢占标志
	if (--p->rt.time_slice)
		return;

	p->rt.time_slice = sched_rr_timeslice;

	/*
	 * Requeue to the end of queue if we (and all of our ancestors) are not
	 * the only element on the queue
	 */
	for_each_sched_rt_entity(rt_se) {
		if (rt_se->run_list.prev != rt_se->run_list.next) {
			requeue_task_rt(rq, p, 0);
			resched_curr(rq);
			return;
		}
	}
}

对于 SCHED_NORMAL 普通调度策略来说,检查是不是需要抢占的实现在函数 check_preempt_tick() 中。

当前线程有一个最小的运行时间,为 0.75ms,如果当前这个线程的运行时间还不足 0.75ms,那么不会设置抢占标志。当实际运行时间大于 0.75ms 的时候,才会设置抢占标志。


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;
	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)
		return;

	se = __pick_first_entity(cfs_rq);
	delta = curr->vruntime - se->vruntime;

	if (delta < 0)
		return;

	if (delta > ideal_runtime)
		resched_curr(rq_of(cfs_rq));
}

3 抢占点

由第一节可以知道,在加自旋锁时,软中断中,处理硬件中断时,这些时候都是禁止了抢占的,那么当 cpu 退出这些区域的时候便会检查当前是不是需要抢占,如果需要抢占并且允许抢占的话,便会抢占。

3.1 释放自旋锁的时候

spin_unlock() 最终会调用到函数 preemt_enable(),使能抢占。在 preempt_enable() 函数中会做判断,判断两个标志,需要抢占标志和抢占计数,如果抢占计数为 0 并且需要抢占,那么便会进行抢占调度。

static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
	spin_release(&lock->dep_map, _RET_IP_);
	do_raw_spin_unlock(lock);
	preempt_enable();
}

#define preempt_enable() \
do { \
	barrier(); \
	if (unlikely(preempt_count_dec_and_test())) \
		__preempt_schedule(); \
} while(0)

#define preempt_count_dec_and_test() \
	({ preempt_count_sub(1); should_resched(0); })

3.1 打开软中断时

在 linux 内核中,当数据会被线程和软中断并发访问时,在线程中加锁时需要关闭软中断。关闭软中断和打开软中断的函数如下。在打开软中断时,会进行判断,如果需要抢占并且允许抢占,便会进行抢占调度。

static inline void local_bh_disable(void)
{
	__local_bh_disable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}

static inline void local_bh_enable(void)
{
	__local_bh_enable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}

3.3 中断退出的时候

中断返回的时候,如果需要抢占调度,那么会调用函数 preempt_schedule_irq()。这段代码一般是使用汇编指令来实现的。如下是 arm 中的实现,下边这段代码,只有定义了 CONFIG_PREEMPTION 时,才会生效。

arch/arm/kernel/entry-armv.S

#ifdef CONFIG_PREEMPTION
svc_preempt:
	mov	r8, lr
1:	bl	preempt_schedule_irq		@ irq en/disable is done inside
	ldr	r0, [tsk, #TI_FLAGS]		@ get new tasks TI_FLAGS
	tst	r0, #_TIF_NEED_RESCHED
	reteq	r8				@ go again
	b	1b
#endif

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值