TSS
起初Intel的建议是给每个任务都关联一个任务状态段(TSS)。在CPU中有一个TR寄存器,始终指向当前正在运行的任务,因此在CPU看来,任务切换的实质就是TR寄存器指向不同的TSS。但是Linux中,所有的任务共享一个TSS。TSS和其他段一样,本质上也是一片储存数据的区域,CPU用这片区域保持任务的最新状态。因此,TSS段就要像其他段一样,需要用个描述符来描述TSS段。因此也要在GDT中注册好。注意的是,TSS是属于系统段描述符,因此 S 为0。TSS描述符用来描述TSS段,TSS段在内存中,它的元素按照固定格式来排列。
TSS段里面基本上全部都是寄存器的名字,这些寄存器就是任务运行中的最新状态。TSS中有三组栈,Linux只使用了SS0和esp0。当任务被换下CPU时候,CPU自动的将当前任务的资源状态保存到该任务对应的TSS中,CPU通过新任务的TSS选择子加载新任务时候,会将新任务的TSS中的数据加到寄存器中,同时更新TR,使其指向新任务的TSS,当然我们的Linux不需要这样,因为只使用一个TSS。TSS段也需要选择子来访问的。TR寄存器保存的是TSS的选择子,任务改变就更新TR寄存器中的选择子可以指向另一个TSS了。
注意分清:TSS只是一个内存段的名字而已,它和数据段,代码段一样的,都需要一个描述符来描述这个段的属性和位置。GDT中储存着这些描述符,GDTR储存着段描述符表的位置,TSS的描述符只是GDT中的一个描述符而已。TR中储存的是TSS的选择子。任务切换就是改变TR的指向。
现代操作系统采用的任务切换
CPU由低特权级向更高特权级转移的一种情况是用户模式下面发生中断,这会发生堆栈的切换。Linux中只用到一个TSS,所以我们必须提前创建一个TSS,并且至少初始化TSS中的 ss0 和 esp0。因此我们使用TSS的唯一理由就是为0特权级的任务提供栈。当用户模式下发生中断时候,CPU会自动的从TSS 中取出 0 级栈,然后一系列 push 指令。
实现用户进程
进程与内核线程最大的区别就是进程有单独的4GB空间。每一个进程都有4GB的空间,也就是说每个进程都有一个页表。进程是基于线程实现的,它与线程一样使用相同的pcb结构,即 struct task_struct。我们在进程的PCB结构中加入两个成员:
uint32_t* pgdir; //进程自己页表的虚拟地址
struct virtual_addr userprog_vaddr; //用户进程的虚拟地址池
页表使用虚拟地址是因为页目录表本身也要占用内存来存储,我们在为进程创建页目录表,肯定要为页目录表申请内存,内存管理系统返回的地址肯定都是虚拟地址,不可能返回物理地址。
为进程创建页表和3特权级栈
不同的进程有各自不同的页表,因为我们创建一个进程就要为进程单独创建一个属于它的页目录表+页表。另外还要为用户进程创建在3特权级下面的栈。
//功能:在虚拟地址池中申请 n 页的页表,并返回起始地址
//需要注意到是:内核的虚拟地址池是全局变量,但是用户进程的虚拟地址池是在用户进程PCB中的,即一个用户进程有个属于它自己的虚拟地址池。
static void* vaddr_get(enum pool_flags pf, uint32_t pg_cnt) {
int vaddr_start = 0, bit_idx_start = -1;
uint32_t cnt = 0;
if (pf == PF_KERNEL) { // 内核内存池
bit_idx_start = bitmap_scan(&kernel_vaddr.vaddr_bitmap, pg_cnt);
if (bit_idx_start == -1) {
return NULL;
}
while(cnt < pg_cnt) {
bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1);
}
vaddr_start = kernel_vaddr.vaddr_start + bit_idx_start * PG_SIZE;
} else { // 用户内存池
struct task_struct* cur = runnin