调试分析 Linux 0.00 多任务切换

当执行完 system_interrupt 函数,执行 153 行 iret 时,记录栈的变化情况。

图1

如图 1,使用 int 0x80 中断调用 system_interrupt 函数时,x86 架构隐式向 task0 对应 TSS0 中指定的内核栈 krn_stk0 0x0010:0x00000e60 (ss0:esp0) 中压入调用前 SS、ESP、EFLAGS、CS、EIP,对应栈中的值 0x0017、0x00000bd8、0x00000246、0x000f、0x000010eb。

图2

如图 2,由 iret 指令返回时,SS、ESP、EFLAGS、CS、EIP 隐式由栈中弹出填充对应寄存器,指向 int 0x80 下条指令 movl $0xfff, %ecx 地址处执行,栈指向 task0 用户栈 init_stack 0x0017:0x00000bd8。

当进入和退出 system_interrupt 时,都发生了模式切换,请总结模式切换时,特权级是如何改变的?栈切换吗?如何进行切换的?

图3
图4

如图 3、4,通过 int 0x80 进入 system_interrupt 函数时, 硬件隐式比较当前 CS 中 RPL(11) 与 IDT 对应 0x80 号描述符中 DPL(11) 进行比较,比较通过后 CS 由 0x000f 变为 0x0008 完成特权级切换(11 - 00),SS 由 0x0017 更改为 TSS0 中指定的 ss0 0x0010,ESP 切换至 TSS0 中指定的 00x00000e60,栈由 task0 的用户栈 init_stack 切换至 task0 的内核栈 krn_stk0。

图1
图2

类似的,如图 1、2,通过 iret 退出 system_interrupt 函数时, 硬件隐式由栈中弹出中断调用前隐式压入的 SS、ESP、EFLAGS、CS、EIP。CS 由 0x0008 恢复为 0x000f 进入用户态,特权级由 00 恢复为 11,栈由 task0 的内核栈 krn_stk0 恢复至调用前用户栈 SS:ESP。

当时钟中断发生,进入到 timer_interrupt 程序,请详细记录从任务 0 切换到任务 1 的过程。

图5
图6

如图 5、6,在 ljmp $TSS1_SEL, $0 处设置断点,观察栈可知 task0 在某次循环至 loop 1b 语句时触发时钟中断进入 timer_interrupt 函数执行,此时 TR 指向 GDT 中 TSS0 对应描述符,LDTR 指向 GDT 中 LDT0 对应描述符。

图7

如图 7,执行 ljmp $TSS1_SEL, $0 后,大部分寄存器均更新为 TSS1 中对应值,如 eax、ebx、esp、eip 等通用寄存器及 CS、DS、SS 等段寄存器。另外的,由于 TR、LDTR 中存储的段选择子改变,其隐式存储的 TSS、LDT 基地址及界限均改变为 TSS1 对应结构,此时 CS:EIP 指向 task1 的函数入口地址执行。

图8
图9

图 8、9 为 ljmp $TSS1_SEL, $0 指令执行前后 TSS0 处存储数据,可以看到在任务切换时会将源任务上下文封装存储在 TSS0 中,以便下次任务切换回 task0 时恢复上下文。

又过了 10ms,从任务1切换回到任务 0,整个流程是怎样的? TSS 是如何变化的?各个寄存器的值是如何变化的?

图10
图11

如图 10、11,在 ljmp $TSS0_SEL, $0 处设置断点,观察栈可知 task1 在某次循环至 loop 1b 语句时触发时钟中断进入 timer_interrupt 函数执行,此时 TR 指向 GDT 中 TSS1 对应描述符,LDTR 指向 GDT 中 LDT1 对应描述符。

图12

如图 12,执行 ljmp $TSS0_SEL, $0 后,大部分寄存器均更新为 TSS0 中对应值,如 eax、ebx、esp、eip 等通用寄存器及 CS、DS、SS 等段寄存器。另外的,由于 TR、LDTR 中存储的段选择子改变,其隐式存储的 TSS、LDT 基地址及界限均改变为 TSS0 对应结构,此时 CS:EIP 指向 task0 先前调用 timer_interrupt 任务切换指令 ljmp $TSS1_SEL, $0 的下一条指令 jmp 2f。

图13
图14

图 13、14 为 ljmp $TSS0_SEL, $0 指令执行前后 TSS1 处存储数据,可以看到在任务切换时会将源任务上下文封装存储在 TSS0 中,以便下次任务切换回 task1 时恢复上下文。

请详细总结任务切换的过程。

执行 ljmp $TSS_SEL, $0 指令进行任务切换时,x86 架构首先将当前 cpu 状态如通用寄存器、控制寄存器、段寄存器、堆栈(SS:ESP)、即将执行的下条指令地址(CS:EIP) 等上下文封装存储在源任务 TSS 中,同时根据所选目的任务 TSS 段选择子 $TSS_SEL 获取目的任务 TSS,并按其中存储的上下文填充对应寄存器。下一次切换回源任务时,同理将当前任务上下文封装在对应 TSS 中,根据先前所存储的源任务对应 TSS 填充对应寄存器,此时源任务得到一个与任务切换前一致的状态视角,仿佛 cpu 从未切换过任务,继续执行下一条指令。x86 架构及操作系统通过该机制为每个任务提供了其独占 cpu 运行的假象。