第一次启动分页管理

4.1.4 第一次启动分页管理

227行开始,著名的分页机制就粉墨登场了:

227page_pde_offset = (__PAGE_OFFSET >> 20);

 228

 229        movl $pa(__brk_base), %edi

 230        movl $pa(swapper_pg_dir), %edx

 231        movl $PTE_IDENT_ATTR, %eax

 23210:

 233        leal PDE_IDENT_ATTR(%edi),%ecx          /* Create PDE entry */

 234        movl %ecx,(%edx)                        /* Store identity PDE entry */

 235        movl %ecx,page_pde_offset(%edx)         /* Store kernel PDE entry */

 

227行设置一个常量page_pde_offset,其值0xc0000000 >> 20 = 0xc00,其作用待会再说。229行,__brk_base,这个东西咱们见过,在链接vmlinux时,__brk_base作为BRK段的开始地址。BRK段是保留给用户进程通过系统调用brk() 向内核申请的内存空间。

 

对这个brk我还要多说几句,传统UNIX系统有两个系统调用brksbrk,主要的工作是实现虚拟内存到实际内存的映射。在GNU C程序中,内存分配是这样的:

 

每个进程可访问的虚拟内存空间为3G,但在程序编译时,不可能也没必要为程序分配这么大的空间,只分配并不大的数据段空间,程序中动态分配的空间就是从这一块分配的。如果这块空间不够,malloc函数族(realloccalloc等)就调用brk系统调用将数据段的下界移动,brk函数在内核的管理下将虚拟地址空间映射到内存,供malloc函数使用。

 

所以,这个__brk_base对应的空间就是编译链接vmlinux时的数据段下界,229行就是把这个下界的物理地址赋给了edi寄存器。

 

继续走,230行,swapper_pg_dir。这个东西就更重要了,我们操作系统的分页单元就全靠它了。它的定义在哪儿呢?同一文件621行:

621ENTRY(swapper_pg_dir)

 622        .fill 1024,4,0

 623#endif

 

我们看到.fill 1024,4,0的意思是重复1024次,每组4个字节,内容为0。这正好是一个页面的大小,4k字节。这个页面的物理地址是$pa(swapper_pg_dir)内核把它作为第一个页表,把它的物理地址赋给edx寄存器。

 

再来,231$PTE_IDENT_ATTR常量,来自arch/x86/include/asm/pgtable_types.h

#define PTE_IDENT_ATTR  0x003           /* PRESENT+RW */

#define PDE_IDENT_ATTR  0x067           /* PRESENT+RW+USER+DIRTY+ACCESSED */

#define PGD_IDENT_ATTR 0x001           /* PRESENT (no other attributes) */

 

0x003保存到eax寄存器中。随后233lea指令,保存地址值。不明白lea指令的注意了,这个指令太重要了。lea指令有两个操作数,其目的操作数,表示操作结果保存在此,该指令目的操作数只能是8个通用寄存器之一。而源操作数只能是一个存储单元,表达存储单元有多种寻址方式。

 

lea指令的功能是将源操作数、即存储单元的有效地址(偏移地址)传送到目的操作数。代码指令中PDE_IDENT_ATTR(%edi)采用间接寻址方式表达存储单元,它表示的存储单元的有效地址是:__brk_base的物理地址再加上0x67,然后被传送到ecx中。

 

这样做的目的是什么呢?我们看到,__brk_base的物理地址是32位的,而一个页面是4KB,于是乎最低12位必须为0,不然这个页面就没法对其了。所以,这里__brk_base物理地址的最低12位是无效的,内核另作他用,这里把最低12位中的0x67置位,就是说明把PRESENTRWUSERDIRTYACCESSED置位,然后把整个值传递到ecx中。然后234行就把这个地址放到swapper_pg_dir的第一个4字节的单元,也就是edx寄存器对应的那个存储单元开始的四个存储单元,作为页全局目录的第1项。

 

继续走,235行,page_pde_offset,其值0xc00我们刚才看到了。这里再把ecx中存放的__brk_base物理地址存放到swapper_pg_dir偏移0xc00的内存单元中。0xc00换算成十进制就是:3072,由于每个表项占4个内存单元,所以这个偏移就是3072/4=768,也就是以swapper_pg_dir开始的全局页目录的第768个表项。这样我们就得出了一个重要的结论,页全局目录为swapper_pg_dir开始的4096个内存单元,正好占用1个页面,包含1024个表项,但是同时从第1个和第768个表项开始分配,而且这些个表项的PRESENTRWUSERDIRTYACCESSED被置位。没有用到表项都为空。

 

236        addl $4,%edx

 237        movl $1024, %ecx

23811:

 239        stosl

 240        addl $0x1000,%eax

 241        loop 11b

 

236行,edx加上4个字节,正好对应页全局目录的第2个表项。237行,ecx的值设为1024。看到这里,大家马上会想到肯定又要进入循环了,不错241行那个loop正是这个循环的标志。

 

239行存储字符串数据(Store String Data),将累加器ecx内容存储到由es:edi寻址的内存地址。如果使用stos指令,必须指定目的操作数,stosl拷贝eaxes:edi所寻址的内存中。所以239行将eax存放的4个字节,32位的数值0x3存放到es:edi对应的内存单元,也就是__brk_base对应的那个内存单元。

 

240行,eax再加0x1000,等于0x1003,这样进入循环,循环1024次后,__brk_base对应的那个内存单元就是首地址为swapper_pg_dir,共有1024个表项的第一个页表。第1个表项的内容为0x3,第2个为0x1003,第3个为0x2003,第4个为0x3003,……,第1024个表项为0x3FF003。很多人分析代码时都不太注意这些细节,其实内核代码主要考验大家的就是掌握计算机知识的基本功,都是我们在学校学过的,没有什么高深的东西。人家Torvalds当年发明Linux的时候不也就是个小国家的本科生嘛。

 

 

 

Intel指令集中有5组处理字节,字和双字数组的指令,称为基本字符串指令,对于他们的这些基本概念请查阅博客“Intel 8086/8088 指令系统(四)”

 245        movl $pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR, %ebp

 246        cmpl %ebp,%eax

 247        jb 10b

 248        addl $__PAGE_OFFSET, %edi

 249        movl %edi, pa(_brk_end)

 250        shrl $12, %eax

 251        movl %eax, pa(max_pfn_mapped)

 252

 253        /* Do early initialization of the fixmap area */

 254        movl $pa(swapper_pg_fixmap)+PDE_IDENT_ATTR,%eax

 255        movl %eax,pa(swapper_pg_dir+0xffc)

 256#endif

 257        jmp 3f

 

由于我们的页表映射关系是以end + MAPPING_BEYOND_END结束所以还有些尾巴摇处理了245255行就是处理这个首先比较一下end + MAPPING_BEYOND_END是否超过了1如果超过了就还要跳回去再申请若干页全局目录的表项和若干个作为页表的页面。最后还要申请些空间来处理fixmap区。

 

 

 

如上图所示,最后以_brk_base开始的内存单元存放了若干个页表,每个页表占据一个页面。页表项的内容从0x003开始,到$pa(_end) + MAPPING_BEYOND_END + PTE_IDENT_ATTR取决于_end的物理地址。所以在这里页全局目录和页表建立完成后,就完成了从物理地址到链接vmlinux后的结束地址_end的所有代码的映射,包括__brk_base__brk_limit这段BRK段本身的代码。

 

略过SMPMMU的相关初始化代码(其实这些代码也很重要,只不过太偏了,感兴趣的同学可以自行研究),来到331行:

 

328/*

 329 * Enable paging

 330 */

 331        movl $pa(swapper_pg_dir),%eax

 332        movl %eax,%cr3          /* set the page table pointer.. */

 333        movl %cr0,%eax

 334        orl  $X86_CR0_PG,%eax

 335        movl %eax,%cr0          /* ..and set paging (PG) bit */

 336        ljmp $__BOOT_CS,$1f     /* Clear prefetch and normalize %eip */

 3371:

 338        /* Set up the stack pointer */

 339        lss stack_start,%esp

 

当页全局目录和页表建立好一会,就可以启动分页机制了,其步骤很简单,就是把页全局目录的32位物理地址传递给cr3寄存器,然后启动cr0的分页装置($X86_CR0_PG项字段)。综上所述,我们看到在32位内核的分页环境是启用的三级机制,即1个页全局目录,若干页表和页面。

 

最后336行,进入分页后的内核代码段,执行lss stack_start,%esp指令,立即为进程0建立内核态堆栈。stack_start定义在657行:

657 ENTRY(stack_start)

658        .long init_thread_union+THREAD_SIZE

659        .long __BOOT_DS

 

我们看到内核态堆栈由init_thread_union表示,其在include/linux/sched.h中被定义成一个全局变量:

extern union thread_union init_thread_union;

union thread_union {

       struct thread_info thread_info;

       unsigned long stack[THREAD_SIZE/sizeof(long)];

};

 

#define THREAD_SIZE       (PAGE_SIZE << THREAD_ORDER)

 

由于PAGE_SIZE4096THREAD_ORDER32x86体系中是1,所以THREAD_SIZE的值为8k。所以这个thread_union的大小也为8k。关于内核栈的详细内容请参考博客“进程相关的数据结构”

 

你可能感兴趣的:(第一次启动分页管理)