文章目录
01、任务和任务切换概述
多任务的系统中,每个任务都有自己的任务状态段TSS
和局部描述符表LDT
,当前任务是由任务寄存器TR
指示,指向当前任务的任务状态段TSS
、局部描述符表LDTR
也指向当前任务的局部描述符表LDT
。
多任务系统是指可以同时执行两个或以上任务的系统,即使一个任务没有执行完也能执行下一个任务,任务切换时TR
和LDTR
也跟着切换到新的任务中。
任务切换方式:
1、协同式任务切换
:需要当前任务主动请求暂时放弃执行权、或者在通过调用门请求操作系统服务时由操作系统乘机转移到另一个任务中。此种方式依赖于任务的自律性,当一个任务失控时其他任务可能得不到控制的机会。
2、抢占式任务切换
:这种方式可以安装一个定时器中断,在中断信号产生的时候实施任务切换。硬件中断会定时发生,不管处理器在做什么,中断到来时任务一定会切换成功。这种情况下任务都会得到平等的执行机会,当一个任务失控时,也不会导致其他任务没有机会执行。
多任务系统:
任务的组成是灵活的,不一定由不同的特权级组成、也不一定是内核和用户程序组成。本章将创建3个任务:
- 1、任务1单纯由内核组成,0特权级。内核除了是一个单独的任务,同时也是其他任务的全局部分。
- 2、任务2由内核与任务2的私有部分组成,其是3特权级。
- 3、任务3由内核与任务3的私有部分组成,其是3特权级。
在本章中,当处理器加电复位之后,进入保护模式之后就直接创建和执行内核的0特权级任务—>之后切换到任务2的私有部分—>之后切换到内核—>之后切换到任务3的私有部分。
每个任务都有自己的状态,特别是当一个任务正在执行时,所有段寄存器、通用寄存器都和当前任务息息相关。段寄存器指向当前任务自己的段、通用寄存器保存着当前任务的数据和临时结果、标志寄存器保存着当前任务产生的各种标志。
当前任务要切换出去时,必须将当前任务的所有状态保护起来以便将来恢复,这叫做保护现场;被切换到的那个任务也必须恢复到原先它被打断时的状态,叫做恢复现场。
为了保护现场和恢复现场,使用每个任务的TSS
来保存数据:CR3
和分页有关。
保存当前任务的现场:
恢复目标任务的现场:
恢复之后被切换到的任务就变为当前任务。
02、内核任务的创建和I/O特权级IOPL
本章程序:
引导程序:c13_mbr0.asm
,加载执行内核。
内核程序:c15_core0.asm
,加了新的内容。
用户程序:c15_app0.asm
,加了新的内容。
I/O许可位图
,之前讲过特权指令,即只有0特权级
才能执行的指令。但是有一些低特权级的程序也需要使用这些特权指令。如下:
为了控制哪些任务能够访问硬件端口,需要用到标志寄存器EFLAGS
:
当前特权级CPL
若高于IOPL
(I/O Privilege Level
),数值上CPL <= IOPL
,则表示所有I/O
访问都是允许的。
03、I/O特权级的修改和POPF指令
标志寄存器中的标志位是随着程序运行而实时改变的,如ZF、CF
;有些则需要特定的指令进行改变,比如DF
。但是IOPL
不会自动随程序修改,也没有特定指令来修改。那么IOPL
的修改需要如下操作:
- 1、先将标志寄存器压栈;
- 2、然后对栈中
IOPL
内容进行修改;
将IOPL
修改为01
。 - 3、最后将栈中修改后的内容弹出到标志寄存器。
04、任务的用户态和内核态
每个任务都有TSS
,其中保存了EFLAGS
,有IOPL
字段。
多任务系统特点:可以在内核任务和用户任务之间来回切换、也可以在两个用户任务之间来回切换。
每当一个任务被切换回后台时,它与之相关的状态都会保存在它的TSS
中,当它恢复是,会从它的TSS
中将各种状态恢复到处理器中。显然每个任务都受自己的IOPL
所限制。
每个任务都可以对自己的标志和状态进行修改,比如标志寄存器中的内容,需要使用如下指令进行压栈、修改、再出栈返回到标志寄存器中。
其中pushf、pushfd
在任何特权级下均可执行,但是popf、popfd
执行时标志寄存器中的有些标志位是否会受到影响(如IOPL字段)、是否能够被修改,需要取决于当前特权级CPL
:
- 1、CPL为0,那么执行
popf、popfd
指令时,标志寄存器的IOPL
字段会被修改; - 2、CPL为1,那么执行
popf、popfd
指令时,标志寄存器的IOPL
字段类似于只读
,不会受到影响;
即低特权级的指令无法使用popf、popfd
指令修改IOPL
字段。popf、popfd
并不是特权指令,特权指令是只能在0特权级下执行,popf、popfd
指令在低特特权级下也可以执行,只不过在低特权级下执行时一些标志位不受影响。
内核任务只能在内核态执行,用户任务可以在内核态和用户态中执行。
05、I/O许可位串和TSS的I/O许可位映射区
当前CPL
高于等于 IOPL
(即数值上CPL <= IOPL
),则所有操作I/O
操作都是允许的;
当前CPL
低于 IOPL
(即数值上CPL > IOPL
),也并不意味这所有I/O
操作都不被允许,而是需要进一步指定哪些允许,那些不允许。在输入输出许可位串(I/O许可位串
)中指定。
TSS中基本长度是104字节,当然也可以包括I/O许可位映射区。
TSS
描述符及其布局:其中段界限是包括I/O许可位映射区
的。
其中I/O许可位
的偏移M
若大于TSS
描述符的段界限,则表示没有I/O许可位
。在这种情况下,如果当前特权级CPL
低于当前IOPL
,那么就必须检查I/O
许可位串,但是没有I/O
许可位串就意味着不允许访问硬件端口,执行任何硬件I/O
指令都会引发处理器异常中断。
处理器检查I/O
许可位方法如下:
1、先根据端口号计算它在I/O
许可位映射区的那个字节中,
2、然后读取该字节,并测试那个byte
位,如out 0x09, al
指令:端口0x09
位于第二个字节,而且位于第二个字节的位1
。处理器读取并测试这个byte位
是0还是1来决定是否允许执行这个out
指令。
I/O端口是按照字节编址的,即每个端口只能用来读取一个字节的数据,那些多字节的端口其实是合并了几个端口组成一个多字节端口的:
由于I/O端口
是按照字节编址的原因,当处理器执行一个字或者双字的I/O
指令时,会检查许可位串中的2个或者4个连续的byte
,而且要求它们必须都是0,否则引发异常中断。
麻烦在于这些连续的byte
可能是跨字节的,即一些byte
位于前一个字节,另一些byte
位于后一个字节,如下:
处理器每次都会从I/O许可映射区
读取2个连续的字节,而不是1个字节。
这种操作方式也导致了另一个问题,即要检查的byte
在最后一个字节中,那么这个2字节的读操作将会越界。为了防止这种情况,处理器要求I/O许可映射区
在最后必须附加一个额外的字节,其值为0xFF
。如下:
若I/O许可映射区
本身只有11个字节,除去最后一个全为1的字节,只剩10个字节,那么只能映射80个端口,访问更高地址的端口(即高于79号端口)将引发异常中断。
06、任务切换的方法及内核任务的确立
内核本身要当作一个独立的任务,内核正在执行,现在要为其补一个合法的手续;创建内核任务的TSS
,接着要在TSS
中填充一些内容,在任务切换之前提前准备好。
设置内核任务的TSS
:
。。。
;为内核任务的TSS分配内存空间
mov ecx,104 ;为该任务的TSS分配内存
call sys_routine_seg_sel:allocate_memory
mov [es:esi+0x14],ecx ;在内核TCB中保存TSS基地址
;在程序管理器的TSS中设置必要的项目
mov word [es:ecx+96],0 ;没有LDT。处理器允许没有LDT的任务。
mov word [es:ecx+102],103 ;没有I/O位图。0特权级事实上不需要。
mov word [es:ecx+0],0 ;反向链=0
mov dword [es:ecx+28],0 ;登记CR3(PDBR)
mov word [es:ecx+100],0 ;T=0
;不需要0、1、2特权级堆栈。0特级不
;会向低特权级转移控制。
。。。
- 1、内核任务不需要
LDT
,所以在内核TSS
偏移0x96
的地方填写数字0即可; - 2、内核任务也不需要
I/O许可位映射区
,内核是0特权级,始终可以进行所有I/O
操作。这里偏移填写103
为内核TSS
的界限值,因为I/O许可位映射区
在TSS
中的偏移 >=TSS
的界限值,即表示保存在I/O许可位映射区
; - 3、反向链相关。硬件任务切换在64位处理器上不再支持,除非以兼容模式允许32位。
用CALL
指令发起任务切换时,任务之间会形成一个任务链,可以通过任务链反向切换到原来的任务中。
所以在TSS
中偏移为0的位置,置0即可:
- 4、和分页相关的位
清0
- 5、设置
T位
为0,因为内核特权级为0,不会向低特权级实施控制转移。
创建内核任务的TSS
描述符,安装到GDT
中:
。。。
;创建TSS描述符,并安装到GDT中
mov eax,ecx ;TSS的起始线性地址
mov ebx,103 ;段长度(界限)
mov ecx,0x00008900 ;TSS描述符,特权级0
call sys_routine_seg_sel:make_seg_descriptor
call sys_routine_seg_sel:set_up_gdt_descriptor
mov word [es:esi+0x18],cx ;登记TSS选择子到TCB
mov word [es:esi+0x04],0xffff ;任务的状态为“忙”
;任务寄存器TR中的内容是任务存在的标志,该内容也决定了当前任务是谁。
;下面的指令为当前正在执行的0特权级任务“程序管理器”后补手续(TSS)。
ltr cx
;现在可认为“程序管理器”任务正执行中
mov ebx,core_msg1
call sys_routine_seg_sel:put_string
。。。
07、用户任务的创建和初始化
TCB
中偏移为0x04
位置:
任务状态为0表示就绪
、为0xFFFF表示忙状态
、为0x3333表示任务已经终止
。
为用户任务创建TCB
:
。。。
;以下开始创建用户任务
mov ecx,0x46
call sys_routine_seg_sel:allocate_memory
mov word [es:ecx+0x04],0 ;任务状态:就绪
call append_to_tcb_link ;将此TCB添加到TCB链中
。。。
之后加载和重定位用户程序,并将其创建为任务。
。。。
push dword 50 ;用户程序位于逻辑50扇区
push ecx ;压入任务控制块起始线性地址
call load_relocate_program
。。。
在例程load_relocate_program
中:
1、创建LDT
;
2、加载用户程序;
3、创建用户程序每个段的描述符,并将其安装到LDT
中;
4、重定位用户和程序的符号地址检索表SALT
,本章在SLAT
中新增一个条目InitTaskSwitch
用于任务切换;
5、创建0、1、2特权级
的栈段描述符及其选择子,为通过调用门转移控制而准备的;
6、在GDT
中等级LDT
描述符;
7、创建用户任务的TSS
;
8、在用户任务的TSS
中登记相关的信息,阅读时参考TSS
结构(TSS.pdf
);
- 8.1、填写任务的反向链
- 8.2、填写
0、1、2特权级
的栈段选择子和栈指针 - 8.3、登记
LDT
选择子 - 8.4、登记
I/O许可位映射区
偏移 - 8.5、登记T标志
- 8.6、等登记
CR3
(和分页有关) - 8.7、登记其他信息
1、以前内核不是独立的任务,而是用户任务的私有部分。所以用户程序加载之后模拟调用门返回,从任务的全局部分返回任务的私有部分。本章中内核为一个独立的任务,是正在执行的任务,所以当我们创建了用户任务之后,将使用任务切换的方式从内核任务切换到用户任务。
2、切换到用户任务时,一定会从用户任务的TSS
中恢复现场,即使是用户任务的第一次执行,为了确保用户任务的第一次切换成功,需要在用户任务的TSS
中设置哪些内容呢?
3、首先是0、1、2特权级
的栈段选择子和栈指针;接着是通用寄存器的内容,一般都是运行时自动设置,也有一些需要单独设置(如EFLAGS
中的IOPL
字段、EIP
要设置为用户任务入口点的偏移量)。
4、段寄存器的内容,可以提前设置也可以在程序中用指令初始化,CS
必须在这里设置为用户程序入口点的代码段选择子。
5、若用户任务有LDT
,则需要设置LDT
的段选择子。
6、若用户任务有I/O许可位映射区
,则需要设置映射区的偏移。
9、创建用户任务的TSS
描述符,并将其安装在GDT
中,安装之后在CX
中返回TSS
的选择子,将其登记在用户任务控制块TCB
中。
10、从例程load_relocate_program
返回。
创建一个用户任务之后还可以创建其他任务:
。。。
;可以创建更多的任务,例如:
;mov ecx,0x46
;call sys_routine_seg_sel:allocate_memory
;mov word [es:ecx+0x04],0 ;任务状态:空闲
;call append_to_tcb_link ;将此TCB添加到TCB链中
;push dword 50 ;用户程序位于逻辑50扇区
;push ecx ;压入任务控制块起始线性地址
;call load_relocate_program
。。。
之后就是任务管理的循环,用来发起内核任务到其他任务的切换、回收已经终止任务的资源、也可以选择创建新的任务。
。。。
.do_switch:
;主动切换到其它任务,给它们运行的机会
call sys_routine_seg_sel:initiate_task_switch
mov ebx,core_msg2
call sys_routine_seg_sel:put_string
;这里可以添加创建新的任务的功能,比如:
;mov ecx,0x46
;call sys_routine_seg_sel:allocate_memory
;mov word [es:ecx+0x04],0 ;任务状态:空闲
;call append_to_tcb_link ;将此TCB添加到TCB链中
;push dword 50 ;用户程序位于逻辑50扇区
;push ecx ;压入任务控制块起始线性地址
;call load_relocate_program
;清理已经终止的任务,并回收它们占用的资源
call sys_routine_seg_sel:do_task_clean
mov eax,[tcb_chain]
.find_ready:
cmp word [es:eax+0x04],0x0000 ;还有处于就绪状态的任务?
jz .do_switch ;有,继续执行任务切换
mov eax,[es:eax]
or eax,eax ;还有用户任务吗?
jnz .find_ready ;一直搜索到链表尾部
;已经没有可以切换的任务,停机
mov ebx,core_msg3
call sys_routine_seg_sel:put_string
hlt
。。。
08、简单的任务调度和切换策略
接上一节。
所有任务都是平等的参与任务切换,切换到用户任务时做的是自己的私事,切换到内核任务时做的是管理整个系统。
使用例程initiate_task_switch
进行任务切换,从任务链表中找到下一个就绪状态的任务,进行切换。tcb_chain
中记录每个任务的TCB
,其中有下一个任务的地址和这个任务的状态(忙或正在执行0
、就绪FFFF
、已终止3333
)。
任务调度策略:顺着链表找到当前正在执行的任务,再找到一个就绪的任务,切换到这个就绪的任务。切换之后将任务状态进行改变(忙改为就绪、就绪改为忙)。
特殊情况1
:如上图,系统中只有一个任务,不执行任务切换。
特殊情况2
:如上图,每次都是从tcb_chain
链表头部开始搜索,先找到忙任务再从后面找到就绪任务。若忙任务处理链表尾部,那么也是从链表头部重新搜索就绪任务。这样任务之间就是公平的进行轮转。
09、遍历TCB链表寻找忙任务和就绪任务
本节是分析例程initiate_task_switch
,具体看代码和视频。
10、通过JMP FAR执行任务切换的过程
上一节中找到了状态忙的任务
和状态为就绪的任务
。
在64位处理器上不再提供硬件任务切换,操作系统也不适用硬件任务切换。
使用jmp far [edi+0x14]
指令进行任务切换时:
EDI
保存就绪任务的TCB
线性地址,TCB
偏移0x14的地方,存放TSS
的基地址和16位TSS
选择子;- 处理器执行这条指令时使用
DS
描述符高速缓存器中的基地址 + 段内偏移(EDI + 0x14
),取出6个字节,假定它们是段选择子和段内偏移;当处理器发现6和字节中后2字节是段选择子则前面的4字节地址会被忽略。 - 用这个选择子到
GDT
中寻找对应的描述符,处理器发现这是TSS
描述符,就知道需要发起任务切换。 - 保存旧任务的状态
当前CS
指向内核公共例程段,EIP
指向下一条指令,则保存状态时,CS
和EIP
保存的是上述值。 - 设置新任务的状态
第一次是从内核任务切换到用户任务,在创建用户任务时,已经在用户任务的TSS
中登记了各种信息,包括3个特权级的栈段选择子SS
和栈指针ESP
,接着登记LDT
选择子,之后是入口点信息,包括CS
、EIP
。一旦从用户任务的TSS
中恢复那些信息,处理器就会转入用户任务执行。
11、内核任务与用户任务轮流执行的过程
看视频、代码即可,主要讲解了从内核任务切换到用户任务的执行流程。
12、任务的终止和清理
看视频、代码即可,主要讲解了从用户任务切换到内核任务的执行流程,其中会对任务进行清理。
Virtual Box
虚拟机:
Bochs
虚拟机: