Linux内核之内存管理
- Linux利用的是分段+分页单元把逻辑地址转换为物理地址;
- RAM的某些部分永久地分配给内核, 并用来存放内核代码以及静态内核数据结构;
- RAM的其余部分称动态内存(dynamic memory); 整个系统的性能取决于如何有效的管理动态内存;
- 尽力优化对动态内存的使用, 尽量做到需要时使用, 不需要时释放;
- 内核如何给自己分配动态内存: 页框管理和内存区管理对连续内存去处理的两种不同的技术;
- 非连续区的管理是处理不连续内存去的一种技术;
- 内存管理必须知道的几个主题技术: 内存管理区, 内核映射, 伙伴系统, slab高速缓存和内存池;
页框管理
- 不同的页框大小(连续物理地址的大小): 4KB或4MB;
- 由分页异常引发的缺页异常很容易得到解释, 或是由于请求的页存在但不允许进程对其访问, 或者是由于请求的页不存在;
- 内存分配器必须找到一个4KB的空闲页框, 并将其分配给进程;
- 虽然页框都是磁盘大小的倍数, 在绝大多数时候, 当主存和磁盘之间传输小块数据时更高效;
页描述符
- 内核必须记录每个页框当前的状态;
- 内核必须区分那些页框包含的是属于进程的页, 那些页框包含的是内核代码或内核数据;
- 内核还必须能够确定动态内存中的页框是否空闲;
- 如果动态内存中的页框不包含有用的数据, 那么该页框就是空闲的;
- 包含用户态进程的数据, 某个软件高速缓存的数据, 动态分配的内核数据结构, 设备驱动程序缓冲区的数据, 内核模块的代码等等都是可用数据;
非一致内存访问(NUMA)
- 我们习惯上认为计算机内存是一种均匀、共享的资源, 在忽略硬件高速缓存作用下, 我们希望不管内存单元处于何处, 也不管CPU处于何处, CPU对内存单元的访问都要相同的时间;
- 非一致访问内存(Non-Uniform Memory Access, NUMA);
- 系统的物理内存被划分为几个节点(node), 每个节点中的物理内存又可以分为几个管理区(Zone);
内存管理区
- 一个理想的计算机体系结构中, 一个页框就是一个内存存储单元, 可用于任何事情: 存放内核数据和用户数据、 缓冲区磁盘数据等等;
- 但实际的计算机体系结构有硬件的制约:
- ISA总线的直接内存存取(DMA)处理器有一个严格的限制: 只能对RAM的前16M寻址;
- 在具有大容量RAM的32为计算机中, CPU不能直接访问所有物理内存, 因为线性地址空间太小;
- 物理内存划分为3个管理区(zone):
- ZONE_DMA 包含低于16MB的内存页框;
- ZONE_NORMAL 包含高于16MB且低于896MB的内存页框
- ZONE_HIGHMEM 包含从896MB开始高于896MB的内存页框
- 但实际的计算机体系结构有硬件的制约:
- 当内核调用一个内存分配函数时, 必须指明请求页框所在的管理区, 使用zonelist数据结构管理描述符指针数组;
保留的页框池
- 有两种不同的方法可以用来满足内存分配请求:
- 如果有足够的空闲内存可用, 请求就会被立刻满足;
- 否则, 必须回收一些内存, 并且将发出请求的内核控制路径阻塞, 直到内存被释放;
- 内核为原子内存分配请求保留了一个页框池, 只有在内存不足的时候才使用;
- 保留池的大小 = (16 X 直接映射内存)的开方;
保留的页框池
- 被称作分区页框器(zoned page frame allocator)的内核子系统, 处理对连续页框组的内存分配请求;
- 管理区分配器 ----> 每CPU页框高速缓存 ----> 伙伴系统;
- 管理区分配器 ----> 伙伴系统;
- 管理区分配器主要是用来接收动态内存分配和释放请求;
- 在请求分配的情况下, 该部分搜索一个能满足所请求的一组梁旭页框内存的管理区;
- 在每个管理区内, 页框被名为"伙伴系统"的部分来处理;
- 为达到更好的性能, 一小部分页框保留在高速缓存中, 用于快速地满足对每个也宽的分配请求;
- 请求和释放页框:
- alloc_pages
- alloc_page
- __get_free_pages
- __get_free_page
- get_zeroed_page
- __get_dma_pages
- 释放页框:
- __free_pages
- free_pages
- __free_page
- free_page
高端内存页框的内核映射
- 高端内存的始端对应的线性地址存放在high_memory变量中, 被设置为896MB;
- 返回页框线性地址的页分配器函数不适用于高端内存, 即不适用于ZONW_HIGHMEM内存管理区内的页框;
- 64位平台不存在这个问题, 因为可使用的线性地址空间远大于能安装的RAM大小;
- 64位系统上, ZONE_HIGHMEM管理区总是空的;
- 高端内存映射是为了打破4GB内存访问的限制, 而专门设计的一门技术;
永久内核映射
- 永久内核映射允许内核建立高端页框到内核地址空间的长期映射;
- 使用的是主内核页表中的一个专门的页表, 其地址存放在pkmap_page_table变量中;
- kmap函数可以用来建立永久内核映射;
- 在kmap_high函数中调用kmap_lock自旋锁, 以保护页表免受多处理器系统上的并发访问;
临时内核映射
- 临时内核映射比永久内核映射的实现要简单;
- 它们可以用在中断处理程序和可延迟函数的内部, 因为他们从不阻塞当前进程;
- 每个CPU都有它自己的包含13个串口的集合, 它们用enum_km_type数据结构表示;
- 该数据结构中定义每个符号, 比如KM_BOUNCE_READ, KM_USRE0或KM_PTE0, 并标识了窗口的线性地址;
- 内核必须确保同一窗口永不会被两个不同的控制路径同时使用;
- 调用kmap_atomic函数建立零食内核映射;
伙伴系统算法
- 内核应该为分配一组连续的页框而建立一种健壮、 高效的分配策略;
- 著名的内存问题 -- 外碎片(external fragmentation);
- 频繁地请求和释放不同大小的义序连续页框, 必然导致在已分配也矿上的块内分散了许多小块的空闲页框;
- 问题是: 即使有足够的空闲页框可以满足请求, 但要分配一个大块的连续页框就无法满足;
- 从本质上说, 避免外碎片的方法有两种:
- 利用分页单元把一组非连续的空闲页框映射到连续的线性地址空间;
- 开发一种合适的技术来记录现存的空闲连续页框块的情况, 以尽量避免为满足对小块的请求二分割大的空闲块;
- Linux内核选择的是第二种方案: 记录空闲连续页框的情况, 避免对小块的请求分割大的空闲块;
- 连续的页框是必要的, 仅仅是连续的线性地址有时候无法满足需求;
- 给DMA处理器分配缓冲区的内存请求, DMA忽略分页单元直接访问地址总线, 所请求的缓冲区必须是在连续的页框中;
- 连续的页框在保持内核页表不变方面起着不可忽视的作用;
- 频繁的修改页表会导致平均访问内存次数的增加, 因为这会使CPU频繁地刷新转换后援缓冲器(TLB)的内容;
- 内核通过4MB的页可以访问大块连续的物理内存, 这样减少了转换后援缓冲区(TLB)的失效率;
- Linux采用Buddy system(伙伴系统)算法来解决外碎片问题;
- 把所有的空闲页框分为11个块链表, 每个块的大小为: 1, 2, 4, 8, 16, 32, 64, 128, 256, 512 和 1024个连续的页框;
- 对1024个页框的最大请求对应着4MB大小的连续RAM块, 每个块的第一个页框的物理地址是该快大小的整数倍;
- 算法举例:
- 假设请求一个256个页框, 算法先在256个页框的链表中检查是否有一个空闲块, 如果存在这样的块, 直接返回即可;
- 如果没有这样的块, 算法会查找下一个更大的页块, 也就是在512个页框的链表中找一个空闲块;
- 如果存在这样的块, 内核就把512的页框分成两等分, 一半用作满足请求, 另一半插入到256个页框的链表中;
- 如果没有512的块, 继续查找更大的内存块(1024);
- 内核试图把大小为b的一对空闲伙伴合并为一个大小为2b的单独块;
- 两个块具有相同的大小, 记作b;
- 他们的物理地址是连续的;
- 第一块的第一个页框的物理地址是2b2^12的倍数;
- buddy算法是迭代的, 如果成功合并所释放的块, 它会试图合并2b的块, 以再次试图形成更大的块;
- 三种不同的伙伴系统:
- 第一种处理适合ISA DMA的页框;
- 第二种处理"常规"页框;
- 第三种处理高端内存页框
管理区分配器
- 管理区分配器是内核页框分配器的前端;
- 必须分配一个包含足够多空闲页框的内存区, 使能满足内存请求;
- 管理区分配器必须满足几个目标:
- 它应当保护保留的页框池;
- 当内存不足且允许组设当前进程时, 它应当触发页框回收算法; 一旦某些压矿被释放, 管理区分配器将再次尝试分配;
- 如果可能, 他应该保存小而珍贵的ZONE_DMA内存管理区;
- buddy系统使用页框作为基本的内存区, 适用于对大块内存的请求;
- 小块内存的访问, 在同一页框中如何分配小内存区 --- slab分配器;
- 所存放数据的类型可以影响内存区的分配方式;
- slab分配器把内存区看做是对象, 由构造函数和析构函数来进行管理;
- slab 不会丢弃已经分配的对象, 而是释放但把他们保存在内存中, 以后要请求对象时, 直接从内存中重新初始化即可;
- 内核倾向于反复请求同一类型的内存区;
- 内核把时间浪费在反复分配和回收那些包含同一内存区的页框上, slab分配器把那些页框保存在高速缓存中并很快地重新使用它们;
- 对内存的请求可以根据它们发生的频率来分类, 对于预期频率请求一个特定大小的内存区而言, 可以通过创建一组具有适当大小的专用对象来高效地处理, 由此来避免内存碎片;
- 借助处理器硬件高速缓存而导致较好的性能;
- buddy系统函数每次调用会弄脏硬件高速缓存, 会增加对内存的平均访问时间;
- slab分配器把对象分组放进高速缓存, 每个高速缓存都是同种类型对象的一种储备;
- 高速缓存的主存区被划分为多个slab, 每个slab由一个或多个连续的页框组成, 这些页框既包含已分配的对象, 也包含空闲的对象;
- slab分配器通过一种叫做slab着色的策略, 尽量降低高速缓存的不愉快的行为, 把颜色的不同随机数分配给slab;
- slab的长度 = (num*osize)+dsize+free;
- slab分配器利用空闲魏永的字节free来对slab着色, 着色其本质是将内存分配器把对象展开在不同的线性地址中;
- 着色的本质导致把slab中的一些空闲区域从末尾一道开始;
- 当free足够大时, 着色才起作用;
通用对象
- 如果对存储区的请求不频繁, 就用一组普通高速缓存来处理, 普通高速缓存中的对象具有几何分布的大小, 范围为32~131072字节;
- 调用kmalloc可以得到这种对象;
内存池
- 内存池允许一个内核成分, 仅在内存不足的紧急情况下分配一些动态内存来使用, 比如块设备子系统;
- 保留的页框池, 只能用于满足中断处理程序或内部临界区发出的原子内存分配请求;
- 内存池是动态内存的设备, 只能被特定的内核成分(即池的拥有者)使用;
- 内存池常常叠加在slab分配器上, 它被用来保存slab对象的储备;
非连续内存区管理
- 把内存区映射到一组连续的页框是最好的选择, 这样会充分利用高速缓存并获得较低的平均访问时间;
- 通过连续的线性地址来访问非连续的页框, 优点是避免了外碎片, 缺点是必须打乱内核页表;
- 非连续内存区的大小必须是4096的倍数;
- 为活动的交换区分配数据结构;
- 为模块分配空间;
- 给某些I/O驱动程序分配缓冲区;
- 调用vmalloc函数给内核分配一个非连续内存区, 分配为页框大小的整数倍;