进程切换时的堆栈切换
下图为产生时间中断后,时间中断处理程序为两个用户态进程P1、P2进行进程切换的过程中,堆栈切换的简单图示:
进程P1在用户态执行,8253可编程计时器产生时间中断。
依据TSS中记录的进程P1的 SS0:EPS0 ,从P1的用户态堆栈切换至P1的内核堆栈(注意啊,不同进程对应的内核堆栈不同!!!),并将P1的现场信息压入内核堆栈中,跳转执行时间中断处理程序。
进程P1的处理时间片耗尽,切换至就绪状态的进程P2,并从当前P1的内核堆栈切换至P2的内核堆栈。
从进程P2的内核堆栈中弹出P2的现场信息,切换至P2的用户态堆栈,从时间中断处理程序返回执行P2。
D O 31 0 31 0
I F +-------+-------+ +-------+-------+
R |#######|#######| OLD |#######|#######|
E E +-------+-------+ SS:ESP OF P1 +-------+-------+
C X |#######|#######| | |#######|#######|
T P +-------+-------+<----+ +-------+-------+
I A | | |#######|#######| NEW
O N | | +-------+-------+ SS:ESP OF P2
N S | | |#######|#######| |
I | | +-------+-------+<----+
| O | | | |
| N | | | |
| | | | |
! * * * *
* * * *
+-- * * * * <-+
| USER STACK OF P1 USER STACK OF P2 |
| |
| ENTER | LEAVE
| KERNEL | KERNEL
| SPACE | SPACE
| |
D O | 31 0 31 0 |
I F +-> +-------+-------+ ---------------> +-------+-------+ --+
R |#######|#######| SWITCH |#######|#######|
E E +-------+-------+ KERNEL STACK +-------+-------+
C X |#######|#######| |#######|#######|
T P +---------------+ +---------------+
I A | PID | | PID |
O N +---------------+ +---------------+
N S | SLEEPTIME | | SLEEPTIME |
I +---------------+ +---------------+
| O | TIMECOUNT | | TIMECOUNT |
| N +---------------+ +---------------+
| | STATE | | STATE |
! +-------+-------+<----+ +-------+-------+<----+
|#######| SS | | |#######| SS | |
+---------------+ SS0:ESP0 OF P1 +---------------+ SS0:ESP0 OF P2
| ESP | FROM TSS | ESP | FROM TSS
+---------------+ +---------------+
| EFLAGS | | EFLAGS |
+-------+-------+ +-------+-------+
|#######| CS | |#######| CS |
+-------+-------+ +-------+-------+
| EIP | | EIP |
+---------------+ +---------------+
| ERROR | | ERROR |
+---------------+ +---------------+
| IRQ | | IRQ |
+---------------+ +---------------+
| EAX | | EAX |
+---------------+ +---------------+
| ECX | | ECX |
+---------------+ +---------------+
| EDX | | EDX |
+---------------+ +---------------+
| EBX | | EBX |
+---------------+ +---------------+
| XXX | | XXX |
+---------------+ +---------------+
| EBP | | EBP |
+---------------+ +---------------+
| ESI | | ESI |
+---------------+ +---------------+
| EDI | | EDI |
+-------+-------+ +-------+-------+
|#######| DS | |#######| DS |
+-------+-------+ +-------+-------+
|#######| ES | |#######| ES |
+-------+-------+ +-------+-------+
|#######| FS | NEW |#######| FS | OLD
+-------+-------+ SS:ESP OF P1 +-------+-------+ SS:ESP OF P2
|#######| GS | | |#######| GS | |
+-------+-------+<----+ +-------+-------+<----+
| | | |
* * * *
* * * *
* * * *
KERNEL STACK OF P1 KERNEL STACK OF P2
在 lab2里,只有一个用户进程,我们将tss中的ss0:esp0设成了一个固定的值,这样用户进程的内核栈固 定。但是在 lab3里,你将面临堆栈切换的问题,也就是每个用户进程的内核堆栈也是不一样的,所以每次切换进程时需要将tss的esp0设置对应用户进程的内核堆栈位置。
回顾:中断
我猜现在很多人已经蒙了,那么我们捋一遍(具体回顾lab2手册)。当我们陷入中断时,硬件做的事情如下:
当没有特权级转换的时候(也就是在内核态产生中断,内核态到内核态):
依次把eflags,cs,eip入栈,按照中断门描述符进入相应处理程序
当有特权级转换的时候(往往是用户态进入内核态):
先从用户进程对应的TSS里面取出对应内核栈的ss和esp
依次把eflags,cs,eip入栈,按照中断门描述符进入相应处理程序
然后,根据我们doIrq.S,下面是软件要做的事情:
如果硬件没有push errorcode的话,就push errorcode
esp入栈(当做StackFrame结构体的指针!)
irqHandle里面有一个很奇妙的处理(暂时不用管):
void irqHandle(struct StackFrame *sf) { // pointer sf = esp
/* Reassign segment register */
asm volatile("movw %%ax, %%ds"::"a"(KSEL(SEG_KDATA)));
/* Save esp to stackTop 为了中断嵌套 */
pcb[current].stackTop=(uint32_t)sf;
switch(sf->irq) {
case -1:
break;
case 0xd:
GProtectFaultHandle(sf);
break;
case 0x20:
timerHandle(sf);
break;
case 0x80:
syscallHandle(sf);
break;
default:assert(0);
}
}
现在,你去看看StackFrame结构体,发现是不是我上面进入irqHandle之前push到栈里的东西,刚好依次对应StackFrame的每一个成员。
现在我们到了烧脑时间了!!!
我们看一下kvm.c里面的initProc函数,这里面初始化了进程:
// user process
pcb[1].stackTop = (uint32_t)&(pcb[1].regs);
pcb[1].state = STATE_RUNNABLE;
pcb[1].timeCount = 0;
这一系列迷惑操作...究竟是啥玩意?我们通过查看pcb的结构,可以看出来:
如果一时半会想不出答案。可以继续往下看,下面提供一个进程切换代码(可以直接用在实验里):
//switch process
uint32_t tmpStackTop=pcb[current].stackTop;
tss.esp0=(uint32_t)&(pcb[current].stackTop);
asm volatile("movl %0,%%esp"::"m"(tmpStackTop));
asm volatile("popl %gs");
asm volatile("popl %fs");
asm volatile("popl %es");
asm volatile("popl %ds");
asm volatile("popal");
asm volatile("addl $8,%esp");
asm volatile("iret");
emmm......把进程控制块的stackTop赋值给了esp,然后一番pop操作,然后iret...好像和陷入中断是反过来的,那么我们会发现,实际上stackTop就是......
这样一番操作有什么用呢?
首先把esp切换到即将运行的进程的内核栈顶(栈顶指着最近的一个stackframe),然后把esp0进行清空。(注意:只有在嵌套的中断形成的stackframe一层层pop出栈,直到清空到最外面一层之后,才会切换到用户态。如果在中断嵌套的情况下,我们是不用管tss.esp0的。)这样,我们就可以iret返回上一层中断,或者返回到用户态。
其实这个道理和函数调用返回是一样的,请思考一下。
中断嵌套
实现中断嵌套
irqHandle里面的一些迷惑操作(save esp之类的)是为什么呢?
实际上是为了中断嵌套,中断嵌套本质上就是从一个用户进程的内核态进入内核态,然后嵌套的一些状态信息会不断保存在对应的内核栈里......下面需要你自己理解了。
临界区问题(拓展)
由于系统调用的处理时间往往很长,为保证进程调度的公平性,需要在系统调用中开启外部硬件中断, 以便当前进程的处理时间片耗尽时,进行进程切换;由于可以在系统调用中进行进程切换,因此可能会 出现多个进程并发地处理系统调用,对共享资源(例如内核的数据结构,视频显存等等)进行竞争,例如以下场景
进程P1在内核态处理系统调用,处理视频显存,此时外部硬件中断开启。
在内核态处理系统调用的进程P1将现场信息压入P1的内核堆栈中,跳转执行时间中断处理程序。
进程P1的处理时间片耗尽,切换至就绪状态的进程P2,并从当前P1的内核堆栈切换至P2的内核堆栈。
从进程P2的内核堆栈中弹出P2的现场信息,从时间中断处理程序返回执行P2。
进程P2在内核态处理系统调用,处理视频显存,与进程P1形成竞争。
多个进程并发地进行系统调用,对共享资源进行竞争可能会产生一致性问题,带来未知的BUG;因此, 在系统调用过程中,对于临界区的代码不宜开启外部硬件中断,而对于非临界区的代码,则可以开启外部硬件中断,允许中断嵌套。
调度策略
说了那么多进程切换的细节,那要如何选择下一个就绪进程呢?这就涉及到调度策略,最基础的是只需要采用轮转调度(Round Robin)的策略即可,即依次调度进程1, 进程2, ..., 进程n, 进程1...
Linux采用多级队列调度的策略, 为每一个进程设定一个优先级, 调度时选择所有就绪进程中优先级最高的 进程进行调度, 这样可以保证一些请求能够尽快得到响应。我们在后面设置了challenge,感兴趣的同学也可以去实现!(同样,这个challenge不写不扣分。也就是说,基本任务全部完成,单次实验也可能获得满分的好成绩。)