操作系统真象还原实验记录之实验十九:实现用户进程
1.相关基础知识
1.1 特权级(书P229)
cs寄存器的最后两位就是CPL,表示了cpu当前特权级。
对于访问数据段(type含有X可执行属性)
CPL和RPL<=DPL
对于访问非一致性代码段(type不含有X可执行属性)
只能平级,如果想执行跟高级的代码段,只能采用调用门,中断门,执行后CPL会改变成非一致性代码段的DPL。
特权级从低到高,调用门、中断门
从高到低,iret,retf
访问一致性代码段(type字段C为1)
数值上CPL>=一致性代码段的DPL
执行一致性代码段,CPL不会变成DPL,因此特权级仍然是与较低级一致的,故称一致性代码段。
1.调用门
call 和 jmp指令调用调用门
call有来有回,可以实现低向高特权转移,执行完毕后可以再通过retf指令实现由高向低。特权级由高向低转移只能使用iret和retf。
jmp有去无回,只能平级转移。
2.中断门
Int指令主动调用中断门,实现由低向高转移
比如Linux的系统调用
3.任务门
内含TSS选择子,用于任务切换
中断发生,中断向量号是任务门即完成调用,也可以call jmp调用。获得TSS选择子后完成根据TSS结构完成任务切换。
特权级高低转移不是任务切换,但需要用当前TSS里不同等级的栈。
1.2 call指令调用调用门完整过程
调用前特权级3,调用后特权级0
该调用门需要两个参数
要求数值上DPL_GATE>=CPL>=DPL_CODE
RPL<=DPL_GATE
这里的RPL是指提供的参数选择子。
进入内核后,这个选择子的RPL会被赋值为调用前的CPL即3
因此进入调用门后,就算CPL=0了,但是要访问的段的选择子仍然是之前的特权级3,仍然只能访问DPL=3的段。
(1)先将两个参数压入3特权级栈
(2)门描述符里的DPL=0,CPL=3,故在TSS里找到0特权级栈SS0,esp0
(3)检查ss0对应的栈段描述符DPL和TYPE是否通过。
(4)先找个地方保存ss_old和esp_old,然后将ss0赋值SS,esp0赋值esp,使用高特权级栈。
(5)压入ss_old,esp_old,然后根据参数个数,将3特权级栈中的两个参数复制到0特权级栈中。
(6)将cs_old,eip_old入栈
(7)最后将调用门描述符的选择子赋值CS,偏移量赋值EIP,执行调用门程序。
执行完调用门程序后,利用retf将特权级由高向低返回流程:
(1)执行retf时,先检查cs_old的RPL与当前CPL,判断转移后特权级是否改变,显然此例子由CPL=0>RPL=3,特权级检查通过。
(2)通过,弹出eip_old,cs_old
(3)reft+参数跳过参数,esp指向esp_old
(4)第一步已经判断了特权级发生了改变,所以弹出esp_old,ss_old。
(5)返回后,还要对段寄存器DS,ES,FS,GS的DPL检查,如果DPL<返回后的CPL,那么就要置0.
1.3 I/O特权级
eflags中由IOPL位,
只有当前CPL<IOPL,才能使用I/O指令in,out,cli,sti访问端口从而访问外设。
但是CPL<IOPL意味着可以用I/O指令访问所有端口。
每个端口都有一个编号,在TSS的I/O位图里
如果TSS的I/O位图哪一位是0,无论CPL和IOPL的大小
那么表示该位对应的端口都可以访问。
1.4 CPU原生支持的任务切换方式
任务门:支持任务切换
两个:一个是中断+任务门、一个是call或jmp+任务门和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
1.5 Linux即本实验采用的任务切换方式
用TSS效率低
Linux一次性加载TSS和TR,初始化0特权级栈,然后不断修改同一个TSS的内容。
这次试验应该只是在一个任务特权级切换的时候,使用了TSS的0特权级栈,任务切换都是在同一个TSS修改,这样相比原生效率大大提升。
2.实验代码
2.1 main.c
#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++;
}
}
main是进入内核的起点。
创建了两个进程,两个线程,均没有启动。
线程创建时在内核内存池申请了一页PCB,未申请页表。
进程创建时在内核内存池申请了一页PCB,一页页表,页表地址赋值给了PCB->pgdir,一页虚拟地址位图,赋值给了PCB->userprog_vaddr。
调度到用户进程时在start_process还在用户内存池创建了一页3特权级栈
注意,用户进程的物理地址使用的是用户内存池,用户内存池有自己的用户内存位图管理,见实验十三——内存管理系统。
进程和线程的启动依然是依靠时钟中断,时间片用完进行调度就绪队列的进程或线程。
所以最开始运行的仍然是主线程,用的页表都是分页实验设计好的主线程的页表,和线程实验一样,主进程页表位于,主线程会一直执行main函数的while(1)死循环,直到时间片用完。
主线程时间片用完,时钟中断处理程序进入schedule函数,
调度用户进程的话,先要将该用户进程的页表激活,同时还要修改tss的esp0,因为这次调度后该用户进程会上CPU,当再次时钟中断时会面临特权级从3向0的转变,CPU将会在进入时钟中断处理函数前取TSS的esp0,并且完成一系列栈的转移,1.2有详细步骤,这和调用门实现特权级从高向低转移原理一致,这属于Linux对于同一任务下特权级转移时利用TSS来进行栈的切换的方法。
也就是说,在切换到特权级为3的用户进程前,要更新页表,同时还要更新TSS的esp0(等于对应进程PCB页首地址)
调度工作完成后进入switch_to,利用ret配合thread_create的初始化执行kernel_thread函数,
然后执行start_process初始化cs为u_prog_a(其实thread_create已经赋值过一次了)等和esp赋值为3特权级栈
然后进入intr_exit,利用iretd实现特权级由0向3转换,
执行该用户进程u_prog_a函数。
接着调度线程的话,(这部分书上一句带过没有细讲)
进入时钟中断处理程序前,esp已经被换成了0特权级栈
process_activiate检测到是线程便不会更换页表,TSS的esp0也不会更新。
程序执行流是switch_to里执行ret,然后执行kernel_thread,然后执行k_thread_a(void* arg)。
很明显调度线程没有start_process的将cs修改成RPL=3,
也没有执行iretd指令,因此没有特权级转移,在时钟触发中断时,中断门描述符已经把0特权级选择子赋值给了cs,并且把TSS的esp0赋值给esp从而使用0特权级栈。
所以这个线程的k_thread_a(void* arg)执行在特权级为0下执行,put_char里的gs访问显存时特权级检查不会有问题。
这里还有一个点要注意:
调度进程的时候,执行iretd完成特权级3的用户进程的切换,同时还把DS,ES,FS,GS均置为了0,本次实验进入进程后的代码没有涉及这几个寄存器所以看不出来,但是再次调度线程的时候,gs已被置为0,无法用来访问显存了,所以put_char内部已经再次构造了RPL=0的显存段选择子。
其次这个线程将采用上次进程的页表来进行虚实地址转换,线程里用了put_char函数,会涉及到访问显存。
每个进程创建页表的函数create_page_dir()的原理是先在内核内存池申请一页作为页目录+页表
然后把主进程的页目录第768到1023个页目录项复制到自己的页表。
根据分页实验我们已经知道,
1.构造页目录时,分页实验的代码只构造了第0个页目录项和第768~1023个页目录项。其中第0和第768个目录项装的是第0个页表的首地址。第1023个页目录项装的是页目录的首地址。第769到1022个页目录项装的是第1到254个页表的首地址。
2.构造页表时,我们只构造了第0个页表中的前256个页表项。由于一块物理页4KB,因此分页机制下能访问的实地址只有低1MB,256*4KB=1MB。
根据第768项
也就是说两个进程的页表地址映射都是3GB~3GB+1MB 映射到实地址
0~1MB
因此线程的put_char函数访问显存不会出现任何问题,虽然不同进程下线程可能会访问到不同的页表中,但是这两个进程的地址映射规则是一致的。
2.2 process.c
#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)
void create_user_vaddr_bitmap(struct task_struct* user_prog)
void process_execute(void* filename, char* name)
为用户进程在内核内存池申请了一页PCB,一页页表,在用户内存池申请了一页3特权级栈
2.3 thread.h(增加)
/* 进程或线程的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; // 用户进程的虚拟地址
2.4 thread.c(增加)
void schedule() {
next->status= TASK_RUNNING;
process_activate(next);
switch_to(next);
}
2.5 global.h(增加)
// ---------------- 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
2.6 tss.c
#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");
}
2.7 tss.h
#ifndef __USERPROG_TSS_H
#define __USERPROG_TSS_H
#include "thread.h"
void update_tss_esp(struct task_struct* pthread);
void tss_init(void);
#endif
2.8 memory.c增加
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);
}
3.实验结果总结
3.1结果
3.2执行流esp寄存器的情况梳理
了解esp的动向,也就清楚了程序在各个时刻使用的是哪个栈。
- 主线程,esp=主线程PCB+size(即主线程0特权级栈)=0x9f000
- 主线程时钟中断,esp仍然是主线程0特权级栈 ,schedule检测到next是用户进程a含页表,tss.esp被赋值为next的0特权级栈。执行switch_to后,esp被赋值成了用户进程a的PCB最低地址,ret后执行start_process后iret,esp被赋值成了该进程3特权级栈,cpu来到用户进程a
- 用户进程a时钟中断切换用户线程a,调用中断门发现特权级将从3变0,esp最终被赋值成tss.esp0(即用户进程a的0特权级栈,含3特权级栈指针),switch_to中esp切换用户线程a的PCB最低地址,pop、ret执行kernel,执行function,cpu来到用户线程a,特权级为0
- 用户线程a时钟中断切换用户线程b,调用中断门时特权级未转换,esp仍位于用户线程a的PCB内,switch_to内esp被用户线程b的PCB最低地址,pop、ret,执行kernel,执行function,cpu来到用户线程b。
- 用户线程b时钟中断切换用户进程a,中断进入schedule,检测到next为用户进程,故tss.esp0被赋值成用户进程a的0特权级栈,switch_to内esp被切换成上次用户进程a切换时的esp(即用户进程a的0特权级栈内),ret时,根据1.2,0特权级栈顶发现eip_old的RPL=3,而当前cpl=0,所以0特权级栈会更换3特权级栈,由于用户进程a的0特权级栈含有上次进程a切换时的eip和3特权级栈指针,故esp最后又被赋值成用户进程a的3特权级栈,同时cpu来到用户进程a。