读Kernel感悟-Linux内核启动-开启页面映射

文章来源:http://www.top-e.org/jiaoshi/class/

 

在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,%edx

101         movl $1024, %ecx

102 11:

103         stosl

104         addl $0x1000,%eax

105         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),%ebp

109         cmpl %ebp,%eax

110         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 paging

185  */

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进程,得首先设置内核堆栈。

193         /* Set up the stack pointer */

194         lss stack_start,%esp

然后设置中断向量表,看到久违的"call"了

215         call setup_idt

检查CPU类型

载入gdt(原来的gdt是临时的)和ldt

302         lgdt cpu_gdt_descr

303         lidt idt_descr

最后,调用start_kernel

327         call start_kernel

到这一步,我们的目的地终于走到了。在摆脱了晦涩的汇编之后,接下去的代码,虽然与用户态程序相比,还有中断,同步等等的干扰,但相比较而言就好懂很多了。

你可能感兴趣的:(读Kernel感悟-Linux内核启动-开启页面映射)