每一个人都知道,Linux有着复杂的内存管理。在介绍之前,我想先问读者一个问题,为什么需要内存管理?
答案无非包括以下这些:
- 分配/释放内存很自然地需要管理
- 不同的内存区域也许有着不同的访问权限
- 用户空间的进程都需要独立的地址空间
- 物理地址不连续时,仍然能提供虚拟地址连续的空间
- 用户空间甚至能分配出比实际物理内存更大的地址空间
- Swap:把内存交换到存储设备上
独立的地址空间:
虚拟地址连续,物理地址不连续:
Swap:
先讲一下Kernel里跟内存管理相关的一些概念。
Linux管理物理内存的最小单位是PAGE。
Linux管理内存的软件概念从大到小有:Node –> Zone -> Buddy System -> PAGE -> Slab/Slub/Slob
Node对应于NUMA的概念。对于手机平台,只会有一个物理内存,也就是只有一个Node。
Zone的分类会多一点:
- ZONE_DMA:这个Zone里的PAGE可以用来做DMA操作
- ZONE_NORMAL:这个Zone就是所谓的线性空间,也就是low memory
- ZONE_HIGHMEM:这个Zone对应于high memory
Low memory是用section的方式作一次大的mapping的。对于一般的平台,物理地址和虚拟地址之间的差别是0xc0000000.
High memory可以参考我的另一篇blog“我所理解的high memory”,与low memory的主要区别是虚拟地址和物理地址没有确定的对应关系。
Buddy System
Linux内核管理物理内存是通过分页机制实现的,它将整个内存划分成无数个4k大小的页,从而分配和回收内存的基本单位便是内存页了。利用分页管理有助于灵活分配内存地址,因为分配时不必要求必须有大块的连续内存,系统可以东一页、西一页的凑出所需要的内存供进程使用。虽然如此,但是实际上系统使用内存时还是倾向于分配连续的内存块,因为分配连续内存时,页表不需要更改,因此能降低TLB的刷新率(频繁刷新会在很大程度上降低访问速度)。
鉴于上述需求,内核分配物理页面时为了尽量减少不连续情况,采用了“伙伴”关系来管理空闲页面。
Slab/Slub/Slob
以页为最小单位分配内存对于内核管理系统中的物理内存来说的确比较方便,但内核自身最常使用的内存却往往是很小(远远小于一页)的内存块——比如存放文件描述符、进程描述符、虚拟内存区域描述符等行为所需的内存都不足一页。这些用来存放描述符的内存相比页面而言,就好比是面包屑与面包。一个整页中可以聚集多个这些小块内存;而且这些小块内存块也和面包屑一样频繁地生成/销毁。
为了满足内核对这种小内存块的需要,Linux系统采用了一种被称为slab分配器的技术。Slab分配器的实现相当复杂,但原理不难,其核心思想就是“存储池”的运用。内存片段(小块内存)被看作对象,当被使用完后,并不直接释放而是被缓存到“存储池”里,留做下次使用,这无疑避免了频繁创建与销毁对象所带来的额外负载。
Slab技术不但避免了内存内部分片带来的不便(引入Slab分配器的主要目的是为了减少对伙伴系统分配算法的调用次数——频繁分配和回收必然会导致内存碎片——难以找到大块连续的可用内存),而且可以很好地利用硬件缓存提高访问速度。
Slab并非是脱离伙伴关系而独立存在的一种内存分配方式,slab仍然是建立在页面基础之上,换句话说,Slab将页面(来自于伙伴关系管理的空闲页面链表)撕碎成众多小内存块以供分配,slab中的对象分配和销毁使用kmem_cache_alloc与kmem_cache_free。
- Slab是最先出现的,也是最基础的
- Slub是后来出现,基于Slab的优化版本,能更好地支持NUMA系统
- Slob是更轻量级的,更适合嵌入式系统
Kernel里关于内存管理的概念介绍完了,现在开始涉及到用户层的内存管理。
首先来看一下linux对虚拟地址的划分:
对任何一个普通进程来讲,它都会涉及到5种不同的数据段。
- 代码段:代码段是用来存放可执行文件的操作指令,也就是说是它是可执行程序在内存中的镜像。代码段需要防止在运行时被非法修改,所以只准许读取操作,而不允许写入(修改)操作——它是不可写的。
- 数据段:数据段用来存放可执行文件中已初始化全局变量,换句话说就是存放程序静态分配的变量和全局变量。
- BSS段:BSS段包含了程序中未初始化的全局变量,在内存中 bss段全部置零。
- 堆:堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)
- 栈:栈是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
几个常用的分配内存的接口:
1. kmalloc/kfree:在内核空间分配物理/虚拟地址连续的内存
2. vmalloc/vfree:在内核空间分配物理地址不连续但虚拟地址连续的内存
3. malloc/free:在用户空间分配虚拟地址连续的内存,但在此函数调用时并不分配物理的页面,只有在访问时才分配(通过page fault)。malloc通过brk或者mmap系统调用来通知内核。
a) brk:会将堆的高位指针brk往高地址推,以得到更多的地址空间
b) mmap:当malloc要求分配的内存大于M_MMAP_THRESHOLD(一般是128K?),那么就会通过mmap去分配。主要原因是堆里面的内存释放必须等到比自己更高位的内存释放之后才能进行,而mmap分配的内存可以单独释放。
一些关键的kernel 结构:
1. page: PAGE的描述符
2. kmem_cache: slab的描述符
3. mm_struct:进程的虚拟地址空间的描述符
4. vm_area_struct:虚拟地址空间的一个区域的描述符
这张图可以简单描述vm_area_struct的作用:
虚拟地址到物理地址的映射,依旧是借用一张图来大概理解一下:
注意在ARM32的linux里,最多只有两级映射,也就是说上图的pmd实际上是没用的。