内核中内存管理

内存页

MMU :内存管理单元,将虚拟内存转化为物理地址的硬件。

  • 因为 MMU 通常以页为单位处理内存,所以从虚拟内存的角度来说,内核中页是最小的单位。
  • 一般 32 位系统使用4k页,64 位系统使用8k页。
  • 内核使用 struct page 结构体存放物理页。page 与物理页相关,而非虚拟页,当内存页被swap后,可能不再和同一个page相关联。
  • 可以通过page_address(page) 函数获取物理页page 对应的逻辑地址。

    page 的内核代码

1
2
3
4
5
6
7
8
9
10
11
12
defined in <linux/mm_types.h>

struct page {
unsigned long flags; //内存页状态,定义在<linux/page-flags.h>.
atomic_t _count; //引用计数
atomic_t _mapcount;
unsigned long private;
struct address_space *mapping;
pgoff_t index;
struct list_head lru;
void *virtual; //在虚拟内存中的地址
};

ZONE

由于硬件的原因,内核对于内存中不同物理地址的内存并不一视同仁。由于这种限制,内存将内存页划分了区(zone)。ZONE的划分是为了管理页的一种逻辑分组。内存分配不能同时在两个zone 分配。

有些硬件存在下面两种缺陷引起的内存寻址问题,所以需要将内存分区。

  1. 一些硬件只能用特定的内存地址来执行DMA(直接内存访问)。
  2. 一些体系结构的内存物理寻址范围大于虚拟寻址范围,导致一些内存不能映射到内核空间。

linux 主要有下面 4 种 ZONE

  1. ZONE_DMA—This zone contains pages that can undergo DMA.
  2. ZONE_DMA32—Like ZOME_DMA, this zone contains pages that can undergo DMA. Unlike ZONE_DMA, these pages are accessible only by 32-bit devices. On some architectures, this zone is a larger subset of memory.
  3. ZONE_NORMAL—This zone contains normal, regularly mapped, pages.
  4. ZONE_HIGHMEM—This zone contains “high memory,” which are pages not perma￾nently mapped into the kernel’s address space.

例如 X86-32 架构,ISA 设备只能访问物理内存的前16M,高于896M的内存不能直接映射。剩下的就是NORMAL区。如果体系结构没有限制,那么全部都是NORMAL区。

Zone Description Physical Memory
ZONE_DMA DMA-able pages < 16MB
ZONE_NORMAL Normally addressable pages 16–896MB
ZONE_HIGHMEM Dynamically mapped pages > 896MB

zone 的水线:每一个zone 都有自己的最小值,最低值,最高值三个水线,使用水线设置合适的内存消耗基准,水线随着空暇内存变化

zone 的内核代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
defined in <linux/mmzone.h>

struct zone {
unsigned long watermark[NR_WMARK]; //持有该区的最小最低最高的水位值。
unsigned long lowmem_reserve[MAX_NR_ZONES];
struct per_cpu_pageset pageset[NR_CPUS];
spinlock_t lock; //自旋锁
struct free_area free_area[MAX_ORDER]
spinlock_t lru_lock;
struct zone_lru {
struct list_head list;
unsigned long nr_saved_scan;
}lru[NR_LRU_LISTS];
struct zone_reclaim_stat reclaim_stat;
unsigned long pages_scanned;
unsigned long flags;
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
int prev_priority;
unsigned int inactive_ratio;
wait_queue_head_t *wait_table;
unsigned long wait_table_hash_nr_entries;
unsigned long wait_table_bits;
struct pglist_data *zone_pgdat;
unsigned long zone_start_pfn;
unsigned long spanned_pages;
unsigned long present_pages;
const char *name;
};

获取页

内核提供了一些请求内存和释放内存的底层接口,

请求内存函数 描述
alloc_page(gfp_mask) Allocates a single page and returns a pointer to its
alloc_pages(gfp_mask, order) Allocates 2order pages and returns a pointer to the first page’s page structure
__get_free_page(gfp_mask) Allocates a single page and returns a pointer to its logical address
__get_free_pages(gfp_mask, order) Allocates 2order pages and returns a pointer to the first page’s logical address
get_zeroed_page(gfp_mask) Allocates a single page, zero its contents and returns a pointer to its logical address

释放内存接口:

1
2
3
void __free_pages(struct page *page, unsigned int order) 
void free_pages(unsigned long addr, unsigned int order)
void free_page(unsigned long addr)

kmalloc()

和上面获取页的接口不同,kmalloc()主要用于申请字节为单位的内存。kmalloc() 返回一个指向内存块的指针,至少是 size 大小,分配的内存区在物理上是连续的。

1
void * kmalloc(size_t size, gfp_t flags)
gfp_mask 标志
  1. 行为修饰符:分配内存时的动作
  2. 区修饰符:从哪个 zone 分配内存
  3. 类型:组合上面两个
    • GFP_ATOMIC 分配内存是不能睡眠,在内存紧缺时容易失败。
    • GFP_KERNEL 可以睡眠,用于安全调度的进程上下文中,成功率高。

Situation | Solution
|—|—|
Process context, can sleep | Use GFP_KERNEL.
Process context, cannot sleep | Use GFP_ATOMIC, or perform your allocations with GFP_KERNEL at an earlier or later point when you can sleep
Interrupt handler | Use GFP_ATOMIC.
Softirq | Use GFP_ATOMIC.
Tasklet | Use GFP_ATOMIC.
Need DMA-able memory, can | Use (GFP_DMA | GFP_KERNEL). sleep
Need DMA-able memory, cannot | Use (GFP_DMA | GFP_ATOMIC), or perform your sleep allocation at an earlier point when you can sleep.

kfree()

kfree() 释放由kmalloc()申请的内存。

1
2
3
4
5
6
7
void kfree(const void *ptr)

//例子
char *buf;
buf = kmalloc(BUF_SIZE, GFP_ATOMIC); if (!buf)
/* error allocating memory ! */
kfree(buf);

vmalloc()

类似kmalloc,但是物理内存地址可以不连续,虚拟内存地址是连续的。可以睡眠。

一般只有硬件要求得到物理地址连续的内存。软件可以使用只有虚拟地址连续的内存。很多内核代码虽然不需要连续的物理内存,但还是使用kmalloc,因为性能好,不需要做逻辑映射。

1
2
3
4
5
6
7
8
9
//declared in <linux/vmalloc.h>

void * vmalloc(unsigned long size)
void vfree(const void *addr)

//例子
char *buf;
buf = vmalloc(16 * PAGE_SIZE); /* get 16 pages */ if (!buf)
vfree(buf);

Slab 层

有很多对象存放在链表结构中,在不用的时候空闲链表也已经占用内存。内核不能不能控制这些空闲链表的回收,尤其是在内存紧缺时。所以引入了 slab 分配器。slab 扮演了一个通用的数据结构缓存角色。

slab 把不同类型的对象放到不同的 caches 中。 一个 slab 由一个或者多个物理上的连续页组成,一般情况只有一个页,每一个cache 有多个 slab。

image

一个 slab 有三个状态:full, partial, or empty. 先从 partial 开始填充。

cache 用 kmem_cache 结构表示 包含三个链表 slabs_full,slabs_partial,slabs_empty,链表中包含所有的 slab。

1
2
3
4
5
6
7
8
9
10
struct slab {
struct list_head list; /* full, partial, or empty list */
unsigned long colouroff; /* offset for the slab coloring */
void *s_mem; /* first object in the slab */
unsigned int inuse; /* allocated objects in the slab */
kmem_bufctl_t free; /* first free object, if any */
};

slab 的创建:
通过 *kmem_getpages()中调用的 _get_free_pages()函数分配内存页。

slab 是在 cache 的基础之上,提供给内核一个简单的接口,通过接口来对 cache 进行分配和撤销。slab 起一个分配器的作用,可以为具体的 object 分配内存。

1
2
3
4
5
6
7
8
9
10
//cache 的创建(slab 分配器的接口):

//返回一个指向 cache 的指针。align 是 slab 第一个对象的偏移量,用于内存对齐
struct kmem_cache * kmem_cache_create(const char *name, size_t size,size_t align, unsigned long flags, void (*ctor)(void *));

//撤销 cache
int kmem_cache_destroy(struct kmem_cache *cachep)

//创建 cache 后,获取对象,没有空闲 slab 的话,通过上面的*kmem_getpages()获取新的页。
void * kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)

task_struct 对象的 slab 和 cache 创建例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//1.首先创建一个全局变量存放 task_struct 的 cache
struct kmem_cache *task_struct_cachep;

task_struct_cachep = kmem_cache_create(“task_struct”, sizeof(struct task_struct),ARCH_MIN_TASKALIGN, SLAB_PANIC | SLAB_NOTRACK, NULL);

//2.进程调用 fork()时,会创建新的process descriptor:

struct task_struct *tsk;
tsk = kmem_cache_alloc(task_struct_cachep, GFP_KERNEL);
if (!tsk)
return NULL;

//3.process descriptor 被撤销
kmem_cache_free(task_struct_cachep, tsk);

//4.task_struct_cachep cache 是不会被撤销的,因为内核经常要用,非要撤销的话:

int err;
err = kmem_cache_destroy(task_struct_cachep); if (err)
/* error destroying cache */

其他

Stack 上内存的静态分配

32位 和 64位 页的大小为4K 和 8K,一般进程有两页的内核栈,也可以设置单页内核栈。

随着运行时间的增加,物理内存碎片增加,分配连续的页越来越难。当单页栈设置后,中断程序不再和进程放在同一个栈内,有自己的中断栈。

栈的溢出会覆盖紧邻堆栈末端的内容,溢出后 down 机还好,否则会破坏数据。

高端内存的映射

高端内存的永久映射数量是有限的,不需要时需要解除。通过函数kmap进行映射,可以睡眠。kmap_atomic提供了原子性的临时映射。不会被阻塞,禁止内核抢断。

per CPU 新接口

对于 smp 系统,多个 cpu 可以有自己才能访问的数据,这样不需要锁,只需要注意内核抢占的问题即可。

1
2
3
4
5
6
7
8
9
10
void *percpu_ptr; 
unsigned long *foo;

percpu_ptr = alloc_percpu(unsigned long); //为 cpu 动态分配内存,类似 kmalloc()
if (!ptr)
/* error allocating memory .. */

foo = get_cpu_var(percpu_ptr); //获取当前 CPU 上的指定数据,会禁止内核抢断
/* manipulate foo .. */
put_cpu_var(percpu_ptr); //开启内核抢断

分配内存函数的选择

需求 函数 特点
连续的物理页 kmalloc() 可以通过 flag 决定是否可以睡眠,
高端内存 alloc_pages() 返回一个 page 的指针,而不是逻辑地址,因为可能没有映射
获取真正的指针 kmap() 会把高端内存映射到逻辑地址
不需要内存连续的地址 vmalloc() 需要映射,有性能损失
创建和撤销数据结构 slab 使用 cache 动态分配