当 Linux 启动时,首先运行在实模式下,随后就要转到保护模式下运行。因为在第二章段机制 中,我们已经介绍了 Linux 对段的设置,在此我们主要讨论与分页机制 相关的问题。 Linux 内核代码的入口点就是 /arch/i386/kernel/head.S 中的 startup_32 。
1 .页表的初步初始化:
|
内核的这段代码执行时,因为页机制 还没有启用,还没有进入保护模式,因此指令寄存器 EIP 中的地址还是物理地址,但因为 pg0 中存放的是虚拟地址 (想想 gcc 编译内核以后形成的符号地址都是虚拟地址 ),因此,“ $pg0-__PAGE_OFFSET ”获得 pg0 的物理地址,可见 pg0 存放在相对于内核代码起点为 0x2000 的地方 , 即物理地址为 0x00102000 ,而 pg1 的物理地址则为 0x00103000 。 Pg0 和 pg1 这个两个页表中的表项则依次被设置为 0x007 、 0x1007 、 0x2007 等。其中最低的三位均为 1 ,表示这两个页为用户页,可写,且页的 内容在内存中(参见图 2.24 )。所映射的物理页的基地址则为 0x0 、 0x1000 、 0x2000 等,也就是物理内存中的页面 0 、 1 、 2 、 3 等等,共映射 2K 个 页面,即 8MB 的存储空间。由此可以看出, Linux 内核对物理内存的最低要求为 8MB 。紧接着存放的是 empty_zero_page 页 ( 即零页 ) ,零页存放的是系统启动参数和命令行参数
2 .启用分页机制
|
我们先来看这段代码的功能。这段代码就是把页目录 swapper_pg_dir 的物理地址装入控制寄存器 cr3 ,并把 cr0 中的最高位置成 1 ,这就开启了分页机制。
但是,启用了分页机制,并不说明 Linux 内核真正进入了保护模式,因为此时,指令寄存器 EIP 中的地址还是物理地址,而不是虚地址。“ jmp 1f ”指令从逻辑上说不起什么作用,但是,从功能上说它起到丢弃指令流水线中内容的作用(这是 Intel 在 i386 技术资料中所建议的),因为这是一个短跳转, EIP 中还是物理地址。紧接着的 mov 和 jmp 指令把第二个标号为 1 的地址装入 EAX 寄存器并跳转到那儿。在这两条指令执行的过程中 , EIP 还是指向物理地址“ 1MB +某处”。因为编译程序使所有的符号地址都在虚拟内存空间中,因此,第二个标号 1 的地址就在虚拟内存空间的某处( (PAGE_OFFSET+ 某处 ) ,于是, jmp 指令执行以后, EIP 就指向虚拟内核空间的某个地址,这就使 CPU 转入了内核空间,从而完成了从实模式到保护模式的平稳过渡。
说明:这里可以推测: movl $1f ,%eax中的$1f是一个符号地址,编译后在二进制文件中看到的地址为 PAGE_OFFSET+某个偏移.jmp 1f,这个1f就不是符号地址了,有可能只是个相对地址。
然后再看页目录 swapper_pg_dir 中的内容。从前面的讨论我们知道 pg0 和 pg1 这两个页表的起始物理地址分别为 0x00102000 和 0x00103000 。从图 2.22 可知,页目录项的最低 12 位用来描述页表的属性。因此,在 swapper_pg_dir 中的第 0 和第 1 个目录项 0x00102007 、 0x00103007 ,就表示 pg0 和 pg1 这两个页表是用户页表、可写且页表的内容在内存。
接着,把 swapper_pg_dir 中的第 2 ~ 767 共 766 个目录项全部置 为 0 。因为一个页表的大小为 4KB ,每个表项占4 个 字节,即每个页表含有 1024 个表项,每个页的大小也为 4KB ,因此这 768 个目录项所映射的虚拟空间为 768 ´ 1024 ´ 4K = 3G ,也就是 swapper_pg_dir 表中的前 768 个目录项映射的是用户空间。
最后,在第 768 和 769 个目录项中又存放 pg0 和 pg1 这两个页表的地址和属性,而把第 770 ~ 1023 共 254 个目录项置 0 。这 256 个目录项所映射的虚拟地址空间为 256 ´ 1024 ´ 4K = 1G ,也就是 swapper_pg_dir 表中的后 256 个目录项映射的是内核空间。
![]() |
由此可以看出,在初始的页目录 swapper_pg_dir 中,用户空间和内核空间都只映射了开头的两个目录项,即 8MB 的空间,而且有着相同的映射,如图 6.6 所示。
说明:要理解这个图,如我们寻址虚拟地址3G,虚拟地址是:0xc0000000,页目录索引为1100000000=768,由此找到页表地址:0x00102007(页对齐),就是上一节代码中:
......
.org 0x2000
......
处定义的那个页表,页表索引为虚拟地址中间10位,为0,即该页表中的第一个表项,其值为:
0x007 ,就是第一个物理页帧,物理地址为0。
读者会问,内核开始运行后运行在内核空间,那么,为什么把用户空间的低区( 8M )也进行映射,而且与内核空间低区的映射相同?简而言之,是为了从实模式到保护模式的平稳过渡。具体地说,当 CPU 进入内核代码的起点 startup_32 后,是以物理地址来取指令的。在这种情况下,如果页目录只映射内核空间,而不 映射用户空间的低区,则一旦开启页映射机制以后就不能继续执行了,这是因为,此时 CPU 中的指令寄存器 EIP 仍指向低 区,仍会以物理地址取指令,直到以某个符号地址为目标作绝对转移或调用子程序为止。所以, Linux 内核就采取了上述的解决办法。
但是,在 CPU 转入内核空间以后,应该把用户空间低区的映射清除掉。后面读者将会看到,页目录 swapper_pg_dir 经扩充后就成为所有内核线程的页目录。在内核线程的正常运行中,处于内核态的 CPU 是不应该通过用户空间的虚拟地址访问内存的。清除了低区的映射以后,如果发生 CPU 在内核中通过用户空间的虚拟地址访问内存,就可以因为产生页面异常而捕获这个错误。
