Linux内存管理详解
内存管理
一、虚拟内存
1.1 为什么需要虚拟内存
前提:单片机的 CPU 是直接操作内存的 物理地址。
问题:想在单片机的内存中同时运行两个程序是做不到的。
原因:单片机没有操作系统,每次写完代码都要借助工具把程序烧录进去,直接使用绝对物理地址。
虚拟内存有什么作用?
- 第一,虚拟内存可以使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。
- 第二,由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。进程也没有办法访问其他进程的页表,所以这些页表是私有的,这就解决了多进程之间地址冲突的问题。
- 第三,页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。在内存访问方面,操作系统提供了更好的安全性。
1.2 虚拟内存的实现方式
1.2.1 内存分段管理
- 分段机制下的虚拟地址:
段选择因子 ➕ 段内偏移量。
- 虚拟地址和物理地址如何映射:
- 段选择因子:保存在段寄存器里面。
- 段选择因子里面最重要的是段号,用作段表的索引。
- 段表里面保存的是这个段的基地址、段的界限和特权等级等。
- 段内偏移量:虚拟地址中的段内偏移量应该位于 0 和段界限之间,如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。

- 不足之处:
- 内存碎片的问题。(外部碎片)
- 内存交换的效率低的问题。
- 对于多进程的系统来说,用分段的方式,外部内存碎片是很容易产生的,产生了外部内存碎片,那不得不重新 Swap 内存区域,这个过程会产生性能瓶颈。
- 如果内存交换的时候,交换的是一个占内存空间很大的程序,这样整个机器都会显得卡顿。
1.2.2 内存分页管理
分段管理的不足:
分段的好处就是能产生连续的内存空间,但是会出现外部内存碎片和内存交换的空间太大的问题。
1.2.2.1 单页表的实现方式
- 分页机制:
- 分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫页(Page)。
- 在 Linux 下,每一页的大小为 4KB。
- 分页机制下内存如何映射
- 虚拟地址分为两部分,页号 ➕ 页内偏移。
- 页号作为页表的索引,页表包含物理页每页所在物理内存的基地址,这个基地址与页内偏移的组合就形成了物理内存地址。
- 页表是存储在内存里的,内存管理单元(MMU)完成映射的工作。
- 当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。

- 不足之处:
- 出现内部碎片:分页机制分配内存的最小单位是一页,即使程序不足一页大小,我们最少只能分配一个页,所以页内会出现内存浪费。
- 多进程的系统中页表可能会占用大量内存:
在 32 位的环境下,虚拟地址空间共有 4GB,假设一个页的大小是 4KB,需要 4MB 的内存来存页表。- 每个进程都是有自己的虚拟地址空间的,也就说都有自己的页表。100 个进程的话,就需要 400MB 的内存来存储页表,这是非常大的内存了。
1.2.2.2 多级页表的实现方式(知道)
解决单页表的内存占用问题。
二级分页
- 将一级页表分为 1024 个二级页表
- 每个表二级页表中包含 1024 个页表项

1.2.2.3 TLB - Translation Lookaside Buffer
程序是有局部性的,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。
多级页表虽然解决了空间上的问题,但是虚拟地址到物理地址的转换就多了几道转换的工序,这显然就降低了这俩地址转换的速度,也就是带来了时间上的开销。
那把最常访问的几个页表项存储到访问速度更快的硬件,在 CPU 芯片中,加入了一个专门存放程序最常访问的页表项的 Cache。
这个 Cache 就是 TLB(Translation Lookaside Buffer) ,通常称为页表缓存、转址旁路缓存、快表等。

1.2.3 段页式内存管理
段页式内存管理实现的方式:
- 先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制;
- 接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页;
这样,地址结构就由段号、段内页号、页内位移三部分组成。
段页式地址变换中要得到物理地址须经过三次内存访问:
- 第一次访问段表,得到页表起始地址;
- 第二次访问页表,得到物理页号;
- 第三次将物理页号与页内位移组合,得到物理地址。

二、Linux 内存管理
2.1 Linux 的虚拟地址空间是如何分布的
2.1.1 虚拟地址空间如何划分
地址空间通常分为两部分:
- **内核空间:**用于操作系统内核和内核模块。内核空间是全局共享的,所有进程都可以访问相同的内核地址空间,但通常只有内核态代码才能访问内核空间。
- **用户空间:**用于运行用户态的应用程序。用户空间中的每个进程都有其独立的虚拟地址空间,不同进程间的用户空间是相互隔离的。

2.1.2 用户空间如何划分
在Linux操作系统中,每个进程都有自己独立的虚拟地址空间。这意味着每个进程都有一份独立的用户空间划分,包括代码段、数据段、堆、栈等。

以 32 位系统为例,每个进程的用户空间划分的情况(高地址 -- > 低地址):
- **内核空间:**所有进程共享内核的代码和数据,独享与进程相关的数据结构
- **栈段:**包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是 8 MB。当然系统也提供了参数,以便我们自定义大小;从高地址往低地址增长
- **文件映射段:**包括动态库、共享内存等,从低地址开始向上增长(跟硬件和内核版本有关 (opens new window));
- **堆段:**包括动态分配的内存,从低地址开始向上增长;
- **BSS 段:**包括未初始化的静态变量和全局变量;
- **数据段:**包括已初始化的静态常量和全局变量;
- **代码段:**包括二进制可执行代码;
堆和文件映射段的内存是动态分配的。
比如说,使用 C 标准库的 malloc() 或者 mmap(),就可以分别在堆和文件映射段动态分配内存。

2.2 Linux 如何分配虚拟内存
2.2.1 malloc() 如何分配虚拟内存
2.2.1.1 malloc 涉及的两种系统调用
malloc申请内存的时候,会有两种方式向操作系统申请堆内存。
- **方式一:**通过 brk() 系统调用从堆分配内存
- free内存后堆内存还存在,放在malloc的内存池里。
- 将堆顶指针向高地址移动,获得新的内存空间。

- **方式二:**通过 mmap() 系统调用在文件映射区域分配内存;不连续的哦!!!!
- free释放内存后就会归还给操作系统
- 在文件映射区分配一块内存,也就是从文件映射区"偷"了一块内存。

2.2.1.2 两种系统调用的阈值
malloc() 源码里默认定义了一个阈值:
- 如果用户分配的内存小于 128 KB,则通过 brk() 申请内存;
- 如果用户分配的内存大于 128 KB,则通过 mmap() 申请内存;
不同的 glibc 版本定义的阈值也是不同的。
2.2.1.3 两种系统调用的异同
不同:
- 申请的内存位置不同
- brk()在堆上
- mmap()在文件映射区域
- free()释放内存的处理不同
- brk()申请的内存不会归给操作系统,先放在malloc的缓冲区,下次再有申请的时候可以直接使用。
- mmap()申请的内存会马上归还给操作系统
共同:
- malloc 返回给用户态的内存起始地址比进程的堆空间起始地址多了 16 字节
多出来的 16 字节就是保存了该内存块的描述信息,比如有该内存块的大小。
当执行 free() 函数时,free 会对传入进来的内存地址向左偏移 16 字节,然后从这个 16 字节的分析出当前的内存块的大小,自然就知道要释放多大的内存

2.2.1.4 两种系统调用的设计目的
问题:malloc()函数会在>某个阈值的时候,通过系统调用mmap()来申请内存而不是brk(),而mmap()又会申请文件映射区域的虚拟内存返回,为什么要这么设计?
2.3 Linux 的 swap 机制
2.3.1 swap 机制工作原理
- 内存页的分类:
- 活跃页(Active Pages): 最近被访问过的内存页,系统认为它们可能会被再次访问。
- 非活跃页(Inactive Pages): 最近未被访问的内存页,系统认为它们不太可能被立刻访问。
- 交换空间(Swap Space):
- 交换空间通常是一个专用的磁盘分区或文件,用于存储被交换出去的内存页。
- 页交换(Paging):
- 当系统的物理内存不足时,内核会选择一些非活跃页,将它们从物理内存中移出,写入到交换空间。
- 当这些页再次被访问时,内核会将它们从交换空间读回物理内存。如果物理内存仍然不足,内核可能会继续交换出其他非活跃页。
三、数组的物理空间连续?
数组的虚拟地址是连续的,对于程序而言,只知道虚拟地址空间,物理地址对程序不可见。
虚拟地址到物理地址的映射是操作系统负责的。
四、用户空间的栈和堆区有什么区别
- 增长方向不同
- 内存回收策略不同,栈自动分配和回收,堆是手动申请和释放(c语言malloc()会有两种堆内存申请策略,对应两个系统调用brk和mmap,所以堆的释放也是有区别的)
- 大小不同:栈一般是8MB(linux下),堆则受虚拟内存大小影响
五、栈上操作比堆要快,为什么
栈有寄存器直接对栈进行访问(esp、ebp),所以快。
堆的数据需要通过指针访问(操作系统管理堆内存是有一个链表的,记录指针),将指针放在寄存器,再去取出这个地址的值,涉及到间接寻址。
六、64 位和 32 位的操作系统,物理内存为4GB,申请 8G 的内存有什么区别
- 32 位的操作系统,最大可用虚拟内存只有 4GB, 申请8GB的话会在申请阶段就失败
- 64 位的操作系统,最大可用虚拟内存有 128TB, 申请8GB的话要考虑是否开启swap分区:
- 未开启swap:由于物理内存不够,进程会被操作系统杀掉(kill),原因是:OOM(内存溢出)。
- 开启了swap:可以正常使用8GB内存,进程可以正常运行。
七、swap 区是动态调整的吗
Macos 是会动态增长,每次1GB。
Linux 不会自动增长,需要手动管理。
八、fork 的写时复制
- Fork 创建子进程的时候,只会复制虚拟内存,不会复制物理内存。因此会先共享这份物理内存。
- 当父/子进程进行写操作的时候,才会复制物理内存。

未发生写操作

发生了写操作