主调度器
在内核中的许多地方, 如果要将CPU分配给与当前活动进程不同的另一个进程, 都会直接调用主调度器函数schedule, 从系统调用返回后, 内核也会检查当前进程是否设置了重调度标志TLF_NEDD_RESCHED
例如, 前述的周期性调度器的scheduler_tick就会设置该标志, 如果是这样则内核会调用schedule, 该函数假定当前活动进程一定会被另一个进程取代.
1.1 调度函数的__sched前缀
在详细论述schedule之前, 需要说明一下__sched
前缀, 该前缀可能用于调用schedule的函数, 包括schedule本身.
__sched前缀的声明, 在include/linux/sched.h, L416, 如下所示
/* Attach to any functions which should be ignored in wchan output. */
#define __sched __attribute__((__section__(".sched.text")))
attribute((_section(“…”)))是一个gcc的编译属性, 其目的在于将相关的函数的代码编译之后, 放到目标文件的以恶搞特定的段内, 即.sched.text中. 该信息使得内核在显示栈转储活类似信息时, 忽略所有与调度相关的调用. 由于调度哈书调用不是普通代码流程的一部分, 因此在这种情况下是没有意义的.
用它修饰函数的方式如下
void __sched some_function(args, ...)
{
......
schedule();
......
}
1.2 schedule函数
schedule就是主调度器的函数, 在内核中的许多地方, 如果要将CPU分配给与当前活动进程不同的另一个进程, 都会直接调用主调度器函数schedule.
该函数完成如下工作
- 确定当前就绪队列, 并在保存一个指向当前(仍然)活动进程的task_struct指针
- 检查死锁, 关闭内核抢占后调用__schedule完成内核调度
- 恢复内核抢占, 然后检查当前进程是否设置了重调度标志TLF_NEDD_RESCHED, 如果该进程被其他进程设置了TIF_NEED_RESCHED标志, 则函数重新执行进行调度
该函数定义在kernel/sched/core.c, L3243, 如下所示
asmlinkage __visible void __sched schedule(void)
{
/* 获取当前的进程 */
struct task_struct *tsk = current;
/* 避免死锁 */
sched_submit_work(tsk);
do {
preempt_disable(); /* 关闭内核抢占 */
__schedule(false); /* 完成调度 */
sched_preempt_enable_no_resched(); /* 开启内核抢占 */
} while (need_resched()); /* 如果该进程被其他进程设置了TIF_NEED_RESCHED标志,则函数重新执行进行调度 */
}
EXPORT_SYMBOL(schedule);
1.2.2 sched_submit_work避免死锁
该函数定义在kernel/sched/core.c, L3231, 如下所示
static inline void sched_submit_work(struct task_struct *tsk)
{
/* 检测tsk->state是否为0 (runnable), 若为运行态时则返回,
* tsk_is_pi_blocked(tsk),检测tsk的死锁检测器是否为空,若非空的话就return
if (!tsk->state || tsk_is_pi_blocked(tsk))
return;
/*
* If we are going to sleep and we have plugged IO queued,
* make sure to submit it to avoid deadlocks.
*/
if (blk_needs_flush_plug(tsk)) /* 然后检测是否需要刷新plug队列,用来避免死锁 */
blk_schedule_flush_plug(tsk);
}
1.2.3 preempt_disable和sched_preempt_enable_no_resched开关内核抢占
内核抢占
Linux除了内核态外还有用户态。用户程序的上下文属于用户态,系统调用和中断处理例程上下文属于内核态. 如果一个进程在用户态时被其他进程抢占了COU则成发生了用户态抢占, 而如果此时进程进入了内核态, 则内核此时代替进程执行, 如果此时发了抢占, 我们就说发生了内核抢占.
内核抢占是Linux 2.6以后引入的一个重要的概念
我们说:如果进程正执行内核函数时,即它在内核态运行时,允许发生内核切换(被替换的进程是正执行内核函数的进程),这个内核就是抢占的。
抢占内核的主要特点是:一个在内核态运行的进程,当且仅当在执行内核函数期间被另外一个进程取代。
这与用户态的抢占有本质区别.
内核为了支撑内核抢占, 提供了很多机制和结构, 必要时候开关内核抢占也是必须的, 这些函数定义在include/linux/preempt.h, L145
#define preempt_disable() \
do { \
preempt_count_inc(); \
barrier(); \
} while (0)
#define sched_preempt_enable_no_resched() \
do { \
barrier(); \
preempt_count_dec(); \
} while (0)
1.3 __schedule开始进程调度
__schedule完成了真正的调度工作, 其定义在kernel/sched/core.c, L3103, 如下所示
1.3.1 __schedule函数主框架
static void __sched notrace __schedule(bool preempt)
{
struct task_struct *prev, *next;
unsigned long *switch_count;
struct rq *rq;
int cpu;
/* ==1==
找到当前cpu上的就绪队列rq
并将正在运行的进程curr保存到prev中 */
cpu = smp_processor_id();
rq = cpu_rq(cpu);
prev = rq->curr;
/*
* do_exit() calls schedule() with preemption disabled as an exception;
* however we must fix that up, otherwise the next task will see an
* inconsistent (higher) preempt count.
*
* It also avoids the below schedule_debug() test from complaining
* about this.
*/
if (unlikely(prev->state == TASK_DEAD))
preempt_enable_no_resched_notrace();
/* 如果禁止内核抢占,而又调用了