Linux进程管理(四)进程调度之抢占式调度

Linux进程管理

Linux进程管理(一)进程数据结构

Linux进程管理(二)进程调度

Linux进程管理(三)进程调度之主动调度

Linux进程管理(四)进程调度之抢占式调度

Linux进程管理(四)进程调度之抢占式调度


上篇文章我们将了内核调度分为主动调度和抢占式调度,主动调度我们已经讲解过了,这篇文章我们来讲解一下抢占式调度

一、抢占式调度

我们说过,进程真正的调度都是通过调用 schedule 函数发生的,所谓抢占抢占式调度,就是非进程自愿调用 schedule 来发生进程调度

抢占式调度主要分为两步

  • 第一步:在current进程设置需要重新调度的标志(TIF_NEED_RESCHED)
  • 第二步:在某些特定的时机,检测是否设置了 TIF_NEED_RESCHED 标志,如果设置了,就调用 schedule 函数发生进程调度

这篇文章的主要目的就是讨论这两个动作发生的时期

二、设置需要重新调度的标志的时机(TIF_NEED_RESCHED)

设置 TIF_NEED_RESCHED 标志有两个时机

扫描二维码关注公众号,回复: 9213039 查看本文章
  • 周期性调度器处理时
  • 唤醒进程时

首先讨论周期性调度器处理

周期性调度器处理

前面我们,在硬件电路有一个滴答定时器,周期性的产生时钟中断(一般为10ms)

每当时钟中断产生的时候,就会调用 scheduler_tick 函数进行处理,我们称之为周期性调度器

scheduler_tick 的定义如下

void scheduler_tick(void)
{
    curr->sched_class->task_tick(rq, curr, 0);
}

它会调用current进程所指向的调度类中的 task_tick 函数

如果该调度类是 fair_sched_class,那么 task_tick 就对应 task_tick_fair,其定义如下

// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202

static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
{
    ...
	entity_tick(cfs_rq, se, queued);
	...
}

entity_tick 函数中会判断当前进程是否需要被抢占,如果需要,那么就会设置 TIF_NEED_RESCHED 标志,其定义如下

// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202

static void
entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
    /* 更新统计信息 */
    update_curr(cfs_rq);
    ...
        
    /* 检查是否需要发生抢占 */
    check_preempt_tick(cfs_rq, curr);
}

update_curr 会更新进程的运行时间,check_preempt_tick 会判断当前进程是否需要被抢占

check_preempt_tick 的定义如下

// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202

static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
   	...
    /* 挑选红黑树最左边的节点 */
	se = __pick_first_entity(cfs_rq);
    
    /* 计算当前进程运行时间是否比红黑树第一个节点长? */
    delta = curr->vruntime - se->vruntime;
    
    /* 如果不是,就退出 */
    if (delta < 0)
        return;
    
    ...
        
	/* 如果是,那么就设置 TIF_NEED_RESCHED 标志 */
	resched_curr(rq_of(cfs_rq));
}

这里面的判断方式是 CFS 算法相关的内容,由于前面文章已经讲解过了,所以这里我不再重述

我们主要看当判断当前进程需要被抢占的时候,会调用 resched_curr,这个函数会设置 TIF_NEED_RESCHED 标志,下面我们看一看它的定义

// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202

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

void resched_curr(struct rq *rq)
{
    ...
	set_tsk_need_resched(curr);
	...
}

可以看到,最终会设置 TIF_NEED_RESCHED 标志

接下下我们看唤醒进程的情况

唤醒进程

进程在等待某个条件的时候,可能会进入睡眠,当条件满足的时候,进程会被唤醒

唤醒进程的过程中,会调用 try_to_wake_up 来唤醒进程,其定义如下,这个函数的调用过程如下

在这里插入图片描述

最终会调用 enqueue_task,将进程添加到运行队列,其定义如下

// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202

static inline void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
{
	...

	p->sched_class->enqueue_task(rq, p, flags);
}

然后调用 check_preempt_curr,检查当前进程是否需要被抢占,如果需要,就会设置 TIF_NEED_RESCHED 标志

check_preempt_curr 的定义如下

// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202

void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags)
{
    ...
	rq->curr->sched_class->check_preempt_curr(rq, p, flags); 
	...
}

它会调用进程所指向的调度类中的 check_preempt_curr 函数,如果调度类是 fair_sched_class,那么 check_preempt_curr 就是 check_preempt_wakeup,其定义如下

// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202

static void check_preempt_wakeup(struct rq *rq, struct task_struct *p, int wake_flags)
{
    ...
        
    resched_curr(rq);
    
    ...
}

根据相应的算法计算,如果需要重新调度,就会调用 resched_curr 函数,这个函数我们上面讲过,它最终会设置 TIF_NEED_RESCHED 标志

关于设置需要重新调度标志的部分已经讲完,下面来看进程的抢占时机

三、进程抢占的时机

进程抢占的时机指的是在什么时候检查 TIF_NEED_RESCHED 标志,然后调用 schedule 函数

进程抢占时机可以分为两部分,一部分是 用户态的抢占时机,一部分是内核态的抢占时机

3.1 用户态的抢占时机

用户态的抢占有两个时机

  • 系统调用返回
  • 中断返回用户态

我们先来看系统调用返回

系统调用返回

对于 X86 来说,发生系统调用时,最终会调用到 do_syscall_32_irqs_on,其定义如下

// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202

static __always_inline void do_syscall_32_irqs_on(struct pt_regs *regs)
{
    ...
        
	/* 系统调用处理 */
        
	...
       
	/* 系统调用返回 */
	syscall_return_slowpath(regs);
}

do_syscall_32_irqs_on 首先做系统调用处理,在系统调用返回的时候,会调用 syscall_return_slowpath

syscall_return_slowpath 最终会调用到 exit_to_usermode_loop,exit_to_usermode_loop 会检查是否设置了 TIF_NEED_RESCHED 标志,如果设置了,就调用 schedule 函数,如下

在这里插入图片描述

exit_to_usermode_loop 的定义如下

// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202

static void exit_to_usermode_loop(struct pt_regs *regs, u32 cached_flags)
{
    ...

	if (cached_flags & _TIF_NEED_RESCHED)
        schedule();
        
	...
}

下面在来看看中断返回用户态时

中断返回用户态

中断的处理流程如下

在这里插入图片描述

在 arch\x86\entry\entry_32.S 文件中,定义了中断处理

当发生中断时,会跳转到 irq_entries_start 处理中断

// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202

ENTRY(irq_entries_start)
	...
    jmp	common_interrupt
    ...
END(irq_entries_start)

irq_entries_start 会跳转到 common_interrupt,其定义如下

// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202

common_interrupt:
	...
	call	do_IRQ
	...
	jmp	ret_from_intr
ENDPROC(common_interrupt)

common_interrupt 会调用 do_IRQ 来进行中断处理,在中断处理完成后,会调用 ret_from_intr 做中断返回处理

ret_from_intr 的定义如下

// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202

ret_from_intr:
	...
	cmpl	$USER_RPL, %eax
	jb	resume_kernel
	
ENTRY(resume_userspace)
	...
	call	prepare_exit_to_usermode
	...
END(ret_from_exception)

ENTRY(resume_kernel)
	...
	call	preempt_schedule_irq
	...
END(resume_kernel)

如果是返回用户态,那么会运行 resume_userspace,然后调用 prepare_exit_to_usermode。如果是返回内核态,那么就跳转到 resume_kernel,然后调用 preempt_schedule_irqs

这里我们看返回用户态的情况

其中的 prepare_exit_to_usermode 我们上面已经分析过了,最终它会检查是否设置了 TIF_NEED_RESCHED 标志,如果设置了,就调用 schedule 发生进程调度

关于用户态的抢占时机这里就介绍完了,下面介绍内核态的抢占时机

3.2 内核态的抢占时机

内核态的抢占有两个时机

  • 中断返回内核态时
  • 开启抢占时

由于上面我们已经分析了中断了,所以这里先看一下中断,中断的处理流程如下

在这里插入图片描述

当返回内核态的时候,调用的是 preempt_schedule_irq 函数,其定义如下

// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202

asmlinkage __visible void __sched preempt_schedule_irq(void)
{
    ...
	do {
        ...
		__schedule(true);
        ...
    } while (need_resched());
    ...
}

在 preempt_schedule_irq 中,调用了 __schedule,你会发现这里调用的是 __schedule 而不是 schedule,其实你可以简单地认为两者是差不多的,在 schedule 函数中会调用 __schedule

接下俩看进程抢占的另一个时机

开启抢占

在某些情况下需要通过 preempt_disable 来关闭抢占,当处理完某件事后,会调用 preempt_enable 来重新开启抢占,preempt_enable 的定义如下

// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202

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

preempt_enable 的调用流程如下

在这里插入图片描述

首先通过 preempt_count_dec_and_test 判断是否开启了抢占和设置了 TIF_NEED_RESCHED 标志,如果返回真,那么就调用 __preempt_schedule,最后调用 __schedule 来发生进程调度

preempt_count_dec_and_test 的定义如下

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

其中 preempt_count_sub 是判断是否开启了抢占,should_resched 是判断是否设置了 TIF_NEED_RESCHED 标志,下面我们看一下 should_resched的定义

// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202

static __always_inline bool should_resched(int preempt_offset)
{
	return unlikely(preempt_count() == preempt_offset &&
			tif_need_resched());
}

其中的 tif_need_resched 就是判断是否设置了 TIF_NEED_RESCHED 标志,其定义如下

#define tif_need_resched() test_thread_flag(TIF_NEED_RESCHED)

可以看到其就是判断是否设置了 TIF_NEED_RESCHED 标志

接下来回到 preempt_enable 函数,我们接着看 __preempt_schedule,其定义如下

// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202

#define __preempt_schedule() preempt_schedule()

asmlinkage __visible void __sched notrace preempt_schedule(void)
{
    ...
    preempt_schedule_common();
}

其中的 preempt_schedule_common 定义如下

// 本文作者:_JT_
// 博客地址:https://blog.csdn.net/weixin_42462202

static void __sched notrace preempt_schedule_common(void)
{
    do {
        ...
        __schedule(true);
        ...
    } while (need_resched());
}

可以看到,最后调用了 __schedule 函数

好了,关于抢占式调度就讲解到这里,下面进入总结时刻

四、总结

下面我们对整个进程调度时机来做一个总结

在这里插入图片描述

  • 进程调度分为主动调度和抢占式调度
  • 主动调度是进程主动调用 schedule 函数,抢占式调度是 进程非自愿调用 schedule 函数
  • 抢占式调度分为两步,第一步是设置 TIF_NEED_RESCHED 标志,第二步是在某个时机调用 schedule 发生进程抢占
  • 设置 TIF_NEED_RESCHED 标志的时机为周期性调度器处理时还有唤醒进程时
  • 检测 TIF_NEED_RESCHED 并调用 schedule 的时机可分为用户态内核态
  • 用户态抢占时机有系统调用返回中断返回用户态
  • 内核态抢占时机有开启抢占还有中断返回内核态
发布了107 篇原创文章 · 获赞 197 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/weixin_42462202/article/details/102954818