页
内核使用物理页来管理内存,内核通过下面的结构体管理物理页:
struct page {
unsigned long flags; (1)
atomic_t _count; (2)
union {
atomic_t _mapcount;
struct {
u16 inuse;
u16 objects;
};
};
union {
struct {
unsigned long private;
struct address_space *mapping;
};
struct kmem_cache *slab;
struct page *first_page;
};
union {
pgoff_t index;
void *freelist;
};
struct list_head lru;
void *virtual;
};
物理页的状态。例如是不是脏的、是不是被锁定在内存中。
页面的引用次数。当值为 -1 时,代表内核没有使用此页,可以重新分配。
页的虚拟地址。
这个结构体的大小为 40B,对于 4GB 内存而言,需要使用 20MB。相比之下,代价可以接受。
区
内核将内存区域分为下面几个区:
区的具体情况和体系结构相同,对于某些结构而言,DMA 操作可以在任意内存区域上,因此 DMA 区域为空,DMA 操作直接使用 NORMAL 区域的内存。对于 x86 结构而言,ISA 设备只能访问物理内存的前 16MB,因此 ZONE_DMA 在 x86 上的页都在 [0, 16MB) 上。
ZONE_HIGHEM 和 DMA 类似,在 32 位 x86 体系中,高端内存为高于 896MB 的所有物理内存,区域内存为低端内存。
高端内存的出现是因为 CPU 寻址的限制。对于 32 CPU 而言,寻址空间为 4GB。多余的物理内存无法进行寻址。为了解决这一问题,使用高端内存对大于 4GB 的物理内存进行映射。 如果 CPU 能够寻址整个地址空间,就无需高端内存了。 |
除了 DMA 和高端内存外,其余所有内存为 ZONE_NORMAL。
内核将内存划分成区,再从区中分配内存。内存的分配无法跨区,但是非 DMA 设备也能够分配 DMA 内存。一般来说,内核更期望从 NORMAL 区分配内存以为 DMA 预留资源。
分配页
所有分配页相关的方法如下:
标志 | 描述 |
---|---|
| 分配一页,返回指向页结构的指针。 |
| 分配 \(2^{order}\) 个页,返回第一页页结构的指针。 |
| 分配一页,返回指向其逻辑地址的指针。 |
| 分配 \(2^{order}\) 个页,返回指向第一页逻辑地址的指针。 |
| 分配一页,将其内容填充为零,返回指向其逻辑地址的指针。 |
内核提供了几个以页为单位分配内存的接口作为分配内存的底层机制,其中最核心的函数为:
struct page * alloc_pages(gfp_t gfp_mask, unsigned int order); (1)
分配 \(2^{order}\) 个连续的物理页并返回指向第一个页的 page 结构体。如果失败,返回 NULL。
如果不需要使用 page 结构体,可以调用:
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);
和 alloc_pages 作用相同,但是直接返回第一个页的逻辑地址。
如果只需要一页,可以使用下面两个封装好的函数:
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
#define __get_free_page(gfp_mask) __get_free_pages((gfp_mask), 0)
分配填充为零的页
如果需要将返回的页初始化为零,使用下面的函数:
extern unsigned long get_zeroed_page(gfp_t gfp_mask);
此函数和 __get_free_pages()
类似,但是将分配的页填充成了零。当需要将内存分配给用户态进程时此函数非常有用。
释放页
使用下面的函数可以将页进行释放。
-
extern void __free_pages(struct page *page, unsigned int order);
-
#define __free_page(page) __free_pages((page), 0)
-
extern void free_pages(unsigned long addr, unsigned int order);
-
#define free_page(addr) free_pages((addr), 0)
内核不会对这些函数的操作进行检查,因此如果传递了错误的参数,很可能会导致系统崩溃,更糟糕的情况是数据遭到破坏。
伙伴系统
Linux 使用伙伴系统负责连续内存页的分配。目的是避免产生内存碎片。
伙伴系统将内存页按照固定数量进行分组,每组包含的页面数量必定是 \(2^n, (0<=2<=10)\)。大小相等且相邻的两组内存互为 Buddy。
现假设需要分配内存页数量为 \(k\),则伙伴系统分配的内存为 \(n=\lceil\log_2{k}\rceil\)。如果没有满足目标 \(n\) 的内存组,则将一个大组等量分成两个 Buddy,然后再进行分配。
将内存组分成 Buddy 的过程是递归的。 |
在回收内存时,如果两个相邻的 Buddy 都空闲,则将两个 Buddy 合并成更大的内存组。此过程同样是递归的。
slab
在涉及到内存的频繁分配和释放时,开发人员经常会使用空闲链表对内存进行缓存。空闲链表实际上类似于对象缓存。
由于内核不知道空闲链表的存在,因此无法在内存紧缺时要求空闲链表丢弃缓存以释放一些内存。为了解决这个问题,内核提供了 slab 层(也就是 slab 分配器)。slab 分配器扮演了通用数据结构缓存层的角色。
在使用上,slab 类似于空闲链表:
创建一个新的 slab:
struct kmem_cache *kmem_cache_create(
const char * name, (1)
size_t size, (2)
size_t align, (3)
unsigned long flags, (4)
void (*ctor)(void *) (5)
);
slab 的名字。
slab 中每个元素的大小。
slab 中第一个对象的编译,用来在页内进行对齐。一般设置为零即可。
flags:
标志 作用 SLAB_RED_ZONE
进行边界检测。
SLAB_POISON
使用 a5a5a5 填充 slab。
SLAB_HWCACHE_ALIGN
将对象按 cache line 进行对齐以防止错误的共享。
SLAB_CACHE_DMA
用于 DMA 对象。
SLAB_PANIC
失败时直接 panic。
分配页时的构造函数,可以设置为 NULL。
成功时返回一个指向 slab 对象的指针,否则返回 NULL。
删除一个 slab 需要调用:
void kmem_cache_destroy(struct kmem_cache *);
此函数可能睡眠,因而无法在中断上下文中调用。
下面两个函数分别用于从 slab 中分配内存和释放内存:
函数 | 作用 |
---|---|
| 分配内存 |
缓存着色
如图所示,cache 在缓存时将内存划分为不同的块,然后依次将其映射到 cache line 中。根据这种算法,memory_idx % 3 相等的内存块会映射到同一个 cache line 上。
缓存着色的目的就是避免 slab 中的内存映射到同一 cache line 上。从而尽量将 slab 中的内存全部缓存起来。
高端内存
在 x86 体系中,所有高于 896MB 的物理内存都是高端内存,无法被永久地映射到内核地址空间。
使用下面的 api 可以将高端内存永久映射到内核空间:
函数 | 作用 |
---|---|
linux/highmem.h
| 如果 page 指向的是低端内存,直接返回此页的虚拟地址。如果指向的是高端内存,则先建立一个永久映射,然后返回地址。此函数可以休眠,因此只能用在进程上下文中。 |
| 解除对高端内存的映射。 |
| 和 kmap 类似,但是不会休眠,也不会阻塞。 |
| 解除对 kvaddr 的映射。 |