Linux kernel学习之进程切换

当前进程可能主动或被动地放弃CPU,如磁盘IO操作阻塞、时间片到期或者进程调用exit退出,此时将发生进程切换,即切换到另一个就绪态的进程。进程切换主要在context_switch()完成,该函数位于kernel/sched.c中,如下:

/*
 * context_switch - switch to the new MM and the new
 * thread's register state.
 */
static inline void
context_switch(struct rq *rq, struct task_struct *prev,
	       struct task_struct *next)
{
	struct mm_struct *mm, *oldmm;

	prepare_task_switch(rq, prev, next);
	mm = next->mm;
	oldmm = prev->active_mm;
	/*
	 * For paravirt, this is coupled with an exit in switch_to to
	 * combine the page table reload and the switch backend into
	 * one hypercall.
	 */
	arch_enter_lazy_cpu_mode();

	/*
	 * 新进程没有mm结构(存储当前进程的虚拟内存),即内核态进程。
	 * 此时借用prev进程的mm结构,内核态进程只访问内核态地址空间,
	 * 而内核态空间是所有进程共享的,不会有问题。
	 */
	if (unlikely(!mm)) {
		next->active_mm = oldmm;	//借用mm结构
		atomic_inc(&oldmm->mm_count);	//增加引用计数
		enter_lazy_tlb(oldmm, next);
	} else
		switch_mm(oldmm, mm, next);	//新进程有mm结构,切换到新进程的虚拟内存空间。

	if (unlikely(!prev->mm)) {
		prev->active_mm = NULL;
		rq->prev_mm = oldmm;
	}
	/*
	 * Since the runqueue lock will be released by the next
	 * task (which is an invalid locking op but in the case
	 * of the scheduler it's an obvious special-case), so we
	 * do an early lockdep release here:
	 */
#ifndef __ARCH_WANT_UNLOCKED_CTXSW
	spin_release(&rq->lock.dep_map, 1, _THIS_IP_);
#endif

	/* Here we just switch the register state and the stack. */
	switch_to(prev, next, prev);	//切换寄存器和栈状态,具体的切换动作在此完成

	barrier();
	/*
	 * this_rq must be evaluated again because prev may have moved
	 * CPUs since it called schedule(), thus the 'rq' on its stack
	 * frame will be invalid.
	 */
	finish_task_switch(this_rq(), prev);
}
具体的切换动作由switch_to()完成,然而在此之前,已经完成了虚拟内存的切换。但是在进程切换过程中,处于内核态,访问的都是内核地址空间,而内核地址空间是由所有进程共享的,因此不会出现问题。

switch_to()位于include/asm-x86/system_32.h中,是一段汇编代码,如下:

#define switch_to(prev,next,last) do {					\
	unsigned long esi,edi;						\
	asm volatile("pushfl\n\t"		/* Save flags */	\
		     "pushl %%ebp\n\t"					\
		     "movl %%esp,%0\n\t"	/* save ESP */		\
		     "movl %5,%%esp\n\t"	/* restore ESP */	\
		     "movl $1f,%1\n\t"		/* save EIP */		\
		     "pushl %6\n\t"		/* restore EIP */	\
		     "jmp __switch_to\n"				\
		     "1:\t"						\
		     "popl %%ebp\n\t"					\
		     "popfl"						\
		     :"=m" (prev->thread.esp),"=m" (prev->thread.eip),	\
		      "=a" (last),"=S" (esi),"=D" (edi)			\
		     :"m" (next->thread.esp),"m" (next->thread.eip),	\
		      "2" (prev), "d" (next));				\
} while (0)

它实际上是一个宏定义,主要完成保存prev进程状态,切换到next进程的操作。

首先将prev进程的EFLAGS和EBP寄存器压入栈中,然后把ESP保存在prev->thread.esp中,以保存prev进程的状态。

随后执行movl next.thread->esp, %esp,切换内核栈。此时,内核已经在next进程的内核栈中执行。

接下来,movl $1f, prev->thread.eip,将标号1的地址存储在prev进程的eip中,到下一次进程切换到prev时,就会从标号1处开始执行,可以看到标号1处就是将之前压入栈中的EFLAGS和EBP弹出栈,恢复环境。

之后pushl next->thread.eip,将next进程的运行地址压入栈中。注意接下来调用__switch_to函数时,使用jmp指令直接跳转,这样就不会将返回地址压栈。当__switch_to函数执行ret指令时,就会跳到之前压入的eip出执行,也就是说,当__switch_to函数返回时,cpu就正式执行next进程的代码了。


另外,切换进程只需要两个进程的信息,为什么switch_to需要3个参数呢?假设进程A切到B,则此刻A的内核栈上,调用context_switch()的两个参数prev和next分别指向A和B。如果B又切换到了C,最后由C切换到A。这时候切换到A时,prev应该指向C,但在A的内核栈上的prev却依然是A。在context_switch()函数的最后,还要使用prev这个参数,这样就导致了错误。于是就需要第三个参数last,在调用__switch_to返回时,eax指向prev,这时候就将其赋给last,那么在新进程的栈上就可以保存正确的值了。

switch_to的Intel风格汇编如下。注意switch_to是一个宏,不是函数。被调用时,prev和next是context_switch()函数的局部变量,利用ebp相对寻址,即ebp+prev_offset的形式。

mov eax, [ebp+prev_offset]                #AT&T汇编风格中输入部分的"2" (prev),和"=a"(last)使用同一个寄存器eax
mov edx, [ebp+next_offset]                #"d" (next)
push eflags                               #EFLAGS入栈
push ebp                                  #EBP入栈
mov [ebp+prev_offset]->thread.esp, esp    #保存esp
mov esp, [ebp+next_offset]->thread.esp    #切换ESP,注意此时已经切换到了next进程的内核栈
mov [ebp+prev_offset]->thread.eip, $1     #将标号1位置的地址保存到prev进程中的eip,再次切换到prev进程时,会从标号1处开始继续执行
push [ebp+next_offset]->thread.eip        #将next进程中保存的eip压入栈。如果next是之前被换出的,则此时的eip也是下面的标号1
jmp __switch_to                           #跳到__switch_to函数执行硬件上下文切换。
                                          #__switch_to是一个fastcall类型的函数,两个参数分别保存在eax和edx中,即prev和next。
                                          #使用jmp而非call进行跳转,ret时,会跳到当前栈顶内容指向的位置,也即刚才压入的next进程的eip
                                          #__switch_to的返回值是prev,即函数ret后,eax指向prev
1:
pop ebp                                   #__switch_to函数返回后,会跳到这里执行,此时将上次被切换出时保存的ebp恢复
pop eflags                                #恢复EFLAGS
mov [ebp+prev_offset], eax                #"=a" (last),注意此时的ebp已经是next进程的ebp,此时eax保存的是切换之前的prev


__switch_to()主要完成硬件上下文的切换,在这里就不过多讨论了。__switch_to函数返回时,cpu就开始执行下一个进程的代码了。

整个切换过程中的函数调用关系如下:

context_switch() --->  switch_to()  --->  __switch_to()

其中,context_switch()完成进程页表项的切换,switch_to()完成寄存器的切换,__switch_to()完成硬件上下文的切换。


参考资料:http://blog.csdn.net/xiaoxiaomuyu2010/article/details/11935393

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值