Linux内存管理
Linux内存管理(三)物理内存管理(下)
前面讲的伙伴系统其管理的基本单位是页,一页有4K字节。但是如果我们需要更小的内存块,那物理内存应该如何分配呢?
一、缓存区数据结构
上一篇文章讲过,对于小块内存,会使用slub分配器,这篇文章就来解析它
在创建进程的时候,会调用 dup_task_struct,它会复制一个 task_struct 对象,需要先调用 alloc_task_struct_node 分配一个 task_struct 大小的内存
从下面这段代码中,可以看到 alloc_task_struct_node 会调用 kmem_cache_alloc_node,从 task_struct_cachep 中分配内存
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);
}
kmem_cache 表示一个缓存区,其是内核一种内存管理的方法,其基本思想是分配几个连续的页,根据缓存对象的大小切分成小块内存
在系统初始化的时候,task_struct_cachep 会被 kmem_cache_create 创建,kmem_cache_create 中指定的是这个缓存区的名字叫 task_struct,每个内存块的大小为 arch_task_struct_size,即为 task_struct 的大小
有了这个缓存区,每次创建 task_struct 的时候,不必到内存里去分配,只需要调用 kmem_cache_alloc_node,从 task_struct_cachep 这个缓存区中看有没有直接可以使用的内存块
当进程结束的时候,task_struct 的内存也不必被释放,而是通过 kmem_cache_free,放到 task_struct_cachep 这个缓存区中,供下次使用
首先来看一下
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];
};
- list:内核中不是只有一个 kmem_cache 对象,task_struct、mm_struct、fs_struct 等等都有它们对应的 kmem_cache 对象,这些对象都串在一个链表上,也就是 LIST_HEAD(slab_caches),list 就是链表节点
上面说过,对于缓存来说,起始就是分配几个连续的页,根据缓存对象的大小切分成小块内存
这里有三个 kmem_cache_order_objects 类型的变量。这里面的 order 表示2的 order 次方个页的内存块,objects 表示能够存放的缓存对象数量
最终内存会被变成下面这样子
对于每一项的结构,都是缓存对象后面跟着一个指向下一个空闲对象的指针,这样讲大内存块中所有的空闲对象连接成一个链表
- 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 的 free_list,如果可以取出空闲的项,那么就直接返回。如果没有空闲了,则只好进入普通通道,调用 __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 的 free_list,为什么呢?因为在分配内存的时候,进程可能被调度睡眠,在进程切换回来的时候,缓存区可能已经有空闲的内存块了,所以再检查一下。如果找到了,就跳到 load_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_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
三、页面的换出
另一个物理内存管理要处理的事情就是页面的换出。每个进程都有自己的虚拟地址空间,虚拟地址空间都非常大,而不可能有这么多的物理内存。所以对于一些长时间不使用的页面,将其换出到磁盘,等到要使用的时候,将其换入到内存中,以此提高物理内存的使用率
什么情况会触发页面换出呢?
最常见的情况就是,分配物理内存的时候,发现没有地方了,那么就试图地回收一些
例如,申请一个页面的时候,会调用 get_page_from_freelist,调用链为 get_page_from_freelist -> node_reclaim -> __node_reclaim -> shrink_node,通过这个调用链可以看出,页面的换出是以节点为单位的
另一种情况是,就是内存管理系统主动去做的,就是内核线程 kswapd。这个线程是一个无限循环,如果内存不紧张,那么就睡眠,如果内存紧张,就唤醒,然后试图去换出一些页面
/*
* 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);
......
}
}
这里的调用链是 balance_pgdat -> kswapd_shrink_node -> shrink_node,最后也是调用的 shrink_node,也是以节点为单位的,也就是每个节点都有一个 kswap 线程
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。如果要换出内存,就从 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,将内存页写入文件系统中;对于内存映射关联了文件的,需要将修改的内容写回文件系统中
四、总结
结合上篇文章,对物理内存做一个总结
- 物理内存分 NUMA 节点,分别进行管理
- 每个 NUMA 节点分为多个区域
- 每个区域有多个物理页面
- 伙伴系统将多个连续的连续的页面作为一个大的内存块管理
- kswapd 内核线程负责将页面换入和换出
- slub allocator 负责将从伙伴系统申请到的大内存切分成小块内存,分配给其他系统