Appearance

Lab 2:从内核到用户

byml2025-03-25OS

Lab 2:从内核到用户

系统接手

当 Bootloader 将操作系统的代码加载到内存以后,就将系统运行的接力棒交到了操作系统手中。操作系统内核会进行一系列初始化,然后开始执行各种用户程序。初始化的流程包括:

  • 初始化中断向量表(DIT):接手各类中断事件的处理
  • 初始化外部中断控制器(8259A):设置外部中断的开关和中断号
  • 初始化全局描述符表(GDT):调整段选择子,写入任务状态段(TSS)和用户相关段的段选择子,并同时设定任务段寄存器(TR)
  • 初始化设备和驱动:初始化显示设备、键盘驱动等 然后,操作系统内核就可以加载用户程序,并通过从内核态退回用户态的 iret 指令跳转到用户程序的入口。 上述操作服务于两条清晰而紧密相连的主线,即:
  1. 特权级跳转:内核态和用户态之间的跳转
  2. 中断:硬件、内核和用户程序之间的通信

特权级跳转

󱜸 特权级跳转是如何发生的?

  • 从用户态到内核态的跳转,通过(主动)系统调用和(被动)外部硬件(例如计时器)两类中断。
  • 从内核态跳转至用户态,是通过 iret 指令。

󱜸 如何跳转到正确的位置? 从低特权级到高特权级:

  • i386 规定了存储任务状态信息的数据结构(任务状态段 TSS),可使用任务状态段寄存器 TR 来指定其地址。TSS 只用于低特权级向高特权级的跳转,特权级跳转时,CPU 会自动处理 TSS。
  • 在 TSS 中,存储了一些寄存器(用于硬件上下文切换,在现代系统中使用的较少),还有三个堆栈,每个里面有 SSESP 两个寄存器,分别存储对应特权级的栈段选择子和栈指针,用以定位。
  • 进入高特权级后,低特权级的内容会被压入高特权级的栈空间。 从高特权级到低特权级:
  • 从栈空间中恢复低特权级的栈指针。
  • 使用 iret 指令,由 CPU 加载 TSS 中的数据进行跳转。
以某次中断为例(ring3 → ring0 → ring3)

  1. 位于 ring3 的用户程序发起系统调用中断 int 0x80,中断参数存储于寄存器中
  2. CPU 将有关的寄存器写入 TSS 的表区,并从 TSS 栈区取出 SS0ESP0 赋值给对应寄存器
  3. 赋值完成后,在新的堆栈将表区的 EFLAGS、CS、EIP 压入栈(即 ring3 的这几个寄存器的值会存放于 ring0 堆栈栈顶)
  4. 根据 IDT,跳转执行中断处理函数,直到遇到 iret 指令
  5. 从 ring0 堆栈栈顶取出寄存器值并赋值,跳转回 ring3 继续执行用户程序
TSS 的堆栈

TSS 的堆栈是静态的,即 SS0/1/2ESP0/1/2 是固定不变的,也就是说,每次从低特权级跳转到高特权级,都等于进入了一个新的函数,拥有独立的堆栈。 由于 TSS 只需要在低特权级向高特权级跳转时找到 SS0/1/2ESP0/1/2 的位置,ring3 的栈段选择子和栈指针没有静态存储在 TSS 中的必要。因此虽然 i386 有四个特权级,TSS 堆栈却只有三组存储 SSESP 的位置。

中断通信

中断连接起了硬件、操作系统内核和用户程序:

󱜸 如何区分不同的硬件中断和系统调用? i386 系统采用了中断向量表(IDT)维护中断号对应的中断处理程序地址。每一个中断号,都有其指定含义,例如 0x00 表示 CPU 除法除零错误。按照规定,0x00-0x1F 这 32 个中断用于硬件中断(CPU 和 8259A 连接的外设);0x20-0xFF 留给操作系统来指定。这 256 个中断,构成了硬件和用户程序与操作系统沟通的桥梁。

BIOS 与 IDT

在 BIOS 和实模式中,我们就可以使用 int 软中断来打印屏幕,此时的 IDT 中其实已经写入了 BIOS 中实现的基本中断处理程序。在操作系统初始化时,实际上是覆盖 IDT 表中原有的处理函数。

一般来说,系统调用被放在了 0x80 的位置,而具体执行何种调用,取决于写入寄存器的传参;返回值也是通过写入和读取寄存器实现的。

变与不变

󱜸 跳转的过程中,哪些寄存器的值会改变?该如何传参? i386 提供了 pushapopa 指令用于将通用寄存器的值压入和弹出当前栈。在内核函数接手前,CPU 已经将SSESP 进行了修改,而其他的寄存器值保持不变。因此,我们可以利用不变的寄存器来传递系统调用的参数。 值得注意的是,用户程序可没法保证传回来的寄存器值不变(因为可能有返回值的存在)。因此,比较稳健的做法是在用户程序发起系统调用前保留寄存器的值。

读与写

读写(I/O)是一个简单但完整的硬件、操作系统内核和用户程序通过中断通信的过程。三个子系统的行为如下:

  • 硬件(键盘):当用户按下按键的时候,通过电路发送信号,8259A 获取信号后发送中断信号(在实验中,中断号为 0x21
  • 操作系统内核:
    • 处理 0x21(键盘按键中断):根据按键,向输入缓存区 keyBuffer 中写入/删除字符,并将结果输出在屏幕上。
    • 处理 0x80(系统调用中断)
      • 读取字符:从 keyBuffer 中取出一个字符
      • 读取字符串:从 keyBuffer 中取出字符串
      • 打印字符/字符串:向显存中写入字符/字符串
  • 用户程序:发起 0x80 系统调用,通过写寄存器传入参数(读/写),从寄存器中获取返回值(字符)

󱜸 键盘按键和读取的调用先后顺序是怎么样的? 在操作系统视角下,键盘按键和读取是独立的,并不存在先放入 keyBuffer 再从中取值的先后顺序。此时,为了保证正常的读入,需要对输入进行缓存。实验中选择实现了比较常见的缓存方式:不断将键盘输入写入缓存,换行为止。

  • syscallGetChar 行为与 getchar() 相似,如果缓存区非空,直接取第一个字符;否则会等待到按下换行,然后只取缓存区中的第一个字符返回。
  • syscallGetStr 行为与 scanf("%s") 相似,如果缓存区非空,直接取从头开始到空格或换行结束(以 \0 而空格或换行结尾);否则会等待到按下换行,然后取缓存区从头开始到空格或换行结束。 实现中,可以使用 hlt 指令让 CPU 休眠直到下一次中断到来。