读Kernel感悟-Linux内核启动-开启页面映射
在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)。