在setup的帮助下,我们顺利地从16位实地址模式过渡到32位段式寻址的保护模式。又在arch/i386/boot/compressed/head.S的帮助下实现了内核的自解压,并且从arch/i386/kernel/head.S中的startup_32开始。现在在线性地址0x100000(1M)处开始就是我们的解压后的内核了。而startup_32()的地址恰好是0x100000。由于还没有开启页面映射,所以必须引用变量的线性地址(即变量的虚拟地址-PAGE_OFFSET),带来了很多不便。所以下一步的任务,就是建立页表,开启页面映射了。我们不妨从arch/i386/kernel/head.S入手。
由于在Linux中,每个进程拥有一个页表,那么,第一个页表也应该有一个对应的进程。通常情况下,Linux下通过fork()系统调用,复制原有进程,来产生新进程。然而第一个进程该如何产生呢?既然不能复制,那就只能像女娲造人一样,以全局变量的方式捏造一个出来。它就是init_thread_union。传说中的0号进程,名叫swapper。只要swapper进程运行起来,调用start_kernel(),剩下的事就好办了。不过,现在离运行swapper进程还差得很远。关键的一步,我们还没有为该进程设置页表。
为了保持可移植性,Linux采用了三级页表。不过x86处理器只使用两级页表。所以,我们需要一个页目录和很多个页表(最多达1024个页表),页目录和页表的大小均为4k。swapper的页目录的创建与该进程的创建思维类似,也是捏造一个页表,叫swapper_pg_dir.
417 ENTRY(swapper_pg_dir)418 .fill 1024,4,0
它的意思是从swapper_pg_dir开始,填充1024项,每项为4字节,值为0,正好是4K一个页面。
页目录有了,接下去看页表。一个问题产生了。该映射几个页表呢?尽管一个页目录最多能映射1024个页表,每个页表映射4M虚拟地址,所以总共可以映射4G虚拟地址空
间。但是,通常应用程序用不了这么多。最简单的想法是,够用就行。先映射用到的代码和数据。还有一个问题:如何映射呢?运行cat /proc/$pid/maps可以看到,用户态进程的地址映射是断断续续的,相当复杂。这是由于不同进程的用户空间相互独立。但是,由于所有进程共享内核态代码和数据,所以映射关系可以大大简化。既然内核态虚拟地址从3G开始,而内核代码和数据事实上是从物理地址0x100000开始,那么本着KISS原则,一切从简,加上3G就作为对应的虚拟地址好了。由此可见,对内核态代码和数据来说:虚拟地址=物理地址+PAGE_OFFSET(3G)
内核中有变量pg0,表示对应的页表。建立页表的过程如下:
091 page_pde_offset = (__PAGE_OFFSET >> 20); 092 093 movl $(pg0 - __PAGE_OFFSET), %edi 094 movl $(swapper_pg_dir - __PAGE_OFFSET), %edx 095 movl $0x007, %eax /* 0x007 = PRESENT+RW+USER */ 096 10: 097 leal 0x007(%edi),%ecx /* Create PDE entry */ 098 movl %ecx,(%edx) /* Store identity PDE entry */ 099 movl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry */ 100 addl $4,%edx101 movl $1024, %ecx 102 11: 103 stosl 104 addl $0x1000,%eax105 loop 11b 106 /* End condition: we must map up to and including INIT_MAP_BEYOND_END */ 107 /* bytes beyond the end of our own page tables; the +0x007 is the attribute bits */ 108 leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp109 cmpl %ebp,%eax110 jb 10b 111 movl %edi,(init_pg_tables_end - __PAGE_OFFSET)
用伪代码表示就是:typedef unsigned int PTE;PTE *pg=pg0;PTE pte=0x007;
for(i=0;;i++){//把线性地址i*4MB~(i+1)*4MB-1(用户空间地址)和3G+i*4MB~3G+(i+1)*4MB-1(内核空间地址)映射到物理地址i*4MB~(i+1)*4MB-1
swapper_pg_dir[i]=pg+0x007;
swapper_pg_dir[i+page_pde_offset]=pg+0x007;for(j=0;j<1024;j++){
pte+=0x1000;
pg[i*1024+j]=pte;
}
if(pte>=((char*)pg+i*1024+j)*4+0x007+INIT_MAP_BEYOND_END){
init_pg_tables_end=pg+i*0x1000+j;break;}}
大致意思是从0开始,把连续的线性地址映射到物理地址。这里的0x007是什么意思呢?由于每个页表项有32位,但其实只需保存物理地址的高20位就够了,所以剩下的低12位可以用来表示页的属性。0x007正好表示PRESENT+RW+USER(在内存中,可读写,用户页面,这样在用户态和内核态都可读写,从而实现平滑过渡)。
那么结束条件是什么呢?从代码中可知,当映射到当前所操作的页表项往下
INIT_MAP_BEYOND_END(128K)处映射结束。nm vmlinux|grep pg0得c0595000。据此可以计算总共映射了多少页(小学计算题:P)
所以映射了2个页表,映射地址从0x0~0x2000-1,大小为8M。最后,关键时刻到来了:
183 /* 184 /* Enable paging185 */ 186 movl $swapper_pg_dir-__PAGE_OFFSET,%eax 187 movl %eax,%cr3 /* set the page table pointer.. */188 movl %cr0,%eax 189 orl $0x80000000,%eax 190 movl %eax,%cr0 /* ..and set paging (PG) bit */
开启页面映射后,可以直接引用内核中的所有变量了。不过离start_kernel还有点距离。要启动swapper进程,得首先设置内核堆栈。
然后设置中断向量表,看到久违的"call"了
215 call setup_idt
检查CPU类型
载入gdt(原来的gdt是临时的)和ldt
302 lgdt cpu_gdt_descr
303 lidt idt_descr
最后,调用start_kerne
l327 call start_kernel
到这一步,我们的目的地终于走到了。在摆脱了晦涩的汇编之后,接下去的代码,虽然与用户态程序相比,还有中断,同步等等的干扰,但相比较而言就好懂很多了。