7.13 Linux内存管理
内存寻址方式的发展历程
- 直接寻址, 程序都是通过硬编码的形式绝对定位到内存地址。这种情况下的程序都有明显的缺点:可控性弱、难以重定位、难以维护等
- 分段, 段的地址存放在寄存器中, 把 1M 的空间分成数个 64K(16位寄存器可寻址)的段来管理, 8086 处理器为程序使用的代码段、数据段、堆栈段分别提供了专门的 16 位寄存器用于保存这些段的段基址。引入了分段机制后,程序的地址不再需要硬编码了,调试错误也更加容易定位,同时更加重要的是支持了更大的内存地址空间。
分页和虚拟内存
- 虚拟内存, 允许程序使用的内存大于计算机的实际内存; 虚拟内存技术本质上将部分硬盘空间映射到内存中的一种技术,这样使得程序可以使用的内存空间变大了, 如果程序访问的地址不在内存中时(放在外部磁盘空间中),就需要将访问地址所对应磁盘空间的程序内容加载到内存中,在加载的过程中可能面临着旧程序内容的置换
- 内存寻址过程分析, 逻辑地址到物理地址的转换过程, 逻辑地址 –>(分段机制) 线性地址 –>(分页机制) 物理地址
- 逻辑地址:程序代码中一个操作数或者一条指令的地址
- 线性地址:虚拟地址可映射 4G 内存空间(分页机制的产物,逻辑上连续)
- 物理地址:处理器的引脚发送到内存总线上的电信号相对应
- 分段机制
- 代码中使用到的逻辑地址由两部分组成: 1). 段选择符: 16 位长的字段; 2). 段内偏移地址:32 位长的字段(最大的段大小为 4GB)
- 段描述符, 段描述符存放在全局描述符表(GDT)和局部描述符表(LDT)中
- 快速访问段描述符, 由段选择符来索引实际的段描述符还需要查找 GDT 或者 LDT 表, 为了加速逻辑地址到线性地址的转换过程,80x86提供了一种附加的非编程寄存器,即每当一个段选择符装入寄存器时,相对应的段描述符就由内存装入到这个非编程寄存器中
- 地址转换的过程, 一个逻辑地址由段选择符和段内偏移组成,在转换成线性地址时, 分段单元执行以下操作:
- 先检查段选择符的 TI 字段,判断这个段的段描述符是存放在 GDT 中还是 LDT 中
- 然后由段选择符中的 Index 索引到实际的段描述符
- 将 Index 索引到实际的段描述符的地址 * 8,再加上 gdtr 或者 ldtr 寄存器中的值。这个过程就完成了段起始的位置的计算
- 最后把计算的结果加上逻辑地址中的段内偏移就得到了线性地址
- 分页机制
- 页, 对应着线性地址,线性地址被分成以固定大小为单位的组,称为页,页大小一般为4K
- 页框, 对应着物理地址,物理地址也就是 RAM,被分成固定大小的页框,每个页框对应一个实际的页
- 页表, 用来将页映射到具体的页框中的数据结构
- 分页
- 每个页的大小为 4KB,为了映射 4GB 的物理空间,页表中将会有 1MB(2^20) 的映射项, 避免每个程序保存大页表, 引出了多级页表的概念,也称为页目录
- 线性地址的转换过程需要两步, 1). 查找页目录找到具体的页表; 2). 然后查找页表,找到具体的页
- 线性地址, 分为3部分; 1). Directory:决定页目录中的目录项,10位大小; 2). Table:决定页表中的表项,10位大小; 3). Offset:页框的相对位置
用户空间
用户空间中进程的内存,称为进程地址空间
- 进程地址空间
- 每个进程都有一个32位或64位的地址空间,取决于体系结构, 一个进程可寻址4GB的虚拟内存(32位地址空间中),但不是所有虚拟地址都有权访问。对于进程可访问的地址空间称为内存区域。每个内存区域都具有对相关进程的可读、可写、可执行属性等相关权限设置。
- 内存区域包含
- 代码段(text section): 可执行文件代码
- 数据段(data section): 可执行文件的已初始化全局变量(静态分配的变量和全局变量)
- bss段: 程序中未初始化的全局变量,零页映射
- 进程用户空间栈的零页映射
- 每一个诸如C库或动态连接程序等共享库的代码段、数据段和bss也会被载入进程的地址空间
- 任何内存映射文件
- 任何共享内存段
- 任何匿名的内存映射
- 内存描述符
struct mm_struct
{
struct vm_area_struct *mmap;
rb_root_t mm_rb;
...
atomic_t mm_users;
atomic_t mm_count;
struct list_head mmlist;
...
};
- 虚拟内存区域(VMA)
struct vm_area_struct {
struct mm_struct * vm_mm; //内存描述符
unsigned long vm_start; //区域的首地址
unsigned long vm_end; //区域的尾地址
struct vm_area_struct * vm_next; //VMA链表
pgrot t_vm_page_prot; //访问控制权限
unsigned long vm_flags; //保护标志位和属性标志位
struct rb_node_ vm_rb; //VMA的红黑树结构
...
struct vm_operations_struct * vm_ops; //相关的操作表
struct file * vm_file; //指向被映射的文件的指针
void * vm_private_data; //设备驱动私有数据,与内存管理无关。
}
缺页中断
- 当一个进程发生缺页中断的时候,进程会陷入内核态,执行以下操作:
- 检查要访问的虚拟地址是否合法
- 查找/分配一个物理页
- 填充物理页内容
- 建立映射关系(虚拟地址到物理地址)
内存分配原理
- 从操作系统角度来看,进程分配内存有两种方式,分别由两个系统调用完成:
brk
和mmap
(不考虑共享内存)- brk是将数据段(.data)的最高地址指针_edata往高地址推
- mmap是在进程的虚拟地址空间中(堆和栈中间,称为文件映射区域的地方)找一块空闲的虚拟内存
- 这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。
- 在标准C库中,提供了malloc/free函数分配释放内存,这两个函数底层是由brk, mmap, munmap这些系统调用实现的