物理内存管理
物理内存的组织方式
把内存想象成它是由连续的一页一页的块组成的。我们可以从 0 开始对物理页编号,这样每个物理页都会有个页号。物理内存按固定的页大小划分为多个页,连续的内存分配给连续页号的页。对于任何一个地址,只要直接除一下每页的大小,很容易直接算出在哪一页。每个页有一个结构 struct page 表示,这个结构也是放在一个数组里面,这样根据页号,很容易通过下标找到相应的 struct page 结构。整个物理内存的布局就非常简单、易管理,这就是最经典的平坦内存模型(Flat Memory Model)。
内存是一整块的,对内存的访问是通过总线的,多个CPU都需要通过总线访问内存,总线会成为瓶颈,这种方式为对称多处理器。后来有了一种更高级的模式,NUMA(Non-uniform memory access),非一致内存访问。在这种模式下,内存不是一整块。每个 CPU 都有自己的本地内存,CPU 访问本地内存不用过总线,因而速度要快很多,每个 CPU 和内存在一起,称为一个 NUMA 节点。但是,在本地内存不足的情况下,每个 CPU 都可以去另外的 NUMA 节点申请内存,这个时候访问延时就会比较长。内存模型就变成了非连续内存模型,管理起来就复杂一些。再后来,支持热插拔内存,不连续成为常态,于是就有了稀疏内存模型。
节点
NUMA(Non-uniform memory access)非连续内存访问节点?
linux源码中,有对应的结构体struct pglist_data
,包含了
- node_id
- node_mem_map 就是这个节点的 struct page 数组,用于描述这个节点里面的所有的页;
- node_start_pfn 是这个节点的起始页号
- node_spanned_pages 是这个节点中包含不连续的物理内存地址的页面数 // total sizeof physical pages range including holes
- node_present_pages // total number of physical pages 真正可用的物理页数量
例如,64M 物理内存隔着一个 4M 的空洞,然后是另外的 64M 物理内存。这样换算成页面数目就是,16K 个页面隔着 1K 个页面,然后是另外 16K 个页面。这种情况下,node_spanned_pages 就是 33K 个页面,node_present_pages 就是 32K 个页面。
每一个节点分成一个个区域 zone,放在数组 node_zones 里面。这个数组的大小为 MAX_NR_ZONES。
1 |
|
不同的ZONE有不同的用途,具体的:
ZONE_DMA 是指可用于作 DMA(Direct Memory Access,直接内存存取)的内存。DMA 是这样一种机制:要把外设的数据读入内存或把内存的数据传送到外设,原来都要通过 CPU 控制完成,但是这会占用 CPU,影响 CPU 处理其他事情,所以有了 DMA 模式。CPU 只需向 DMA 控制器下达指令,让 DMA 控制器来处理数据的传送,数据传送完毕再把信息反馈给 CPU,这样就可以解放 CPU。本人现在所处的项目也有对应的dma的程序,主要用来向控制面传送数据的, 不占用转发面的处理逻辑。间接的实现转发面和控制面的隔离,跟这里的DMA还是有点区别。
ZONE_NORMAL 是直接映射区,从物理内存到虚拟内存的内核区域,通过加上一个常量直接映射。
ZONE_HIGHMEM 是高端内存区,对于 32 位系统来说超过 896M 的地方,对于 64 位没必要有的一段区域。
ZONE_MOVABLE 是可移动区域,通过将物理内存划分为可移动分配区域和不可移动分配区域来避免内存碎片。
这边对于区域的划分,都是针对物理内存的。
nr_zones 表示当前节点的区域的数量。node_zonelists 是备用节点和它的内存区域的情况。具体如何搞的备用节点,需要抽空看一下源码对应这部分的处理。 TODO:
就像区域或者页一样,节点也是存在一个数组里面的。 至于这个节点数,跟具体cpu数一致。每个CPU分配一个节点。
1 | struct pglist_data *node_data[MAX_NUMNODES] __read_mostly; // numa.c |
区域
1 | struct zone; |
在一个 zone 里面,zone_start_pfn 表示属于这个 zone 的第一个页。
spanned_pages = zone_end_pfn - zone_start_pfn,也即 spanned_pages 指的是不管中间有没有物理内存空洞,反正就是最后的页号减去起始的页号。
present_pages = spanned_pages - absent_pages(pages in holes),也即 present_pages 是这个 zone 在物理内存中真实存在的所有 page 数目。
managed_pages = present_pages - reserved_pages,也即 managed_pages 是这个 zone 被伙伴系统管理的所有的 page 数目。
per_cpu_pageset 用于区分冷热页。如果一个页被加载到 CPU 高速缓存里面,这就是一个热页(Hot Page),CPU 读起来速度会快很多,如果没有就是冷页(Cold Page)。由于每个 CPU 都有自己的高速缓存,因而 per_cpu_pageset 也是每个 CPU 一个。
页
物理内存首先是被拆分成页这种单位的。 然后组成区域, 区域再组成节点。
1 | struct page; |
这个结构体是由多个union组成的, 这在嵌入式系统中很常见,一个是为了复用内存,一个是实现类似抽象类/父类的功能。根据内存页被使用的模式,使用对应的成员。
整页使用
这一整页的内存,或者直接和虚拟地址空间建立映射关系,我们把这种称为匿名页(Anonymous Page)。或者用于关联一个文件,然后再和虚拟地址空间建立映射关系,这样的文件,我们称为内存映射文件(Memory-mapped File)。
如果某一页是这种使用模式,则会使用 union 中的以下变量:
- struct address_space *mapping 就是用于内存映射,如果是匿名页,最低位为 1;如果是映射文件,最低位为 0;
- pgoff_t index 是在映射区的偏移量;
- atomic_t _mapcount,每个进程都有自己的页表,这里指有多少个页表项指向了这个页;
- struct list_head lru 表示这一页应该在一个链表上,例如这个页面被换出,就在换出页的链表中;
- compound 相关的变量用于复合页(Compound Page),就是将物理上连续的两个或多个页看成一个独立的大页。
小块内存使用
有时候,我们不需要一下子分配这么多的内存,例如分配一个 task_struct 结构,只需要分配小块的内存,去存储这个进程描述结构的对象。为了满足对这种小内存块的需要,Linux 系统采用了一种被称为 slab allocator 的技术,用于分配称为 slab 的一小块内存。它的基本原理是从内存管理模块申请一整块页,然后划分成多个小块的存储池,用复杂的队列来维护这些小块的状态(状态包括:被分配了 / 被放回池子 / 应该被回收)。
如果某一页是用于分割成一小块一小块的内存进行分配的使用模式,则会使用 union 中的以下变量:
- s_mem 是已经分配了正在使用的 slab 的第一个对象;
- freelist 是池子中的空闲对象;
- rcu_head 是需要释放的列表。
页分配
对于要分配比较大的内存,例如到分配页级别的,可以使用伙伴系统(Buddy System)。
针对页级别的分配,把所有的空闲页分组为 11 个页块链表,每个块链表分别包含很多个大小的页块,有 1、2、4、8、16、32、64、128、256、512 和 1024 个连续页的页块。最大可以申请 1024 个连续页,对应 4MB 大小的连续内存。每个页块的第一个页的物理地址是该页块大小的整数倍。
在struct zone
里面有这样的定义
1 | struct free_area free_area[MAX_ORDER]; |
存储每个区域不同指数级空闲的内存。
当向内核请求分配 (2^(i-1),2^i]数目的页块时,按照 2^i 页块请求处理。如果对应的页块链表中没有空闲页块,那我们就在更大的页块链表中去找。当分配的页块中有多余的页时,伙伴系统会根据多余的页块大小插入到对应的空闲页块链表中。
具体的分配算法可以看alloc_pages_current(mm/mempolicy.c)函数, 其实没有什么高深的, 如我此等常人页看得明白。复杂的东西都是由简单的器件组合而成的。
行动,才不会被动!
欢迎关注个人公众号 微信 -> 搜索 -> fishmwei,沟通交流。