物理内存管理:会议室管理员如何分配会议室
物理内存的组织方式
最经典的内存使用方式:CPU是通过总线去访问内存的
平坦内存模型(最经典的模型):
- 内存是由连续的一页一页的块组成的。我们可以从0开始对物理页进程编号,这样每个物理页都会有个页号。
- 由于物理地址是连续的,页也是连续的,每个页大小也是一样的。因此对于任何一个地址,只要直接除一下每页的大小,很容易直接算出在哪一页。每个页有一个结构struct page表示,这个结构也是放在一个数组里面,这样根据页号,很容易通过下标找到相应的struct page结构。
- 在这种模式下,CPU也会有多个,在总线的一侧
- 所有的内存条组成一大块内存,在总线的另一侧,所有的CPU访问内存都要过总线,而且距离是一样的,这种模式叫做SMP(Symmetric multiprocessing),即对称多处理器。
- 当然,缺点是,总线会成为瓶颈,因为数据都要走它
为了提高性能和可扩展性,后来有了一种更高级的模式,NUMA(Non-uniform memory access),非一致性内存访问:
- 在这种模式下,内存不是一整块。
- 每个CPU都有自己的本地内存,CPU访问本地内存不用过总线,因而速度快很多,每个CPU核内存在一起,称为一个NUMA节点
- 但是,在本地内存不足的情况下,每个CPU都可以去另外的NUMA节点申请内存,这个时候访问延时就会比较长。
- 这样,内存被分成了多个节点,每个节点再被分成一个一个的页面。由于页需要全局唯一定位,页还是需要有全局唯一的页号的。但是由于物理内存不是连起来的了,页号也就不再连续了。于是内存模型就变成了非连续内存模型,管理起来就复杂一些。
这里需要指出的是,NUMA往往是非连续内存模型。而非连续内存模型不一定就是NUMA,有时候一大片内存的情况下,也会有物理内存地址不连续的情况。
由来内存技术牛了,可以支持热插播了。这个时候,不连续成为常态,于是就有了稀疏内存模型
节点
我们主要解析当前的主流场景,NUMA方式。我们首先要能够表示NUMA节点的概念,于是就有了typedef struct pglist_data pg_data_t
:
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;
-
每一个节点都有自己的ID:node_id
-
node_mem_map :这个节点的struct page数组,用于描述这个节点里面的所有的页
-
node_start_pfn:这个节点的起始页号
-
node_present_pages:真正可用的物理页面的数目;node_spanned_pages:这个节点中包含不连续的物理内存地址的页面数
- 比如,64M物理内存隔着一个4M的空洞,然后是另外的64M物理内存。这样换算成页面数目就是,16K个页面隔着1K个页面,然后是另外16K个页面。
- 这种情况下,node_spanned_pages 就是33K 个页面,node_present_pages 就是 32K 个页面
-
每个节点分成一个个区域
zone
,放在数组node_zones里面。这个数组的大小为MAX_NR_ZONES。 -
nr_zones表示当前节点的区域数量
-
node_zonelists是备用节点和它的内存区域的情况。前面讲NUMA的时候,我们讲了CPU访问内存,本节点速度最快,但是如果本节点内存不够就需要去其他节点进行分配。毕竟,就算在备用节点里面选择,慢了点也比没有强。
既然,内存被分成了多个节点,那么pglist_data 应该放在一个数组里面。每个节点一项,如下:
struct pglist_data *node_data[MAX_NUMNODES] __read_mostly;
区域
到这里,我们把内存分成了节点,把节点分成了区域。
区域的类型(什么是区域)?
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控制完成,但是这会占用CPU,影响CPU处理其他事情,所以有了DMA模式。CPU只需要DMA控制器下达指令,让DMA控制器来处理数据的传送,数据传送完毕再把信息反馈给CPU,这样就可以解放CPU。
- 对于64位的系统,有两个DMA区域。除了上面说的 ZONE_DMA,还有 ZONE_DMA32。
- ZONE_NORMAL是直接映射区,就是这里提到的,从物理内存到虚拟内存的内核区域,通过加上一个常量直接映射
- ZONE_HIGHMEM是高端内存区,就是这里提到的,对于32位系统来说超过896M的地方,对于64位没必要有的一段区域
- ZONE_MOVABLE是可移动区域,通过将物理内存划分为可移动分配区域和不可移动分配区域来避免内存碎片。
注意,上面对区域的划分都是针对物理内存的
一个区域里面是如何组织的
表示区域的数据结构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快速访问段描述符,在CPU里面有段描述符缓存。CPU访问这个缓存的速度比内存快得多。同样对于页面来讲,也是这样的。如果一个页被加载到CPU高速缓存里面,这就是一个热页(hot page),CPU读起来速度会快很多,如果没有就是冷页(Cold Page)。由于每个 CPU 都有自己的高速缓存,因而 per_cpu_pageset 也是每个 CPU 一个。
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_
页
页是组成物理内存的基本单位,其数据结构是struct page。这是一个特别复杂的结构,里面有很多union,union结构是在C语言中被用于同一块内存根据情况保存不同类型数据的一种方式。这里之所以用了union,是因为一个物理页面使用模式有很多种
(1)第一种模式,要用就用一整页。这一整页的内存
- 或者直接和虚拟地址空间建立映射关系,我们把这种叫做匿名页(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),就是将物理上连续的两个或者多个页看成一个独立的大页。
(2)第二种模式,仅需分配小块内存。
- 有时候,我们不需要一下子分配这么多的内存,比如分配一个task_struct结构,只需要分配小块的内存,去存储这个进程描述结构的对象。
- 为了满足对这种小内存块的需要,linux系统采用了一种被称为
slab allocator
的技术,用于分配称为slab的一小块内存。它的基本原理是从内存管理模块申请一整块页,然后划分成多个小块的存储池,用复杂的队列来维护这些小块的状态(状态包括:被分配了、分放回池子、应该被回收)。 - 也正是因为 slab allocator 对于队列的维护过于复杂,后来就有了一种不使用队列的分配器
slub allocator
,后面我们会解析这个分配器。但是你会发现,它里面还是用了很多 slab 的字眼,因为它保留了 slab 的用户接口,可以看成 slab allocator 的另一种实现。 - 还有一种小块内存的分配器叫做
slob
,非常简单,主要使用在小型的嵌入式系统。 - 如果某一页是用于分隔成一小块一小块的内存进行分配的使用模式,则会使用union中的以下变量:
- s_mem 是已经分配了正在使用的 slab 的第一个对象;
- freelist 是池子中的空闲对象;
- rcu_head 是需要释放的列表
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 */
};
......
}
页的分配
上面我们讲了物理内存的组织,从节点到区域到页到小块。接下来,我们来看物理内存的分配。
对于要分配比较大的内存,比如到分配页级别的,可以使用伙伴系统(buddy system):
- linux中内存管理的“页”大小为4KB。把所有的空闲页分组为11个页块链表,每个块链表分别包含很多个大小的页块,有1、2、4、8、16、32、128、256、512和1024个连续页的页块。最大可以申请1024个连续页,对应4MB大小的连续内存。每个页块的第一个页的物理地址是该页块大小的整数倍。
- 第 i 个页块链表中,页块中页的数目为 2^i
- 在struct zone里面有如下定义:
struct free_area free_area[MAX_ORDER];
- MAX_ORDER 就是指数。
#define MAX_ORDER 11
- 当向内存请求分配 ( 2 ( i − 1 ) , 2 i ] (2^{(i-1)}, 2^i] (2(i−1),2i]数目的页块时,按照2^i页块请求处理。如果对应的页块链表中没有空闲页块时,那我们就到更大的页块链表中去找。当分配的页块中有多余的页时,伙伴系统会根据多余的页块大小插入到对应的空闲页块链表中
- 比如,要请求一个128个页的页块时,先检查128个页的页块链表是否有空闲块。如果没有,则查256个页的页块链表;如果有空闲块的话,则将256个页的页块分成两份,一份使用,一份插入到128个页的页块链表中。如果还是没有,就查512个页的页块链表;如果有的话,就分裂为128、128、256三个页块,一个128的使用,剩余两个插入到对应页块链表(伙伴系统的意思就是劫富济贫)
上面这个过程,我们可以在分配页的函数alloc_pages中看到:
static inline struct page *
alloc_pages(gfp_t gfp_mask, unsigned int order)
{
return alloc_pages_current(gfp_mask, order);
}
/**
* 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;
......
page = __alloc_pages_nodemask(gfp, order,
policy_node(gfp, pol, numa_node_id()),
policy_nodemask(gfp, pol));
......
return page;
}
alloc_pages 会调用 alloc_pages_current,这里面的注释比较容易看懂了,gfp表示希望在哪个区域中分配这个内存:
GFP_USER
:用于分配一个页映射到用户进程的虚拟地址空间,并且希望直接被内核或者硬件访问,主要用于一个用户进程希望通过内存映射的方式,访问某些硬件的缓存,比如显卡缓存GFP_KERNEL
:用于内核中分配页,主要分配ZONE_NORAML区域,也就是直接映射区GFP_HIGHMEM
:主要分配高端区域的内存
另一个参数 order,就是表示分配 2 的 order 次方个页。
接下来调用__alloc_pages_nodemask。这是伙伴系统的核心方法。它会调用get_page_from_freelist。这里面的逻辑就是在一个循环中先看当前节点的zone。如果找不到空闲页,则再看备用节点的zone
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
const struct alloc_context *ac)
{
......
for_next_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx, ac->nodemask) {
struct page *page;
......
page = rmqueue(ac->preferred_zoneref->zone, zone, order,
gfp_mask, alloc_flags, ac->migratetype);
......
}
每一个zone,都有伙伴系统维护的各种大小的队列,rmqueue就是找到合适大小的那个队列,把页面取下来。
接下来的调用链是 rmqueue->__rmqueue->__rmqueue_smallest。在这里,我们能清楚看到伙伴系统的逻辑。
static inline
struct page *__rmqueue_smallest(struct zone *zone, unsigned int order,
int migratetype)
{
unsigned int current_order;
struct free_area *area;
struct page *page;
/* Find a page of the appropriate size in the preferred list */
for (current_order = order; current_order < MAX_ORDER; ++current_order) {
area = &(zone->free_area[current_order]);
page = list_first_entry_or_null(&area->free_list[migratetype],
struct page, lru);
if (!page)
continue;
list_del(&page->lru);
rmv_page_order(page);
area->nr_free--;
expand(zone, page, order, current_order, area, migratetype);
set_pcppage_migratetype(page, migratetype);
return page;
}
return NULL;
- 从当前的order,也就是指数开始,在伙伴系统的free_area找2^order大小的页块
- 如果链表的第一个不为空,就找到了
- 如果为空,就到更大的order的页块链表里面去找。
- 找到之后,除了将页块从链表中取下来,我们还要把多余的部分放在其他页块链表里面。expand就是干这个事情的
- area–就是伙伴系统那个表里面的前一项
- 前一项里面的页块大小是当前项的页块大小除以 2,size 右移一位也就是除以 2
- list_add 就是加到链表上
- nr_free++ 就是计数加 1。
static inline void expand(struct zone *zone, struct page *page,
int low, int high, struct free_area *area,
int migratetype)
{
unsigned long size = 1 << high;
while (high > low) {
area--;
high--;
size >>= 1;
......
list_add(&page[size].lru, &area->free_list[migratetype]);
area->nr_free++;
set_page_order(&page[size], high);
}
}
小内存的分配
如果遇到小的对象,会使用slub分配器进行分配。接下来将分析其原理。
在创建进程的时候,会调用dup_task_struct,它想要试图复制一个task_struct对象,需要先调用alloc_task_struct node,分配一个task_struct对象。
从下面代码可以看出,它调用了kmem_cache_alloc_node
函数,在task_struct的缓存区域task_struct_cahcep分配了一块内存
static struct kmem_cache *task_struct_cachep;
task_struct_cachep = kmem_cache_create("task_struct",
arch_task_struct_size, align,
SLAB_PANIC|SLAB_NOTRACK|SLAB_ACCOUNT, NULL);
static inline struct task_struct *alloc_task_struct_node(int node)
{
return kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node);
}
static inline void free_task_struct(struct task_struct *tsk)
{
kmem_cache_free(task_struct_cachep, tsk);
}
- 在系统初始化的时候,task_struct_cachep会被kmem_cache_create函数创建。这个函数是专门用于分配task_struct对象的缓存。这个缓冲区的名字叫做task_struct。缓冲区中每一块的大小正好等于task_struct的大小,也就是arch_task_struct_size
- 有了这个缓冲区,每次创建task_struct的时候,我们不用到内存里面去分配,先在缓存里面看有没有直接可用的,这就是
kmem_cache_alloc_node
的作用 - 当一个进程结束,task_struct也不用直接被销毁,而是放回缓冲中,这就是kmem_cache_free的作用。这样,新进程创建的时候,我们就可以直接用现成的缓存中的task_struct了
那么缓冲区struct kmem_cache 是什么样子的呢?
struct kmem_cache {
struct kmem_cache_cpu __percpu *cpu_slab;
/* Used for retriving partial slabs etc */
unsigned long flags;
unsigned long min_partial;
int size; /* The size of an object including meta data */
int object_size; /* The size of an object without meta data */
int offset; /* Free pointer offset. */
#ifdef CONFIG_SLUB_CPU_PARTIAL
int cpu_partial; /* Number of per cpu partial objects to keep around */
#endif
struct kmem_cache_order_objects oo;
/* Allocation and freeing of slabs */
struct kmem_cache_order_objects max;
struct kmem_cache_order_objects min;
gfp_t allocflags; /* gfp flags to use on each alloc */
int refcount; /* Refcount for slab cache destroy */
void (*ctor)(void *);
......
const char *name; /* Name (only for display!) */
struct list_head list; /* List of slab caches */
......
struct kmem_cache_node *node[MAX_NUMNODES];
};
- 在struct kmem_cache里面,有个变量struct list_head list,这个结构我们已经看到很多次了。我们可以想象一下,对于操作系统来讲,要创建和管理的缓存绝对不止task_struct,还有mm_struct、fs_struct等,因此,所有的缓存最后都会放在一个链表里面,也就是LIST_HEAD(slab_caches)
- 对于缓存来讲,其实就是分配了连续几页的大内存块,然后根据缓存对象的对象,切成小内存块
- 所以,我们这里有三个kmem_cache_order_objects 类型的变量。这里面的 order,就是 2 的 order 次方个页面的大内存块,objects 就是能够存放的缓存对象的数量。
看不懂了orz
最终,我们将大内存块切分成小内存块,样子就像下面这样。
- 每一项的结构都是缓存对象后面跟一个下一个空闲对象的指针,这样非常方便将所有的空闲对象连成一个链。(相当于用数组实现一个可以随机插入和删除的链表)
- 所以,这里面就有三个变量:size是包含这个指针的大小,object_size是纯对象的大小,offset就是把下一个空闲对象的指针存放在这一项里的偏移量
那这些缓存对象哪些被分配了,哪些在空着,什么情况下整个大内存都被分配完了,需要向伙伴系统申请几个页形成新的大内存块?这些信息该由谁来维护呢?
- 接下来就是最重要的两个成员变量出场的时候了。kmem_cache_cpu 和kmem_cache_node,它们都是每个节点上有一个,一模一样
- 在分配缓存块的时候,要分两种路径,fast path和slow path,也就是快速通道和普通通道。
- 其中kmem_cache_cpu 就是快速通道,kmem_cache_node 是普通通道。
- 每次分配的时候,要先从kmem_cache_cpu 进行分配,如果kmem_cache_cpu 里面没有空闲的块,才去kmem_cache_node 中进行分配;如果还是没有空闲的块,才去伙伴系统分配新的页。
我们来看一下,kmem_cache_cpu 里面是如何存放缓存块的。
struct kmem_cache_cpu {
void **freelist; /* Pointer to next available object */
unsigned long tid; /* Globally unique transaction id */
struct page *page; /* The slab from which we are allocating */
#ifdef CONFIG_SLUB_CPU_PARTIAL
struct page *partial; /* Partially allocated frozen slabs */
#endif
......
};
- page指向大内存块的第一个页,缓存块就是从里面分配的
- freelist指向大内存块里面第一个空闲的项。如上面所说,这一项会有指针指向下一个空闲的项,最终所有空闲的项会形成一个链表
- partial指向的也是大内存块的第一个页,之所以叫做partial(部分),就是因为它里面部分被分配出去了,部分是空的。这是一个备用列表,当page满了,就会从这里找
我们再来看 kmem_cache_node 的定义。
struct kmem_cache_node {
spinlock_t list_lock;
......
#ifdef CONFIG_SLUB
unsigned long nr_partial;
struct list_head partial;
......
#endif
};
- 这里的partial,是一个链表。这个链表里存放的是部分空闲的大内存块。这是kmem_cache_cpu 里面的partial的备用列表,如果那里没有,就到这里来找。
下面我们就来看看这个分配过程。kmem_cache_alloc_node 会调用 slab_alloc_node。你还是先重点看这里面的注释,这里面说的就是快速通道和普通通道的概念。
/*
* Inlined fastpath so that allocation functions (kmalloc, kmem_cache_alloc)
* have the fastpath folded into their functions. So no function call
* overhead for requests that can be satisfied on the fastpath.
*
* The fastpath works by first checking if the lockless freelist can be used.
* If not then __slab_alloc is called for slow processing.
*
* Otherwise we can simply pick the next object from the lockless free list.
*/
static __always_inline void *slab_alloc_node(struct kmem_cache *s,
gfp_t gfpflags, int node, unsigned long addr)
{
void *object;
struct kmem_cache_cpu *c;
struct page *page;
unsigned long tid;
......
tid = this_cpu_read(s->cpu_slab->tid);
c = raw_cpu_ptr(s->cpu_slab);
......
object = c->freelist;
page = c->page;
if (unlikely(!object || !node_match(page, node))) {
object = __slab_alloc(s, gfpflags, node, addr, c);
stat(s, ALLOC_SLOWPATH);
}
......
return object;
}
快速通道很简单,取出cpu_slab也就是kmem_cache_cpu 的freelist,这就是第一个空闲的项,可以直接返回了。如果没有空闲的了,就进入普通通道,调用__slab_alloc
static void *___slab_alloc(struct kmem_cache *s, gfp_t gfpflags, int node,
unsigned long addr, struct kmem_cache_cpu *c)
{
void *freelist;
struct page *page;
......
redo:
......
/* must check again c->freelist in case of cpu migration or IRQ */
freelist = c->freelist;
if (freelist)
goto load_freelist;
freelist = get_freelist(s, page);
if (!freelist) {
c->page = NULL;
stat(s, DEACTIVATE_BYPASS);
goto new_slab;
}
load_freelist:
c->freelist = get_freepointer(s, freelist);
c->tid = next_tid(c->tid);
return freelist;
new_slab:
if (slub_percpu_partial(c)) {
page = c->page = slub_percpu_partial(c);
slub_set_percpu_partial(c, page);
stat(s, CPU_PARTIAL_ALLOC);
goto redo;
}
freelist = new_slab_objects(s, gfpflags, node, &c);
......
return freeli
在这里,我们首先再次尝试一下 kmem_cache_cpu 的 freelist。为什么呢?万一当前进程被中断,等回来的时候,别人已经释放了一些缓存,说不定又有空间了呢。如果找到了,就跳到 load_freelist,在这里将 freelist 指向下一个空闲项,返回就可以了。
如果 freelist 还是没有,则跳到 new_slab 里面去。这里面我们先去 kmem_cache_cpu 的 partial 里面看。如果 partial 不是空的,那就将 kmem_cache_cpu 的 page,也就是快速通道的那一大块内存,替换为 partial 里面的大块内存。然后 redo,重新试下。这次应该就可以成功了。
如果真的还不行,那就要到 new_slab_objects 了。
static inline void *new_slab_objects(struct kmem_cache *s, gfp_t flags,
int node, struct kmem_cache_cpu **pc)
{
void *freelist;
struct kmem_cache_cpu *c = *pc;
struct page *page;
freelist = get_partial(s, flags, node, c);
if (freelist)
return freelist;
page = new_slab(s, flags, node);
if (page) {
c = raw_cpu_ptr(s->cpu_slab);
if (c->page)
flush_slab(s, c);
freelist = page->freelist;
page->freelist = NULL;
stat(s, ALLOC_SLAB);
c->page = page;
*pc = c;
} else
freelist = NULL;
return freelis
在这里面,get_partial 会根据 node id,找到相应的 kmem_cache_node,然后调用 get_partial_node,开始在这个节点进行分配。
/*
* Try to allocate a partial slab from a specific node.
*/
static void *get_partial_node(struct kmem_cache *s, struct kmem_cache_node *n,
struct kmem_cache_cpu *c, gfp_t flags)
{
struct page *page, *page2;
void *object = NULL;
int available = 0;
int objects;
......
list_for_each_entry_safe(page, page2, &n->partial, lru) {
void *t;
t = acquire_slab(s, n, page, object == NULL, &objects);
if (!t)
break;
available += objects;
if (!object) {
c->page = page;
stat(s, ALLOC_FROM_PARTIAL);
object = t;
} else {
put_cpu_partial(s, page, 0);
stat(s, CPU_PARTIAL_NODE);
}
if (!kmem_cache_has_cpu_partial(s)
|| available > slub_cpu_partial(s) / 2)
break;
}
......
return object;
acquire_slab 会从 kmem_cache_node 的 partial 链表中拿下一大块内存来,并且将 freelist,也就是第一块空闲的缓存块,赋值给 t。并且当第一轮循环的时候,将 kmem_cache_cpu 的 page 指向取下来的这一大块内存,返回的 object 就是这块内存里面的第一个缓存块 t。如果 kmem_cache_cpu 也有一个 partial,就会进行第二轮,再次取下一大块内存来,这次调用 put_cpu_partial,放到 kmem_cache_cpu 的 partial 里面。
如果 kmem_cache_node 里面也没有空闲的内存,这就说明原来分配的页里面都放满了,就要回到 new_slab_objects 函数,里面 new_slab 函数会调用 allocate_slab。
static struct page *allocate_slab(struct kmem_cache *s, gfp_t flags, int node)
{
struct page *page;
struct kmem_cache_order_objects oo = s->oo;
gfp_t alloc_gfp;
void *start, *p;
int idx, order;
bool shuffle;
flags &= gfp_allowed_mask;
......
page = alloc_slab_page(s, alloc_gfp, node, oo);
if (unlikely(!page)) {
oo = s->min;
alloc_gfp = flags;
/*
* Allocation may have failed due to fragmentation.
* Try a lower order alloc if possible
*/
page = alloc_slab_page(s, alloc_gfp, node, oo);
if (unlikely(!page))
goto out;
stat(s, ORDER_FALLBACK);
}
......
return page;
}
在这里,我们看到了 alloc_slab_page 分配页面。分配的时候,要按 kmem_cache_order_objects 里面的 order 来。如果第一次分配不成功,说明内存已经很紧张了,那就换成 min 版本的 kmem_cache_order_objects。
页面换出
另一个物理内存管理必须要处理的事情就是,页面换出:
- 每个进程都有自己的虚拟地址空间,无论是32位还是64位,虚拟地址空间都比较大,物理内存不可能有这么多的空间放得下。所以,一般情况下,页面只有在被使用的时候,才会放在物理内存中
- 如果过了一段时间不被使用,即便用户进程并没有释放它,物理内存也有责任做一定的干预。比如,将这些物理内存中的页面换出到硬盘上去;将空出的物理内存,交给活跃的进程去使用
什么情况下会触发页面换出呢?
- 可以想象,最常见的情况就是,分配内存的时候,发现没有地方了,就试图回收一下。比如,咱们解析申请一个页面的时候,会调用 get_page_from_freelist,接下来的调用链为 get_page_from_freelist->node_reclaim->__node_reclaim->shrink_node,通过这个调用链可以看出,页面换出也是以内存节点为单位的。
- 另外一种情况是,作为内存管理系统应该主动去做的,而不能等真的出了事情再做,这就是内核线程
kswapd
。这个内核线程,在系统初始化的时候就被创建。这样它会进入一个无限循环,直到系统停止。在这个循环中,如果内存使用没有那么紧张,拿它就可以放心sleep;如果内存紧张了,就要去检查一下内存,看看是否需要换出一些内存页。这里的调用链是 balance_pgdat->kswapd_shrink_node->shrink_node,是以内存节点为单位的,最后也是调用 shrink_node。
/*
* The background pageout daemon, started as a kernel thread
* from the init process.
*
* This basically trickles out pages so that we have _some_
* free memory available even if there is no other activity
* that frees anything up. This is needed for things like routing
* etc, where we otherwise might have all activity going on in
* asynchronous contexts that cannot page things out.
*
* If there are applications that are active memory-allocators
* (most normal use), this basically shouldn't matter.
*/
static int kswapd(void *p)
{
unsigned int alloc_order, reclaim_order;
unsigned int classzone_idx = MAX_NR_ZONES - 1;
pg_data_t *pgdat = (pg_data_t*)p;
struct task_struct *tsk = current;
for ( ; ; ) {
......
kswapd_try_to_sleep(pgdat, alloc_order, reclaim_order,
classzone_idx);
......
reclaim_order = balance_pgdat(pgdat, alloc_order, classzone_idx);
......
}
}
`
shrink_node 会调用 shrink_node_memcg。这里面有一个循环处理页面的列表,看这个函数的注释,其实和上面我们想表达的内存换出是一样的。
/*
* This is a basic per-node page freer. Used by both kswapd and direct reclaim.
*/
static void shrink_node_memcg(struct pglist_data *pgdat, struct mem_cgroup *memcg,
struct scan_control *sc, unsigned long *lru_pages)
{
......
unsigned long nr[NR_LRU_LISTS];
enum lru_list lru;
......
while (nr[LRU_INACTIVE_ANON] || nr[LRU_ACTIVE_FILE] ||
nr[LRU_INACTIVE_FILE]) {
unsigned long nr_anon, nr_file, percentage;
unsigned long nr_scanned;
for_each_evictable_lru(lru) {
if (nr[lru]) {
nr_to_scan = min(nr[lru], SWAP_CLUSTER_MAX);
nr[lru] -= nr_to_scan;
nr_reclaimed += shrink_list(lru, nr_to_scan,
lruvec, memcg, sc);
}
}
......
}
......
这里面有个lru列表,从下面的定义,我们可以想象,所有的页面都被挂载LRU列表中,LRU就是 Least Recent Use,也就是最近最少使用。也就是说,这个列表里面会按照活跃程度进行排序,这样就很容易把不怎么用的内存页拿出来做处理。
内存页一共分两类,一类是匿名页,和虚拟地址进行管理;一类是内存映射,不但和虚拟地址空间管理,还和文件管理管理。
它的每一类都有两个列表,一共是active,一共是inactive。active就是比较活跃的,inactive就是不怎么活跃的。这里面的页会变化,过一段时间,活跃的可能变成不活跃的,不活跃的可能变成活跃的。如果要换出内存,那就是从不活跃列表中找出最不活跃的,换出到硬盘上
enum lru_list {
LRU_INACTIVE_ANON = LRU_BASE,
LRU_ACTIVE_ANON = LRU_BASE + LRU_ACTIVE,
LRU_INACTIVE_FILE = LRU_BASE + LRU_FILE,
LRU_ACTIVE_FILE = LRU_BASE + LRU_FILE + LRU_ACTIVE,
LRU_UNEVICTABLE,
NR_LRU_LISTS
};
#define for_each_evictable_lru(lru) for (lru = 0; lru <= LRU_ACTIVE_FILE; lru++)
static unsigned long shrink_list(enum lru_list lru, unsigned long nr_to_scan,
struct lruvec *lruvec, struct mem_cgroup *memcg,
struct scan_control *sc)
{
if (is_active_lru(lru)) {
if (inactive_list_is_low(lruvec, is_file_lru(lru),
memcg, sc, true))
shrink_active_list(nr_to_scan, lruvec, sc, lru);
return 0;
}
return shrink_inactive_list(nr_to_scan, lruvec, sc, lru);
从上面的代码可以看出,shrink_list 会先缩减活跃页面列表,再压缩不活跃的页面列表。对于不活跃列表的缩减,shrink_inactive_list 就需要对页面进行回收;对于匿名页来讲,需要分片swap,将内存页写入文件系统;对于内存映射关联了文件的,我们需要将在内存中对于文件的修改写回到文件中
小结
物理内存的组织形式如下图:
- 如果有多个CPU,就有多个节点。每个节点用struct pglist_data表示,放在一个数组里面
- 每个节点分为多个区域,每个区域用struct zone表示,也放在一个数组里面
- 每个区域分为多个页。为了方便分配,空闲页放在struct free_area 里面,使用伙伴系统进行管理和分配,每一页用struct page表示
对于物理内存,从下到上的关系以及分配模式如下:
- 物理内存分NUMA节点,分布进行管理
- 每个NUMA节点分成多个内存区域
- 每个内存区域分成多个物理页面
- 伙伴系统将多个连续的页面作为一个大的内存块分配给上层
- kswapd负责物理页面的换入换出
- Slub Allocator 将从伙伴系统申请的大内存块切成小块,分配给其他系统