进程切换
前面所介绍的schedule()中调用了switch_to宏,这个宏实现了进程之间的真正切换,其代码存放于include/ i386/system.h:
1 #define switch_to(prev,next,last) do { \
2 asm volatile("pushl %%esi\n\t" \
3 "pushl %%edi\n\t" \
4 "pushl %%ebp\n\t" \
5 "movl %%esp,%0\n\t" /* save ESP */ \
6 "movl %3,%%esp\n\t" /* restore ESP */ \
// 将要运行进程next的内核栈指针next->thread.esp置入esp寄存器中。从现在开始,内核对next的内核栈 进行操作,因此,这条指令执行从prev到next真正的上下文切换,因为进程描述符的地址与其内核栈的地址 紧紧地联系在一起(参见第四章),因此,改变内核栈就意味着改变当前进程。如果此处引用current的话, 那就已经指向next的task_struct结构了。从这个意义上说,进程的切换在这一行指令执行完以后就已经完 成。但是,构成一个进程的另一个要素是程序的执行,这方面的切换尚未完成。
第七行用mov,因为是老的进程,在6行时栈就被切换了
7 "movl $1f,%1\n\t" /* save EIP */ \
//这里用的是movl把11行的pop %%ebp这句话的地址保存到pre->eip(下次将要执行的地址)
第八行用pushl ,因为是新的进程,在6行时栈就切换到了新进程栈
8 "pushl %4\n\t" /* restore EIP */ \
// 将next->thread.eip压入next的内核栈(第六行已经切换栈了)。那么,next->thread.eip究竟指向那个地 址?实际上,它就是 next 上一次被调离时通过第7行保存的地址,也就是第11行popl指令的地址。因 为,每个进程被调离时都要执行 这里的第7行,这就决定了每个进程(除了新创建的进程)在受到调度而恢复 执行时都从这里的第11行开始。
9 "jmp __switch_to\n" \
// 通过jump指令(而不是 call指令,call会保存返回地址,而这里事先压入了保存地址,见8行)转入一个函数__switch_to()。这个函数的具体实现将在下面介绍。当 CPU执行到__switch_to()函数的ret指令时,最后进 入堆栈的next->thread.eip就变成了返回地址,这就是标号 “1”的地址。
10 "1:\t" \
11 "popl %%ebp\n\t" \
12 "popl %%edi\n\t" \
13 "popl %%esi\n\t" \
14 :"=m" (prev->thread.esp),"=m" (prev->thread.eip), \ 输出 %0,%1
15 "=b" (last) \ %2
16 :"m" (next->thread.esp),"m" (next->thread.eip), \ 输入 %3,%4
17 "a" (prev), "d" (next), \
18 "b" (prev)); \
19 } while (0)
下面我们来讨论__switch_to()函数。
在调用__switch_to()函数之前,对其定义了fastcall :
extern void FASTCALL(__switch_to(struct task_struct *prev, struct task_struct *next));
表5.1
参数类型 | 参数名 | 内存变量 | 寄存器 | 函数参数 |
输出参数 | 0% | prev->thread.esp |
|
|
1% | prev->thread.eip |
|
| |
2% |
| ebx | last | |
输入参数 | 3% | next->thread.esp |
|
|
4% | next->thread.eip |
|
| |
5% |
| eax | prev | |
6% |
| edx | next | |
7% |
| ebx | prev |
fastcall对函数的调用不同于一般函数的调用,因为__switch_to()从寄存器(如表5.1)取参数,而不像一般函数那样从堆栈取参数,也就是说,通过寄存器eax和edx把prev和next 参数传递给__switch_to()函数。
void __switch_to(struct task_struct *prev_p, struct task_struct *next_p)
{
struct thread_struct *prev = &prev_p->thread,
*next = &next_p->thread;
struct tss_struct *tss = init_tss + smp_processor_id();
unlazy_fpu(prev_p);/* 如果数学处理器工作,则保存其寄存器的值*/
/* 将TSS中的内核级(0级)堆栈指针换成next->esp0,这就是next 进程在内核
栈的指针
tss->esp0 = next->esp0;
/* 保存fs和gs,但无需保存es和ds,因为当处于内核时,内核段
总是保持不变*/
asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->fs));
asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->gs));
/*恢复next进程的fs和gs */
loadsegment(fs, next->fs);
loadsegment(gs, next->gs);
/* 如果next挂起时使用了调试寄存器,则装载0~7个寄存器中的6个寄存器,其中第4、5个寄存器没有使用 */
if (next->debugreg[7]){
loaddebug(next, 0);
loaddebug(next, 1);
loaddebug(next, 2);
loaddebug(next, 3);
/* no 4 and 5 */
loaddebug(next, 6);
loaddebug(next, 7);
}
if (prev->ioperm || next->ioperm) {
if (next->ioperm) {
/*把next进程的I/O操作权限位图拷贝到TSS中 */
memcpy(tss->io_bitmap, next->io_bitmap,
IO_BITMAP_SIZE*sizeof(unsigned long));
/* 把io_bitmap在tss中的偏移量赋给tss->bitmap */
tss->bitmap = IO_BITMAP_OFFSET;
} else
/*如果一个进程要使用I/O指令,但是,若位图的偏移量超出TSS的范围,
* 就会产生一个可控制的SIGSEGV信号。第一次对sys_ioperm()的调用会
* 建立起适当的位图 */
tss->bitmap = INVALID_IO_BITMAP_OFFSET;
}
}