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 = ((int
unsigned
*)PAGE_DIR);int
unsigned
*page_table = ((int
unsigned
*)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, int
unsigned
ss, int
unsigned
gs,int
unsigned
fs, int
unsigned
es, int
unsigned
ds, int
unsigned
edi, int
unsigned
esi, int
unsigned
ebp,int
unsigned
esp, int
unsigned
ebx, int
unsigned
edx, int
unsigned
ecx, int
unsigned
eax, int
unsigned
isr_nr, int
unsigned
err, int
unsigned
eip, int
unsigned
cs, int
unsigned
eflags,int
unsigned
old_esp, int
unsigned
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
你可以自由使用我的代码,如有疑问请联系我。