重新认识Intel任务切换(一)
文章系列:
重新认识Intel任务切换(一)
重新认识Intel任务切换(二)
利用Intel TSS硬件机制实现的任务调度
任务调度原理
- 任务管理数据结构
控制一个任务的执行和挂起,不仅要知道任务代码段的指针,还要知道任务执行期间CPU所有寄存器的值,方便下次执行时加载。所以描述一个任务的数据结构分成:
- 任务主体,要执行任务的代码段和执行期间需要的数据、堆栈段
- 任务状态段,TSS(Task-State Segment),任务执行时需要的所有寄存器
- 执行任务的方式
- 传入指向TSS的选择子,直接通过CALL和JMP指令调用
- 传入指向调用门的选择子间接调用,调用门包括:中断门、陷阱门、任务门
- iret指令
- 任务切换时的关键动作
- 保存当前任务的执行环境到该任务的TSS中,执行环境就是硬件上下文,就是所有寄存器的值
- 从下一个任务的TSS中加载寄存器的值到物理寄存器。包括加载cs和eip,然后开始执行任务
- 任务管理相关寄存器
- TR(Task Register),存放指向TSS段描述符的选择子
LTR(Load TR),加载选择子到寄存器,初始化任务管理时通过这个指令写TR。之后每次任务切换,CPU自动把下一个任务的TSS选择子加载到TR。 STR(Store TR),读取选择子到寄存器。TR除了显示存放TSS的选择子,还隐式存放了TSS段描述符中的基值,用来缓存TSS在内存中的地址。 - 任务门描述符,存放指向TSS段描述符的选择子
可以通过任务门间接发起任务调度,任务门描述符指向GDT中的TSS描述符。
任务门可以放在GDT,LDT和IDT中。任务门提供了多一层调度保护,任务门的DPL在任务切换时控制着对TSS段描述符的访问。只有CPL和RPL权限高于任务门DPL的任务调度才被允许。可以把任务门看做数据段。使用调用门时TSS描述符中的DPL不参与权限检查 - TSS 段描述符,存放TSS的内存基址
TSS描述符属于系统段描述符,和代码段、数据段描述符、门描述符结构类似,但它只能放在GDT中,不能放在LDT和IDT中。TSS描述符中的DPL控制访问权限,CPL和RPL权限必须高于TSS描述符中的DPL才能发起调用。
5. TSS(Task-State Segment),存放一个执行一个任务所需的所有信息。
使用TSS时,先分配一段内存空间,按照TSS的数据结构初始化这段内存,然后将其基址放在TSS描述符中,写入GDT表。这样,CPU只要加载TSS描述符的选择子到TR,就能访问这个TSS。
任务切换流程
- CPU从CALL、JMP指令的立即数或者门描述符中取出目标任务的TSS选择子
- 任务切换权限检查,通过后继续下一步,否则报错
- 检查目标任务TSS描述符,present是否设置,没设置表示内存中不存在,不允许切换;长度是否大于0x67字节,小于0x67字节不允许切换
- 检查目标任务是否available,如果标记为busy,不允许切换
- 检查任务使用的内存是否被映射为系统内存,不是不允许切换
- 如果当前任务是通过JMP指令发起的,切换前将busy标记清空,表示available,如果是CALL指令就不做
- 保存当前任务的TSS,通过TR找到TSS后将所有通用寄存器,段寄存器,标志寄存器,CS,EIP,CR3等都拷贝到TSS对应的位置。(可以看到CR3也更新了,表示不同任务地址内存空间不同)
- 从目标任务的TSS中取出寄存器值,加载到物理寄存器中。同时更新TR指向目标任务的TSS,开始执行任务。
实验
- 目标
实现两个任务来回切换,分别在屏幕上打印’H’,'Y’字符 - 准备
任务切换要编写的代码和使用的数据较多,光是两个TSS就需要至少296字节。在MBR中的512字节中难以实现,因此使用一个boot.bin引导程序加载我们的代码,从预先写入的FAT12格式光盘中拷贝到内存地址:0x9000:0x100。引导程序取自《orange’s一个操作系统实现》的源码。 - 任务切换实现
1)准备任务代码,两个代码段,分别打印两个字符
LABEL_SEG_TASK0:
mov ax, SelectorVideo ;显存首地址(0B8000h)数据段选择子
mov gs, ax
mov edi,(80 * 11 + 79) * 2
mov ah, 0Ch
mov al, 'H'
mov [gs:edi], ax ;写入显存
mov ecx, 0ffffffh ;延时计数器
.0: ;延时
dec ecx
jecxz .1
jmp .0
.1:
jmp SelectorTSS1:0 ; 切换到task1
jmp SelectorTaskCode0:0 ; 下一次被调度时开始执行的地方
Task0CodeLen equ $ - LABEL_SEG_TASK0
LABEL_SEG_TASK1:
mov ax, SelectorVideo
mov gs, ax
mov edi,(80 * 11 + 79) * 2
mov ah, 0Ch
mov al, 'Y'
mov [gs:edi], ax
mov ecx, 0ffffffh
.2:
dec ecx
jecxz .3
jmp .2
.3:
jmp SelectorTSS0:0
jmp SelectorTaskCode1:0
Task1CodeLen equ $ - LABEL_SEG_TASK1
2)准备TSS结构,两个任务需要两个TSS
[SECTION .tss0]
ALIGN 32
[BITS 32]
LABEL_TSS0:
DD 0 ; Back
DD TopOfStack0 ; 0 级堆栈
DD SelectorStack0 ;
DD 0 ; 1 级堆栈
DD 0 ;
DD 0 ; 2 级堆栈
DD 0 ;
DD 0 ; CR3
DD 0 ; EIP
DD 0 ; EFLAGS
DD 0 ; EAX
DD 0 ; ECX
DD 0 ; EDX
DD 0 ; EBX
DD 0 ; ESP
DD 0 ; EBP
DD 0 ; ESI
DD 0 ; EDI
DD 0 ; ES
DD 0 ; CS
DD 0 ; SS
DD 0 ; DS
DD 0 ; FS
DD 0 ; GS
DD 0 ; LDT
DW 0 ; 调试陷阱标志
DW $ - LABEL_TSS0 + 2 ; I/O位图基址
DB 0ffh ; I/O位图结束标志
TSS0Len equ $ - LABEL_TSS0
[SECTION .tss1]
ALIGN 32
[BITS 32]
LABEL_TSS1:
DD 0 ; Back
DD TopOfStack1 ; 0 级堆栈
DD SelectorStack1 ;
DD 0 ; 1 级堆栈
DD 0 ;
DD 0 ; 2 级堆栈
DD 0 ;
DD 0 ; CR3
DD 0 ; EIP
DD 0 ; EFLAGS
DD 0 ; EAX
DD 0 ; ECX
DD 0 ; EDX
DD 0 ; EBX
DD 0 ; ESP
DD 0 ; EBP
DD 0 ; ESI
DD 0 ; EDI
DD 0 ; ES
DD SelectorTaskCode1; CS ; 指向任务代码段
DD SelectorStack1; SS ; 执行此任务时用的堆栈
DD 0 ; DS
DD 0 ; FS
DD 0 ; GS
DD 0 ; LDT
DW 0 ; 调试陷阱标志
DW $ - LABEL_TSS1 + 2 ; I/O位图基址
DB 0ffh ; I/O位图结束标志
TSS1Len equ $ - LABEL_TSS1
3)将两个任务代码段、两个TSS段组成段描述符,写入GDT,并设置相应段属性,同时生成段选择子供调用,此时段基址都初始化为0。
[SECTION .gdt]
; GDT
; 段基址, 段界限 , 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_INITCODE: Descriptor 0, InitCodeLen - 1, DA_C + DA_32; 非一致代码段
LABEL_DESC_TASKCODE0: Descriptor 0, Task0CodeLen - 1, DA_C + DA_32 ; 非一致代码段
LABEL_DESC_TASKCODE1: Descriptor 0, Task1CodeLen - 1, DA_C + DA_32 ; 非一致代码段
LABEL_DESC_STACK0: Descriptor 0, TopOfStack0, DA_DRW; 堆栈段
LABEL_DESC_STACK1: Descriptor 0, TopOfStack1, DA_DRW; 堆栈段
LABEL_DESC_TSS0: Descriptor 0, TSS0Len -1, DA_386TSS;
LABEL_DESC_TSS1: Descriptor 0, TSS1Len -1, DA_386TSS;
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW; 显存首地址
; GDT 结束
GdtLen equ $ - LABEL_GDT; GDT长度
GdtPtr dw GdtLen - 1 ; GDT界限
dd 0 ; GDT基地址
; GDT 选择子
SelectorInitCode equ (LABEL_DESC_INITCODE - LABEL_GDT)
SelectorTaskCode0 equ (LABEL_DESC_TASKCODE0 - LABEL_GDT)
SelectorTaskCode1 equ (LABEL_DESC_TASKCODE1 - LABEL_GDT)
SelectorStack0 equ (LABEL_DESC_STACK0 - LABEL_GDT)
SelectorStack1 equ (LABEL_DESC_STACK1 - LABEL_GDT)
SelectorTSS0 equ (LABEL_DESC_TSS0 - LABEL_GDT)
SelectorTSS1 equ (LABEL_DESC_TSS1 - LABEL_GDT)
SelectorVideo equ (LABEL_DESC_VIDEO - LABEL_GDT)
4)写入段基址
[SECTION .s16]
[BITS 16]
;ebx: code base
;ecx: descripter entry start address
SetDescBase:
xor eax, eax
mov ax, cs
shl eax, 4
add eax, ebx
mov word [ecx + 2], ax
shr eax, 16
mov byte [ecx + 4], al
mov byte [ecx + 7], ah
ret
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0100h
; 保护模式代码段初始化
mov ebx, LABEL_SEG_INIT
mov ecx, LABEL_DESC_INITCODE
call SetDescBase
mov ebx, LABEL_SEG_TASK0
mov ecx, LABEL_DESC_TASKCODE0
call SetDescBase
mov ebx, LABEL_SEG_TASK1
mov ecx, LABEL_DESC_TASKCODE1
call SetDescBase
mov ebx, LABEL_TSS0
mov ecx, LABEL_DESC_TSS0
call SetDescBase
mov ebx, LABEL_TSS1
mov ecx, LABEL_DESC_TSS1
call SetDescBase
5)进入保护模式,发起任务调用
; 准备切换到保护模式
mov eax, cr0
or eax, 1
mov cr0, eax
; 真正进入保护模式
jmp dword SelectorInitCode:0 ; 执行这一句会把 SelectorCode32 装入 cs,
; 并跳转到 Code32Selector:0 处
[SECTION .s32]; 32 位代码段. 由实模式跳入.
[BITS 32]
LABEL_SEG_INIT:
mov ax, SelectorTSS0 ;把TSS0设置成当前的任务
ltr ax ;将TSS0的选择子加载到TR寄存器
mov ax, SelectorStack0 ;设置当前任务堆栈,切换时会放到TSS0中,如果堆栈检查不合法会报错
mov ss, ax
xor eax, eax ;清零其他段寄存器,以防切换任务时存放TSS前的检查报错
mov ds, ax
mov es, ax
jmp SelectorTSS1:0 ;JMP指令发起任务调用
jmp SelectorTaskCode0:0 ; 下一次被调度时任务开始执行的地方
- 结果分析
- 执行指令
ltr ax
前
CS为0x8,表明处于程序运行在GDT第1个代码段;TR为0;info tss查看当前程序的TSS,内容比较乱,没有初始化。GDT中TSS0和TSS1被标记为可用。 - 执行指令
ltr ax
后
TR=0x30指向TSS0;info tss查看当前程序的TSS,所有段寄存器值都为0,ring0的堆栈ss:esp(0): 0x0020:0x00000003
和TSS0初始化时设置的堆栈选择子相同。可以确认ltr 指令使CPU加载TR后可以访问TSS;查看GDT中的TSS,被标记为busy状态,因为程序当下处于此任务中 - 执行指令
jmp SelectorTSS1:0
前
当前ss值为0x0020
,除CS以外,其它段寄存器被清零,TSS0中ss为0,任务切换时CPU会把当前ss的值放到TSS0的ss中,下一次任务切换回来我们可以验证。
4. 执行指令jmp SelectorTSS1:0
后
当前寄存器的ss被TSS1的ss替换,CS变成TSS1中的CS,TR=0x38被CPU更新成了TSS1段选择符的选择子。查看GDT表,TSS1被标记为busy,TSS0被标记为available。GDT表中两个TSS的状态切换了
5. 执行指令jmp SelectorTSS0:0
后
当前ss的值变成0x0020
,TR被更新成TSS0的选择子。GDT表中TSS描述符的状态再次变化,各寄存器的值再次切换
Intel任务切换总结
- TSS包含了一个任务执行需要的所有内容,任务切换时会将目标TSS的寄存器数据全部加载到CPU寄存器中,同时保存当前寄存器值到当前任务的TSS以供下一次调用。
- 每个任务有一个相应的TSS。
- TR指向TSS的选择子,每次任务切换时,TR都会切换成当前任务对应TSS的选择子。
- 准备好TSS、描述符等信息后,任务切换只需要一条JMP、CALL指令或者通过调用门就能完成,硬件支持。
附: 实验完整源码见my github Intel任务切换