美文网首页
Linux 内存管理篇(2)内核初始化与内存管理启用

Linux 内存管理篇(2)内核初始化与内存管理启用

作者: 陌城小川 | 来源:发表于2019-08-01 12:48 被阅读0次

    前言

    内存寻址之后, 本篇开始介绍Linux内核地址空间初始化过程。

    通过内存寻址篇我们知道, Linux 系统运行过程中位于保护模式,系统必须要是用MMU来完成地址寻址, 这就依赖于段表页表

    但是问题来了, 系统是如何将段表跟页表是如何装入的呢?

    本文通过 Linux 系统初始化过程,开始介绍内存管理的构建过程。

    BIOS 时代:

    当PC机加电的那一刻,主机开始获取操作指令,初始化操作系统。

    这个时候,系统cpu是运行在实模式(详情见说明)下的, CPU最开始从0xFFFF0 处定位BIOS通过影子内存(详情见说明)定位BIOS第一条指令。

    BIOS 就开始地检测内存、显卡等外设信息,当硬件检测通过之后,就在内存的物理内存的起始位置 0x000 ~ 0x3FF建立中断向量表

    然后, BIOS 将启动磁盘中的第1个扇区(MBR 扇区,Master Boot Record)的 512 个字节的数据加载到物理内存地址为 0x7C00 ~ 0x7E00 的区域,然后程序就跳转到 0x7C00 处开始执行,至此,BIOS 就完成了所有的工作,将控制权转交到了 MBR 中的代码。通过MBR加载Linux 内核映像。

    实模式运行阶段:

    先将内核镜像文件中的起始第一部分 boot/setup.bin 加载到 0x7c00 地址之上的物理内存中,然后跳转到 setup.bin 文件中的入口地址开始执行。

    涉及的文件有 arch/x86/boot/header.S链接脚本setup.ldarch/x86/boot/main.cheader.S 第一部分定义了 .bstext.bsdata.header 这 3 个节,共同构成了vmlinuz 的第一个512字节(即引导扇区的内容)。常量 BOOTSEGSYSSEG 定义了引导扇区和内核的载入的地址。

    BOOTSEG     = 0x07C0        /* original address of boot-sector */
    SYSSEG      = 0x1000        /* historical load address >> 4 */
    

    主要完成的工作:

    • 初始化早期启动状态下的控制台(console)。
    • 初始化临时堆栈空间。
    • 检测 CPU 相关信息。
    • 通过向 BIOS 查询的方式,收集硬件相关信息,并将结果存放在第 0 号物理页中。

    实模式下的最终内存模型

    image.png

    保护模式运行模式

    第一次处于保护模式下-内核加载

    为了进入保护模式,需要先设置gdt,这个时候的gdt为boot_gdt,代码段和数据段描述符中的基址都为0.

    arch/x86/boot/pm.c    
    
    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));   //加载段描述符
     } 
    

    当完成以上内容后, 置 CPU PE标志为1, 打开保护模式, 这时候分页还没有开启。
    进入保护模式后,就设置各个段选择子.所有段寄存器(dsesfsgsss)都为设置为 _BOOT_DS 选择子.

    再由于没有分页,所以线性地址就是物理地址.

    然后, 把内核镜像 bzImage 中的第二部分 boot/vmlinux.bin 加载到物理内存中起始地址为 0x100000 的位置.

    • boot/vmlinux.bin 文件中解压内核的代码拷贝到物理内存中 boot/vmlinux.bin 的后面。
    • 初始化 stack 和 heap 空间。
    • 解压缩内核,解压缩后的内核就是我们从源码编译得到的 vmlinux ELF 可执行文件。

    第二次设置 gdtr

    解压完内核后就应该跳入真正的内核,即内核中第二个 startup_32() .这个时候的整个vmlinux的编译链接地址都是从虚拟地址(线性地址) 0xc0000000(__PAGE_OFFSET) 开始的,所以需要重新设置下段寻址。

    这个是linux内核第二次设置段寻址,称为第二次进入保护模式.

    这一次设置的原因是在之前的处理过程中,指令地址是从物理地址0x100000 开始的,而此时整个 vmlinux 的编译链接地址是从虚拟地址 0xC0000000(__PAGE_OFFSET) 开始的,所以需要在这里重新设置 boot_gdt 的位置。

    ENTRY(startup_32)
        cld
        lgdt boot_gdt_descr - __PAGE_OFFSET
        movl $(__BOOT_DS),%eax
        movl %eax,%ds
        movl %eax,%es
        movl %eax,%fs
        movl %eax,%gs
    
    /*
     * Clear BSS first so that there are no surprises...
     * No need to cld as DF is already clear from cld above...
     */
        xorl %eax,%eax
        movl $__bss_start - __PAGE_OFFSET,%edi
        movl $__bss_stop - __PAGE_OFFSET,%ecx
        subl %edi,%ecx
        shrl $2,%ecx
        rep ; stosl
    

    内核运行到这个时候,所有段基址都是0x00000000开始,而内核链接的线性地址都是从虚拟地址0xc0000000,但是这个时候还没有开启分页,那如果要访问一个变量应该怎么寻址呢?
    则使用 X-__PAGE_OFFSET 如上所示,或者使用 __pa, __va 宏定义:

    #define __pa(x)         ((unsigned long)(x)-PAGE_OFFSET)
    #define __va(x)         ((void *)((unsigned long)(x)+PAGE_OFFSET))
    

    进入分页模式 - 建立临时内核页表

    虽然可以使用X-__PAGE_OFFSET来获得真实位置,但是依然不是长久之计,当务之急是开启分页,在内核编译链接时,就已经存在了一张全局目录:

    ENTRY(swapper_pg_dir)
        .fill 1024,4,0
    

    内核通过把swapper_pg_dir所有项都填充为0来创建期望映射,不过 0, 1, 0x300(768项),0x301(769项)除外。

    • 0 项和 0x300 项的地址字段置位 pg0 的物理地址,
    • 1 项和 0x301 项的地址字段置为紧随 pg0后的页框的物理地址。
    • 这四项的 Present, Read/WriteUser/Supervisor 标志都置位
    • 把这四项中的 Accessed, Dirty, PCD, PWD 和Page Size 标志清零。

    pg0 这两个页表分别实现如下范围内的映射关系,依次实现对物理地址前8M 的寻址

    0x00000000 - 0x007fffff -> 0x00000000 - 0x007fffff
    0xc0000000 - 0xc07fffff -> 0x00000000 - 0x007fffff
    

    在第一次开启分页时就把这张表作为页全局目录,将其地址给cr3寄存器,并开启分页.

    movl $swapper_pg_dir-__PAGE_OFFSET,%eax
    movl %eax,%cr3      /* set the page table pointer.. */
    movl %cr0,%eax
    orl $0x80000000,%eax
    movl %eax,%cr0      /* ..and set paging (PG) bit */
    

    第三次设置 gdtr:

    开启分页之后, 接下来就通过分页寻址得到编译好的最终全局描述符表 gdt 的地址(cpu_gdt_table),将其地址付给gdtr,把段寄存器初始化为最终值。

    收尾

    通过以上系统初始过程, Linux 进入保护保护模式, 并完成对前 8M RAM内存空间的映射关系。

    接下来便可以在保护模式下对内核代码段与数据段进行寻址。

    image.png

    后续,代码跳转到 start_kernel() 函数, 完成 Linux内核初始化工作。

    • 调度程序初始化
    • 内存管理区初始化
    • 伙伴系统分配程序初始化
    • IDT 初始化
    • slab 分配器初始化

    ...


    说明:

    • 内核版本 2.6.11.2
    • 处理机: i386

    实模式 :

    它是 Intel公司 80286 及以后的x86(80386,80486等)处理器的一种操作模式。
    实模式被特殊定义为20位地址内存可访问空间上,这就意味着它的容量是2^{20}(1M)的可访问内存空间(物理内存和BIOS-ROM),软件可通过这些地址直接访问BIOS程序和外围硬件。
    实模式下处理器没有硬件级的内存保护概念和多道任务的工作模式。但是为了向下兼容,所以80286及以后的x86系列兼容处理器仍然是开机启动时工作在实模式下。

    在寻址上实模式采用了分段寻址模式, 具体为: [16位段基地址DS]:[16位偏移EA] 组成。
    其地址换算方式为: 物理地址 = (DS << 4) +EA, 例如 1000:FFFF = 1FFFFF

    虽然理论上这种寻址模式支持的最大值为FFFF:FFFF=10FFEF, 但是由于只有20为有效地址总线,所以无法对第21为进行寻址。
    为了解决上述兼容性问题,IBM使用键盘控制器上剩余的一些输出线来管理第21根地址线(从0开始数是第20根) 的有效性,被称为A20
    如果A20 Gate被打开,则当程序员给出100000H-10FFEFH之间的地址的时候,系统将真正访问这块内存区域

    影子内存

    影子内存(Shadow RAM,或称ROM shadow)是为了提高系统效率而采用的一种专门技术。它把系统主板上的系统ROM BIOS和适配器卡上的视频ROM BIOS等拷贝到系统RAM内存中去运行,其地址仍使用它们在上位内存中占用的原地址。

    确切地说,是将ROM中的数据,拷贝至RAM。

    “影子”内存所占用的空间是768KB—1024KB之间的区域。

    参考文档 :

    Linux内核初始化阶段内存管理的几种阶段(1) maxwellxxx's Blog

    Linux 内核加载启动过程分析

    setup.s 分析—— Linux-0.11 学习笔记(二) - ARM的程序员敲着诗歌的梦 - CSDN博客

    CPU 实模式 保护模式 和虚拟8086模式 - 辉仔 の专栏 - CSDN博客

    Linux页表机制初始化 - vanbreaker的专栏 - CSDN博客

    document/深入理解linux内核中文第三版.pdf at master · saligia-eva/document · GitHub

    The Linux Kernel Archives

    相关文章

      网友评论

          本文标题:Linux 内存管理篇(2)内核初始化与内存管理启用

          本文链接:https://www.haomeiwen.com/subject/pwsqdctx.html