Lab 3:进程调度
Lab 3:进程调度
进程切换
在状态机视角下,进程切换的本质是状态机的切换。对于用户程序来说,进程切换是无感知的。 一个完整的状态机具体会包含哪些元素? 完整的状态机被存储在 PCB 块中,一个简易的 PCB 块包含了下面这些元素:
struct ProcessTable {
uint32_t stack[MAX_STACK_SIZE]; // 程序栈
struct StackFrame regs; // 所有寄存器
uint32_t stackTop;
uint32_t prevStackTop;
int state; // 程序运行状态,包括 Running, Runnable, Blocked, Dead
int timeCount; // 运行时间计数,用于调度
int sleepTime; // 挂起时间计数,用于阻塞
uint32_t pid; // 进程号
char name[32];
};
一个完整的进程切换流程是怎么样的?
- 程序 1 发起中断/时钟中断,进入内核态
- CPU 访问 TSS 表,获取 SS0 和 ESP0,赋值给对应寄存器
- CPU 切换至内核态,将
SS
和ESP
压入内核栈 - CPU 将
EFLAGES
、CS
、EIP
依次加入内核态的栈 - CPU 根据 IDT 跳转到中断处理程序所在地址
此时完整的内核栈:
SS → ESP → EFLAGS → CS → EIP
- 进入进程调度函数
- 操作系统内核保存当前进程的更多上下文信息
- 向栈顶压入中断号和错误码
pushal
:将 CPU 寄存器压入内核态栈- 依次将
ds
、es
、fs
、gs
选择子压入内核态栈
- 操作系统内核保存当前进程的更多上下文信息
此时完整的内核栈:
SS → ESP → EFLAGS → CS → EIP → IRQ → ERROR_CODE → CPU 寄存器 → DS → ES → FS → GS
- 进程调度
- 通过调度算法选择/更新进程状态
- 对于
BLOCKED
进程,更新其状态 - 对于
RUNNING
进程,判断是否应当切换进程
- 对于
- 切换到选定的进程
- 通过修改
ESP
,从进程 1 的内核栈切换到进程 2 的内核栈- 两个进程的内核栈在形式上是完全相同的
- 修改 TSS 表的 SS0 和 ESP0,使得下次中断进入进程 2 的内核栈
- 从进程 2 的内核栈恢复上下文
- 依次弹出
gs
、fs
、es
、ds
popal
:将 CPU 寄存器赋值addl $8, %esp
:跳过ERROR_CODE
和IRQ
信息iret
- 依次弹出
- 通过修改
- 通过调度算法选择/更新进程状态
此时完整的内核栈:
SS → ESP → EFLAGS → CS → EIP
- 切换到新进程的用户态
- CPU 依次从内核中弹出并赋值
EIP
、CS
、EFLAGS
、ESP
、SS
- CPU 切换回用户态执行进程 2 的代码
- CPU 依次从内核中弹出并赋值
进程创建
进程的初始状态是什么? 根据进程切换的流程,在被切换时进程应处于内核态、堆栈完整的时刻。进程的初始状态正是如此:内核态栈指针指向完整的内核栈。以如下内核栈为例:
struct StackFrame {
uint32_t gs, fs, es, ds;
uint32_t edi, esi, ebp, xxx, ebx, edx, ecx, eax;
uint32_t irq, error;
uint32_t eip, cs, eflags, esp, ss;
};
结构体变量的地址实际上是栈顶地址:
&pcb[valid].regs = &pcb[valid].regs.gs = 0x00108a48
&pcb[valid].regs.ss = 0x00108a90

因此,只需将内核栈栈顶指针指向内核栈结构体的地址,即可指向完整内核栈。 fork 系统调用应该如何实现? fork 系统调用需要确保拷贝了程序的完整状态机,应包含寄存器和内存两部分。如果使用完整复制内存的方法,需要确保复制完成后才设置子进程状态为 RUNNABLE
,否则在允许嵌套中断后可能出现未复制完整就开始运行的错误
嵌套中断
嵌套中断是在进程 1 处于内核态时,产生了新的时钟中断等其他中断的情况。此时需要考虑的是进程原来的内核态栈指针应如何处理:
- 为了保证处理完中断后可以继续执行,原来的栈指针需要保存
- 为了保证新的中断能被正确处理,新的栈应当是进程切换的结构,指针也要进行相应的调整
使用中间变量暂存的方法即可处理。但由于为单个 PCB 分配的 StackFrame 空间只够一次进程切换,新中断实际上存储在了 PCB 的栈空间中(自然上溢到栈空间了)。