Task

CPU通过任务管理的方式提供了基于硬件的多任务,为了实现多任务CPU提供了一下额外的数据结构和寄存器。这篇文章并不打算详细的介绍任务管理的结构和原理,因为Intel官方文档关于任务管理的章节描写的非常清楚,我只是想通过一个例子来理解整个任务管理的机制。


任务管理结构

多任务就是要实现多个任务之间的并行执行,就会涉及到任务之间的切换,既然要切换就要保证任务在切换回来之后还能够顺利的继续执行下去,那么在切换之前就要保存任务能够继续执行下去所必须的堆栈,寄存器等等上下文环境,这个上下文环境叫做任务状态。Intel官方手册中已经明确的描述了任务状态包含那些内容,这里就不赘述了。为了保存任务状态CPU提供了一种叫做TSS(任务状态段)的数据结构。TSS是与代码段,数据段等一样的一块内存区域,为了找到它还要在GDT中保存一个TSS的描述符,但是与其他的段描述符不同的是TSS描述符只能在GDT中,而不能够出现在LDT或者IDT中。与代码段或者数据段一样也需要将TSS的段选择子加载到一个段寄存器中来获得TSS,这个加载TSS段选择子的寄存器就叫做TR(任务寄存器),通过ltr指令来加载TSS段选择子,不过TR寄存器都是在任务切换的过程中由CPU自动的加载的,只有在初始化代码的时候需要程序员显式的调用ltr加载。

多任务环境中各个任务的任务状态是相对独立的,但是每个任务的状态都包很多段,如果把这些段描述符都放在GDT中,GDT会变得很长,很乱。CPU提供了LDT来保存一个任务自身相关的段,LDT是与GDT相似的结构,就是描述符的表,但是与GDT不同的是,LDT可能有很多,对于当前的任务,并不是所有的LDT都是可见的,只有LDTR寄存器中保存的选择子选定的LDT是可见的。此外LDT没有NULL descriptor,并且每一个LDT必须都在GDT中有相应的LDT描述符。

CPU还提供了一种任务门描述符的结构,它与TSS描述符不同,任务门描述符保存的是TSS描述符在GDT中的选择子,并且任务门描述符可以保存在GDT,LDT和IDT中。


任务切换

任务切换是一个很复杂的过程,因为切换过程中每一个步骤都要进行相应的检查,如果违规就会触发相应的异常。但是切换的流程是挺清晰的:

  1. 通过指令(ljmp/lcall/iret)获得新任务的选择子。
  2. 将当前的任务状态保存到当前的TSS中。
  3. 读取新任务的TSS,并从TSS中加载新任务的任务状态。
  4. 跳转到新任务开始之行。

其他步骤都是很简单的,第三步其实是挺复杂的过程,这里涉及到了多个数据结构交互,用图形表示一下最复杂的情况:


这里是最复杂的情况,一个任务涉及到的段都保存在自己的LDT中:

  1. 通过任务切换指令获得TSS selector,利用这个TSS selector获取GDT中的kernel TSS descriptor。
  2. 通过kernel TSS descriptor获得kernel TSS。
  3. kernel TSS中的LDT selector有效,通过LDT selector获得GDT中的kernel LDT descriptor。
  4. 通过kernel LDT descriptor获得kernel LDT。
  5. 通过kernel TSS中的CS selector获得kernel LDT中的CS descriptor。
  6. 通过CS descriptor获得kernel code segment。
这里以kernel任务的码段为例,对于数据段或堆栈段也是类似的。


示例

关于任务管理的例子代码比较多,托管在https://github.com/activesys/learning_cpu/tree/master/x86/task这里仅仅是对于各个文件的简单介绍:

  • boot.s是启动扇区代码,它还负责将setup,中断处理代码和数据,kernel代码和数据以及user代码和数据从相应的扇区中读取出来放到指定的内存位置。
  • common.inc定义了一下常量和宏,主要是对于整个代码的内存布局,以及GDT,LDT,IDT中的各个项的常量的定义。
  • setup.s负责安装GDT,LDT,IDT以及TSS,还负责切换到保护模式并且初始化保护模式后的任务环境。
  • kernel.s是ring0级别的代码和数据。
  • user.s是ring3级别的代码和数据。
  • build.sh是编译脚本,并且生成最终的镜像文件。
整个例子代码设置了三个任务,setup任务,kernel任务和user任务。setup任务是从实模式切换到保护模式后进入的任务并且中断和异常处理都是属于这个任务的。kernel任务单纯的模仿ring0级别的任务,user任务单纯的模仿ring3级别的任务。kernel和user任务都拥有自己的LDT,代码段和数据段等都放在LDT中,切换到这两个任务的流程就和上一节介绍的最复杂的流程一致。
这个例子中采用了动态设置GDT,IDT,LDT以及TSS的方式,通过调用_setup_gdt,_setup_idt,_setup_ldt以及_setup_tss,_setup_tss_segment来动态的更改GDT,IDT,LDT以及TSS中的内容。这些全局结构设置完成之后代码开启CR0.PE,然后跳转到保护模式代码_setup:
    # switch to protected-mode
    movl %cr0, %eax
    orl $1, %eax
    movl %eax, %cr0

    # far jmp
    ljmp $SETUP_SELECTOR, $_setup
保护模式代码初始化各个段,然后调用ltr来加载TR寄存器:
###############################################################
# Protected-mode code for setup
.code32
.type _setup, @function
_setup:
    xorl %eax, %eax
    movw $INT_DATA_SELECTOR, %ax
    movw %ax, %ds
    movw $INT_STACK_SELECTOR, %ax
    movw %ax, %ss
    movl $INT_STACK_INIT_ESP, %esp
    movw $INT_VIDEO_SELECTOR, %ax
    movw %ax, %es
    movw $NULL_SELECTOR, %ax
    movw %ax, %fs
    movw %ax, %gs

    movl $SETUP_TSS_SELECTOR, %eax
    ltr %ax

    ljmp $KERNEL_TSS_SELECTOR, $0x00
最后使用ljmp跳转到kernel任务,进行任务切换。ljmp后面跟着的选择是不再是段选择子而是tss的选择子。kernel任务的代码很简单只是向屏幕中打印了一些信息,然后使用lcall切换到了user任务,等user任务切换后来之后再次打印了一些信息并且进入无限循环:
###############################################################
# code for kernel
.code32
.globl _start
_start:
    movl $KERNEL_STACK_INIT_ESP, %esp

    movl $KERNEL_MSG_OFFSET, %edi
    movl $KERNEL_MSG_LENGTH, %ecx
    movl $KERNEL_FIRST_VIDEO_OFFSET, %edx
    call _kernel_echo

    # jmp to user code.
    lcall $USER_TSS_SELECTOR, $0x00

    movl $KERNEL_MSG2_OFFSET, %edi
    movl $KERNEL_MSG2_LENGTH, %ecx
    movl $KERNEL_SECOND_VIDEO_OFFSET, %edx
    call _kernel_echo

    jmp .
user任务的代码更是简单,只是向屏幕上打印了一些消息,展示了一下我们位于user任务代码段,然后使用iret指令切换回kernel任务:
###############################################################
# code for user
.code32
.globl _start
_start:
    movl $USER_STACK_INIT_ESP, %esp

    movl $USER_MSG_OFFSET, %edi
    movl $USER_MSG_LENGTH, %ecx
    movl $USER_FIRST_VIDEO_OFFSET, %edx
    call _user_echo

    iret
最终的运行结果:


从屏幕中输出的消息可以看出,代码首先进入了ring0级别的kernel任务,然后切换到了ring3级别的user任务,然后又切换回ring0级别的kernel任务。


参考

《Intel® 64 and IA-32 Architectures Software Developer’s Manual Volume 3 (3A, 3B & 3C): System Programming Guide》
《自己动手写操作系统》
《x86/x64体系探索与编程》

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值