内存页
MMU :内存管理单元,将虚拟内存转化为物理地址的硬件。
- 因为 MMU 通常以页为单位处理内存,所以从虚拟内存的角度来说,内核中页是最小的单位。
- 一般 32 位系统使用4k页,64 位系统使用8k页。
- 内核使用 struct page 结构体存放物理页。page 与物理页相关,而非虚拟页,当内存页被swap后,可能不再和同一个page相关联。
- 可以通过page_address(page) 函数获取物理页page 对应的逻辑地址。
page 的内核代码
1 | defined in <linux/mm_types.h> |
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 的内核代码
1 | defined in <linux/mmzone.h> |
获取页
内核提供了一些请求内存和释放内存的底层接口,
请求内存函数 | 描述 |
---|---|
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
3void __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 标志
- 行为修饰符:分配内存时的动作
- 区修饰符:从哪个 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 | 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 | void kfree(const void *ptr) |
vmalloc()
类似kmalloc
,但是物理内存地址可以不连续,虚拟内存地址是连续的。可以睡眠。
一般只有硬件要求得到物理地址连续的内存。软件可以使用只有虚拟地址连续的内存。很多内核代码虽然不需要连续的物理内存,但还是使用kmalloc
,因为性能好,不需要做逻辑映射。
1 | //declared in <linux/vmalloc.h> |
Slab 层
有很多对象存放在链表结构中,在不用的时候空闲链表也已经占用内存。内核不能不能控制这些空闲链表的回收,尤其是在内存紧缺时。所以引入了 slab 分配器。slab 扮演了一个通用的数据结构缓存角色。
slab 把不同类型的对象放到不同的 caches 中。 一个 slab 由一个或者多个物理上的连续页组成,一般情况只有一个页,每一个cache 有多个 slab。
一个 slab 有三个状态:full, partial, or empty. 先从 partial 开始填充。
cache 用 kmem_cache 结构表示 包含三个链表 slabs_full,slabs_partial,slabs_empty,链表中包含所有的 slab。
1 | struct slab { |
slab 是在 cache 的基础之上,提供给内核一个简单的接口,通过接口来对 cache 进行分配和撤销。slab 起一个分配器的作用,可以为具体的 object 分配内存。
1 | //cache 的创建(slab 分配器的接口): |
task_struct 对象的 slab 和 cache 创建例子
1 | //1.首先创建一个全局变量存放 task_struct 的 cache |
其他
Stack 上内存的静态分配
32位 和 64位 页的大小为4K 和 8K,一般进程有两页的内核栈,也可以设置单页内核栈。
随着运行时间的增加,物理内存碎片增加,分配连续的页越来越难。当单页栈设置后,中断程序不再和进程放在同一个栈内,有自己的中断栈。
栈的溢出会覆盖紧邻堆栈末端的内容,溢出后 down 机还好,否则会破坏数据。
高端内存的映射
高端内存的永久映射数量是有限的,不需要时需要解除。通过函数kmap
进行映射,可以睡眠。kmap_atomic
提供了原子性的临时映射。不会被阻塞,禁止内核抢断。
per CPU 新接口
对于 smp 系统,多个 cpu 可以有自己才能访问的数据,这样不需要锁,只需要注意内核抢占的问题即可。1
2
3
4
5
6
7
8
9
10void *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 动态分配 |