page在内核中的前世今生

“book-mark https://0xax.gitbooks.io/linux-insides/content/Booting/linux-bootstrap-1.html

其实上学的时候内存管理在课堂是提了很多,大家都学过,但是真正的实现其实有很多细节可以去探究的和注意的,本文末尾列出了很多参考书籍,包括”understanding the linux virtual memory manager”[1],和”linux-insides”[2],此文仅仅是作为笔记对这些内容的部分提炼.内存管理说起来是一个非常大的话题,本文只会从加电启动开始的页表构建到最后的页与虚拟地址之间的转化做一个讨论,这也是标题为什么叫”page的前世今生”的原因了.而内核中的伙伴系统和slab分配器不会提及,因为这些要说的话,又是一个非常大的话题,希望以后我有机会能做个总结.

计算机加电以后主板会启动CPU,CPU中寄存器含有默认值,这个时候X86是实模式(real mode).

实模式的意思是物理寻址地址只有20bit,顶多访问0-2^20(1MB)范围的内存,权限全开.然后通过

segment:offset来访问物理地址.例如 address = CS << 4 + IP
, CS是segment基址,IP是指令地址,每个寄存器只有16位.

CPU启动后默认的CS是 0xffff0000
,IP是 0xfff0
,然后构成地址为 0xfffffff0
,这个叫做”reset vector”,是CPU重置之后

第一条寻址的指令.

从这条指令开始BIOS开始启动,并且寻找可加载设备,加载顺序在BIOS里面设置了的.从硬盘加载的时候会从找到”boot sector”并且加载.其中包括接下来的启动过程和分区表.

在实模式中内存分布是这样的:

0x00000000 - 0x000003FF - Real Mode Interrupt Vector Table
0x00000400 - 0x000004FF - BIOS Data Area
0x00000500 - 0x00007BFF - Unused
0x00007C00 - 0x00007DFF - Our Bootloader
0x00007E00 - 0x0009FFFF - Unused
0x000A0000 - 0x000BFFFF - Video RAM (VRAM) Memory
0x000B0000 - 0x000B7777 - Monochrome Video Memory
0x000B8000 - 0x000BFFFF - Color Video Memory
0x000C0000 - 0x000C7FFF - Video ROM BIOS
0x000C8000 - 0x000EFFFF - BIOS Shadow Area
0x000F0000 - 0x000FFFFF - System BIOS

最初的CPU默认指令指向的地址不是RAM而是ROM,BIOS存在于ROM中.

0xFFFE_0000 - 0xFFFF_FFFF: 128 kilobyte ROM mapped into address space

BIOS把bootloader加载进去,一般是grub,把控制权交给grub,再把内核加载进来.在是模式下新的内存分布是这样的,`

100000   +------------------------+
         | I/O memory hole        |
0A0000   +------------------------+
         | Reserved for BIOS      | Leave as much as possible unused
         ~                        ~
         | Command line           | (Can also be below the X+10000 mark)
X+10000  +------------------------+
         | Stack/heap             | For use by the kernel real-mode code.
X+08000  +------------------------+
         | Kernel setup           | The kernel real-mode code.
         | Kernel boot sector     | The kernel legacy boot sector.
       X +------------------------+
         | Boot loader            |

跳转到”Kernel setup”(地址是:0x1000 + X + sizeof(KernelBootSector) + 1, X是Kernel boot sector加载的地址),设置寄存器,并执行跳转,把控制权交给kernel.

“Kernel Setup”的代码入口在 arch/x86/boot/header.S
里面,前半部分是头信息,后面 start_of_setup
才是真正代码执行部分.

在汇编中进行一些寄存器,栈和推进行初始化,其实还是把对应的寄存器设置到指定的地址,然后把bss清空成0就可以,最后调用 call main
跳到 arch/x86/boot/main.c
中的 main
函数.

进入 main
以后主要完成的是一些前期准备,heap和console的初始化,内存探测,cpu验证,键盘初始化.

还记得我们的实模式是把20bit的地址分成了16个64KB的segment吗?现在我们对内存还要作分段寻址,因为进入保护模式以后寻址范围变大了,我们的段也分出了更多级.

分级的好处是为了后面构造页表,有页表的出现也是因为把全部虚拟地址映射的话,一级表结构映射虚拟地址本身的内存开销会很大.所以多级表结构有点像按需分配的意思,不需要完全映射,只映射使用的部分,相对来说消耗就比较少.这只是一个题外话.

最大的页表结构是 Global Descriptor Table
,寄存器 gdt
指向的就是它的地址,这个寄存器有32位是用于寻址的,还有一些位是用于控制位的.

main
当中也有很多初始化的工作,最后才转换到实模式,其中我们只看一看GDT(全局页表)是如何初始化的.

对应的代码是:

/*
 * Set up the GDT
 */

struct gdt_ptr {
        u16 len;
        u32 ptr;
} __attribute__((packed));

static void setup_gdt(void)
{
        /* There are machines which are known to not boot with the GDT
           being 8-byte unaligned.  Intel recommends 16 byte alignment. */
        static const u64 boot_gdt[] __attribute__((aligned(16))) = {
                /* CS: code, read/execute, 4 GB, base 0 */
                [GDT_ENTRY_BOOT_CS] = GDT_ENTRY(0xc09b, 0, 0xfffff),
                /* DS: data, read/write, 4 GB, base 0 */
                [GDT_ENTRY_BOOT_DS] = GDT_ENTRY(0xc093, 0, 0xfffff),
                /* TSS: 32-bit tss, 104 bytes, base 4096 */
                /* We only have a TSS here to keep Intel VT happy;
                   we don't actually use it for anything. */
                [GDT_ENTRY_BOOT_TSS] = GDT_ENTRY(0x0089, 4096, 103),
        };      
        /* Xen HVM incorrectly stores a pointer to the gdt_ptr, instead
           of the gdt_ptr contents.  Thus, make it static so it will
           stay in memory, at least long enough that we switch to the
           proper kernel GDT. */
        static struct gdt_ptr gdt;

        gdt.len = sizeof(boot_gdt)-1;
        gdt.ptr = (u32)&boot_gdt + (ds() << 4);

        asm volatile("lgdtl %0" : : "m" (gdt));
}

小贴士: __attribute__((aligned(16)))是一个C扩展,表示数据以16字节对齐,而packed表示用最小内存来存放数据,也就是禁止产生padding.

这里全局表有三个入口,如注释说的一样,然后通过指令 lgdtl
把地址加载到 gdt
寄存器中让全局表生效.最后设置CR0(control register)寄存器设置PE(protection enable)位使得实模式生效.置位之后CPU就开始进入实模式.

小贴士: “page fault”是指虚拟地址对应的页还没有存在于内存当中.

小贴士: “paging”是换页的意思,表示页置换到二级存储介质的行为.

小贴士: “bzImage”是vmlinux,header,kernel setup的gzip压缩.

第一步就是进入保护模式(protected mode).

  1. 关中断
  2. 加载通过 ldgt
    从寄存器 gdt
    加载全局页表.
  3. 在CR0(控制寄存器0)当中把PE(保护使能)置位.
  4. 跳转到保护模式的代码.

建立起页表以后(有点没说清楚)

就说一下内存的数据结构.

然后地址转换.

稿源:yueyue.gao (源链) | 关于 | 阅读提示

本站遵循[CC BY-NC-SA 4.0]。如您有版权、意见投诉等问题,请通过eMail联系我们处理。
酷辣虫 » 后端存储 » page在内核中的前世今生

喜欢 (0)or分享给?

专业 x 专注 x 聚合 x 分享 CC BY-NC-SA 4.0

使用声明 | 英豪名录