1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 进程的调度时机
发生进程调度的本质,是因为系统资源的限制:如果系统只有 8
个 CPU 核,那么同一时间只能有 8
个进程同时并行。那么在 Linux
系统下,什么时候会发生调度?或者说什么时候会发生进程切换?让我们来深入下 Linux
内核代码实现的细节。本文基于 Linux 4.14
源码进行分析。
所有进程的调度,最终都会经过 kernel/sched/core.c
中的 __schedule()
,查找内核源码中所有对 __schedule()
的调用,可以找出所有的调度时机点
。笔者将所有调度点按调度切换过程是否立即执行,将它们分为两类
:
延迟调度
调度切换过程不会立即发生
,而是先设置进程的_TIF_NEED_RESCHED
标志,然后在某个延迟调度点
,检查_TIF_NEED_RESCHED
标志是否有设置:如果有,则会调用__schedule()
执行实际的进程调度切换过程。也就是说,延迟调度
是一个两步走
的过程。即时调度
调度切换过程会立即执行
,即立即调用__schedule()
。
2.1 延迟调度
前面有说过,延迟调度
是一个分两步走
的过程:
1. 通过设置进程的 _TIF_NEED_RESCHED 标志,发起调度请求。
2. 在延迟调度点检测进程的 _TIF_NEED_RESCHED 标志:
如果有设置,则调用 __schedule() 执行进程调度切换。
2.1.1 延迟调度第 1 步:发起调度请求
本节讨论延迟调度
的第 1 步
,即设置进程
的 _TIF_NEED_RESCHED
标志、发起调度请求
的各种场景。
2.1.1.1 进程创建
系统创建进程时,会试图唤醒新进程执行。不管调用 kernel_thread()
接口创建内核线程,还是调用 fork(), vfork(), clone()
接口创建用户态进程,最终都会调用 _do_fork()
:
/* kernel/fork.c */
long _do_fork(unsigned long clone_flags,
unsigned long stack_start,
unsigned long stack_size,
int __user *parent_tidptr,
int __user *child_tidptr,
unsigned long tls)
{
struct task_struct *p;
...
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace, tls, NUMA_NO_NODE);
...
if (!IS_ERR(p)) {
...
wake_up_new_task(p); /* 唤醒 新创建的 子进程 参与调度 */
...
} else {
...
}
...
}
/* kernel/sched/core.c */
void wake_up_new_task(struct task_struct *p)
{
...
p->state = TASK_RUNNING; /* 将新进程标记为 TASK_RUNNING 状态 */
...
/* 检测新进程是否要抢占当前进程: 可能触发调度(即设置进程的 _TIF_NEED_RESCHED 标志位) */
check_preempt_curr(rq, p, WF_FORK);
...
}
void check_preempt_curr(struct rq *rq, struct task_struct *p, int flags)
{
const struct sched_class *class;
if (p->sched_class == rq->curr->sched_class) { /* 使用 相同调度类 进程 的 抢占检测 */
/*
* 如果抢占条件成立,则调用 resched_curr() 设置 当前进程
* 的 _TIF_NEED_RESCHED 发起调度请求,实际的调度发生调度检
* 测点(系统调用返回、中断处理返回 等等情形)。
*/
rq->curr->sched_class->check_preempt_curr(rq, p, flags);
} else { /* 使用 不同调度类 进程 的 抢占检测: 高优先级调度类进程 抢占 低优先级类进程 */
for_each_class(class) { /* 由 高优先级调度类 往 低优先级调度类 遍历 */
/*
* 如果进程 @p 的调度类优先级 比 当前进程 的 调度类优先级
* 要低,结束检测过程:
* 低优先级类进程 不允许 抢占 高优先级类进程。
*/
if (class == rq->curr->sched_class)
break;
if (class == p->sched_class) { /* 高优先级调度类进程 [无条件抢占] 低优先级类进程 */
resched_curr(rq); /* 发起调度请求 */
break;
}
}
}
...
}
/* 设置进程的 _TIF_NEED_RESCHED 标志,发起调度请求 */
void resched_curr(struct rq *rq)
{
struct task_struct *curr = rq->curr;
int cpu;
...
cpu = cpu_of(rq);
if (cpu == smp_processor_id()) { /* 在当前 CPU 上发起调度请求 */
set_tsk_need_resched(curr); /* 设置 _TIF_NEED_RESCHED 标记,发起调度请求 */
...
return;
}
if (set_nr_and_not_polling(curr))
smp_send_reschedule(cpu); /* 在非当前 CPU 上请求调度 */
else
trace_sched_wake_idle_without_ipi(cpu);
}
/* include/linux/sched.h */
static inline void set_tsk_need_resched(struct task_struct *tsk)
{
set_tsk_thread_flag(tsk,TIF_NEED_RESCHED);
}
从代码分析看出,新进程创建时的调度,是一种延迟调度
,进程的调度切换过程不会立即发生。
2.1.1.2 周期性调度
系统时钟中断按 HZ
频率发生,在时钟中断处理过程中,触发周期性调度:
tick_periodic()
update_process_times()
scheduler_tick()
/* 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;
...
/*
* STOP: task_tick_stop()
* DL : task_tick_dl()
* RT : task_tick_rt()
* CFS : task_tick_fair()
*/
curr->sched_class->task_tick(rq, curr, 0);
...
}
这里以 CFS
调度器为例,简要分析下 CFS
调度器周期性调度的逻辑:
/* 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);
}
...
}
static void
entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
/*
* o 更新当前进程的运行时间:真实 + 虚拟 时间
* o 更新运行队列上进程的最小虚拟运行时间 min_vruntime
*/
update_curr(cfs_rq);
...
/*
* 运行队列上进程数大于 1 个,随着系统运行,有可能会
* 发生抢占,进行调度抢占检测处理。
*/
if (cfs_rq->nr_running > 1)
check_preempt_tick(cfs_rq, curr);
}
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;
/* 计算一个 调度周期 内, 进程 @curr 在运行队列 @cfs_rq 上的【理论真实运行时间】 */
ideal_runtime = sched_slice(cfs_rq, curr);
/* 计算进程 @curr 在 本次调度周期内 的 真实运行时间 */
delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
if (delta_exec > ideal_runtime) { /* 如果 进程 @curr 的 调度周期 内的 时间份额 已经耗完, */
/*
* 为保证 调度周期其它进程也得到执行,耗完 本次调度周期
* 内 时间份额 的 进程应暂停执行,于是发起重新调度请求。
*/
resched_curr(rq_of(cfs_rq));
...
return;
}
/*
* 确保 进程 的 最短运行 时间:
* 进程一次在 CPU 上的 运行时间 不小于 调度粒度 时间
*/
if (delta_exec < sysctl_sched_min_granularity)
return;
/*
* 挑选下一个可运行的进程:
* 即运行队列中 @cfs_rq 中 vruntime 最小的进程。
*/
se = __pick_first_entity(cfs_rq);
/*
* 计算运行队列 @cfs_rq 【当前运行进程】 和
* 【挑选的下一个可运行进程】 之间 的 虚拟运行时间差值,
* 用该差值来决定 【挑选的下一个可运行进程】 是否抢占。
*/
delta = curr->vruntime - se->vruntime;
/* 当前进程 仍然是 虚拟运行时间最小 的 进程, 则 不发生抢占, 继续 运行 当前进程 */
if (delta < 0)
return;
if (delta > ideal_runtime)
resched_curr(rq_of(cfs_rq)); /* 设置 _TIF_NEED_RESCHED 标志,发起调度请求 */
}
2.1.1.3 进程唤醒
进程等待的事件到达
、超时时间到期
、锁持有者释放锁
等情形下,将进入进程唤醒
过程。进程唤醒
过程最终都通过 wake_up()
系列接口来达成:
/* include/linux/wait.h */
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
/* kernel/sched/wait.c */
__wake_up()
__wake_up_common_lock()
__wake_up_common()
static int __wake_up_common(struct wait_queue_head *wq_head, unsigned int mode,
int nr_exclusive, int wake_flags, void *key,
wait_queue_entry_t *bookmark)
{
wait_queue_entry_t *curr, *next;
if (bookmark && (bookmark->flags & WQ_FLAG_BOOKMARK)) {
...
} else
curr = list_first_entry(&wq_head->head, wait_queue_entry_t, entry);
...
list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {
...
ret = curr->func(curr, mode, wake_flags, key); /* 如 pollwake() */
...
}
}
接下来的唤醒过程,按不同的睡眠场景而各有不同,这里以进程因 poll()
陷入睡眠为例,分析其唤醒过程:
pollwake()
__pollwake()
default_wake_function()
try_to_wake_up()
剩余的唤醒过程,不管因什么场景下进入睡眠,都汇聚到到 try_to_wake_up()
,我们重点关注调度请求的发起过程
:
/* kernel/sched/core.c */
try_to_wake_up()
ttwu_queue()
ttwu_do_activate()
ttwu_do_wakeup()
check_preempt_curr()
看看,执行流程又走到前面分析过的 check_preempt_curr()
的函数,该函数检测唤醒抢占
的可能,如果被唤醒进程
符合对当前进程发起抢占的条件,则设置 _TIF_NEED_RESCHED
标志,发起抢占调度请求;之后在延迟调度触发点
,做实际的进程调度切换工作。
2.1.1.4 CPU 亲和性设置
在设置进程的 CPU 亲和性
时,也可能会发起调度请求:
__set_cpus_allowed_ptr()
...
if (task_running(rq, p) || p->state == TASK_WAKING) {
...
stop_one_cpu(cpu_of(rq), migration_cpu_stop, &arg);
...
} else if (task_on_rq_queued(p)) {
rq = move_queued_task(rq, &rf, p, dest_cpu);
}
...
migration_cpu_stop() / move_queued_task()
...
check_preempt_curr() /* 可能发起调度请求(即设置 _TIF_NEED_RESCHED 标志)*/
...
2.1.1.5 CPU 热插拔:启停
CPU 核启用时,可能将一些进程迁移到它上面执行;CPU 停止时,将其上的进程迁移到别的 CPU 上执行。本文对 CPU 核启停触发调用的代码细节不做展开,感兴趣的读者可查阅相关资料。
2.1.1.6 CPU 负载均衡
进程在 CPU 核间进行负载均衡时,也可能触发调度。本文对这些代码细节不做展开,感兴趣的读者可查阅相关资料。
2.1.1.7 NUMA 节点间迁移和均衡
进程在 NUMA 节点间迁移、或做负载均衡时,也可能触发调度。本文对这些代码细节不做展开,感兴趣的读者可查阅相关资料。
2.1.1.8 用户修改调度参数
在用户修改进程调度参数时,也会引发进程调度。Linux
内核向用户提供如下接口修改调度参数:
int setpriority(int which, id_t who, int prio);
int nice(int inc);
int sched_setparam(pid_t pid, const struct sched_param *param);
int sched_setattr(pid_t pid, struct sched_attr *attr,
unsigned int flags);
int sched_setscheduler(pid_t pid, int policy,
const struct sched_param *param);
2.1.1.4.1 setpriority() / nice()
sys_setpriority()
set_one_prio()
set_user_nice()
sys_nice()
set_user_nice(current, nice)
void set_user_nice(struct task_struct *p, long nice)
{
...
if (queued) {
enqueue_task(rq, p, ENQUEUE_RESTORE | ENQUEUE_NOCLOCK);
/*
* If the task increased its priority or is running and
* lowered its priority, then reschedule its CPU:
*/
if (delta < 0 || (delta > 0 && task_running(rq, p)))
resched_curr(rq); /* 发起调度请求 */
}
...
}
2.1.1.4.2 sched_setparam() / sched_setattr() / sched_setscheduler()
sys_sched_setparam()
do_sched_setscheduler(pid, SETPARAM_POLICY, param)
sched_setscheduler(p, policy, &lparam)
_sched_setscheduler(p, policy, param, true)
static int _sched_setscheduler(struct task_struct *p, int policy,
const struct sched_param *param, bool check)
{
struct sched_attr attr = {
.sched_policy = policy, // 修改调度策略
.sched_priority = param->sched_priority, // 修改调度优先级
.sched_nice = PRIO_TO_NICE(p->static_prio), // 修改 nice 值
};
...
return __sched_setscheduler(p, &attr, check, true);
}
__sched_setscheduler()
check_class_changed(rq, p, prev_class, oldprio)
static inline void check_class_changed(struct rq *rq, struct task_struct *p,
const struct sched_class *prev_class,
int oldprio)
{
if (prev_class != p->sched_class) {
/*
* 进程 从 前一调度类别 切出:
* STOP: NULL
* DL : switched_from_dl()
* RT : switched_from_rt()
* CFS : switched_from_fair()
*/
if (prev_class->switched_from)
prev_class->switched_from(rq, p);
/*
* 进程 切入 下一调度类别:
* STOP: switched_to_stop()
* DL : switched_to_dl()
* RT : switched_to_rt()
* CFS : switched_to_fair()
*/
p->sched_class->switched_to(rq, p);
} else if (oldprio != p->prio || dl_task(p))
/*
* 进程 优先级 变换:
* STOP: prio_changed_stop()
* DL : prio_changed_dl()
* RT : prio_changed_rt()
* CFS : prio_changed_fair()
*/
p->sched_class->prio_changed(rq, p, oldprio);
}
不管是因为优先级的变化引起调度类别的切换,还是只是仅仅是同一调度类的优先级的变化,在合适的条件下,都会调用 resched_curr()
发起调度请求。
sched_setattr()
,sched_setscheduler()
和 sched_setparam()
触发调度的流程类似:
sys_sched_setattr()
__sched_setscheduler(p, attr, true, true)
// 参考 sched_setparam() 调用流程
sys_sched_setscheduler()
do_sched_setscheduler(pid, policy, param)
// 参考 sched_setparam() 调用流程
2.1.2 延迟调度第 2 步:检测调度请求并执行调度切换
本节讨论延迟调度
的第 2 步
,即检测进程的 _TIF_NEED_RESCHED
标志、执行进程调度切换
的各种场景。
2.1.2.1 中断异常处理返回
这里以 ARM
架构的中断处理过程为例,来说明中断处理过程退出时发生的延迟调度
。中断异常
既可以发生在内核态
,也可以发生在用户态
。我们分别从内核态
和用户态
来进行分析调度发生的过程。
2.1.2.1.1 内核态 中断 处理结束时的 调度
/* arch/arm/kernel/entry-armv.S */
.align 5
__irq_svc:
svc_entry
irq_handler /* 处理 内核态 中断 */
/* 检查进程是否设置了 _TIF_NEED_RESCHED,发起调度请求 */
#ifdef CONFIG_PREEMPT
ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
ldr r0, [tsk, #TI_FLAGS] @ get flags
teq r8, #0 @ if preempt count != 0
movne r0, #0 @ force flags to 0
tst r0, #_TIF_NEED_RESCHED /* 检查是否有调度需求(通过检查 _TIF_NEED_RESCHED 标记) */
blne svc_preempt /* 中断处理结束后,发起 内核态 抢占 */
#endif
svc_exit r5, irq = 1 @ return from exception
UNWIND(.fnend )
ENDPROC(__irq_svc)
.ltorg
#ifdef CONFIG_PREEMPT
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
从上面的代码分析可以看到,内核态中断处理结束时的发生调度,要满足两个条件:
. 开启了内核态抢占,即开启了内核配置项 CONFIG_PREEMPT
. 被中断进程设置了 _TIF_NEED_RESCHED,发起了调度请求
这里的调度过程是立即发生的,但发生调度的前提之一是,被中断进程设置了 _TIF_NEED_RESCHED
标志,所以这可以说是延迟调度
的一种情形。对进程设置 _TIF_NEED_RESCHED
标志的场景,后面的分析会有讨论。
2.1.2.1.2 用户态 中断、异常 处理结束时的 调度
/* arch/arm/kernel/entry-armv.S */
.align 5
__irq_usr:
...
irq_handler /* 处理 用户态 中断 */
get_thread_info tsk
mov why, #0
b ret_to_user_from_irq /* 从中断返回用户态空间 */
UNWIND(.fnend )
ENDPROC(__irq_usr)
/* arch/arm/kernel/entry-common.S */
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:
...
ENDPROC(ret_to_user_from_irq)
ENDPROC(ret_to_user)
...
/* 有挂起的工作要做,先做完挂起的工作(如处理进程调度),再返回用户空间 */
slow_work_pending:
mov r0, sp @ 'regs'
mov r2, why @ 'syscall'
bl do_work_pending
...
/* arch/arm/kernel/signal.c */
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)) { /* 检查是否设置了 _TIF_NEED_RESCHED 标志位 */
schedule(); /* 执行调度:schedule() -> __schedule(false) */
} else {
...
}
...
thread_flags = current_thread_info()->flags;
} while (thread_flags & _TIF_WORK_MASK);
return 0;
}
事实上,不光用户态中断
会触发可能的进程调度(如果设置了 _TIF_NEED_RESCHED
标志位的话),一些异常
也会触发可能的进程调度:
/* arch/arm/kernel/entry-armv.S */
.align 5
__und_usr: /* 用户模式未定义指令异常入口 */
...
badr r9, ret_from_exception
...
...
.align 5
__pabt_usr:
...
/* fall through */
/*
* This is the return code to user mode for abort handlers
*/
ENTRY(ret_from_exception)
...
// ret_to_user 的定义见前面的代码分析,最终根据是否设置了 _TIF_NEED_RESCHED,
// 确定是否触发调度过程,即调用 __schedule()
b ret_to_user
...
ENDPROC(__pabt_usr)
ENDPROC(ret_from_exception)
2.1.2.2 系统调用 返回 (用户态) 时 的 调度
/* arch/arm/kernel/entry-common.S */
.align 5
ENTRY(vector_swi) // 系统调用内核入口
...
/* 调用系统调用接口 */
invoke_syscall tbl, scno, r10, __ret_fast_syscall // 系统调用 return 返回到 __ret_fast_syscall 标号处
...
ret_fast_syscall:
__ret_fast_syscall:
// 禁用中断
disable_irq_notrace @ disable interrupts
...
// r1 = thread_info::flags
ldr r1, [tsk, #TI_FLAGS] @ re-check for syscall tracing
tst r1, #_TIF_SYSCALL_WORK | _TIF_WORK_MASK // 检查是否有挂起的工作要做
/*
* 检查到有挂起的工作要做,先跳转到 fast_work_pending
* 做完挂起的工作,然后再返回用户空间。
*/
bne fast_work_pending
...
/* Ok, we need to do extra processing, enter the slow path. */
fast_work_pending:
...
slow_work_pending:
mov r0, sp @ 'regs'
mov r2, why @ 'syscall'
bl do_work_pending
...
最终调用了 do_work_pending()
来处理调度请求等工作
,这在前面已经分析过了,这里就不再赘述。
2.1.2.3 使能抢占时 的 调度
/* include/linux/preempt.h */
#ifdef CONFIG_PREEMPT
#define preempt_enable() \
do { \
barrier(); \
if (unlikely(preempt_count_dec_and_test())) \
__preempt_schedule(); \
} while (0)
#define preempt_enable_notrace() \
do { \
barrier(); \
if (unlikely(__preempt_count_dec_and_test())) \
__preempt_schedule_notrace(); \
} while (0)
...
#else /* !CONFIG_PREEMPT */
#define preempt_enable() \
do { \
barrier(); \
preempt_count_dec(); \
} while (0)
#define preempt_enable_notrace() \
do { \
barrier(); \
__preempt_count_dec(); \
} while (0)
#endif /* CONFIG_PREEMPT */
/* include/asm-generic/premmpt.h */
#ifdef CONFIG_PREEMPT
extern asmlinkage void preempt_schedule(void);
#define __preempt_schedule() preempt_schedule()
extern asmlinkage void preempt_schedule_notrace(void);
#define __preempt_schedule_notrace() preempt_schedule_notrace()
#endif /* CONFIG_PREEMPT */
/* kernel/sched/core.c */
#ifdef CONFIG_PREEMPT
asmlinkage __visible void __sched notrace preempt_schedule(void)
{
/*
* If there is a non-zero preempt_count or interrupts are disabled,
* we do not want to preempt the current task. Just return..
*/
if (likely(!preemptible()))
return;
preempt_schedule_common();
}
...
static void __sched notrace preempt_schedule_common(void)
{
do {
...
__schedule(true); /* 抢占调度 */
...
} while (need_resched());
}
asmlinkage __visible void __sched notrace preempt_schedule_notrace(void)
{
...
do {
...
__schedule(true); /* 抢占调度 */
...
} while (need_resched()); /* 检测 _TIF_NEED_RESCHED,处理 更多的 调度请求 */
}
/* include/linux/sched.h */
static __always_inline bool need_resched(void)
{
return unlikely(tif_need_resched());
}
/* include/linux/thread_info.h */
#define tif_need_resched() test_thread_flag(TIF_NEED_RESCHED)
/* arch/arm/include/asm/thread_info.h */
#define TIF_NEED_RESCHED 1 /* rescheduling necessary */
#define _TIF_NEED_RESCHED (1 << TIF_NEED_RESCHED)
从代码分析了解到,使能抢占式的调度,发生在内核态
,且只有在开启了内核抢占
(即使能了配置 CONFIG_PREEMPT
)时才会发生。另外,值得注意的是,系统中一些接口封装了对 preempt_enable()
的调用,因此它们也成了延迟调度点
,如 spin_unlock()
:
spin_unlock()
raw_spin_unlock()
_raw_spin_unlock()
__raw_spin_unlock()
spin_release(&lock->dep_map, 1, _RET_IP_);
do_raw_spin_unlock(lock);
preempt_enable();
2.1.2.4 主动插入延迟调度点
在很耗时的代码片段中,调用 cond_resched()
主动插入延迟调度检测点,触发可能的进程调度,以降低系统的响应延迟。如内存压缩场景:
static unsigned long do_shrink_slab(struct shrink_control *shrinkctl,
struct shrinker *shrinker,
unsigned long nr_scanned,
unsigned long nr_eligible)
{
...
while (total_scan >= batch_size ||
total_scan >= freeable) {
...
cond_resched(); /* 内存压缩可能是比较耗时的,主动在循环每一轮进行一次延迟调度 */
}
...
}
/* include/linux/sched.h */
#define cond_resched() ({ \
___might_sleep(__FILE__, __LINE__, 0); \
_cond_resched(); \
})
#ifndef CONFIG_PREEMPT
int __sched _cond_resched(void)
{
if (should_resched(0)) { /* 检测延迟调度请求 */
preempt_schedule_common(); /* 执行进程调度切换 */
return 1;
}
return 0;
}
EXPORT_SYMBOL(_cond_resched);
#endif
/* include/asm-generic/preempt.h */
static __always_inline bool should_resched(int preempt_offset)
{
return unlikely(preempt_count() == preempt_offset &&
tif_need_resched());
}
2.2 即时调度
本节讨论调用 __schedule()
立即执行进程调度切换的即时调度的各种场景。
2.2.1 进程退出
/* kernel/exit.c */
void __noreturn do_exit(long code)
{
...
do_task_dead();
}
/* kernel/sched/core.c */
void __noreturn do_task_dead(void)
{
...
__schedule(false); /* 当前进程退出,挑选新进程执行,调度过程立即执行 */
...
}
从代码分析看出,进程退出时的调度
是一种调度切换过程立即发生
的即时调度
。发生的场景,如程序调用 exit()
主动退出,或因为某些异常被动退出情形(典型的如 segment fault
)。
2.2.2 进程主动放弃 CPU 的情形
进程主动主动放弃 CPU
的情形,大概可以分为以下两种:
o 进程进入睡眠
o 进程放弃 CPU
2.2.2.1 进程进入睡眠
进程进入睡眠,意味着一段时间内放弃在 CPU 上执行,那必然要调度一个新的进程来执行,也就是会进程睡眠会导致进程切换调度。
2.2.2.1.1 调用睡眠函数
先看内核态
的睡眠函数调用:
/* kernel/time/timer.c */
void msleep(unsigned int msecs)
{
unsigned long timeout = msecs_to_jiffies(msecs) + 1;
while (timeout)
timeout = schedule_timeout_uninterruptible(timeout);
}
signed long __sched schedule_timeout_uninterruptible(signed long timeout)
{
__set_current_state(TASK_UNINTERRUPTIBLE);
return schedule_timeout(timeout);
}
signed long __sched schedule_timeout(signed long timeout)
{
struct timer_list timer;
unsigned long expire;
...
expire = timeout + jiffies;
/* 超时后 通过 process_timeout() 唤醒 */
setup_timer_on_stack(&timer, process_timeout, (unsigned long)current);
__mod_timer(&timer, expire, false);
schedule(); /* 调度出去:调度切换过程立即执行 */
del_singleshot_timer_sync(&timer);
/* Remove the timer from the object tracker */
destroy_timer_on_stack(&timer);
timeout = expire - jiffies;
out:
return timeout < 0 ? 0 : timeout;
}
再看用户态
睡眠函数 sleep()
调用,其最终实现系统调用 clock_nanosleep()
:
clock_nanosleep (CLOCK_REALTIME, 0, requested_time, remaining)
而系统调用 clock_nanosleep()
的实现为高精度定时器:
// 启用 CONFIG_POSIX_TIMERS 的情形
/* kernel/time/posix-timers.c */
SYSCALL_DEFINE4(clock_nanosleep, const clockid_t, which_clock, int, flags,
const struct timespec __user *, rqtp,
struct timespec __user *, rmtp)
{
const struct k_clock *kc = clockid_to_kclock(which_clock);
...
...
return kc->nsleep(which_clock, flags, &t); /* common_nsleep() */
}
static int common_nsleep(const clockid_t which_clock, int flags,
const struct timespec64 *rqtp)
{
return hrtimer_nanosleep(rqtp, flags & TIMER_ABSTIME ?
HRTIMER_MODE_ABS : HRTIMER_MODE_REL,
which_clock);
}
/* kernel/time/hrtimer.c */
hrtimer_nanosleep()
do_nanosleep()
static int __sched do_nanosleep(struct hrtimer_sleeper *t, enum hrtimer_mode mode)
{
...
hrtimer_init_sleeper(t, current); /* 配置超时唤醒接口 */
do {
set_current_state(TASK_INTERRUPTIBLE); /* 进入 可中断睡眠态 */
hrtimer_start_expires(&t->timer, mode); /* 启动定时器 */
if (likely(t->task))
freezable_schedule(); /* 进行调度 */
...
} while (t->task && !signal_pending(current));
...
}
void hrtimer_init_sleeper(struct hrtimer_sleeper *sl, struct task_struct *task)
{
sl->timer.function = hrtimer_wakeup; /* 设置 睡眠 超时唤醒接口 hrtimer_wakeup() */
sl->task = task;
}
freezable_schedule()
schedule()
/* 睡眠 超时唤醒回调接口 */
static enum hrtimer_restart hrtimer_wakeup(struct hrtimer *timer)
{
struct hrtimer_sleeper *t =
container_of(timer, struct hrtimer_sleeper, timer);
struct task_struct *task = t->task;
t->task = NULL;
if (task)
wake_up_process(task); /* 唤醒进程 */
return HRTIMER_NORESTART;
}
2.2.2.1.2 等待特定事件
进程等待特定事件
到来时,也可能进入睡眠
,从而引发进程调度
。进程通过接口 wait_event()
等待特定事件到来:
/* include/linux/wait.h */
#define wait_event(wq_head, condition) \
do { \
might_sleep(); \
if (condition) \
break; \
__wait_event(wq_head, condition); \
} while (0)
#define __wait_event(wq_head, condition) \
(void)___wait_event(wq_head, condition, TASK_UNINTERRUPTIBLE, 0, 0, \
schedule())
#define ___wait_event(wq_head, condition, state, exclusive, ret, cmd) \
({ \
__label__ __out; \
struct wait_queue_entry __wq_entry; \
long __ret = ret; /* explicit shadow */ \
\
init_wait_entry(&__wq_entry, exclusive ? WQ_FLAG_EXCLUSIVE : 0); \
for (;;) { \
long __int = prepare_to_wait_event(&wq_head, &__wq_entry, state);\
\
if (condition) \
break; \
\
if (___wait_is_interruptible(state) && __int) { \
__ret = __int; \
goto __out; \
} \
\
cmd; /* schedule(); */ \
} \
finish_wait(&wq_head, &__wq_entry); \
__out: __ret; \
})
可以看到,wait_event()
在等待特定事件时,进行了进程调度切换,而其自身则进入不可中断睡眠态(TASK_UNINTERRUPTIBLE)
,直到特定事件到来时被唤醒。
2.2.2.1.3 锁竞争
进程在试图获取锁
时,如果失败,也会导致进入睡眠
,从而引发进程调度:
mutex_lock()
__mutex_lock_slowpath()
__mutex_lock(lock, TASK_UNINTERRUPTIBLE, 0, NULL, _RET_IP_)
__mutex_lock_common()
...
preempt_disable();
/* 成功获取到锁的情形,立马就返回了 */
...
/* 没有成功获取锁的情形,调度其它进程执行,而进程自身进入睡眠 等待 锁持有者 释放锁 后 被唤醒 */
set_current_state(state);
for (;;) {
...
schedule_preempt_disabled(); /* 发起调度 */
...
}
/* kernel/sched/core.c */
void __sched schedule_preempt_disabled(void)
{
sched_preempt_enable_no_resched(); /* 当前抢占处于禁用状态,要能调度,先得启用抢占 */
schedule(); /* 发起调度 */
preempt_disable(); /* 平衡 进入函数时 的 调度抢占使能计数 */
}
2.2.3 进程放弃 CPU
进程用户态通过 sched_yield()
系统调用主动放弃 CPU,而内核态通过调用 yield()
接口:
/* kernel/sched/core.c */
void __sched yield(void)
{
set_current_state(TASK_RUNNING);
sys_sched_yield();
}
SYSCALL_DEFINE0(sched_yield)
{
...
/*
* STOP: yield_task_stop()
* DL : yield_task_dl()
* RT : yield_task_rt()
* CFS : yield_task_fair() => 标记为主动放弃 CPU 的进程,指示 CFS 挑选下次执行进程时,尽量不要去选它
*/
current->sched_class->yield_task(rq);
...
schedule(); /* 发起调度 */
return 0;
}
sched_yield()
和进程睡眠
有相似的地方,都是主动发起调度放弃 CPU,但它们不同的是:
o sched_yield()
进程试图放弃 CPU,但下次挑选执行的进程还可能会是它,因此有可能继续执行,且进程一直
处于【可运行状态】。
o 进程睡眠
进程睡眠一段时间后被唤醒,也就是说一定会有一段事件得不到执行,且期间处于【睡眠状态】。