罗冲 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
work_resched:
call schedule ---第597行代码
LOCKDEP_SYS_EXIT
当系统接收到中断的时候(一般是时间中断),系统进入中断程序,然后调用schedule函数
系统调用schedule()函数后,其最后会调用__schedule()
static void __sched __schedule(void)
{
... ...
// 获取下一个调度实体
next = pick_next_task(rq, prev);
... ...
//进程的上下文切换
context_switch(rq, prev, next); /* unlocks the rq */
... ...
}
对于调度来说,可以简化为两个步骤。进程的选择,与进程上下文件的切换
(原图:http://blog.chinaunix.net/uid-26772321-id-4847184.html
从这个图中,可以看出,CPU对应包含一个运行队列结构(struct rq),而每个运行队列又包含有实时运行队列(struct rt_rq)、普通进程运行队列(struct cfs_rq)与dl队列。我们可以理解成对于每个CPU而言,它都会管理一个运行队列。当我们创建一个进程的时候,都会将创建的进程加入到这个队列中。
其简单的过程:
回忆一下进程的创建。通过系统调调用sys_clone —> do_fork(),在这个do_fork()中,系统调用copy_process()函数,将父进程copy一份给子进程。而在这个copy_process()中会调用函数
/* Perform scheduler related setup. Assign this task to a CPU. */ retval = sched_fork(clone_flags, p);
查看这个函数的实现:
int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
unsigned long flags;
/* 获取当前CPU,并且禁止抢占 */
int cpu = get_cpu();
/* 初始化跟调度相关的值,比如调度实体,运行时间等 */
__sched_fork(clone_flags, p);
/*
*将进程标志为running状态,但是此时没有实际运行。 在它被插入到运行队列之前,没有任务信号或外部事件能够唤醒它。
*/
p->state = TASK_RUNNING;
/*
* 根据父进程的运行优先级设置设置进程的优先级
*/
p->prio = current->normal_prio;
.... ....
} else if (rt_prio(p->prio)) {
/* 根据优先级判断,如果是实时进程,设置其调度类为rt_sched_class */
p->sched_class = &rt_sched_class;
} else {
/* 如果是普通进程,设置其调度类为fair_sched_class .注意这个函数实现:
*
*const struct sched_class fair_sched_class = {
* .next = &idle_sched_class,
* .enqueue_task = enqueue_task_fair,
*/
p->sched_class = &fair_sched_class;
}
... ...
/* 设置新进程的CPU为当前CPU */
set_task_cpu(p, cpu);
... ...
return 0;
}
经过此函数后,进程所对应的CPU、调度策略都已经设置完成。但此时进程还不能被调度。直到do_fork()函数中的
long do_fork(...)
{
... ...
p = copy_process(clone_flags, stack_start, stack_size,
child_tidptr, NULL, trace);
... ....
wake_up_new_task(p); //新进程被加入到调度队列中
... ....
}
查看一下wake_up_new_task的代码
void wake_up_new_task(struct task_struct *p)
{
... ...
//将新进程放入到队列中去
activate_task(rq, p, 0);
... ...
}
activate_task()函数比较简单,它最后会调到enqueue_task(rq, p, flags)中
static void enqueue_task(struct rq *rq, struct task_struct *p, int flags)
{
update_rq_clock(rq);
sched_info_queued(rq, p);
p->sched_class->enqueue_task(rq, p, flags);
}
而此时p->sched_class->enqueue_task的赋值,可以看到,它是
const struct sched_class fair_sched_class = {
.next = &idle_sched_class,
.enqueue_task = enqueue_task_fair,
... ...
}
正是通过enqueue_task_fair()将新进程加入到队列中去
当系统调用schedule()函数时,会调用pick_next_task函数来选择一个新进程。
static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev)
{
const struct sched_class *class = &fair_sched_class;
... ....
//一般来说都是使用fair_sched调用,开始选择进程了
p = fair_sched_class.pick_next_task(rq, prev);
.....
}
通过fair_sched_class的定义,可以查到pick_next_task的函数定义:
const struct sched_class fair_sched_class = {
.next = &idle_sched_class,
.enqueue_task = enqueue_task_fair,
... ...
.pick_next_task = pick_next_task_fair,
.put_prev_task = put_prev_task_fair,
查看pick_next_task_fail的代码
static struct task_struct *
pick_next_task_fair(struct rq *rq, struct task_struct *prev)
{
//从cfs队列中查找
struct cfs_rq *cfs_rq = &rq->cfs;
... ....
do {
if (curr && curr->on_rq)
update_curr(cfs_rq);
else
curr = NULL;
//链表中查找。
se = pick_next_entity(cfs_rq, curr);
cfs_rq = group_cfs_rq(se);
} while (cfs_rq);
p = task_of(se);
}
当进程被选出来之后,就会调用context_switch()函数进行进程切换,而这其中,切换的实际动作是由函数
static inline void
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next)
{
... ...
/* Here we just switch the register state and the stack. */
switch_to(prev, next, prev);
... ...
}
switch_to的实现是switch_to.h通过汇编实现
volatile("pushfl\n\t" /* save flags */ \//保存当前进程的flags
"pushl %%ebp\n\t" /* save EBP */ \//把当前进程的堆栈的基址压栈
"movl %%esp,%[prev_sp]\n\t" /* save ESP */ \//把当前的栈顶保存到prev->thread.sp
"movl %[next_sp],%%esp\n\t" /* restore ESP */ \//把下一进程的thread.sp保存到esp中,此时完成内核堆栈的切换。 从此句开始,所有的动作都是在next内核堆栈里面完成了。
"movl $1f,%[prev_ip]\n\t" /* save EIP */ \//保存当前进程的eip
"pushl %[next_ip]\n\t" /* restore EIP */ \//把next进程的thread->ip保存到next进程的堆栈中
__switch_canary \
"jmp __switch_to\n" /* regparm call */ \//使用寄存器传递参数。 即下面的a与d。 当函数__switch_to不是通过call来调用,因此,不会保存eip。当函数返回的时候,它还是会pop eip,此eip就是 $1f,即从下一条开始执行
"1:\t" \ //从此处开始,被认为是next进程开始执行了
"popl %%ebp\n\t" /* restore EBP */ \
"popfl\n" /* restore flags */ \
\
/* output parameters */ \
: [prev_sp] "=m" (prev->thread.sp), \ //当前进程是在内核态,此时sp为内核堆栈
[prev_ip] "=m" (prev->thread.ip), \//当前进程的eip
"=a" (last), \
\
/* clobbered output registers: */ \
"=b" (ebx), "=c" (ecx), "=d" (edx), \
"=S" (esi), "=D" (edi) \
\
__switch_canary_oparam \
\
/* input parameters: */ \
: [next_sp] "m" (next->thread.sp), \//下一进程的内核堆栈的栈底
[next_ip] "m" (next->thread.ip), \//下一个进程的执行的起点
\
/* regparm parameters for __switch_to(): */ \
[prev] "a" (prev), \
[next] "d" (next) \
\
__switch_canary_iparam \
\
: /* reloaded segment registers */ \
"memory"); \