赞
踩
x86硬件设计原生支持的任务切换,每一个任务都有一个TSS和一个LDT
TSS、LDT的段描述符位于GDT。
中断发生依靠中断向量号调用任务门描述符,任务门中有TSS描述符的选择子
LDT包括只属于当前任务的各种代码段、数据段;TSS结构见上,有各种寄存器。
TR、LDTR为当前任务的TSS、LDT的描述符在GDT中的选择子。
每次任务切换,先在TR中找到旧任务的TSS,保存环境,再依靠任务门描述符的TSS选择子,在TSS描述符中找到新任务的TSS地址,更新新任务环境,再更新新TR,LDTR,最后旧任务指针写入新TSS。
这样太低效。但是高低特权级切换又必须依靠硬件规定的,从TSS获取0级栈指针。
所以Linux采用一个CPU创建一个TSS,只更新该TSS的SS0、esp0、I/O位图字段。由于每次任务切换,一定都是在0特权级下完成,所以切换的时候,将旧任务的环境全部push入0特权级栈,其余均不变。
LDT完全可以由GDT替代。所以Linux不涉及LDT
一个任务分为低特权级和高特权级两部分,即用户代码段和内核代码段。
特权级高低转移不是任务切换,但需要用当前TSS里不同等级的栈。
Linux任务切换也不使用call / jmp
x86硬件原本提供的任务切换方式有
中断调用任务门,call/jmp 任务门,call/jmp TSS选择子。
如果是嵌套调用别的任务,那么两个任务TSS的段描述符B都为1,B为1,无法再次调用该任务
任务门:支持任务切换
两个:一个是中断+任务门、一个是call或jmp+任务门/TSS选择子和iretd
1.中断+任务门实现任务切换过程
通过中断获得中断向量号,该中断向量号对应的就是任务门。
iretd有两个作用:
eflags的第14位NT位为0,用于中断返回
NT为1,用于从新任务返回旧任务。
(1)中断发生时,CPU接收到中断向量号,在IDT索引到任务门描述符,
分析S和TYPE字段,确定是门描述符,开始进行任务切换,取出要切换的新任务的TSS选择子。
(2)再用新任务的TSS选择子在GDT索引TSS描述符
(3)判断描述符P位是否为1,为一表示TSS位于内存
(4)从TR获取旧任务TSS的地址,将CPU的当前任务状态(也就是寄存器,cr3,eflags等等)保存到旧任务的TSS
(5)将新任务TSS赋值到CPU(即各种寄存器中)
(6)使TR指向新TSS
(7)新TSS描述符B位置1
(8)将eflags的NT位置1
(9)旧任务TSS选择子写入新TSS"上一个任务的TSS指针"
(10)执行新任务
新任务执行完成后,调用iretd指令返回到旧任务,处理器检查NT位为1,开始返回工作
(1)eflags的NT位置0
(2)新TSS描述符B位置0
(3)当前任务状态写入TR指向的新TSS
(4)将“上一个任务的TSS指针“加载到TR,恢复上一个任务的状态
(5)执行旧任务。
2.通过jmp/call实现任务切换过程
任务门也可以在GDT中
call/jmp+TSS选择子或者call/jmp+任务门选择子,忽略偏移量。
其他步骤同理,也就是旧任务保存到旧TSS,新TSS赋值到CPU,iretd指令返回旧任务。
唯一多的一个步骤就是特权级检查
CPL和TSS选择子的RPL<=TSS描述符的DPL
用TSS效率低
Linux一次性加载TSS和TR,初始化0特权级栈,然后不断修改同一个TSS的内容。
这次试验应该只是在一个任务特权级切换的时候,使用了TSS的0特权级栈,任务切换都是在同一个TSS修改,这样相比原生效率大大提升。
main函数创造了1个主线程,两个内核线程,两个用户进程。共5个PCB,进程PCB->pgdir为进程自己的页表首虚拟地址,线程为null。
因此本次实验是五个进/线程在来回切换。他们切换的方式无疑都是利用自己PCB->kstack保存切换前esp,然后不断pop完成cs:eip的跳转。
关于线程的调度,和多线程调度实验是一样的。
在切换内核线程前,就将PCB的线程栈结构设置好,PCB->kstack指向了线程栈栈顶,当主线程时间片耗尽中断,scheduel,switch上半部将自己esp保存于PCB->kstack后,switch下半部esp就指向了内核线程早已设置好的线程栈栈顶,依次弹栈,ret执行kernel_thread,在kernel_thread内调用线程函数k_thread_b,完成线程的切换。
线程切换一直位于0特权级,一直使用的都是自己的0特权级栈
当内核线程运行的时候,esp是指向线程栈内部的,所以内核线程的中断栈其实位于线程栈下方,而线程栈上方的预留的中断栈的空间其实被浪费了。
进程的创立不仅预留了中断栈,填写了线程栈,而且还申请了两页用作进程虚拟地址位图和页表,虚拟首地址均保存在PCB。
依然是当主线程时间片耗尽中断,scheduel,switch上半部将自己esp保存于PCB->kstack后,switch下半部esp就指向了用户进程早已设置好的线程栈栈顶,依次弹栈,ret执行start_process(u_prog_b)。
start_process仍然位于0特权级,该函数设置了用户进程中断栈,其中PCB->cs设置成了指向DPL等于3的代码段描述符,PCB->ds、PCB->ss设置成指向了DPL等于3的数据段,PCB->esp设置成了Linux默认的3特权级栈地址,PCB->eip指向了u_prog_b。最后将esp置为中断栈栈顶,jmp intr_exit,程序一路pop中断栈内容,最后iretd指令,用户进程切换到了特权级3,成功执行了u_prog_b,也成功访问了全局变量,相当于在3特权级用自己的页表访问了DPL为3的代码段、数据段、栈段。
值得注意的是,Linux中,TSS的唯一作用就是保存当前用户进程0特权级栈,所以schedule确定下一个要切换的进程next后,就可以process_activate(next);
把该进程PCB的中断栈存入TSS.esp0,此时该中断栈为空,这从上面的例子就可以看出来,jmp intr_exit,中断处理程序恢复环境,一路pop,一直pop到最后ss:esp。所以此栈就是PCB+PGSIZE。
并且,切换cr3寄存器页表地址。
当用户进程被中断的时候,由于需要执行中断处理程序,特权级会从3变0,故cpu会自动从TSS.esp0处取得0特权级栈,所以,在schedule决定要切换某个用户进程后,switch_to前,就要把TSS.esp0更新,这样切换后,tss.esp0才是指向当前进程0特权级栈。
进程调度与线程调度的区别在于,
1.进程在利用提前设置好线程栈,利用switch_to切换到kernel_thread后,进一步jmp intr_exit,利用中断处理程序iretd指令,实现了cs:eip来到3特权级
2.在schedule确定下一个要切换的用户进程后,进程更新了自己的页表和将自己PCB+PGSIZE赋值给了tss.esp0。如果schedule的是线程,那么cr3切换成0x100000,tss不变
3.从第二点结合整个流程就可以看出,用户进程和线程都依靠中断切换,用户进程的中断栈在线程栈之上,而线程的中断栈在线程栈之下。这是由于线程在切换到自己的main.c里的函数去执行时,esp位于线程栈。而进程在切换到自己的main.c的函数执行时,0特权级栈esp位于PCB页顶。
当然,为了实现用户进程,也设置了TR,TSS描述符,DPL为3的代码段、数据段,不过它们的段基址依然是0,还有全局变量tss结构体,所以tss的地址有编译器设定。
#include "interrupt.h" #include "init.h" #include "thread.h" #include "print.h" #include "process.h" #include "console.h" void k_thread_a(void* ); void k_thread_b(void* ); void u_prog_a (void); void u_prog_b (void); int test_var_a=0, test_var_b=0; int main(void) { put_str("I am kernel\n"); init_all(); thread_start("k_thread_a", 31, k_thread_a, "argA "); thread_start("k_thread_b", 8, k_thread_b, "argB "); process_execute(u_prog_a, "user_prog_a"); process_execute(u_prog_b, "user_prog_b"); intr_enable(); while(1); return 0; } void k_thread_a(void* arg){ char* para = arg; while(1){ console_put_str("v_a:0x"); console_put_int(test_var_a); } } void k_thread_b(void* arg){ char* para = arg; while(1){ console_put_str("v_b:0x"); console_put_int(test_var_b); } } void u_prog_a(void) { while(1) { test_var_a++; } } void u_prog_b(void) { while(1) { test_var_b++; } }
#include "process.h" #include "global.h" #include "debug.h" #include "memory.h" #include "thread.h" #include "list.h" #include "tss.h" #include "interrupt.h" #include "string.h" #include "console.h" extern void intr_exit(void); /* 构建用户进程初始上下文信息 */ void start_process(void* filename_) { void* function = filename_; struct task_struct* cur = running_thread(); cur->self_kstack += sizeof(struct thread_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) { /******************************************************** * 执行此函数时,当前任务可能是线程。 * 之所以对线程也要重新安装页表, 原因是上一次被调度的可能是进程, * 否则不恢复页表的话,线程就会使用进程的页表了。 ********************************************************/ /* 若为内核线程,需要重新填充页表为0x100000 */ 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); /* 内核线程特权级本身就是0,处理器进入中断时并不会从tss中获取0特权级栈地址,故不需要更新esp0 */ if (p_thread->pgdir) { /* 更新该进程的esp0,用于此进程被中断时保留上下文 */ update_tss_esp(p_thread); } } /* 创建页目录表,将当前页表的表示内核空间的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; } /************************** 1 先复制页表 *************************************/ /* page_dir_vaddr + 0x300*4 是内核页目录的第768项 */ memcpy((uint32_t*)((uint32_t)page_dir_vaddr + 0x300*4), (uint32_t*)(0xfffff000+0x300*4), 1024); /*****************************************************************************/ /************************** 2 更新页目录地址 **********************************/ 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) { /* pcb内核的数据结构,由内核来维护进程信息,因此要在内核内存池中申请 */ 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); }
包括的函数
start_process(void* filename_)
void process_activate(struct task_struct* p_thread)
刚开始的主线程特权级一直是0,被中断的是0特权级,进入中断不涉及特权级改变,不会从tss取esp0
如果是从3中断,那么就是特权级从3到0,进入中断意味着在此任务下特权级转变,cpu会自动从tss取esp0更新esp寄存器。
void page_dir_activate(struct task_struct* p_thread)
uint32_t* create_page_dir(void):每次创建新进程,都需要把内核页目录表中3GB以上内容复制到自己的页目录表,这样所有进程都可以访问内核代码
void create_user_vaddr_bitmap(struct task_struct* user_prog)
void process_execute(void* filename, char* name)
为用户进程在内核内存池申请了一页PCB,一页页表,在用户内存池申请了一页3特权级栈
/* 进程或线程的pcb,程序控制块 */ struct task_struct { uint32_t* self_kstack; // 各内核线程都用自己的内核栈 pid_t pid; enum task_status status; char name[TASK_NAME_LEN]; uint8_t priority; uint8_t ticks; // 每次在处理器上执行的时间嘀嗒数 /* 此任务自上cpu运行后至今占用了多少cpu嘀嗒数, * 也就是此任务执行了多久*/ uint32_t elapsed_ticks; /* general_tag的作用是用于线程在一般的队列中的结点 */ struct list_elem general_tag; /* all_list_tag的作用是用于线程队列thread_all_list中的结点 */ struct list_elem all_list_tag; uint32_t* pgdir; // 进程自己页表的虚拟地址 struct virtual_addr userprog_vaddr; // 用户进程的虚拟地址 uint32_t stack_magic; // 用这串数字做栈的边界标记,用于检测栈的溢出 };
增加
struct virtual_addr userprog_vaddr; // 用户进程的虚拟地址位图
void schedule() {
next->status= TASK_RUNNING;
process_activate(next);
switch_to(next);
}
// ---------------- GDT描述符属性 ---------------- #define DESC_G_4K 1 #define DESC_D_32 1 #define DESC_L 0 // 64位代码标记,此处标记为0便可。 #define DESC_AVL 0 // cpu不用此位,暂置为0 #define DESC_P 1 #define DESC_DPL_0 0 #define DESC_DPL_1 1 #define DESC_DPL_2 2 #define DESC_DPL_3 3 /* 代码段和数据段属于存储段,tss和各种门描述符属于系统段 s为1时表示存储段,为0时表示系统段. */ #define DESC_S_CODE 1 #define DESC_S_DATA DESC_S_CODE #define DESC_S_SYS 0 #define DESC_TYPE_CODE 8 // x=1,c=0,r=0,a=0 代码段是可执行的,非依从的,不可读的,已访问位a清0. #define DESC_TYPE_DATA 2 // x=0,e=0,w=1,a=0 数据段是不可执行的,向上扩展的,可写的,已访问位a清0. #define DESC_TYPE_TSS 9 // B位为0,不忙 #define SELECTOR_K_CODE ((1 << 3) + (TI_GDT << 2) + RPL0) #define SELECTOR_K_DATA ((2 << 3) + (TI_GDT << 2) + RPL0) #define SELECTOR_K_STACK SELECTOR_K_DATA #define SELECTOR_K_GS ((3 << 3) + (TI_GDT << 2) + RPL0) /* 第3个段描述符是显存,第4个是tss */ #define SELECTOR_U_CODE ((5 << 3) + (TI_GDT << 2) + RPL3) #define SELECTOR_U_DATA ((6 << 3) + (TI_GDT << 2) + RPL3) #define SELECTOR_U_STACK SELECTOR_U_DATA #define GDT_ATTR_HIGH ((DESC_G_4K << 7) + (DESC_D_32 << 6) + (DESC_L << 5) + (DESC_AVL << 4)) #define GDT_CODE_ATTR_LOW_DPL3 ((DESC_P << 7) + (DESC_DPL_3 << 5) + (DESC_S_CODE << 4) + DESC_TYPE_CODE) #define GDT_DATA_ATTR_LOW_DPL3 ((DESC_P << 7) + (DESC_DPL_3 << 5) + (DESC_S_DATA << 4) + DESC_TYPE_DATA) //--------------- TSS描述符属性 ------------ #define TSS_DESC_D 0 #define TSS_ATTR_HIGH ((DESC_G_4K << 7) + (TSS_DESC_D << 6) + (DESC_L << 5) + (DESC_AVL << 4) + 0x0) #define TSS_ATTR_LOW ((DESC_P << 7) + (DESC_DPL_0 << 5) + (DESC_S_SYS << 4) + DESC_TYPE_TSS) #define SELECTOR_TSS ((4 << 3) + (TI_GDT << 2 ) + RPL0) struct gdt_desc { uint16_t limit_low_word; uint16_t base_low_word; uint8_t base_mid_byte; uint8_t attr_low_byte; uint8_t limit_high_attr_high; uint8_t base_high_byte; }; #define EFLAGS_MBS (1 << 1) // 此项必须要设置 #define EFLAGS_IF_1 (1 << 9) // if为1,开中断 #define EFLAGS_IF_0 0 // if为0,关中断 #define EFLAGS_IOPL_3 (3 << 12) // IOPL3,用于测试用户程序在非系统调用下进行IO #define EFLAGS_IOPL_0 (0 << 12) // IOPL0 #define NULL ((void*)0) #define DIV_ROUND_UP(X, STEP) ((X + STEP - 1) / (STEP)) #define bool int #define true 1 #define false 0 #define PG_SIZE 4096
#include "tss.h" #include "stdint.h" #include "global.h" #include "string.h" #include "print.h" #include "boot.inc" /* 任务状态段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,把tss放到第4个位置,也就是0x900+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)); asm volatile ("ltr %w0" : : "r" (SELECTOR_TSS)); put_str("tss_init and ltr done\n"); }
tss.c配合tss.h,创造了c语言的make_gdt_desc函数,之前在中断处理实验还创造了make_idt_desc。
(注意:idt和gdt段描述符格式不同,所以硬件对他们的处理也不同。
TSS段、代码段、数据段、任务门的段描述符都是gdt。
中断门、任务门(含tss选择子)都是idt内的)
接着,tss_init函数,
在gdt第4个段描述符出增加了TSS描述符、dpl为3的数据段、代码段。
由于gdt段界限变了,所以lgdt重新加载了gdt,
然后ltr将TR寄存器加载了TSS选择子,Linux的任务切换方式意味着TR将永远不变。
#ifndef __USERPROG_TSS_H
#define __USERPROG_TSS_H
#include "thread.h"
void update_tss_esp(struct task_struct* pthread);
void tss_init(void);
#endif
struct pool { struct bitmap pool_bitmap; // 本内存池用到的位图结构,用于管理物理内存 uint32_t phy_addr_start; // 本内存池所管理物理内存的起始地址 uint32_t pool_size; // 本内存池字节容量 struct lock lock; // 申请内存时互斥 }; 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 = running_thread(); bit_idx_start = bitmap_scan(&cur->userprog_vaddr.vaddr_bitmap, pg_cnt); if (bit_idx_start == -1) { return NULL; } while(cnt < pg_cnt) { bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx_start + cnt++, 1); } vaddr_start = cur->userprog_vaddr.vaddr_start + bit_idx_start * PG_SIZE; /* (0xc0000000 - PG_SIZE)做为用户3级栈已经在start_process被分配 */ ASSERT((uint32_t)vaddr_start < (0xc0000000 - PG_SIZE)); } return (void*)vaddr_start; } /* 在用户空间中申请4k内存,并返回其虚拟地址 */ void* get_user_pages(uint32_t pg_cnt) { lock_acquire(&user_pool.lock); void* vaddr = malloc_page(PF_USER, pg_cnt); if (vaddr != NULL) { // 若分配的地址不为空,将页框清0后返回 memset(vaddr, 0, pg_cnt * PG_SIZE); } lock_release(&user_pool.lock); return vaddr; } /* 将地址vaddr与pf池中的物理地址关联,仅支持一页空间分配 */ void* get_a_page(enum pool_flags pf, uint32_t vaddr) { struct pool* mem_pool = pf & PF_KERNEL ? &kernel_pool : &user_pool; lock_acquire(&mem_pool->lock); /* 先将虚拟地址对应的位图置1 */ struct task_struct* cur = running_thread(); int32_t bit_idx = -1; /* 若当前是用户进程申请用户内存,就修改用户进程自己的虚拟地址位图 */ if (cur->pgdir != NULL && pf == PF_USER) { bit_idx = (vaddr - cur->userprog_vaddr.vaddr_start) / PG_SIZE; ASSERT(bit_idx >= 0); bitmap_set(&cur->userprog_vaddr.vaddr_bitmap, bit_idx, 1); } else if (cur->pgdir == NULL && pf == PF_KERNEL){ /* 如果是内核线程申请内核内存,就修改kernel_vaddr. */ bit_idx = (vaddr - kernel_vaddr.vaddr_start) / PG_SIZE; ASSERT(bit_idx > 0); bitmap_set(&kernel_vaddr.vaddr_bitmap, bit_idx, 1); } else { PANIC("get_a_page:not allow kernel alloc userspace or user alloc kernelspace by get_a_page"); } void* page_phyaddr = palloc(mem_pool); if (page_phyaddr == NULL) { lock_release(&mem_pool->lock); return NULL; } page_table_add((void*)vaddr, page_phyaddr); lock_release(&mem_pool->lock); return (void*)vaddr; } /* 得到虚拟地址映射到的物理地址 */ uint32_t addr_v2p(uint32_t vaddr) { uint32_t* pte = pte_ptr(vaddr); /* (*pte)的值是页表所在的物理页框地址, * 去掉其低12位的页表项属性+虚拟地址vaddr的低12位 */ return ((*pte & 0xfffff000) + (vaddr & 0x00000fff)); } static void mem_pool_init(uint32_t all_mem) { /* 将位图置0*/ bitmap_init(&kernel_pool.pool_bitmap); bitmap_init(&user_pool.pool_bitmap); lock_init(&kernel_pool.lock); lock_init(&user_pool.lock); }
了解esp的动向,也就清楚了程序在各个时刻使用的是哪个栈。
思考一种情况:当某用户进程A执行while死循环的时候,突然敲击键盘引发执行键盘中断处理程序会发生什么。
首先需要明确三点,
有了前面各种实验基础的回顾,我们可以知道,当用户进程处理外部中断执行键盘中断处理程序的时候,键盘中断处理程序内调用的所有操作系统函数,对应的函数符号都是0xC000XXXX,当然在3GB到3GB+1MB范围。任何用户进程根据自己的页目录表寻址0xC000XXXX这个3GB到3GB+1MB的虚拟地址,映射出来的物理地址都是实际物理地址0MB到1MB,
所以任何一个用户进程,中断后取指并执行的代码,是操作系统编译好的源码,而这些源码存储在内存的0MB到1MB处,被所有用户进程的虚拟地址3GB到3GB+1MB共享,任何进程访存操作系统函数都是通过访存虚拟地址3GB+3GB+1MB。
这里的拓展只是分析了取指时的访存访的是哪里的内存,并没有涉及特权级的分析,也就是段寄存器cs的CPL的情况。
特权级的分析在加载内核博客、以及中断处理博客已有。
简单的逻辑就是,中断进入内核态后,假如需要调用键盘中断处理函数,此时eip为0xC000XXXX,cs.CPL会变成0,cs指向的代码段的DPL也是0,所以可以访问代码段,从而对应的代码段段基址(是0)以及eip这个段偏移地址计算出虚拟地址0XC000XXXX,然后这个虚拟地址会走该用户进程的页目录表、页表映射出实际的物理地址。由于所有用户进程3GB到3GB+1MB的页目录项一样,所以会访问同一份公共页表,得到同一个0MB到1MB共享内存地址,这个内存地址存储的就是操作系统指令源码。
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。