重新认识Intel任务切换(一)

文章系列:
重新认识Intel任务切换(一)
重新认识Intel任务切换(二)

利用Intel TSS硬件机制实现的任务调度

任务调度原理

  • 任务管理数据结构
    控制一个任务的执行和挂起,不仅要知道任务代码段的指针,还要知道任务执行期间CPU所有寄存器的值,方便下次执行时加载。所以描述一个任务的数据结构分成:
  1. 任务主体,要执行任务的代码段和执行期间需要的数据、堆栈段
  2. 任务状态段,TSS(Task-State Segment),任务执行时需要的所有寄存器
  • 执行任务的方式
  1. 传入指向TSS的选择子,直接通过CALL和JMP指令调用
  2. 传入指向调用门的选择子间接调用,调用门包括:中断门、陷阱门、任务门
  3. iret指令
  • 任务切换时的关键动作
  1. 保存当前任务的执行环境到该任务的TSS中,执行环境就是硬件上下文,就是所有寄存器的值
  2. 从下一个任务的TSS中加载寄存器的值到物理寄存器。包括加载cs和eip,然后开始执行任务
  • 任务管理相关寄存器
  1. TR(Task Register),存放指向TSS段描述符的选择子
    LTR(Load TR),加载选择子到寄存器,初始化任务管理时通过这个指令写TR。之后每次任务切换,CPU自动把下一个任务的TSS选择子加载到TR。 STR(Store TR),读取选择子到寄存器。TR除了显示存放TSS的选择子,还隐式存放了TSS段描述符中的基值,用来缓存TSS在内存中的地址。
    在这里插入图片描述
  2. 任务门描述符,存放指向TSS段描述符的选择子
    可以通过任务门间接发起任务调度,任务门描述符指向GDT中的TSS描述符。
    任务门可以放在GDT,LDT和IDT中。任务门提供了多一层调度保护,任务门的DPL在任务切换时控制着对TSS段描述符的访问。只有CPL和RPL权限高于任务门DPL的任务调度才被允许。可以把任务门看做数据段。使用调用门时TSS描述符中的DPL不参与权限检查
    在这里插入图片描述在这里插入图片描述
  3. 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。
在这里插入图片描述

任务切换流程

  1. CPU从CALL、JMP指令的立即数或者门描述符中取出目标任务的TSS选择子
  2. 任务切换权限检查,通过后继续下一步,否则报错
  3. 检查目标任务TSS描述符,present是否设置,没设置表示内存中不存在,不允许切换;长度是否大于0x67字节,小于0x67字节不允许切换
  4. 检查目标任务是否available,如果标记为busy,不允许切换
  5. 检查任务使用的内存是否被映射为系统内存,不是不允许切换
  6. 如果当前任务是通过JMP指令发起的,切换前将busy标记清空,表示available,如果是CALL指令就不做
  7. 保存当前任务的TSS,通过TR找到TSS后将所有通用寄存器,段寄存器,标志寄存器,CS,EIP,CR3等都拷贝到TSS对应的位置。(可以看到CR3也更新了,表示不同任务地址内存空间不同)
  8. 从目标任务的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 ; 下一次被调度时任务开始执行的地方
  • 结果分析
  1. 执行指令ltr ax
    CS为0x8,表明处于程序运行在GDT第1个代码段;TR为0;info tss查看当前程序的TSS,内容比较乱,没有初始化。GDT中TSS0和TSS1被标记为可用。
    在这里插入图片描述
  2. 执行指令ltr ax
    TR=0x30指向TSS0;info tss查看当前程序的TSS,所有段寄存器值都为0,ring0的堆栈ss:esp(0): 0x0020:0x00000003和TSS0初始化时设置的堆栈选择子相同。可以确认ltr 指令使CPU加载TR后可以访问TSS;查看GDT中的TSS,被标记为busy状态,因为程序当下处于此任务中
    在这里插入图片描述
  3. 执行指令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任务切换

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

享乐主

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值