为了支持多任务,CPU 厂商提供了 LDT 及 TSS 这两种原生支持,他们要求为每个任务分配一个 LDT 和 TSS(都有程序员自己构建),LDT 中保存的是任务的数据和代码,TSS 保存的是任务的上下文状态以及三种特权级指针、IO位图等信息。
那么任务切换就是切换这两个结构:将新任务对应的 LDT 加载到 LDTR,对应的 TSS 加载到 TR 寄存器。
任务切换的方式:三种。
还差两种没写上来,只写了一种,具体看书吧,这里我也是简单的了解了一下…
iretd 指令的作用:
NT 位是 elflags 中的第 14 位,1bit 宽度,它表示 Nest Task Flag(任务嵌套)。任务嵌套是指当前任务是被前一个任务所调用的,也就是当前任务嵌套在另一个任务中,当前任务执行完毕后,将会返回上一个任务,即返回当前任务的调用者的那个任务。
当处理器执行 iretd 指令时,会先判断 NT 的值:
B 位:主要用来给 CPU 做重入判断。
并不是只有当前任务 B = 1,那些被 call 嵌套调用的新任务,除了新任务的 B 位置为 1 外,旧任务的 B 位依然还是 1。
TSS 字段中的 “上一个任务的 TSS 指针”,用于记录是哪个任务调用了当前任务,这是一个单链表的关系,如图:
图意:A 调用任务 A.1,A.1 调用任务 A.1.1,…。
当调用一个新任务时,处理器做了两件事:
中断发生时,处理器要把 NT 和 TF 位置为 0,若对应的描述符是中断描述符,还要再将标志寄存器中的 IF 位置为 0,这是为了避免中断嵌套,防止正在处理的中断尚未完成时,相同的中断源又发出中断信号,避免引发 GP 异常。
综上所述,中断发生时,通过任务门进行任务切换的过程如下:
为什么旧任务不修改 B 位?
答:因为旧任务并没有执行完,它现在执行的新任务也只是因为要完成某些必要的工作,才不得不调用新任务,新任务完成后就会立刻回去执行旧任务,因此旧任务的 B = 1,不需要修改。
当新任务完成后,调用 iretd 指令返回到旧任务,此时处理器会检查 NT 位,若为 1,则返回旧任务:
任务的状态信息存储位置: 当用户态触发中断后,由特权级 3 陷入特权级 0 后…
userprog/tss.c:
/* 任务状态段tss结构 */
struct tss {
uint32_t backlink;
uint32_t* esp0;
uint32_t ss0;
uint32_t* esp1;
uint32_t ss1;
uint32_t* esp2;
uint32_t ss2;
uint32_t cr3;
uint32_t (*eip) (void);
uint32_t eflags;
uint32_t eax;
uint32_t ecx;
uint32_t edx;
uint32_t ebx;
uint32_t esp;
uint32_t ebp;
uint32_t esi;
uint32_t edi;
uint32_t es;
uint32_t cs;
uint32_t ss;
uint32_t ds;
uint32_t fs;
uint32_t gs;
uint32_t ldt;
uint32_t trace;
uint32_t io_base;
};
static struct tss tss; // 全局共享
// 更新 TSS 中 esp0 字段的值为 pthread 的 0 级栈
void update_tss_esp(struct task_struct* pthread) {
tss.esp0 = (uint32_t*)((uint32_t) pthread + PG_SIZE);
}
// 创建 GDT 描述符
static struct gdt_desc make_gdt_desc(uint32_t* desc_addr, uint32_t limit, uint8_t attr_low, uint8_t attr_high) {
uint32_t desc_base = (uint32_t)desc_addr;
struct gdt_desc desc;
desc.limit_low_word = limit & 0x0000ffff;
desc.base_low_word = desc_base & 0x0000ffff;
desc.base_mid_byte = ((desc_base & 0x00ff0000) >> 16);
desc.attr_low_byte = (uint8_t)(attr_low);
desc.limit_high_attr_high = (((limit & 0x000f0000) >> 16) + (uint8_t)(attr_high));
desc.base_high_byte = desc_base >> 24;
return desc;
}
// 在 GDT 中创建 TSS 并且重新加载 GDT
void tss_init() {
put_str("tss_init start.\n");
uint32_t tss_size = sizeof(tss);
memset(&tss, 0, tss_size);
tss.ss0 = SELECTOR_K_STACK;
tss.io_base = tss_size;
// GDT 段基地址为 0x900,需要把新的描述符放到第 4 个位置,即 0x900 + 0x20
// 4 * 0x08 = 0x20
// 在 GDT 中添加 DPL 为 0 的 TSS 描述符
*((struct gdt_desc*) 0xc0000920) = make_gdt_desc((uint32_t*) &tss, tss_size - 1, TSS_ATTR_LOW, TSS_ATTR_HIGH);
// 在 GDT 中添加 DPL 为 3 的数据段和代码段描述符
*((struct gdt_desc*)0xc0000928) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_CODE_ATTR_LOW_DPL3, GDT_ATTR_HIGH);
*((struct gdt_desc*)0xc0000930) = make_gdt_desc((uint32_t*)0, 0xfffff, GDT_DATA_ATTR_LOW_DPL3, GDT_ATTR_HIGH);
// GDT 16 位的 LIMIT 32 位的段基址
uint64_t gdt_operand = ((8 * 7 - 1) | ((uint64_t)(uint32_t)0xc0000900 << 16)); // 7 个描述符大小
asm volatile("lgdt %0" : : "m" (gdt_operand)); // 重载 GDT
asm volatile("ltr %w0" : : "r" (SELECTOR_TSS));// 加载 TSS
put_str("tss_init and ltr done.\n");
}
userprog/process.c:
extern void intr_exit(void);
/**
* 构建用户进程、初始化其上下文
* 该函数由 kernel_thread() 所调用
* filename_ 是用户进程的名称
*/
void start_process(void* filename_) {
void* function = filename_;
struct task_struct* cur = running_thread(); // 进程是基于线程构建的
cur -> self_kstack += sizeof(struct thread_stack); // 将 ESP 指向 intr_stack 栈底
struct intr_stack* proc_stack = (struct intr_stack*) cur -> self_kstack;
proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy = 0;
proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0;
proc_stack -> gs = 0; // 用户态不允许操作显存,因此直接初始化为 0
proc_stack -> ds = proc_stack -> es = proc_stack -> fs = SELECTOR_U_DATA;
proc_stack -> eip = function; // 待执行的程序
proc_stack -> cs = SELECTOR_U_CODE;
proc_stack -> eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1);
proc_stack -> esp = (void*) ((uint32_t) get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE);
proc_stack -> ss = SELECTOR_U_DATA;
asm volatile("movl %0, %%esp; jmp intr_exit" : : "g"(proc_stack) : "memory");
}
// 激活页表
void page_dir_activate(struct task_struct* p_thread) {
/**
* 为什么要重载线程的 CR3?
* 我们知道进程才有独立的地址空间,而线程用的是线程间共享的同一套,按理说只需要重载进程就行,为什么线程也要呢?
* 说实话我感觉没啥好说的...
* 就是你线程用的是自己的,而进程们用的是各自独立的,你看线程和进程用的必然都不是同一套,你若要执行对应想线程或进程,是不是必须要重载 CR3 到自己所对应的页目录物理地址去?
*/
uint32_t pagedir_phy_addr = 0x100000; // 默认为内核的页目录表物理地址
if(p_thread -> pgdir != NULL) // 用户态进程自己的页目录表物理地址
pagedir_phy_addr = addr_v2p((uint32_t) p_thread -> pgdir);
// 更新页目录寄存器 CR3,使新页表生效
asm volatile("movl %0, %%cr3" : : "r" (pagedir_phy_addr) : "memory");
}
// 激活线程或进程的页表,并且更新 TSS 中的 ESP0 为进程的特权级0 的栈
void process_activate(struct task_struct* p_thread) {
ASSERT(p_thread != NULL);
// 激活该进程或线程的页表
page_dir_activate(p_thread);
// 若当前 p_thread 是内核线程,则不需要更新 ESP,因为其本身特权级就是0
if(p_thread -> pgdir)
update_tss_esp(p_thread); // 更新用户进程的 ESP0,用于此进程被中断时恢复上下文
}
// 创建也目录表,将当前页表的 表示内核空间的 PDE 复制
// 成功则返回页目录的虚拟地址,否则返回 -1
uint32_t* create_page_dir(void) {
// 用户进程的页表不能让用户直接访问到,所以在内核空间来申请
uint32_t* page_dir_vaddr = get_kernel_pages(1);
if(page_dir_vaddr == NULL) {
console_put_str("create_page_dir: get_kernel_page failed!");
return NULL;
}
/************* 先复制页表 *************/
// page_dir_vaddr + 0x300 * 4 表示内核页目录的第 768 项
memcpy((uint32_t*) ((uint32_t) page_dir_vaddr + 0x300 * 4), (uint32_t*) (0xfffff000 + 0x300 * 4), 1024);
/************* 更新页目录地址 *************/
uint32_t new_page_dir_phy_addr = addr_v2p((uint32_t) page_dir_vaddr);
// 页目录地址在页目录的最后一项,更新页目录地址为新的物理地址
page_dir_vaddr[1023] = new_page_dir_phy_addr | PG_US_U | PG_RW_W | PG_P_1;
return page_dir_vaddr;
}
// 创建用户进程的虚拟地址的位图
void create_user_vaddr_bitmap(struct task_struct* user_prog) {
user_prog -> userprog_vaddr.vaddr_start = USER_VADDR_START;
uint32_t bitmap_pg_cnt = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8, PG_SIZE);
user_prog -> userprog_vaddr.vaddr_bitmap.bits = get_kernel_pages(bitmap_pg_cnt);
user_prog -> userprog_vaddr.vaddr_bitmap.btmp_bytes_len = (0xc0000000 - USER_VADDR_START) / PG_SIZE / 8;
bitmap_init(&user_prog -> userprog_vaddr.vaddr_bitmap);
}
// 创建用户进程
void process_execute(void* filename, char* name) {
struct task_struct* thread = get_kernel_pages(1); // 向内核物理内存池申请空间
init_thread(thread, name, default_prio);
create_user_vaddr_bitmap(thread);
thread_create(thread, start_process, filename);
thread -> pgdir = create_page_dir();
enum intr_status old_status = intr_disable();
ASSERT(!elem_find(&thread_ready_list, &thread -> general_tag));
list_append(&thread_ready_list, &thread -> general_tag);
ASSERT(!elem_find(&thread_all_list, &thread -> all_list_tag));
list_append(&thread_all_list, &thread -> all_list_tag);
intr_set_status(old_status);
}
操作系统是为了服务用户进程的,它提供了各种各样的系统功能供用户进程调用。为了用户进程可以访问到内核服务,必须确保用户进程在自己的地址空间中能够访问到内核才行,也就是说内核空间必须是用户空间的一部分。
在用户进程 4GB 的虚拟地址空间的高 3GB 以上划分给操作系统,0~3GB 划分给用户进程自己。
为了实现共享操作系统,让所有用户进程的 3~4GB 的虚拟地址空间都指向同一个操作系统,也就是把所有用户进程的 3~4GB 的虚拟地址中的页表项所对应的物理页地址都指向同一片物理页地址,而这片物理页上是操作系统的实体代码。
总结:虚拟地址空间的 0~3GB 是用户进程,3~4GB 是操作系统。
userprog/process.c:
// 创建也目录表,将当前页表的 表示内核空间的 PDE 复制
// 成功则返回页目录的虚拟地址,否则返回 -1
uint32_t* create_page_dir(void) {
// 用户进程的页表不能让用户直接访问到,所以在内核空间来申请
uint32_t* page_dir_vaddr = get_kernel_pages(1);
if(page_dir_vaddr == NULL) {
console_put_str("create_page_dir: get_kernel_page failed!");
return NULL;
}
/************* 先复制页表 *************/
// page_dir_vaddr + 0x300 * 4 表示内核页目录的第 768 项
memcpy((uint32_t*) ((uint32_t) page_dir_vaddr + 0x300 * 4), (uint32_t*) (0xfffff000 + 0x300 * 4), 1024);
/************* 更新页目录地址 *************/
uint32_t new_page_dir_phy_addr = addr_v2p((uint32_t) page_dir_vaddr);
// 页目录地址在页目录的最后一项,更新页目录地址为新的物理地址
page_dir_vaddr[1023] = new_page_dir_phy_addr | PG_US_U | PG_RW_W | PG_P_1;
return page_dir_vaddr;
}
(0xc0000000 - USER_VADDR_START)
的误解