浅析Linux中的进程调度
2016-11-22
前面在看软中断的时候,牵扯到不少进程调度的知识,这方面自己确实一直不怎么了解,就趁这个机会好好学习下。
现代的操作系统都是多任务的操作系统,尽管随着科技的发展,硬件的处理器核心越来越多,但是仍然不能保证一个进程对应一个核心,这就势必需要一个管理单元,负责调度进程,由管理单元来决定下一刻应该由谁使用CPU,这里充当管理单元的就是进程调度器。
进程调度器的任务就是合理分配CPU时间给运行的进程,创造一种所有进程并行运行的错觉。这就对调度器提出了要求:
1、调度器分配的CPU时间不能太长,否则会导致其他的程序响应延迟,难以保证公平性。
2、调度器分配的时间也不能太短,每次调度会导致上下文切换,这种切换开销很大。
而调度器的任务就是:1、分配时间给进程 2、上下文切换
所以具体而言,调度器的任务就明确了:用一句话表述就是在恰当的实际,按照合理的调度算法,切换两个进程的上下文。
调度器的结构
在Linux内核中,调度器可以分成两个层级,在进程中被直接调用的成为通用调度器或者核心调度器,他们作为一个组件和进程其他部分分开,而通用调度器和进程并没有直接关系,其通过第二层的具体的调度器类来直接管理进程。具体架构如下图:
如上图所示,每个进程必然属于一个特定的调度器类,Linux会根据不同的需求实现不同的调度器类。各个调度器类之间具备一定的层次关系,即在通用调度器选择进程的时候,会从最高优先级的调度器类开始选择,如果通用调度器类没有可运行的进程,就选择下一个调度器类的可用进程,这样逐层递减。
每个CPU会维护一个调度队列称之为就绪队列,每个进程只会出现在一个就绪队列中,因为同一进程不能同时被两个CPU选中执行。就绪队列的数据结构为struct rq,和上面的层次结构一样,通用调度器直接和rq打交道,而具体和进程交互的是特定于调度器类的子就绪队列。
调度器类
在linux内核中实现了一个调度器类的框架,其中定义了调度器应该实现的函数,每一个具体的调度器类都要实现这些函数 。
在当前linux版本中(3.11.1),使用了四个调度器类:stop_sched_class、rt_sched_class、fair_sched_class、idle_sched_class。在最新的内核中又添加了一个调度类dl_sched_class,但是由于笔者能力所限,且大部分进程都是属于实时调度器和完全公平调度器,所以我们主要分析实时调度器和完全公平调度器。
看下调度器类的定义:
1 struct sched_class { 2 const struct sched_class *next; 3 4 void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags); 5 void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags); 6 void (*yield_task) (struct rq *rq); 7 bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt); 8 9 void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags); 10 11 struct task_struct * (*pick_next_task) (struct rq *rq); 12 void (*put_prev_task) (struct rq *rq, struct task_struct *p); 13 14 #ifdef CONFIG_SMP 15 int (*select_task_rq)(struct task_struct *p, int sd_flag, int flags); 16 void (*migrate_task_rq)(struct task_struct *p, int next_cpu); 17 18 void (*pre_schedule) (struct rq *this_rq, struct task_struct *task); 19 void (*post_schedule) (struct rq *this_rq); 20 void (*task_waking) (struct task_struct *task); 21 void (*task_woken) (struct rq *this_rq, struct task_struct *task); 22 23 void (*set_cpus_allowed)(struct task_struct *p, 24 const struct cpumask *newmask); 25 26 void (*rq_online)(struct rq *rq); 27 void (*rq_offline)(struct rq *rq); 28 #endif 29 30 void (*set_curr_task) (struct rq *rq); 31 void (*task_tick) (struct rq *rq, struct task_struct *p, int queued); 32 void (*task_fork) (struct task_struct *p); 33 34 void (*switched_from) (struct rq *this_rq, struct task_struct *task); 35 void (*switched_to) (struct rq *this_rq, struct task_struct *task); 36 void (*prio_changed) (struct rq *this_rq, struct task_struct *task, 37 int oldprio); 38 39 unsigned int (*get_rr_interval) (struct rq *rq, 40 struct task_struct *task); 41 42 #ifdef CONFIG_FAIR_GROUP_SCHED 43 void (*task_move_group) (struct task_struct *p, int on_rq); 44 #endif 45 };
enqueue_task向就绪队列添加一个进程,该操作发生在一个进程变成就绪态(可运行态)的时候。
dequeue_task就是执行enqueue_task的逆操作,在一个进程由运行态转为阻塞的时候就会发生该操作。
yield_task用于进程自愿放弃控制权的时候。
pick_next_task用于挑选下一个可运行的进程,发生在进程调度的时候,又调度器调用。
set_curr_task当进程的调度策略发生变化时,需要执行此函数
task_tick,在每次激活周期调度器时,由周期调度器调用。
task_fork用于建立fork系统调用和调度器之间的关联,每次新进程建立后,就调用该函数通知调度器。
就绪队列
如前所述,每个CPU维护一个就绪队列,由结构struct rq表示,通用调度器直接和rq交互,在rq中又维护了子就绪队列,这些子就绪队列和具体的调度器类相关,进程入队出队都需要根据调度器类的具体算法。
rq为维护针对当前CPU而言全局的信息,其结构比较庞大,这里就不详细列举。其大致内容包括当前CPU就绪队列包含的所有进程数、记载就绪队列当前负荷的度量,并嵌入子就绪队列cfs_rq和rt_rq等,系统所有的就绪队列位于一个runqueues数组中,每个CPU对应一个元素。
内核也定义了一些宏,操作这些全局的队列:
1 #define cpu_rq(cpu) (&per_cpu(runqueues, (cpu))) 2 #define this_rq() (&__get_cpu_var(runqueues)) 3 #define task_rq(p) cpu_rq(task_cpu(p)) 4 #define cpu_curr(cpu) (cpu_rq(cpu)->curr) 5 #define raw_rq() (&__raw_get_cpu_var(runqueues))
调度实体
linux中可调度的不仅仅是进程,也可能是一个进程组,所以LInux就把调度对象抽象化成一个调度实体。就像是很多结构中嵌入list_node用于连接链表一样,这里需要执行调度的也就需要加入这样一个调度实体。实际上,调度器直接操作的也是调度实体,只是会根据调度实体获取到其对应的结构。
1 struct sched_entity { 2 struct load_weight load; /* for load-balancing */ 3 struct rb_node run_node; 4 struct list_head group_node; 5 unsigned int on_rq; 6 7 u64 exec_start; 8 u64 sum_exec_runtime; 9 u64 vruntime; 10 u64 prev_sum_exec_runtime; 11 12 u64 nr_migrations; 13 14 #ifdef CONFIG_SCHEDSTATS 15 struct sched_statistics statistics; 16 #endif 17 18 #ifdef CONFIG_FAIR_GROUP_SCHED 19 struct sched_entity *parent; 20 /* rq on which this entity is (to be) queued: */ 21 struct cfs_rq *cfs_rq; 22 /* rq "owned" by this entity/group: */ 23 struct cfs_rq *my_q; 24 #endif 25 26 #ifdef CONFIG_SMP 27 /* Per-entity load-tracking */ 28 struct sched_avg avg; 29 #endif 30 };
load用于负载均衡,决定了各个实体占队列中负荷的比例,计算负荷权重是调度器的主要责任,因为选择下一个进程就是要根据这些信息。run_node是一个红黑树节点,用于把实体加入到红黑树,on_rq表明该实体是否位于就绪队列,当为1的时候就说明在就绪队列中,一个进程在得到调度的时候会从就绪队列中摘除,在让出CPU的时候会重新添加到就绪队列(正常调度的情况,不包含睡眠、等待)。在后面有一个时间相关的字段,exec_start记录进程开始在CPU上运行的时间;sum_exec_time记录进程一共在CPU上运行的时间,pre_sum_exec_time记录本地调度之前,进程已经运行的时间。在进程被调离CPU的时候,会把sum_exec_time的值保存到pre_sum_exec_time,而sum_exec_time并不重置,而是一直随着在CPU上的运行递增。而vruntime 记录在进程执行期间,在虚拟时钟上流逝的时间,用于CFS调度器,后面会具体讲述。后面的parent、cfs_rq、my_rq是和组调度相关的,这里我们暂且不涉及。
看下load字段
struct load_weight { unsigned long weight, inv_weight; };
这里有两个字段,weight和inv_weight。前者是当前实体优先级对应的权重,这个可以根据prio_to_weight数组转化得到。而后者是是用于快速计算vruntime用的,可以通过prio_to_wmult数组得到,后者是一个和prio_to_weight同样大小的数组,每一项的值为2^32/weight,内核中的除法运算没那么简单,为了加速操作,选取的折中办法。vruntime的计算可以参考calc_delta_mine函数。
到这里,调度器的基本架构就比较清楚了,调度过程中需要计算进程的优先级,这点是比较复杂的过程,我们单独分一节描述,下面根据CFS调度类探索下进程调度的过程。
- 什么时候调度
- 如何进行调度
进程调度并不是什么时候都可以,前面也说过,系统会有一个周期调度器,根据频率自动调用schedule_tick函数。其主要作用就是根据进程运行时间触发调度;在进程遇到资源等待被阻塞也可以显示的调用调度器函数进行调度;另外在有内核空间返回到用户空间时,会判断当前是否需要调度,在进程对应的thread_info结构中,有一个flag,该flag字段的第二位(从0开始)作为一个重调度标识TIF_NEED_RESCHED,当被设置的时候表明此时有更高优先级的进程,需要执行调度。另外目前的内核支持内核抢占功能,在适当的时机可以抢占内核的运行。关于内核抢占,我们最后论述。
而至于如何进行调度呢?就要看具体调度器类了。一旦确定了要进行调度,那么schedule函数被调用。注意,周期性调度器并不直接调度,至多设置进程的重调度位TIF_NEED_RESCHED,这样在返回用户空间的时候仍然由主调度器执行调度。跟踪下schedule函数,其实具体实现由__schedule函数完成,直接看该函数:
1 static void __sched __schedule(void) 2 { 3 struct task_struct *prev, *next; 4 unsigned long *switch_count; 5 struct rq *rq; 6 int cpu; 7 8 need_resched: 9 /*禁止内核抢占*/ 10 preempt_disable(); 11 cpu = smp_processor_id(); 12 /*获取CPU 的调度队列*/ 13 rq = cpu_rq(cpu); 14 rcu_note_context_switch(cpu); 15 /*保存当前任务*/ 16 prev = rq->curr; 17 18 schedule_debug(prev); 19 20 if (sched_feat(HRTICK)) 21 hrtick_clear(rq); 22 23 /* 24 * Make sure that signal_pending_state()->signal_pending() below 25 * can't be reordered with __set_current_state(TASK_INTERRUPTIBLE) 26 * done by the caller to avoid the race with signal_wake_up(). 27 */ 28 smp_mb__before_spinlock(); 29 raw_spin_lock_irq(&rq->lock); 30 31 switch_count = &prev->nivcsw; 32 /* 如果内核态没有被抢占, 并且内核抢占有效 33 即是否同时满足以下条件: 34 1 该进程处于停止状态 35 2 该进程没有在内核态被抢占 */ 36 if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) { 37 if (unlikely(signal_pending_state(prev->state, prev))) { 38 prev->state = TASK_RUNNING; 39 } else { 40 deactivate_task(rq, prev, DEQUEUE_SLEEP); 41 prev->on_rq = 0; 42 43 /* 44 * If a worker went to sleep, notify and ask workqueue 45 * whether it wants to wake up a task to maintain 46 * concurrency. 47 */ 48 if (prev->flags & PF_WQ_WORKER) { 49 struct task_struct *to_wakeup; 50 51 to_wakeup = wq_worker_sleeping(prev, cpu); 52 if (to_wakeup) 53 try_to_wake_up_local(to_wakeup); 54 } 55 } 56 switch_count = &prev->nvcsw; 57 } 58 59 pre_schedule(rq, prev); 60 61 if (unlikely(!rq->nr_running)) 62 idle_balance(cpu, rq); 63 /*告诉调度器prev进程即将被调度出去*/ 64 put_prev_task(rq, prev); 65 /*挑选下一个可运行的进程*/ 66 next = pick_next_task(rq); 67 /*清除pre的TIF_NEED_RESCHED标志*/ 68 clear_tsk_need_resched(prev); 69 rq->skip_clock_update = 0; 70 /*如果next和当前进程不一致,就可以调度*/ 71 if (likely(prev != next)) { 72 rq->nr_switches++; 73 /*设置当前调度进程为next*/ 74 rq->curr = next; 75 ++*switch_count; 76 /*切换进程上下文*/ 77 context_switch(rq, prev, next); /* unlocks the rq */ 78 /* 79 * The context switch have flipped the stack from under us 80 * and restored the local variables which were saved when 81 * this task called schedule() in the past. prev == current 82 * is still correct, but it can be moved to another cpu/rq. 83 */ 84 cpu = smp_processor_id(); 85 rq = cpu_rq(cpu); 86 } else 87 raw_spin_unlock_irq(&rq->lock); 88 89 post_schedule(rq); 90 91 sched_preempt_enable_no_resched(); 92 if (need_resched()) 93 goto need_resched; 94 }
调度器运行期间是要禁止内核抢占的,从级别上来讲,LInux中的调度器不见得比其他进程的级别高,但是肯定不会低于普通进程,即调度器运行期间会禁止内核抢占。相比之下,windows中使用中断请求级别的概念,普通进程运行在passive level,而调度器运行在DPC level,调度器运行期间只有硬件中断可以打断。从函数代码来看,这里首先调用了preempt_disable函数设置了preempt_count禁止内核抢占,然后获取当前CPU的就绪队列结构rq,prev保存当前任务,下面的prev->state && !(preempt_count() & PREEMPT_ACTIVE)是对有些进行移除运行队列。具体就是如果当前进程是阻塞并且PREEMPT_ACTIVE没有被设置,就有了移除就绪队列的条件,然后判断是否又挂起的信号,如果有,那么暂时不移除队列,否则就执行deactivate_task函数移除队列,并设置prev->on_rq=0,表明该进程不在就绪队列中。下面的if是判断如果当前进程是一个工作线程,那么就通知工作队列,看是否需要唤醒另一个worker。
出了if就调用了pre_schedule,该函数在CFS中没有实现,而在实时调度器中实现了,具体什么作用不太清楚。
下面的一个if判断当前CPU就绪队列是否存在可运行的进程,如果不存在即没有进程可以运行就调用idle_balance从其他的CPU平衡一下任务。当然这种情况极少见。
接下来就要进行正式工作了,调用put_prev_task预处理下,具体是调用对应调度器类的实现函数:prev->sched_class->put_prev_task(rq, prev);主要任务是把当前任务重新加入就绪队列。当然在此之前如果当前任务还在就绪队列(或者说是当前任务是否是可运行状态),就调用update_curr更新下其进程时间,包括vruntime等。
重要的是下面的pick_next_task,它使用对应的调度器类选择一个具体的任务作为下一个占用CPU的任务,选定好之后就调用clear_tsk_need_resched清楚prev的重调度标识。
之后进行if判断,如果prev不是我们选择的下一个进程,就执行进程的切换。具体 先设置就绪队列的切换计数nr_switches,然后设置rq->curr=next,这里就从就绪队列而言,已经标识next为当前进程了。然后就调用context_switch函数切换上下文,主要包含两部分:切换地址空间、切换寄存器域。之后就开启内核抢占,这样一个进程切换就完成了。最后会判断是否又有新设置的高优先级进程,有的话再次执行调度。
前面大致过程如前所述,但是具体而言,下一个进程的执行是从替换了进程的EIP开始执行的,即调度器函数不到结束就开始运行另一个进程了,而待下次这个进程重新获得控制权时,就从之前保存的状态开始运行。在__schedule函数的最后仍然会判断是否被设置了重调度位,如果被设置了,那么很不幸,又要被调度出去了,但是这种几率很小,只是以防万一而已。这样出了调度函数,正常运行了。
核心函数实现分析:
pick_next_task
1 static inline struct task_struct * 2 pick_next_task(struct rq *rq) 3 { 4 const struct sched_class *class; 5 struct task_struct *p; 6 7 /* 8 * Optimization: we know that if all tasks are in 9 * the fair class we can call that function directly: 10 */ 11 /*如果所有任务都处于完全公平调度类,则可以直接选择下一个任务*/ 12 if (likely(rq->nr_running == rq->cfs.h_nr_running)) { 13 p = fair_sched_class.pick_next_task(rq); 14 if (likely(p)) 15 return p; 16 } 17 /*从优先级最高的调度器类开始遍历,顺序为stop_sched_class->rt_scheduled_class->fair_schedled_class->idle_sched_class*/ 18 /* 19 #define for_each_class(class) \ 20 for (class = sched_class_highest; class; class = class->next) 21 */ 22 for_each_class(class) { 23 p = class->pick_next_task(rq); 24 if (p) 25 return p; 26 } 27 28 BUG(); /* the idle class will always have a runnable task */ 29 }
该函数还是处于主调度器的层面,没有涉及到核心逻辑,所以还比较好理解。首先判断当前CPu就绪队列上的可运行进程数和CFS就绪队列上的可运行进程数是否一致,如果一致就说明当前主就绪队列上没有只有CFS调度类的进程,那么这样直接调用CFS调度类的方法挑选下一个进程即可。否则还需要从最高级的调度类,层层选择。下面的for_each_class便是实现这个功能。它按照stop_sched_class->rt_scheduled_class->fair_schedled_class->idle_sched_class这个顺序,依次调用其pick函数,只有前一个调度类没有找到可运行的进程,才会查找后一个调度类。我们这里值看CFS的实现:
在fair.c中,对应的函数是pick_next_task_fair
1 static struct task_struct *pick_next_task_fair(struct rq *rq) 2 { 3 struct task_struct *p; 4 /*从CPU 的就绪队列找到公平调度队列*/ 5 struct cfs_rq *cfs_rq = &rq->cfs; 6 struct sched_entity *se; 7 /*如果公平调度类没有可运行的进程,直接返回*/ 8 if (!cfs_rq->nr_running) 9 return NULL; 10 /*如果调度的是一组进程,则需要进行循环设置,否则执行一次就退出了*/ 11 do { 12 /*从公平调度类中找到一个可运行的实体*/ 13 se = pick_next_entity(cfs_rq); 14 /*设置红黑树中下一个实体,并标记cfs_rq->curr为se*/ 15 set_next_entity(cfs_rq, se); 16 cfs_rq = group_cfs_rq(se); 17 } while (cfs_rq); 18 /*获取到具体的task_struct*/ 19 p = task_of(se); 20 if (hrtick_enabled(rq)) 21 hrtick_start_fair(rq, p); 22 23 return p; 24 }
代码也是比较简单的,核心在下面的那个do循环中,从这里看该循环做了两个事情,调用pick_next_entity从CFS就绪队列中选择一个调度实体,然后调用set_next_entity设置下一个可以调度的任务,由于CFS的调度实体通过红黑树维护,所以这里实际上是调整红黑树的过程。而使用循环时应用与组调度的场合,这里我们暂且忽略。
看下pick_next_entity
1 static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq) 2 { 3 /*从红黑树中找到最左边即等待时间最长的那个实体*/ 4 struct sched_entity *se = __pick_first_entity(cfs_rq); 5 struct sched_entity *left = se; 6 7 /* 8 * Avoid running the skip buddy, if running something else can 9 * be done without getting too unfair. 10 */ 11 if (cfs_rq->skip == se) { 12 struct sched_entity *second = __pick_next_entity(se); 13 if (second && wakeup_preempt_entity(second, left) < 1) 14 se = second; 15 } 16 17 /* 18 * Prefer last buddy, try to return the CPU to a preempted task. 19 */ 20 if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1) 21 se = cfs_rq->last; 22 23 /* 24 * Someone really wants this to run. If it's not unfair, run it. 25 */ 26 if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1) 27 se = cfs_rq->next; 28 29 clear_buddies(cfs_rq, se); 30 31 return se; 32 }
该函数核心在__pick_first_entity,其本身也是很简答的,不妨看下代码:
1 struct sched_entity *__pick_first_entity(struct cfs_rq *cfs_rq) 2 { 3 struct rb_node *left = cfs_rq->rb_leftmost; 4 5 if (!left) 6 return NULL; 7 8 return rb_entry(left, struct sched_entity, run_node); 9 }
以为每次选择后都会设置好下一个应该选择的,所以这里仅仅获取下cfs_rq->rb_leftmost就可以了,然后就进行了三个if判断,但是都使用了同一个函数wakeup_preempt_entity
然后返回相应的调用实体。last表示最后一个调用唤醒操作的进程,next表示最后一个被唤醒的进程。
1 static int 2 wakeup_preempt_entity(struct sched_entity *curr, struct sched_entity *se) 3 { 4 s64 gran, vdiff = curr->vruntime - se->vruntime; 5 6 if (vdiff <= 0) 7 return -1; 8 9 gran = wakeup_gran(curr, se); 10 if (vdiff > gran) 11 return 1; 12 13 return 0; 14 }
这里是对比两个实体的vruntime,如果curr->vruntime - se->vruntime大于一个固定值,那么就返回1.这个值一般是sysctl_sched_wakeup_granularity。
所以这里逻辑当选定一个实体后,判断该实体是否是cfs_rq指定跳过的实体,如果是就选择下一个实体,判断该实体和上一个实体的vruntime的差距,只要不大于阈值,就可以接收从而选择后者。
在设定好初始实体后,判断cfs_rq->last和left的vruntime,如果在可接受的范围内,则选择cfs_rq->last,然后接着判断cfs_rq->next,如果仍然在可接收的范围内,就选择cfs_rq->next作为最终选定的调度实体。NEXT_BUDDY表示在cfs选择next sched_entity的时候会优先选择最后一个唤醒的sched_entity,而 LAST_BUDDY表示在cfs选择next sched_entity的时候会优先选择最后一个执行唤醒操作的那个sched_entity,这两种调度策略都有助于提高cpu cache的命中率。从代码来看,next比last优先级更高!
1 static void 2 set_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *se) 3 { 4 /* 'current' is not kept within the tree. */ 5 /*如果该实体处于就绪态,就可以被调度*/ 6 if (se->on_rq) { 7 /* 8 * Any task has to be enqueued before it get to execute on 9 * a CPU. So account for the time it spent waiting on the 10 * runqueue. 11 */ 12 update_stats_wait_end(cfs_rq, se); 13 /*把se出队列,然后选取一个实体设置到红黑树的最左边*/ 14 __dequeue_entity(cfs_rq, se); 15 } 16 17 update_stats_curr_start(cfs_rq, se); 18 cfs_rq->curr = se; 19 #ifdef CONFIG_SCHEDSTATS 20 /* 21 * Track our maximum slice length, if the CPU's load is at 22 * least twice that of our own weight (i.e. dont track it 23 * when there are only lesser-weight tasks around): 24 */ 25 if (rq_of(cfs_rq)->load.weight >= 2*se->load.weight) { 26 se->statistics.slice_max = max(se->statistics.slice_max, 27 se->sum_exec_runtime - se->prev_sum_exec_runtime); 28 } 29 #endif 30 se->prev_sum_exec_runtime = se->sum_exec_runtime; 31 }
该函数首先判断选定的实体是否可运行,如果可以就调用调度器的出队函数,把进程出队,然后调整红黑树,这里为何用个if笔者不是很懂,进程被调用前都要入队,所以这里选定的se,其on_rq肯定为1呀。难道不是么?之后设置se->exec_start记录开始运行的时间,然后设置cfs—>curr=se。之后就是设置时间等操作。然后就执行返回了。回到pick_next_task_fair函数中,这里接下来就返回了实体对应的task_struct。
context_switch函数
1 static inline void 2 context_switch(struct rq *rq, struct task_struct *prev, 3 struct task_struct *next) 4 { 5 struct mm_struct *mm, *oldmm; 6 /*进程切换准备工作,需要枷锁和关中断,最后需要调用finish_task_switch*/ 7 prepare_task_switch(rq, prev, next); 8 9 mm = next->mm; 10 oldmm = prev->active_mm; 11 /* 12 * For paravirt, this is coupled with an exit in switch_to to 13 * combine the page table reload and the switch backend into 14 * one hypercall. 15 */ 16 arch_start_context_switch(prev); 17 /*如果将要执行的是内核线程*/ 18 if (!mm) { 19 next->active_mm = oldmm; 20 atomic_inc(&oldmm->mm_count); 21 enter_lazy_tlb(oldmm, next); 22 } else 23 switch_mm(oldmm, mm, next); 24 /*如果被调度的是内核线程*/ 25 if (!prev->mm) { 26 prev->active_mm = NULL; 27 rq->prev_mm = oldmm; 28 } 29 /* 30 * Since the runqueue lock will be released by the next 31 * task (which is an invalid locking op but in the case 32 * of the scheduler it's an obvious special-case), so we 33 * do an early lockdep release here: 34 */ 35 #ifndef __ARCH_WANT_UNLOCKED_CTXSW 36 spin_release(&rq->lock.dep_map, 1, _THIS_IP_); 37 #endif 38 39 context_tracking_task_switch(prev, next); 40 /* Here we just switch the register state and the stack. */ 41 /*切换寄存器域和栈*/ 42 switch_to(prev, next, prev); 43 44 barrier(); 45 /* 46 * this_rq must be evaluated again because prev may have moved 47 * CPUs since it called schedule(), thus the 'rq' on its stack 48 * frame will be invalid. 49 */ 50 finish_task_switch(this_rq(), prev); 51 }
这部分内容主要做了两件事情:切换地址空间、切换寄存器域和栈空间。整个切换过程需要加锁和关中断,首先切换的是地址空间,mm 和active_mm分别代表调度和被调度的进程的 mm_struct,如果mm为空,则表明next是内核线程,内核线程没有自己独立的地址空间,所以其mm为null,运行的时候使用prev的active_mm即可。如果非空,则是用户进程,那么可以直接切换,这里调用
switch_mm函数进行切换;如果prev为内核线程,由于其没有独立地址空间,所以需要设置其active_mm为null。
接下来就要调用switch_to来切换寄存器域和栈了。这也是进程切换的最后部分。
1 #define switch_to(prev, next, last) \ 2 do { \ 3 /* \ 4 * Context-switching clobbers all registers, so we clobber \ 5 * them explicitly, via unused output variables. \ 6 * (EAX and EBP is not listed because EBP is saved/restored \ 7 * explicitly for wchan access and EAX is the return value of \ 8 * __switch_to()) \ 9 */ \ 10 unsigned long ebx, ecx, edx, esi, edi; \ 11 \ 12 asm volatile("pushfl\n\t" /* save flags */ \ 13 "pushl %%ebp\n\t" /* save EBP */ \ 14 "movl %%esp,%[prev_sp]\n\t" /* save ESP */ \ 15 "movl %[next_sp],%%esp\n\t" /* restore ESP */ \ 16 "movl $1f,%[prev_ip]\n\t" /* save EIP */ \ 17 "pushl %[next_ip]\n\t" /* restore EIP */ \ 18 __switch_canary \ 19 "jmp __switch_to\n" /* regparm call */ \ 20 "1:\t" \ 21 "popl %%ebp\n\t" /* restore EBP */ \ 22 "popfl\n" /* restore flags */ \ 23 \ 24 /* output parameters */ \ 25 : [prev_sp] "=m" (prev->thread.sp), \ 26 [prev_ip] "=m" (prev->thread.ip), \ 27 "=a" (last), \ 28 \ 29 /* clobbered output registers: */ \ 30 "=b" (ebx), "=c" (ecx), "=d" (edx), \ 31 "=S" (esi), "=D" (edi) \ 32 \ 33 __switch_canary_oparam \ 34 \ 35 /* input parameters: */ \ 36 : [next_sp] "m" (next->thread.sp), \ 37 [next_ip] "m" (next->thread.ip), \ 38 \ 39 /* regparm parameters for __switch_to(): */ \ 40 [prev] "a" (prev), \ 41 [next] "d" (next) \ 42 \ 43 __switch_canary_iparam \ 44 \ 45 : /* reloaded segment registers */ \ 46 "memory"); \ 47 } while (0)
实际上switch_to是一个宏,由一大串的汇编代码实现,这段代码稍微有点复杂,首先把标识寄存器压栈,然后ebp入栈,接着保存当前esp指针到prev->thread.sp变量中,第15行就把next->thread.sp设置到当前esp寄存器了,也就是说从现在开始使用的是next进程的栈,但是EBP 还没有切换,所以还可以使用prev进程的变量。接下来movl $1f,%[prev_ip]是把标号1的地址保存到prev->thread.ip,这个就是下次prev进程被调度的时候,开始执行的IP。到目前为止,prev进程的状态域就保存好了,接下来pushl %[next_ip]是把next进程的起始EIP压栈,因为后面直接调用一个函数,所以在函数返回后执行执行ret指令,就直接从栈中取出地址放到EIP开始执行,next进程正是从此处开始执行即代码中标号1的位置。第19行直接使用jmp跳转到目标函数__switch_to,主要是使用call会自动push eip,这样函数返回后又从原位置开始,要执行next还需要手动切换EIP,比较麻烦。
切换到next后,就从next进程的标号1位置开始,即popl %%ebp,popfl.需要注意的是,__switch_to的参数在最下方的部分
[prev] "a" (prev), \
[next] "d" (next)
被放到eax和edx中,不明白的人可能怎么也发现不了,涉及到AT&T汇编语法,这里就不在多说,具体可以参考相关文档。
到这里进程便切换过来了,但是细心的人可能会注意到,这里switch_to本来仅仅是切换两个进程,却传递进去三个参数,这是为何?
具体来说,这是为了让被调度到的进程知道在他之前运行的实际进程,为何这么说呢?
下面三个调度,在switch_to执行的时候,状态如下:
1、A——>B prev=A next=B
2、B——>C prev=B next=C
3、C——>A prev=C next=A
看第三次调度的时候,这个时候A重新获得控制权,恢复了A的栈状态,即在A的进程空间,prev=A next=B,而A并不知道在他之前实际运行的是C,所以需要一种方式告知A,在他之前实际运行的进程是C。
参考资料:
- http://blog.chinaunix.net/uid-27767798-id-3548384.html
- linux内核3.11.1源码