386处理器为每个任务提供了独立地内存空间和内存保护的机制,最好的放最后,每个任务都可以存取4GB的内存。
这些机制可以分成两部分:分段和分页。分段从第一节教程中就开始使用了,它允许每个任务有独立地代码、数据和堆栈模块。分页允许按照需要将内存映射到磁盘上,我们准备在这一节中使用它。
因为我们不可能真让每个任务都有4GB物理内存,所以我们必须用其它东西来虚拟内存空间,这个过程被处理器的分页机制处理。它把每个段分成多个页面(我们准备用4KB大小的页),每个页可以存储在磁盘或内存中。操作系统通过页目录和页表跟踪它们的页状态。页目录存储着关于页表的信息,页表存储着关于页的信息。
当允许分页时,处理器以如下的步骤转换一个给定的地址,
通过当前选择子找到我们所使用的在GDT或LDT中的描述符,并做权限和长度检查以确定允许访问。
和描述符中的基地址相加得到一个线性地址。
把线性地址除以页大小以得到所在的页号。
检查本页存在与否,如果不存在页面失效异常发生。
异常处理程序将分配一个未使用页或从磁盘载入。
因为这是个异常,所以处理器重新执行引起异常的指令,这回页面已经在内存中了。
处理器使用的数据结构是页目录和页表,它们都是包含32位项的数组。
第一个图是页表项的格式,第二个图是页目录项的格式。如你所见,它们的格式很相似,先看一下相同的部分,
| 位0 | P | 页或页表是否在内存中。当p=0是,页不在内存,页面失效异常被触发 |
| 位1 | R/W | 一页或多页(是页目录项的时候)是只读(=0)还是可写(=1) |
| 位2 | U/S | 一页或多页(是页目录项的时候)的权限,当它们在管理者级别时(=0),只有PL0-2能访问它们;在用户级别时(=3),每一个任务都可以访问它们 |
| 位3, 4, (6), 7, 8 | X | Intel保留,设为0 |
| 位5 | A | 一页或多页是否被存取 |
| 位9-11 | 用户定义 | 我们准备使用位11来指示如果它不在内存的话,是否存在于磁盘中 |
在页目录中的页表的物理地址存储了页表地址的最高20位。因为只有20位,所以页表必须4KB对齐。页表项的31-12位存储了它指向页面的地址的最高20位,因为它有20位,所以它可以表示2^20 = 1M个页面,也就是1M*4K = 4GB的内存空间。页表项中的D位表示了页面内容是否被改变,当把此页换出时它是有用的,如果此页还没有被修改过并且本页是从磁盘载入的,那么我们可以仅仅把此页抛弃而不必再把它写入磁盘。
为了转换逻辑地址到物理地址,逻辑地址被分成三部分,
| 位31-22 | 页目录项的索引,我们可以得到它指向的页表的物理地址 |
| 位21-12 | 页表项的索引,我们可以得到它指向的页的物理地址 |
| 位11-0 | 页中的偏移 |
例如,我们有逻辑地址0x3E837B0A,我们检查他的头10位,是0x0FA,所以它指向页目录的第0x0FA项,假如它开始于0x0005C000,我们再接着看这项的头20位以得到页表的地址,假如是0x0003F000,接着我们再看逻辑地址的第二个10位0x037,计算开始于0x0003F000的页表的第0x037项的头20位,我们就可以得到页面的物理地址,假如是0x0001B000,接着我们获得物理地址的最后12位0xB0A,最终我们把它们加到一起得到物理地址0x0001B000+0xB0A = 0x0001BB0A。
但是这里有个问题,我们怎样才能找到头呢?答案是为页目录准备的新寄存器:CR3,它保存了当前使用的页目录的物理地址,所以它也被称为PDBR。
CR3必须在允许分页前被装入,它的值可以被MOV指令改变或被任务切换时TSS结构中的CR3域改变。
一旦处理器遇到一个不存在的页或页表或者权限冲突,页面无效异常程序被执行。CR2存储了导致这个异常的逻辑地址,错误码被压入栈,格式如下,
异常处理程序通常做如下步骤,
找到内存中的一个未用页面或者从磁盘载入。
设置相应的页表项和页目录项。
无效TLBs。
处理器把最近使用的页目录项和页表项存在一个叫做快表(TLBs)的缓存中,因此只有当要访问的项在快表中不存在时才会访问页目录和页表。只要我们修改了页目录或页表的内容,快表必须被无效,它才会丢弃旧有内容,
但是我们不能直接访问快表,所以我们通过MOV一个新值到CR3的办法,或者任务切换也可以。
看一下代码段,定义了一些常量
08/include/kernel.h
#define PAGE_DIR ((HD0_ADDR+HD0_SIZE+(4*1024)-1) & 0xfffff000)
把页目录放在IDT之后,页目录必须4KB对齐。
#define PAGE_SIZE (4*1024)
#define PAGE_TABLE (PAGE_DIR+PAGE_SIZE)
把页表放在页目录之后。
#define MEMORY_RANGE (4*1024*1024)
Skelix使用4MB内存。
08/mm.c
static char mmap[MEMORY_RANGE/PAGE_SIZE] = {PG_REVERSED, };
这是物理内存位图。
void
mm_install(void) {
unsigned *page_dir = ((intunsigned *)PAGE_DIR);int
unsigned *page_table = ((intunsigned *)PAGE_TABLE);int
unsigned int address = 0;
int i;
for(i=0; i<MEMORY_RANGE/PAGE_SIZE; ++i) {
/* attribute set to: kernel, r/w, present */
page_table[i] = address|7;
address += PAGE_SIZE;
};
初始化0-4MB内存的页表项。
page_dir[0] = (PAGE_TABLE|7);
因为一个页目录项可以表示4MB内存,所以我们只需要设置页目录的第一项就可以了。
for (i=1; i<1024; ++i)
page_dir[i] = 6;
随后的1023个页目录项,1024项共可以表示4GB内存空间。
/* set lower 1MB memory to used */
for (i=(1*1024*1024)/PAGE_SIZE-1; i>=0; --i)
mmap[i] = PG_REVERSED;
因为内核使用最低的1MB内存,所以我们把这些页面保留,不允许换出,让它们总是存于内存。
__asm__ (
"movl %%eax, %%cr3\n\t"
"movl %%cr0, %%eax\n\t"
"orl $0x80000000,%%eax\n\t"
"movl %%eax, %%cr0"::"a"(PAGE_DIR));
}
通过设定CR0第31位,我们开启了分页,简单吧。
我们可以简单的通过搜索mmap找到一个未分配的页。
unsigned int
alloc_page(int type) {
int i;
for (i=(sizeof mmap)-1; i>=0 && mmap[i]; --i)
;
if (i < 0) {
kprintf(KPL_PANIC, "NO MEMORY LEFT");
halt();
}
mmap[i] = type;
return i;
}
void *
page2mem(unsigned int nr) {
return (void *)(nr * PAGE_SIZE);
}
void
do_page_fault(enum KP_LEVEL kl,
unsigned ret_ip, intunsigned ss, intunsigned gs,int
unsigned fs, intunsigned es, intunsigned ds, int
unsigned edi, intunsigned esi, intunsigned ebp,int
unsigned esp, intunsigned ebx, intunsigned edx, int
unsigned ecx, intunsigned eax, intunsigned isr_nr, int
unsigned err, intunsigned eip, intunsigned cs, int
unsigned eflags,intunsigned old_esp, intunsigned old_ss) {int
unsigned int cr2, cr3;
()ret_ip; (void)ss; (void)gs; (void)fs; (void)es; void
()ds; (void)edi; (void)esi; (void)ebp; (void)esp; void
() ebx; (void)edx; (void)ecx; (void)eax; void
()isr_nr; (void)eip; (void)cs; (void)eflags; void
()old_esp; (void)old_ss; (void)kl;void
__asm__ ("movl %%cr2, %%eax":"=a"(cr2));
__asm__ ("movl %%cr3, %%eax":"=a"(cr3));
kprintf(KPL_PANIC, "\n The fault at %x cr3:%x was caused by a %s. "
"The accessing cause of the fault was a %s, when the "
"processor was executing in %s mode, page %x is free\n",
cr2, cr3,
(err&0x1)?"page-level protection voilation":"not-present page",
(err&0x2)?"write":"read",
(err&0x4)?"user":"supervisor",
alloc_page(PG_NORMAL));
}
异常处理程序只是输出一些关于本异常的信息。
动态分配内存,new_task这样改变,
static void
new_task(unsigned int eip) {
struct TASK_STRUCT *task = page2mem(alloc_page(PG_TASK));
memcpy(&(task->tss), &(TASK0.tss), sizeof(struct TSS_STRUCT));
task->tss.esp0 = (unsigned int)task + PAGE_SIZE;
task->tss.eip = eip;
task->tss.eflags = 0x3202;
task->tss.esp = (unsigned int)page2mem(alloc_page(PG_TASK))+PAGE_SIZE;
task->priority = INITIAL_PRIO;
task->ldt[0] = DEFAULT_LDT_CODE;
task->ldt[1] = DEFAULT_LDT_DATA;
task->next = current->next;
current->next = task;
task->state = TS_RUNABLE;
}
现在,让我们把mm_install加入到08/init.c,不要忘记修改08/exceptions.c中相应的行,接着尝试存取4MB内存以上的空间。
08/init.c
idt_install();
pic_install();
mm_install(); /* &&&&& Her it is */
kb_install();
08/exceptions.c
void
page_fault(void) {
__asm__ ("pushl %%eax;call do_page_fault"::"a"(KPL_PANIC));
halt();
}
最后,在Makefile中把mm.o加入到KERNEL_OBJS中去。
08/Makefile
KERNEL_OBJS= load.o init.o isr.o timer.o libcc.o scr.o kb.o task.o kprintf.o hd.o \
exceptions.o fs.o mm.o
你可以自由使用我的代码,如有疑问请联系我。