前言

继内存寻址之后, 本篇开始介绍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 GDTbeing 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, insteadof the gdt_ptr contents.  Thus, make it static so it willstay in memory, at least long enough that we switch to theproper 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)cldlgdt boot_gdt_descr - __PAGE_OFFSETmovl $(__BOOT_DS),%eaxmovl %eax,%dsmovl %eax,%esmovl %eax,%fsmovl %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,%eaxmovl $__bss_start - __PAGE_OFFSET,%edimovl $__bss_stop - __PAGE_OFFSET,%ecxsubl %edi,%ecxshrl $2,%ecxrep ; 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位地址内存可访问空间上,这就意味着它的容量是(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

作者:陌城小川
链接:https://www.jianshu.com/p/c5770a06507a
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

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

  1. Linux内核初始化阶段内存管理的几种阶段

    本系列旨在讲述从引导到完全建立内存管理体系过程中,内核对内存管理所经历的几种状态.阅读本系列前,建议先阅读memblock的相关文章. 一些讲在前面的话 在很久很久以前,linux内核还是支持直接从磁 ...

  2. Linux内核机制总结内存管理之连续内存分配器(二十七)

    文章目录 1 连续内存分配器 1.1 使用方法 1.2 技术原理 重要:本系列文章内容摘自<Linux内核深度解析>基于ARM64架构的Linux4.x内核一书,作者余华兵.系列文章主要用 ...

  3. Linux kernel 3.10内核源码分析--slab原理及相关代码

    1.基本原理 我们知道,Linux保护模式下,采用分页机制,内核中物理内存使用buddy system(伙伴系统)进行管理,管理的内存单元大小为一页,也就是说使用buddy system分配内存最少需 ...

  4. linux内核初始化卡死,armlinux内核启动--内存初始化管理

    linux版本:2.6.36 相关数据结构 arch/arm/include/asm/setup.h #ifdef CONFIG_ARCH_LH7A40X # define NR_BANKS 16 # ...

  5. linux初始化内存盘卡住,分析内核初始化时根内存盘的加载过程(init/main.c)-嵌入式系统-与非网...

    作者:opera 概述 ==== 1)当内核配置了内存盘时, 内核在初始化时可以将软盘加载到内存盘中作为根盘. 当同时配置了初始化内存盘(Initail RAM Disk)时, 内核在初始化时可以在安 ...

  6. linux 增加 ip_conntrack_max 造成 内核内存问题

    1.由ip_conntrack引出的Linux内存映射 有很多文章在讨论关于ip_conntrack表爆满之后丢弃数据包的问题,对此研究深入一些的知道Linux有个内核参数ip_conntrack_m ...

  7. linux内存分配器类型,内核早期内存分配器:memblock

    原标题:内核早期内存分配器:memblock 本文转载自Linux爱好者 本文来自 程雪涛的自荐投稿 Linux内核使用伙伴系统管理内存,那么在伙伴系统工作前,如何管理内存?答案是memblock. ...

  8. 深入理解Linux内核01:内存寻址

    目录 1. 内存地址 1.1 三种地址 1.1.1 逻辑地址(logical address) 1.1.2 线性地址(linear address) 1.1.3 物理地址(physical addre ...

  9. linux 内核高端内存意义,Linux内核高端内存管理

    原先一直都对Linux高端内存的管理认识模模糊糊的,可能主要是初次接触Linux kernel 是0.11版吧,当初的内存设计是16M,Linus对拥有32M的内存都是觊觎万分,1G内存恐怕是天方夜谭 ...

最新文章

  1. jquery mobile实例
  2. pytorch学习笔记 torchnn.ModuleList
  3. Python 内部:可调用对象是如何工作的
  4. 项目: 用Easyx绘制围棋和象棋的棋盘
  5. Linux系统下快速配置HugePages的完整步骤
  6. keepalived 负载 mysql 3306端口问题
  7. hist 和imhist的区别
  8. 最大子段和三种算法实现
  9. 从jupyter转换为exe格式
  10. php倒序正序,php foreach正序倒序输出示例代码
  11. 【机器学习基础】三层神经网络
  12. [JQuery]关于使用jsp:include标签及%@ include标签时要注意的事项
  13. Java实现23种设计模式教程(作者原创)
  14. 为什么梦里常有视觉和听觉,却没有痛觉和嗅觉?
  15. mysql sql并列排名_教你用SQL实现统计排名
  16. 有理谱估计的参数化方法
  17. Z80 CPU资料调查
  18. python删除网页弹出对话框_python selenium-webdriver 处理JS弹出对话框
  19. 基于深度学习的视频检测(三) 目标跟踪
  20. 一步一步实现五子棋5

热门文章

  1. [黑金原创教程] FPGA那些事儿《数学篇》- CORDIC 算法
  2. linux下IO口模拟I2C的一些总结
  3. 事件ID 5775 NETLOGON
  4. [原创].图解一招搞定UCWEB@Nokia S60v5无法在博客园手机版发闪存的问题
  5. 科学家揭示灵长类早期胚胎发育多能性的变化模式
  6. Web常用函数介绍(LoadRunner相关)
  7. RT/Metro商店应用如何如何获取图片的宽高
  8. Vmware iSCSi 配置
  9. Bad version number in .class file
  10. 亮剑:PHP,我的未来不是梦(3)