代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。
1.bss简介
我们要完成堆内存,就得考虑用户进程内存空间布局。
仿照Linux 下 C 程序的布局方案来做

在C程序的内存空间中,位于低处的三个段是代码段、数据段和bss段,它们由编译器和链接器规划地址空间,在程序被操作系统加载之前它们地址就固定了。
而堆是位于bss段的上面,栈是位于堆的上面,它们共享4GB空间中除了代码段、数据段及顶端命令行参数和环境变量等以外的其余可用空间,它们的地址由操作系统来管理,在程序加载时为用户进程分配栈空间,运行过程中为进程从堆中分配内存。 堆向上扩展,栈向下扩展,因此在程序的加载之初,操作系统必须为堆和栈分别指定起始地址。
在Linux中,堆的起始地址是固定的,这是由 struct mm_struct 结构中的start_brk来指定的,堆的结束地址并不固定,这取决于堆中内存的分配情况,堆的上边界是由同结构中的 brk 来标记的。
C语言程序上大体上分为预处理、编译、汇编、链接四个阶段,在链接阶段链接器将这些目标文件中属性相同的节(section)合并成段(segment),因此一个段是由多个节组成的,我们平时所说的C程序内存空间中的数据段、代码段就是指合并后的segment。
在保护模式下对内存的访问必须要经过段描述符,段描述符用来描述一段内存区域的访问属性,其中的S位和TYPE位可组合成多种权限属性,处理器用这些属性来限制程序对内存的使用,如果程序对某片内存的访问方式不符合该内存所对应的段描述符(由访问内存时所使用的选择子决定)中设置的权限,比如对代码这种具备只读属性的内存区域执行了写操作,处理器会检查到这种情况并抛出GP异常。程序必须要加载到内存中才能执行,为了符合安全检查,程序中不同属性的节必须要放置到合适的段描述符指向的内存中。比如为程序中具有只读可执行的指令部分所分配的内存,最好是通过具有只读、可执行属性的段描述符来访问,否则若通过具有可写属性的段描述符来访问指令区域的话,程序有可能会将自己的指令部分改写,从而引起破坏。处理器对内存访问的安全检查主要体现在使用的段描述符,段描述符是由选择子决定的,而选择子是由操作系统提供的,所以针对程序中不同属性的区域,操作系统得知道用哪个段描述符来匹配程序中这些不同属性的区域片段,也就是要在程序运行之前提前设置程序在运行时各种段寄存器(如cs、ds)中的选择子。
在上面的图中text段是代码段,里面存放的是程序的指令,data段是数据段,里面存放的是程序运行时的数据,它们的共同点是都存在于程序文件中,也就是在文件系统中存在,而bss并不存在于程序文件中,它仅存在于内存中,其实际内容是在程序运行过程中才产生的,程序文件中仅在elf头中有bss节的虚拟地址、大小等相关记录,这通常是由链接器来处理的,对程序运行并不重要,因此程序文件中并不存在bss实体。bss中的数据是未初始化的全局变量和局部静态变量,程序运行后才会为它们赋值,因此在程序运行之初,里面的数据没意义,由操作系统的程序加载器将其置为0就可以了,虽然这些未初始化的全局变量及局部静态变量起初是用不上的,但它们毕竟也是变量,即使是短暂的生存周期也要占用内存,必须提前为它们在内存中占好座, bss区域的目的也正在于此,就是提前为这些未初始化数据预留内存空间。
未运行之前或运行之初,程序中bss中的内容都是未初始化的数据,它们也是变量,只不过这些变量的值在最初时是多少都无所谓,它们的意义是在运行过程中才产生的,故程序文件中无需存在bss实体,因此不占用文件大小。在程序运行后那些位于bss中的未初始化数据便被赋予了有意义的值,那时bss开始变得有意义,故bss仅存在于内存中,既然bss中的数据也是变量,就肯定要占用内存空间,需要把空间预留出来,但它们并不在文件中存在,对于这种只占内存又不占文件系统空间的数据,链接器采取了合理的做法:由于bss中的内容是变量,其属性为可读写,这和数据段属性一致,故链接器将bss占用的内存空间大小合并到数据段占用的内存中,这样便在数据段中预留出bss的空间以供程序在将来运行时使用。注意,这里所说的是bss的尺寸会被合并到数据段,并不是bss中的实际内容也会被合并到数据段中,毕竟起初bss中的内容无意义,将它的内容合并到其他段中真的是毫无意义。当程序文件被操作系统加载器加载时,加载器会为程序的各个段分配内存,由于bss已被归并到数据段中,故bss仅存在于数据段所在的内存中。因此,bss的作用就是为程序运行过程中使用的未初始化数据变量提前预留了内存空间。程序的bss段(数据段的一部分)会由该加载器填充为0。由此可见,为生成在某操作系统下运行的用户程序,编译器和操作系统需要相互配合。
我们在不久的将来也要实现malloc,因此也必须支持堆内存管理,堆的起始地址应该在bss之上,现在我们知道bss已经被归并到数据段了,操作系统的程序加载器会为该程序的数据段分配内存,也就是 bss段的内存区域也会顺便被分配,因此要实现用户进程的堆,已不需要知道bss的结束地址,将来加载程序时会获取程序段的起始地址及大小,因此只要堆的起始地址在用户进程地址最高的段之上就可以了。
2.实现用户进程
完整代码
修改/userprog/process.c
#include "thread.h"
#include "process.h"
#include "tss.h"
#include "console.h"
#include "memory.h"
#include "global.h"
#include "bitmap.h"
#include "interrupt.h"
#include "debug.h"
extern void intr_exit(void);
/*构建用户进程filename_的初始上下文信息,即struct intr_stack*/
void start_process(void* filename_){ //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;
proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA;
proc_stack->eip = function; //cs:eip是程序入口地址
proc_stack->cs = SELECTOR_U_CODE; //cs寄存器赋值为用户级代码段
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); //3特权级下的栈
proc_stack->ss = SELECTOR_U_DATA;
asm volatile("movl %0,%%esp;jmp intr_exit" : : "g"(proc_stack) : "memory"); //通过假装从中断返回的方式,使filename_运行
}
/*激活页表*/
void page_dir_activate(struct task_struct* p_thread){
/*执行此函数时,当前任务可能是线程。
*因为线程是内核级,所以线程用的是内核的页表。每个用户进程用的是独立的页表。
*之所以对线程也要重新安装页表,原因是上一次被调度的可能是进程,否则不恢复页表的话,线程就会使用进程的页表了
*/
uint32_t pagedir_phy_addr = 0x100000; /*若为内核线程,需要重新填充页表为内核页表:00x10_0000*/
//默认为内核的页目录物理地址,也就是内核线程所用的页目录表
if(p_thread->pgdir != NULL){
pagedir_phy_addr = addr_v2p((uint32_t)p_thread->pgdir); //判断是线程/进程的方法:进程的pcb中pgdir指向页表的虚拟地址,而线程中无页表所以pgdir = NULL
}
/*更新页目录表寄存器cr3,使新页表生效*/
asm volatile("movl %0,%%cr3" : : "r"(pagedir_phy_addr):"memory"); //将进程/线程的页表/内核页表加载到cr3寄存器中
//每个进程都有独立的虚拟地址空间,本质上是各个进程都有自己单独的页表。
//页表是存储在页表寄存器cr3中的,cr3只有一个。在不同进程执行前,我们要在cr3寄存器中为其换上配套的页表,从而实现虚拟地址空间的隔离。
}
/*激活线程/进程的页表,更新tss中的esp0为进程的特权级0的栈*/
void process_activate(struct task_struct* p_thread){
ASSERT(p_thread != NULL);
/*激活该进程/线程的页表*/
page_dir_activate(p_thread);
/*内核线程特权级本身是0,CPU进入中断时并不会从tss中获取0特权级栈地址,故不需要更新esp0*/
if(p_thread->pgdir){
/*更新该进程的esp0,用于此进程被中断时保留上下文*/
update_tss_esp(p_thread); //更新tss中的esp0为进程的特权级0栈
//时钟中断进行进程调度,中断需要用到0特权级的栈
}
}
/*创建页目录表,将当前页表的表示内核空间的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项 [0x300是十六进制的768,4是页目录项的大小]*/
memcpy((uint32_t*)((uint32_t)page_dir_vaddr + 0x300*4),(uint32_t*)(0xfffff000 + 0x300*4),1024); //dst src size
//0xfffff000 + 0x300*4表示内核页目录表中第768个页目录项的地址,1024/4=256个页目录项的大小,相当于低端1GB的内容
/*********************************************/
/* 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);
}唯一的变化就是增加了三个函数
create_page_dir函数
create_page_dir函数用来创建页目录表。
为了用户进程可以访问到内核服务,必须确保用户进程必须在自己的地址空间中能够访问到内核才行,也就是说内核空间必须是用户空间的一部分。
实现方法就是虚拟地址空间由页表来控制,页表由操作系统管理,因此用户进程的虚拟空间是由操作系统规划分配的。每个用户进程都拥有4GB虚拟地址空间,操作系统把这4GB空间分为用户空间和内核空间两部分,因此内核空间和用户空间的大小是不固定的,它们可以以任意比例分配这4GB,比如内核和用户空间可以各占2GB。Linux分了3GB给用户空间,自己本身占1GB,所有用户进程的最高1GB空间都指向Linux所在的内存空间,这样操作系统就被所有用户进程共享。
内存布局如图所示:

目前我们的系统使用的是二级页表,加载到页目录表寄存器CR3中的是页目录表的物理地址,页目录表中一共包含1024个页目录项(pde),页目录项大小为4B,故页目录表大小为4KB,每个页目录项仅表示1个页表,页目录项中存储的是页表所在物理页框的物理地址及页目录项的属性。每个页表可容纳1024个页表项(pte),页表项大小为4B,故每个页表本身占用4KB。每个页表项仅表示一个物理页框,页表项中存储的是4KB大小的物理页框的物理地址及页表项的属性,因此每个页表可表示的地址空间为1024*4KB-4MB,一个页目录表中可包含 1024 个页表,因此可表示1024*4MB=4GB 大小的地址空间。 目前我们的内核位于0xc0000000以上的地址空间,也就是位于页目录表中第768~1023个页目录项所指向的页表中,这一共是256个页目录项,即1GB空间。
在任意进程的页目录表中页表与内核的关系如图所示:

用户进程占据页目录表中第0~767个页目录项,内核占据页目录表中第768~1023个页目录项。 所以如果想要在用户进程中能够访问到内核,就要为每一个用户进程准备一份内核的符号链接(软链接)。
页表是记录在页目录项中,此处页目录项对于内核物理内存的作用,相当于Linux中文件的符号链接,页目录项是访问内核所在物理内存的入口。
因此,为了访问到内核,我们只要给每个用户进程创建访问内核的入口即可。也就是把用户进程页目录表中的第768~1023个页目录项用内核页目录表的第768~1023个页目录项代替,其实就是将内核所在的页目录项复制到进程页目录表中同等位置,这样就能让用户进程的高1GB空间指向内核。每创建一个新的用户进程,就将内核页目录项复制到用户进程的页目录表,这样就为内核物理内存创建了多个入口,从而实现了所有用户进程共享内核。
回到create_page_dir函数。
通过get_kernel_pages(1)在内核内存池中申请一页内存,将返回地址存储到指针变量page_dir_vaddr中,注意, page_dir_vaddr中的值是虚拟地址。
接下来的工作分为两部分,首先我们要把内核的页目录项复制到用户进程使用的页目录表中。
通过memcpy((uint32_t*)((uint32_t)page_dir_vaddr + 0x300*4),(uint32_t*)(0xfffff000 + 0x300*4),1024)完成的,memcpy的第一个形参是复制的目标地址,此处的实参为(uint32_t*)((uint32_t)page_dir_vaddr + 0x300*4),其中page_dir_vaddr是为用户进程申请作为页目录表的基地址,0x300是十进制的768,4是每个页目录项的大小,0x300*4表示768个页目录项的偏移量,因此,这表示复制的目标地址为偏移用户进程页目录表基地址768个页目录项的地方,此处正是内核起始地址0xc0000000被映射的页表所在的页目录项地址。memepy的第二个形参是复制的源地址,此处的实参是(uint32_t*)(0xfffff000+0x300*4),用户进程的创建是在内核中完成的,因此目前是在内核的页表中,其中0xfffff000便是用来访问内核页目录表的基地址(也是第0个页目录项),0x300*4是内核起始页目录项在页目录表中的偏移量,也就是内核起始地址0xc0000000所在的页目录项地址,因此0xfffff000+0x300*4是内核页目录表中第768个页目录项的地址。memcpy的第三个形参是复制的字节量,这里是1024,即1024/4=256个页目录项的大小。这样内核占用的页目录项被复制到了用户进程的页目录表中,也就是为用户进程创建了访问内核的入口。
其次需要把用户页目录表中最后一个页目录项更新为用户进程自己的页目录表的物理地址。执行期间有可能会有页表操作,页表操作是由内核代码完成的,因此内核需要知道该用户进程的页目录表在哪里。如果用户进程通过系统调用申请内存,它会陷入内核态,那时内核除了为其分配内存外,如果申请的内存大小跨越了物理页框(大于4KB),甚至跨越了页表(大于4MB),还需要在它的页目录表中创建页目录项和在页表中创建页表项,否则会引起pagefault异常。每个用户有自己单独的页表,为了让用户进程能够使用系统为其分配的地址空间,肯定需要内核事先在该用户进程自己的页表中创建该地址对应的页目录项和页表项,无论怎样操作页表,必须要让内核获取页目录表的地址,内核访问页目录表的方法是通过虚拟地址0xfffff000,这会访问到当前页目录表的最后一个页目录项。为了保证内核操作的是该用户进程自己的页目录表,此时必须把页目录表的物理地址写入用户进程页目录表的最后一个页目录项中。
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将物理地址new_page_dirphy_addr加上属性PG_US_U和PG_RW_W以及PG_P_1后,写入用户进程页目录表的最后一个页目录项,即 page_dir_vaddr[1023]。
create_user_vaddr_bitmap函数
create_user_vaddr_bitmap函数为用户进程创建虚拟内存池。
用户进程有自己的4GB虚拟地址空间,这空间中除了存放用户进程自己的指令和数据外,还要包括用户进程自己的堆和栈,用户进程可以在自己的堆中申请、释放内存,因此必须有一套方法跟踪内存的分配情况。和内核一样,用户进程也是用位图来管理地址分配的,每个进程有自己单独的位图,存储在进程pcb中的userprog_vaddr中。
在C语言中用户进程用malloc申请的内存是在进程自己的堆空间中,操作系统在用户进程的堆空间找到可用的内存后,返回该内存空间的起始地址。我们也要实现堆内存管理,为了实现简单,现在并没有为堆单独规划起始地址,而是由用户进程自己的虚拟内存池统一管理,用户进程被加载到内存后,剩余未用的高地址都被作为堆和栈的共享空间。
create_user_vaddr_bitmap函数它接受一个参数user_prog,表示用户 进程,函数功能是创建用户进程的虚拟地址位图user_prog->userprog_vaddr,也就是按照用户进程的虚拟内存信息初始化位图结构体struct virtual_addr。
user_prog->userprog_vaddr.vaddr_start 是位图中所管理的内存空间的起始地址,我们为用户进程定的起始地址是USER_VADDR_START,该值定义在process.h中,其值为0x8048000,这是Linux用户程序入口地 址。
变量bitmap_pg_cnt用来记录位图需要的内存页框数,计算过程中用到了宏DIV_ROUND_UP,它用来 实现除法的向上取整,此宏定义在global.h中。
接下来通过get_kernel_pages(bitmap_pg_cnt)为位图分配内存,返回的地址记录在位图指针user_prog->userprog_vaddr.vaddr_bitmap.bits中。然后将位图长度记录在user_prog->userprog_vaddr. vaddr_bitmap.btmpbytes_len 中。最后调用函数 bitmap_init(&user_prog->userprog_vaddr.vaddr_bitmap)进行位图初始化,至此用户虚拟地址位图创建完成。
process_execute 函数
process_execute 函数的目的是创建用户进程并加入就绪队列,申请1页空间存储PCB,初始化PCB,向PCB中添加位图信息,向PCB中添加栈上下文信息,给PCB中的pgdir赋值(通过调用create_page_dir 函数),到这里PCB的内容已经填充完毕了,接下来关中断,把PCB加入就绪队列和全部队列中,开中断,实现原子操作。
对应process.h如下:
#ifndef __USERPROG_PROCESS_H #define __USERPROG_PROCESS_H #include "stdint.h" #define USER_STACK3_VADDR (0xc0000000 - 0x1000) #define USER_VADDR_START 0x8048000 //Linux用户程序的入口地址 #define default_prio 31 // 默认优先级 #define NULL ((void*)0) //函数声明 /*构建用户进程filename_的初始上下文信息,即struct intr_stack*/ void start_process(void* filename_); /*激活页表*/ void page_dir_activate(struct task_struct* p_thread); /*激活线程/进程的页表,更新tss中的esp0为进程的特权级0的栈*/ void process_activate(struct task_struct* p_thread); /*创建页目录表,将当前页表的表示内核空间的pde复制,成功则返回页目录表的虚拟地址,否则返回-1*/ uint32_t* create_page_dir(void); /*创建用户进程虚拟地址位图*/ void create_user_vaddr_bitmap(struct task_struct* user_prog); /*创建用户进程并加入到就绪队列中*/ void process_execute(void* filename,char* name); #endif
3.用户进程的调度
内核线程是0特权级,并且它使用内核的页表,进程的特权级是3,有自己单独的页表,我们需要改进调度器,增加对进程的处理。
修改/thread/thread.c的schedule函数
/*实现任务调度*/
void schedule(){
ASSERT(intr_get_status() == INTR_OFF);
struct task_struct* cur = running_thread();
if(cur->status == TASK_RUNNING){
//若此线程只是cpu时间片到了,将其加入就绪队列队尾
ASSERT(!elem_find(&thread_ready_list,&cur->general_tag));
list_append(&thread_ready_list,&cur->general_tag);
cur->ticks = cur->priority;
cur->status = TASK_READY;
}else{
//若此线程需要某事件发生后才能继续上cpu运行,不需要将其放入队列中,因为当前线程不在就绪队列中
}
ASSERT(!list_empty(&thread_ready_list));
thread_tag = NULL;
thread_tag = list_pop(&thread_ready_list);
struct task_struct* next = elem2entry(struct task_struct,general_tag,thread_tag); //将thread_tag转化为线程(链表)
next->status = TASK_RUNNING;
/*激活任务页表等*/
process_activate(next);
switch_to(cur,next);
}就是增加了代码”process activate(next);”,process_activate除了用来更新任务的页表外,还要根据任务是否为进程,修改tss中的esp0。
当然还要再/thread/thread.c加上#include “process.h”
对应/thread/thread.h加上
extern struct list thread_ready_list; //就绪队列 extern struct list thread_all_list; //所有任务队列
用户进程的创建是由函数process execute完成的,在main.c中调用它
#include "print.h"
#include "init.h"
#include "debug.h"
#include "memory.h"
#include "thread.h"
#include "interrupt.h"
#include "console.h"
#include "process.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() {
put_str("I am kernel\n");
init_all();
thread_start("k_thread_a", 31, k_thread_a, "argA ");
thread_start("k_thread_b", 31, k_thread_a, "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++;
}
}这里分别创建了两个线程和两个进程,至此我们的系统中并行了 4 个任务。
process_execute(u_prog_a, “user_prog_a”)创建用户进程,其中参数 u_prog_a是用户进程地址,是待运行的进程。 功能是执行死循环,使全局变量test_var_a++。
u_prog_a是在main.c中定义的,它只是个函数,目前还没有实现文件系统,使用函数展示替代一下。
查看u_prog_a

u_prog_a 的地址是 Oxc0001624,按地址来说,它确实是内核空间的函数,但这并不是说此函数就不能模拟用户进程了,只要处理器在执行用户进程时能够访问该地址就行。我们把所有页目录项和页表项的US位都置为1 (loader.S和memory.c中所有涉及到PDE和PTE的地方都用的是PG_US_U),这表示处理器允许所有特权级的任务可以访问目录项或页表项指向的内存,所以用内核空间中的函数来模拟用户进程是没有问题的。
我们创建的两个用户进程u_prog_a和u_prog_b,它们分别将全局变量test_var_a和test_var_b自增,不过目前我们的用户进程无法直接访问0特权级的显存段,故调用打印函数时处理器会抛出一般保护性异常,因此干脆由高特权级的线程来帮忙打印。用k thread a和k thread b分别将变量test var a和test var b的值打印出来。
kernel/interrupt.c修改pic_init开启时钟中断。
/*初始化可编程中断控制器8259A*/
static void pic_init(void){
/*初始化主片*/
outb(PIC_M_CTRL,0x11); //ICW1:边沿触发,级联8259,需要ICW4
outb(PIC_M_DATA,0x20); //ICW2:起始中断向量号为0x20,也就是IR[0-7]为0x20~0x27
outb(PIC_M_DATA,0x04); //ICW3:IR2接从片
outb(PIC_M_DATA,0x01); //ICW4:8086模式,正常EOI
/*初始化从片*/
outb(PIC_S_CTRL,0x11); //ICW1:边沿触发,级联8259,需要ICW4
outb(PIC_S_DATA,0x28); //ICW2:起始中断向量号为0x28,也就是IR[8-15]为0x28~0x2F
outb(PIC_S_DATA,0x02); //ICW3:设置从片连接到主片的IR2引脚
outb(PIC_S_DATA,0x01); //ICW4:8086模式,正常EOI
/*打开主片上IR0,打开时钟产生的中断*/
outb(PIC_M_DATA,0xfe);
outb(PIC_S_DATA,0xff);
// /*测试键盘,打开键盘中断*/
// outb(PIC_M_DATA,0xfd);
// outb(PIC_S_DATA,0xff);
/*打开时钟中断和键盘中断*/
// outb(PIC_M_DATA,0xfc);
// outb(PIC_S_DATA,0xff);
put_str(" pic_init done\n");
}
makefile文件加入对应
执行结果如下:
成功

我们调试一下,
用 lb命令使 bochs在虚拟地址0xc0001624处停下,用命令c持续执行,当bochs停住后,执行命令sreg查看段寄存器,此时cs的值为0x002b,我们关注最低4位,其值为b,换为二进制是1011,最低 2 位为 rpl,也就是3,所以可以判断此时用户进程确实是在 3 特权级下,与预期符合。

4.参考
郑钢著操作系统真象还原
田宇著一个64位操作系统的设计与实现
丁渊著ORANGE’S:一个操作系统的实现


