[深入理解 Linux 内核 —— 内存寻址 - 知乎]( https://zhuanlan.zhihu.com/p/388649122
计算机虚拟内存
1.1 逻辑地址 (logic address)
在机器语言指令中用来指定一个操作数或一条指令的地址,每一个逻辑地址都由以下两部分组成
- 段 (segment)指明段位置
- 偏移量 (offset)指明段开始处到实际地址的距离
1.2 虚拟地址 (virtual address)
根据机器的位数不同而不同,32位机器即32位无符号整数、64位即64位无符号整数,这里取32位为例。
- 可用于表达 即 4GB 的地址空间
- 通常用16进制表示,值的范围从 0x00000000 ~ 0xffffffff
1.3 物理地址 (physical address)
内存芯片级的内存单元寻址,从CPU的地址引脚发送到内存总线上的电信号相对应,由 32 位或 36 位无符号整数表示
1.4 内存管理单元 (memory management unit)
内存控制单元以下简称MMU,其集成在CPU上进行地址翻译,转换过程为两阶段
- 分段: 由逻辑地址到虚拟地址
- 分页: 由虚拟地址到物理地址
1.5 内存仲裁器 (memory arbitration)
内存仲裁器简称 MA
在多核系统中,所有的CPU核心共享同一内存,则代表着CPU可以并发的访问内存。而内存的读写必须是串行执行,所以需要专用元器件对内存访问进行排序,其称为内存仲裁器。
内存仲裁器是在内存总线和RAM芯片之间的硬件电路
- 若内存空闲: 允许访问
- 若内存被占用: 延迟CPU访问
注:由于单处理器上存在一个叫做DMA控制器的特殊处理器,因此其实单处理器上也有内存仲裁器
1.6 分段和分页的意义
分段和分页是用于划分进程的物理地址空间的
- 分段: 每个进程分配不同的虚拟地址空间
- 分页: 把同一虚拟地址空间映射到不同的物理地址
Linux更多使用分页的方式
- 不同进程共享同一组虚拟地址空间,内存管理简单
- 跨平台,因为RISC体系结构对分段支持有限
二、内存分段
2.1 硬件分段
2.1.1 实模式和保护模式
从 80286 模型开始,Intel处理器采用两种不同方式进行地址转换,称为实模式(real mode)和保护模式(protected mode)
- 实模式
其作用是为维持处理器和早期模型的兼容,因为早期寄存器位数太少,物理地址有20位,最多1MB的内存空间。而段基址寄存器有16位,最多只能访问64kb。为了访问64kb以上的空间,需要对内存进行分段,使用段基址+段偏移的模式寻址。
通过 物理地址 = 段基址 << 4 + 段内偏移
的方式表示物理地址。这个实模式的 “实” 体现在其反应的是真实物理地址。
但是由于实模式没有区分代码和数据,如果用户程序的一个指针如果指向了系统程序区域或其他用户程序区域,并修改了内容,那么后果就很可能是灾难性的。
- 保护模式
随着寄存器硬件的扩展,地址位数和寄存器位数都变成了32/64位,现代CPU已经不需要使用上述实模式了,当然为了兼容老版本所以还是得支持实模式。
同时由于实模式不安全,我们通过一些手段来实现比较安全的寻址,这也是保护模式的命名的由来。
- 地址保护: 程序内部的地址(虚拟地址)要由操作系统转化为物理地址去访问,程序对此一无所知
- 边界保护: 段寄存器中不再储存的是段地址而是段索引。我们将数据放在一个叫做 全局描述符表 (GDT) 的结构中,其中表项称为段描述符,段描述符存放了段基址、段界限、内存段类型属性,用来索引段地址和标记段边界。
【Segmentation Fault 的出处】
2.1.2 段选择符和段寄存器
- 段选择符
逻辑地址 = 段标识符 (16位) + 段偏移量 (32位),我们又将段标识符称为段选择符,其结构如下图所示
- index 描述符的入口,在2.1.4节中会详细讲解
- TI (Table Indicator)标明段在GDT还是LDT中,在GDT中为0,LDT中为1
- RPL 请求特权级,cs寄存器改变时指示出CPU当前特权级
- 段寄存器
段寄存器存放段选择符,段寄存器共有 cs,ss,ds,es,fs和gs六个,其作用如图三所示。
注:cs寄存器中还有一个两位的字段,指明CPU当前特权级别(CPL)0~3,Linux只用0和3,代表内核态和用户态
2.1.3 段描述符
每个段被一个8字节的段描述符(Segment Descriptor)表示,描述了段的特征。其放在 全局描述符表(GDT - Global Descriptor Table) 或 局部描述符表 (LDT - Local Descriptor Table) 中
-
全局描述符表(GDT - Global Descriptor Table) - 特点:进程共享
-
存放:gdtr寄存器(基址+大小)
-
局部描述符表 (LDT - Local Descriptor Table) - 特点:进程独享
-
存放:ldtr寄存器(基址+大小)
-
描述符段分类
1. 代码段描述符
2. 数据段描述符
3. 任务状态段描述符 (TSSD) -
代表任务状态段(TSS)用于保存寄存器内容,仅在GDT中
2.1.4 快速访问段描述符
- 段描述符的索引规则:
段基址 + 段选择符 index [13位] << 3
因此描述符最大数目为
2.1.5 分段单元
图六已经较为清楚的展示了分段单元把逻辑地址转为虚拟地址的过程,段选择符在段寄存器中,offset存储在ip寄存器中
2.2 Linux 分段
2.2.1 Linux中的段结构
2.6版的Linux只有在 80x86 结构下才进行分段,下图为Linux的分段结构
所有段都从0x00000000开始, 所以在Linux下,逻辑地址和虚拟地址相同
相应端选择符由宏 __USER_CS
、__USER_DS
、__KERNEL_CS
、__KERNEL_DS
定义
CPU的CPL存储在 cs 寄存器的 RPL 字段中,特权级别改变,某些段寄存器必须更新
例如当CPL由 3 变为 0 时 ds寄存器必须从含有用户态数据段的段选择符变为含有内核数据段的段选择符,ss类似
2.2.2 Linux GDT
每个CPU对应一个GDT,所有的GDT都存放在 cpu_gdt_table 里,所有的GDT地址和大小被存放在 cpu_gdt_descr数组中。
这些符号在 arch/i386/kernel/head.S 中被定义
每个GDT包含 18个段描述符和14个空的保留项,保留项保证了常用的描述符可以在同一个32字节的 Cache 中,防止 Cache 抖动。
三、内存分页
3.1 硬件分页
分页单元(Paging Unit)是将虚拟地址转化为物理地址
关键任务: 是将所请求的访问类型和虚拟地址访问权限相比较,如果访问无效,则产生缺页异常
页: 一组虚拟地址,又指包含在这组地址中的数据。把RAM分成固定长度的页框(Page Frame)每个页框_(结构)包含一个页(数据)_。
页表: 将虚拟地址映射到物理地址的数据结构
cr0寄存器: PG标志为0,虚拟地址就解释为物理地址,否则如果 PG = 1 代表启用分页。
3.2 Linux 分页
Linux采用4级分页模式,节省内存空间花费,页表基址寄存器cr3
- 页全局目录 (Page Global Directory)
- 页上级目录(Page Upper DIrectory)
- 页中间目录(Page Middle Directory)
- 页表 (Page Table)
如图九所示,虚拟地址翻译过程,其将虚拟地址分为五部分,标准页大小4kb,所以offset占12位,剩下 52 位 每 13 位代表相应目录偏移量,先取出cr3寄存器中页全局目录的基址,和偏移量相加,索引到下级页上级目录的极致,如此重复,直到索引到页表取出 PPN,由于物理地址偏移量和虚拟地址相同,所以直接和虚拟地址偏移量 VPO 拼接得到物理地址
3.2.1 分页机制的优势
虚拟地址到物理地址的自动转换使得下述设计目标变得现实
- 给每个进程分配不同的物理地址空间,防止寻址错误
- 区别页和页框不同,允许页被装入不同的页框中,是虚拟内存机制的基本要素
每个进程有自己的页全局目录和页表集合,每次进行进程上下文切换时,Linux内核把前一个进程的cr3寄存器值存入在前一个进程的进程描述符中,并载入新进程的全局目录基址进入cr3寄存器中。
3.2.2 进程页表
进程的虚拟内存空间被分为两部分
- 用户态寻址部分:0x00000000 ~ 0xbfffffff
- 内核态寻址部分:0xc0000000 ~ 0xffffffff
进程运行在用户态时,其产生的线性地址小于 0xc0000000,在内核态则随意
3.2.3 内核页表
内核有自己的一组页表,存放在主内核页全局目录中。主内核页全局目录的最高目录项部分作为参考模型,为系统中每个普通进程对应的页全局目录项提供参考模型。