Linux 调度:进程调度时机
文章目录
- 1. 前言
- 2. 进程的调度时机
- 2.1 延迟调度
- 2.1.1 延迟调度第 1 步:发起调度请求
- 2.1.1.1 进程创建
- 2.1.1.2 周期性调度
- 2.1.1.3 进程唤醒
- 2.1.1.4 用户修改调度参数
- 2.1.1.4.1 setpriority() / nice()
- 2.1.1.4.2 sched_setparam() / sched_setattr() / sched_setscheduler()
- 2.1.2 延迟调度第 2 步:检测调度请求并执行调度切换
- 2.1.2.1 中断异常处理返回
- 2.1.2.1.1 内核态 中断 处理结束时的 调度
- 2.1.2.1.2 用户态 中断、异常 处理结束时的 调度
- 2.1.2.2 系统调用 返回 (用户态) 时 的 调度
- 2.1.2.3 使能抢占时 的 调度
- 2.1.2.4 主动插入延迟调度点
- 2.2 即时调度
- 2.2.1 进程退出
- 2.2.2 进程主动放弃 CPU 的情形
- 2.2.2.1 进程进入睡眠
- 2.2.2.1.1 调用睡眠函数
- 2.2.2.1.2 等待特定事件
- 2.2.2.1.3 锁竞争
- 2.2.3 进程放弃 CPU
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 上请求调度 */elsetrace_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)) {...} elsecurr = 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 用户修改调度参数
在用户修改进程调度参数时,也会引发进程调度。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_entryirq_handler /* 处理 内核态 中断 *//* 检查进程是否设置了 _TIF_NEED_RESCHED,发起调度请求 */
#ifdef CONFIG_PREEMPTldr r8, [tsk, #TI_PREEMPT] @ get preempt countldr r0, [tsk, #TI_FLAGS] @ get flagsteq r8, #0 @ if preempt count != 0movne r0, #0 @ force flags to 0tst r0, #_TIF_NEED_RESCHED /* 检查是否有调度需求(通过检查 _TIF_NEED_RESCHED 标记) */blne svc_preempt /* 中断处理结束后,发起 内核态 抢占 */
#endifsvc_exit r5, irq = 1 @ return from exceptionUNWIND(.fnend )
ENDPROC(__irq_svc).ltorg#ifdef CONFIG_PREEMPT
svc_preempt:mov r8, lr/* 发起 内核态 抢占调度 */
1: bl preempt_schedule_irq @ irq en/disable is done insideldr r0, [tsk, #TI_FLAGS] @ get new tasks TI_FLAGStst r0, #_TIF_NEED_RESCHEDreteq r8 @ go againb 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 tskmov why, #0b 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_MASKbne 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 tracingtst 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 进程睡眠进程睡眠一段时间后被唤醒,也就是说一定会有一段事件得不到执行,且期间处于【睡眠状态】。