😭进程切换和堆栈切换

进程切换时的堆栈切换

下图为产生时间中断后,时间中断处理程序为两个用户态进程P1、P2进行进程切换的过程中,堆栈切换的简单图示:

  1. 进程P1在用户态执行,8253可编程计时器产生时间中断。

  2. 依据TSS中记录的进程P1的 SS0:EPS0 ,从P1的用户态堆栈切换至P1的内核堆栈(注意啊,不同进程对应的内核堆栈不同!!!),并将P1的现场信息压入内核堆栈中,跳转执行时间中断处理程序。

  3. 进程P1的处理时间片耗尽,切换至就绪状态的进程P2,并从当前P1的内核堆栈切换至P2的内核堆栈。

  4. 从进程P2的内核堆栈中弹出P2的现场信息,切换至P2的用户态堆栈,从时间中断处理程序返回执行P2。

exercise4:请你说说,为什么不同用户进程需要对应不同的内核堆栈?

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手册)。当我们陷入中断时,硬件做的事情如下:

  • 当没有特权级转换的时候(也就是在内核态产生中断,内核态到内核态):

    1. 依次把eflags,cs,eip入栈,按照中断门描述符进入相应处理程序

  • 当有特权级转换的时候(往往是用户态进入内核态):

    1. 先从用户进程对应的TSS里面取出对应内核栈的ss和esp

    2. 把用户栈的ss和esp入栈

    3. 依次把eflags,cs,eip入栈,按照中断门描述符进入相应处理程序

然后,根据我们doIrq.S,下面是软件要做的事情:

  1. 如果硬件没有push errorcode的话,就push errorcode

  2. 把中断号入栈

  3. pusha

  4. 把ds,es,fs,gs入栈

  5. esp入栈(当做StackFrame结构体的指针!)

  6. 然后调用irqHandle

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的结构,可以看出来:

exercise5:stackTop有什么用?为什么一些地方要取地址赋值给stackTop?

如果一时半会想不出答案。可以继续往下看,下面提供一个进程切换代码(可以直接用在实验里):

		//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之类的)是为什么呢?

实际上是为了中断嵌套,中断嵌套本质上就是从一个用户进程的内核态进入内核态,然后嵌套的一些状态信息会不断保存在对应的内核栈里......下面需要你自己理解了。

exercise6:请说说在中断嵌套发生时,系统是如何运行的?(把关键的地方说一下即可,简答)

临界区问题(拓展)

由于系统调用的处理时间往往很长,为保证进程调度的公平性,需要在系统调用中开启外部硬件中断, 以便当前进程的处理时间片耗尽时,进行进程切换;由于可以在系统调用中进行进程切换,因此可能会 出现多个进程并发地处理系统调用,对共享资源(例如内核的数据结构,视频显存等等)进行竞争,例如以下场景

  • 进程P1在内核态处理系统调用,处理视频显存,此时外部硬件中断开启。

  • 8253可编程计时器产生一个时间中断。

  • 在内核态处理系统调用的进程P1将现场信息压入P1的内核堆栈中,跳转执行时间中断处理程序。

  • 进程P1的处理时间片耗尽,切换至就绪状态的进程P2,并从当前P1的内核堆栈切换至P2的内核堆栈。

  • 从进程P2的内核堆栈中弹出P2的现场信息,从时间中断处理程序返回执行P2。

  • 进程P2在内核态处理系统调用,处理视频显存,与进程P1形成竞争。

多个进程并发地进行系统调用,对共享资源进行竞争可能会产生一致性问题,带来未知的BUG;因此, 在系统调用过程中,对于临界区的代码不宜开启外部硬件中断,而对于非临界区的代码,则可以开启外部硬件中断,允许中断嵌套。

调度策略

说了那么多进程切换的细节,那要如何选择下一个就绪进程呢?这就涉及到调度策略,最基础的是只需要采用轮转调度(Round Robin)的策略即可,即依次调度进程1, 进程2, ..., 进程n, 进程1...

Linux采用多级队列调度的策略, 为每一个进程设定一个优先级, 调度时选择所有就绪进程中优先级最高的 进程进行调度, 这样可以保证一些请求能够尽快得到响应。我们在后面设置了challenge,感兴趣的同学也可以去实现!(同样,这个challenge不写不扣分。也就是说,基本任务全部完成,单次实验也可能获得满分的好成绩。)

Last updated