#define switch_to(prev,next,last) do { \
asm volatile("pushl %%esi\n\t" \
"pushl %%edi\n\t" \
"pushl %%ebp\n\t" \
"movl %%esp,%0\n\t" /* save ESP */ \
"movl %3,%%esp\n\t" /* restore ESP */ \
"movl $1f,%1\n\t" /* save EIP */ \
"pushl %4\n\t" /* restore EIP */ \
"jmp __switch_to\n" /* 当__switch_to 从 ret返回时, 实际上相当于pop eip , 将22 行压入的地址 prec->thread.eip 放入eip, 也就是 1: 处 */ \
"1:\t" \
"popl %%ebp\n\t" \
"popl %%edi\n\t" \
"popl %%esi\n\t" \
:"=m" (prev->thread.esp),"=m" (prev->thread.eip), \
"=b" (last) \
:"m" (next->thread.esp),"m" (next->thread.eip), \
"a" (prev), "d" (next), \
"b" (prev)); \
} while (0)
这段代码玄机挺大的,这里面经过特殊有处理的寄存器有两个,一个是esp,一个是eip
先说esp,esp寄存器是很重要的寄存器,因为它代表了整个栈空间的首地址,栈中的局部变量的寻址全是靠esp寄存器,前面需要将edi,ebp,esp压入当前进程的内核空间栈中,所以esp寄存器是最后切换的,关于esp的地址没有压在堆栈中没有压在堆栈中的原因是:假使现在有3个进程ABC,每个进程的内核空间都有一个自己的栈,假如把A进程内核空间的栈,压入B进程的内核空间,那么从C进程切换到B进程的内核空间就无法切换,所以esp寄存器存放在了prev->thread.esp中,
关于restore eip,有一个问题是为什么使用jmp而不是使用call,其实几乎所有的任务换后,前者的eip都会指向1f处,但是有一个例外,如果prev进程也被switch_to出去过,那没问题,使用call也行,但如果该prev进程刚刚被创建,之前没有被switch_to出去过,那么next->thread.eip里存的将是ret_ftom_fork(参看copy_thread()函数)。这就是这里为什么不用call__switch_to而用jmp,因为call会导致自动把下面这句话的地址(也就是1f)压栈,然后__switch_to()就必然只能ret到这里,而无法根据需要ret到ret_from_fork。
int copy_thread(int nr, unsigned long clone_flags, unsigned long esp,
unsigned long unused,
struct task_struct * p, struct pt_regs * regs)
{
struct pt_regs * childregs;
childregs = ((struct pt_regs *) (THREAD_SIZE + (unsigned long) p)) - 1;
struct_cpy(childregs, regs);
childregs->eax = 0;
childregs->esp = esp;
p->thread.esp = (unsigned long) childregs;
p->thread.esp0 = (unsigned long) (childregs+1);
p->thread.eip = (unsigned long) ret_from_fork; //第一次创建的进程
savesegment(fs,p->thread.fs);
savesegment(gs,p->thread.gs);
unlazy_fpu(current);
struct_cpy(&p->thread.i387, ¤t->thread.i387);
return 0;
}
这里说的是子进程,第一次调度到子进程的时候,子进程的SAVE_ALL保存的寄存器内容和regs实际上被复制进入了子进程,而子进程的esp和esp0是被刚初始化好的,这时候还没有进行过任务切换,所以不存在之前的现场,所以switch_to中的pop操作执行就会有问题,所以如果要是切换到这个进程的时候,子进程直接从返回到ret_from_fork就行。