物理内存的组织方式
平坦内存模型
把内存想象成由连续的一页一页的块组成的,从 0 开始对物理页编号,每个物理页都会有个页号。
由于物理地址是连续的,页也是连续的,每个页大小也是一样的。因而对于任何一个地址,只要直接除一下每页的大小,就能直接算出在哪一页。
每个页有一个结构 struct page 表示,并放在一个数组里面,这样很容易根据页号,通过数组下标找到相应的 struct page 结构。
对称多处理器
图片来自极客时间趣谈linux操作系统
CPU 有多个,在总线的一侧。所有的内存条组成一大片内存,在总线的另一侧,所有的 CPU 访问内存都要过总线,而且距离都是一样的,这种模式称为 SMP(Symmetric multiprocessing),即对称多处理器。
有一个显著的缺点,就是总线速度会成为瓶颈
非一致内存访问
提高了性能和可扩展性,非连续内存模型,页号不连续
内存不是一整块。每个 CPU 都有自己的本地内存,CPU 访问本地内存不用过总线,每个 CPU 和内存在一起,称为一个 NUMA 节点。
在本地内存不足的情况下,每个 CPU 都可以去另外的 NUMA 节点申请内存,这个时候访问延时就会比较长。
对称多处理器与非一致内存访问对比
图片来自极客时间趣谈linux操作系统
NUMA 往往是非连续内存模型。而非连续内存模型不一定就是 NUMA
节点
解析当前的主流场景,NUMA 方式。NUMA 节点,使用结构 typedef struct pglist_data pg_data_t表示。
整个内存被分成了多个节点,pglist_data 放在一个数组里面,每个节点一项。
/arch/sh/mm/numa.c
struct pglist_data *node_data[MAX_NUMNODES] __read_mostly;
pglist_data 结构
/include/linux/mmzone.h
typedef struct pglist_data {
struct zone node_zones[MAX_NR_ZONES];
struct zonelist node_zonelists[MAX_ZONELISTS];
int nr_zones;
struct page *node_mem_map;
unsigned long node_start_pfn;
unsigned long node_present_pages; /* total number of physical pages */
unsigned long node_spanned_pages; /* total size of physical page range, including holes */
int node_id;
......
} pg_data_t;
- node_id
每一个节点的 ID - node_mem_map
struct page 数组,用于描述这个节点里面的所有的页 - node_start_pfn
这个节点的起始页号 - node_spanned_pages
节点中包含不连续的物理内存地址的页面数 - node_present_pages
真正可用的物理页面的数目
例如,64M 物理内存隔着一个 4M 的空洞,然后是另外的 64M 物理内存。这样换算成页面数目就是,16K 个页面隔着 1K 个页面,然后是另外 16K 个页面。这种情况下,node_spanned_pages 就是 33K 个页面,node_present_pages 就是 32K 个页面。 - node_zones
每一个节点分成一个个区域 zone,放在数组 node_zones 里面 - nr_zones
当前节点的区域的数量 - node_zonelists
备用节点和它的内存区域的情况
区域
内存分成了节点,节点分成了区域,pglist_data 结构 中 的 node_zones
区域分类
\include\linux\mmzone.h
enum zone_type {
#ifdef CONFIG_ZONE_DMA
ZONE_DMA,
#endif
#ifdef CONFIG_ZONE_DMA32
ZONE_DMA32,
#endif
ZONE_NORMAL,
#ifdef CONFIG_HIGHMEM
ZONE_HIGHMEM,
#endif
ZONE_MOVABLE,
__MAX_NR_ZONES
};
- ZONE_DMA
可用于作 DMA(Direct Memory Access,直接内存存取)的内存
DMA 机制是:CPU 向 DMA 控制器下达指令,让 DMA 控制器来处理同外设的数据传送,数据传送完毕再把信息反馈给 CPU,可以解放 CPU。
64 位系统:有两个 DMA 区域: ZONE_DMA,还有 ZONE_DMA32 - ZONE_NORMAL
直接映射区
从物理内存到虚拟内存的内核区域,通过加上一个常量直接映射 - ZONE_HIGHMEM
高端内存区
32 位系统来说超过 896M 的地方,对于 64 位没必要有的一段区域 - ZONE_MOVABLE
可移动区域
物理内存划分为可移动分配区域和不可移动分配区域来避免内存碎片
都是针对物理内存的区域的划分
数据结构 zone 的定义
\include\linux\mmzone.h
struct zone {
......
struct pglist_data *zone_pgdat;
struct per_cpu_pageset __percpu *pageset;
unsigned long zone_start_pfn;
/*
* spanned_pages is the total pages spanned by the zone, including
* holes, which is calculated as:
* spanned_pages = zone_end_pfn - zone_start_pfn;
*
* present_pages is physical pages existing within the zone, which
* is calculated as:
* present_pages = spanned_pages - absent_pages(pages in holes);
*
* managed_pages is present pages managed by the buddy system, which
* is calculated as (reserved_pages includes pages allocated by the
* bootmem allocator):
* managed_pages = present_pages - reserved_pages;
*
*/
unsigned long managed_pages;
unsigned long spanned_pages;
unsigned long present_pages;
const char *name;
......
/* free areas of different sizes */
struct free_area free_area[MAX_ORDER];
/* zone flags, see below */
unsigned long flags;
/* Primarily protects free_area */
spinlock_t lock;
......
} ____cacheline_internodealigned_in_
- zone_start_pfn
zone 的第一个页 - spanned_pages
不管中间有没有物理内存空洞,就是最后的页号减去起始的页号
spanned_pages = zone_end_pfn - zone_start_pfn - present_pages
zone 在物理内存中真实存在的所有 page 数目,去掉空洞
present_pages = spanned_pages - absent_pages(pages in holes) - managed_pages
zone 被伙伴系统管理的所有的 page 数目
managed_pages = present_pages - reserved_pages - per_cpu_pageset
区分冷热页(热页, 被 CPU 缓存的页)
页
页是物理内存的基本单位,其数据结构是 struct page
数据结构 page 的定义
\include\linux\mm_types.h
struct page {
unsigned long flags;
union {
struct address_space *mapping;
void *s_mem; /* slab first object */
atomic_t compound_mapcount; /* first tail page */
};
union {
pgoff_t index; /* Our offset within mapping. */
void *freelist; /* sl[aou]b first free object */
};
union {
unsigned counters;
struct {
union {
atomic_t _mapcount;
unsigned int active; /* SLAB */
struct { /* SLUB */
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
int units; /* SLOB */
};
atomic_t _refcount;
};
};
union {
struct list_head lru; /* Pageout list */
struct dev_pagemap *pgmap;
struct { /* slub per cpu partial pages */
struct page *next; /* Next partial slab */
int pages; /* Nr of partial slabs left */
int pobjects; /* Approximate # of objects */
};
struct rcu_head rcu_head;
struct {
unsigned long compound_head; /* If bit zero is set */
unsigned int compound_dtor;
unsigned int compound_order;
};
};
union {
unsigned long private;
struct kmem_cache *slab_cache; /* SL[AU]B: Pointer to slab */
};
......
}
数据结构 struct page,里面有很多的 union,之所以用了 union,是因为一个物理页面使用模式有多种。
第一种模式
要用就用一整页
这一整页的内存直接和虚拟地址空间建立映射关系,称为匿名页(Anonymous Page)
这一整页的内存用于关联一个文件,然后再和虚拟地址空间建立映射关系,这样的文件称为内存映射文件(Memory-mapped File)
第二种模式
仅需分配小块内存
不需要一下子分配这么多的内存,例如分配一个 task_struct 结构,只需要分配小块的内存,去存储这个进程描述结构的对象
Linux 系统采用了一种被称为 slab allocator 的技术,用于分配称为 slab 的一小块内存。
- slab allocator 分配器
基本原理是从内存管理模块申请一整块页,然后划分成多个小块的存储池,用复杂的队列来维护这些小块的状态(状态包括:被分配了 / 被放回池子 / 应该被回收)。 - slub allocator 分配器
slab allocator 对于队列的维护过于复杂,后来就有了一种不使用队列的分配器 slub allocator
依旧保留了 slab 的用户接口,可以看成 slab allocator 的另一种实现 - slob 分配器
非常简单,主要使用在小型的嵌入式系统
页的分配
伙伴系统(Buddy System) 用于分配比较大的内存,例如到分配页级别的
Linux 中的内存管理的“页”大小为 4KB。把所有的空闲页分组为 11 个页块链表,每个块链表分别包含很多个大小的页块,有 1、2、4、8、16、32、64、128、256、512 和 1024 个连续页的页块。
最大可以申请 1024 个连续页,对应 4MB 大小的连续内存。每个页块的第一个页的物理地址是该页块大小的整数倍。
图片来自极客时间趣谈linux操作系统
struct zone 里面变量 struct free_area free_area[MAX_ORDER]; MAX_ORDER 就是指数
\include\linux\mmzone.h
如何分配
原理
当向内核请求分配 (2(i-1),2i]数目的页块时,按照 2^i 页块请求处理,如果对应的页块链表中没有空闲页块,那就在更大的页块链表中去找。当分配的页块中有多余的页时,伙伴系统会根据多余的页块大小插入到对应的空闲页块链表中。
例子
请求一个 128 个页的页块时,先检查 128 个页的页块链表是否有空闲块。如果没有,则查 256 个页的页块链表;如果有空闲块的话,则将 256 个页的页块分成两份,一份使用,一份插入 128 个页的页块链表中。如果还是没有,就查 512 个页的页块链表;如果有的话,就分裂为 128、128、256 三个页块,一个 128 的使用,剩余两个插入对应页块链表。
相关函数
alloc_pages 函数
如何分配在分配页的函数alloc_pages 函数中实现,alloc_pages 会调用 alloc_pages_current
include/linux/gfp.h
static inline struct page *
alloc_pages(gfp_t gfp_mask, unsigned int order)
{
return alloc_pages_current(gfp_mask, order);
}
alloc_pages_current 函数
/mm/mempolicy.c
/**
* alloc_pages_current - Allocate pages.
*
* @gfp:
* %GFP_USER user allocation,
* %GFP_KERNEL kernel allocation,
* %GFP_HIGHMEM highmem allocation,
* %GFP_FS don't call back into a file system.
* %GFP_ATOMIC don't sleep.
* @order: Power of two of allocation size in pages. 0 is a single page.
*
* Allocate a page from the kernel page pool. When not in
* interrupt context and apply the current process NUMA policy.
* Returns NULL when no page can be allocated.
*/
struct page *alloc_pages_current(gfp_t gfp, unsigned order)
{
struct mempolicy *pol = &default_policy;
struct page *page;
if (!in_interrupt() && !(gfp & __GFP_THISNODE))
pol = get_task_policy(current);
/*
* No reference counting needed for current->mempolicy
* nor system default_policy
*/
if (pol->mode == MPOL_INTERLEAVE)
page = alloc_page_interleave(gfp, order, interleave_nodes(pol));
else
page = __alloc_pages_nodemask(gfp, order,
policy_node(gfp, pol, numa_node_id()),
policy_nodemask(gfp, pol));
return page;
}
-
gfp 表示希望在哪个区域中分配这个内存
- GFP_USER
并且希望直接被内核或者硬件访问,主要用于一个用户进程希望通过内存映射的方式,访问某些硬件的缓存,例如显卡缓存; - GFP_KERNEL
用于内核中分配页,主要分配 ZONE_NORMAL 区域,也即直接映射区; - GFP_HIGHMEM
主要分配高端区域的内存
- GFP_USER
-
order 表示分配 2 的 order 次方个页
接下来调用 __alloc_pages_nodemask 以及后面的调用链
__alloc_pages_nodemask 函数以及后续调用链
调用链 __alloc_pages_nodemask ->get_page_from_freelist -> rmqueue->__rmqueue->__rmqueue_smallest
这几个函数都在 /mm/page_alloc.c
总结
NUMA 方式的物理内存的组织形式
如果有多个 CPU,那就有多个节点。每个节点用 struct pglist_data 表示,放在一个数组里面。
每个节点分为多个区域,每个区域用 struct zone 表示,放在一个数组里面。
每个区域分为多个页。为了方便分配,空闲页放在** struct free_area** 里面,使用伙伴系统进行管理和分配,每一页用 *struct page 表示。
图片来自极客时间趣谈linux操作系统
参考资料:
趣谈Linux操作系统(极客时间)链接:
http://gk.link/a/10iXZ
欢迎大家来一起交流学习