Appearance

Lab 3:进程调度

byml2025-04-16OS

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. 程序 1 发起中断/时钟中断,进入内核态
    • CPU 访问 TSS 表,获取 SS0 和 ESP0,赋值给对应寄存器
    • CPU 切换至内核态,将 SSESP 压入内核栈
    • CPU 将 EFLAGESCSEIP 依次加入内核态的栈
    • CPU 根据 IDT 跳转到中断处理程序所在地址

此时完整的内核栈: SS → ESP → EFLAGS → CS → EIP

  1. 进入进程调度函数
    • 操作系统内核保存当前进程的更多上下文信息
      • 向栈顶压入中断号和错误码
      • pushal:将 CPU 寄存器压入内核态栈
      • 依次将 dsesfsgs 选择子压入内核态栈

此时完整的内核栈: SS → ESP → EFLAGS → CS → EIP → IRQ → ERROR_CODE → CPU 寄存器 → DS → ES → FS → GS

  1. 进程调度
    • 通过调度算法选择/更新进程状态
      • 对于 BLOCKED 进程,更新其状态
      • 对于 RUNNING 进程,判断是否应当切换进程
    • 切换到选定的进程
      • 通过修改 ESP,从进程 1 的内核栈切换到进程 2 的内核栈
        • 两个进程的内核栈在形式上是完全相同的
      • 修改 TSS 表的 SS0 和 ESP0,使得下次中断进入进程 2 的内核栈
      • 从进程 2 的内核栈恢复上下文
        • 依次弹出 gsfsesds
        • popal:将 CPU 寄存器赋值
        • addl $8, %esp:跳过 ERROR_CODEIRQ 信息
        • iret

此时完整的内核栈: SS → ESP → EFLAGS → CS → EIP

  1. 切换到新进程的用户态
    • CPU 依次从内核中弹出并赋值 EIPCSEFLAGSESPSS
    • CPU 切换回用户态执行进程 2 的代码

进程创建

󱜸 进程的初始状态是什么? 根据进程切换的流程,在被切换时进程应处于内核态、堆栈完整的时刻。进程的初始状态正是如此:内核态栈指针指向完整的内核栈。以如下内核栈为例:

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 的栈空间中(自然上溢到栈空间了)。