内存页
MMU :内存管理单元,将虚拟内存转化为物理地址的硬件。
- 因为 MMU 通常以页为单位处理内存,所以从虚拟内存的角度来说,内核中页是最小的单位。
- 一般 32 位系统使用4k页,64 位系统使用8k页。
- 内核使用 struct page 结构体存放物理页。page 与物理页相关,而非虚拟页,当内存页被swap后,可能不再和同一个page相关联。
- 可以通过page_address(page) 函数获取物理页page 对应的逻辑地址。
page 的内核代码
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 分配。
有些硬件存在下面两种缺陷引起的内存寻址问题,所以需要将内存分区。
- 一些硬件只能用特定的内存地址来执行DMA(直接内存访问)。
- 一些体系结构的内存物理寻址范围大于虚拟寻址范围,导致一些内存不能映射到内核空间。
linux 主要有下面 4 种 ZONE
- ZONE_DMA—This zone contains pages that can undergo DMA.
- 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.
- ZONE_NORMAL—This zone contains normal, regularly mapped, pages.
- ZONE_HIGHMEM—This zone contains “high memory,” which are pages not permanently 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 的内核代码
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 |
释放内存接口:
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 大小,分配的内存区在物理上是连续的。
void * kmalloc(size_t size, gfp_t flags)
gfp_mask 标志
- 行为修饰符:分配内存时的动作
- 区修饰符:从哪个 zone 分配内存
- 类型:组合上面两个
- 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 |
Need DMA-able memory, cannot | Use (GFP_DMA |
kfree()
kfree() 释放由kmalloc()申请的内存。
void kfree(const void *ptr)
//例子
char *buf;
buf = kmalloc(BUF_SIZE, GFP_ATOMIC); if (!buf)
/* error allocating memory ! */
kfree(buf);
vmalloc()
类似kmalloc
,但是物理内存地址可以不连续,虚拟内存地址是连续的。可以睡眠。
一般只有硬件要求得到物理地址连续的内存。软件可以使用只有虚拟地址连续的内存。很多内核代码虽然不需要连续的物理内存,但还是使用kmalloc
,因为性能好,不需要做逻辑映射。
//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。
一个 slab 有三个状态:full, partial, or empty. 先从 partial 开始填充。
cache 用 kmem_cache 结构表示 包含三个链表 slabs_full,slabs_partial,slabs_empty,链表中包含所有的 slab。
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 分配内存。
//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.首先创建一个全局变量存放 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 可以有自己才能访问的数据,这样不需要锁,只需要注意内核抢占的问题即可。
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 动态分配 |