linux内核把物理页作为内存管理的基本单位。每一页的大小根据系统架构不同有所区别,32位系统下为4KB,64位系统下为8KB。内存管理单元(MMU)以页为单位来管理系统中的页表,负责虚拟地址到物理地址的转换,用户所使用的内存地址一般都是虚拟地址。
内核中页的结构体中比较重要的成员为:
struct page {
unsigned long flags; /* 页的状态,包括脏标志、是否锁定 */
atomic_t _count; /* 引用计数 */
void *virtual; /* 页的虚拟地址 */
};
内存中区的划分:
名称 | 物理内存 | 描述 |
ZONE_DMA | 0~16MB | DMA使用的内存 |
ZONE_NORMAL | 16~896MB | 正常可寻址的内存 |
ZONE_HIGHEM | 896MB之后的内存 | 动态映射的内存 |
区的划分实际是和体系结构相关,如x86体系结构下,ISA设备只能在物理内存的前16MB执行DMA。所以在某些体系结构下,ZONE_DMA和ZONE_NORMAL可能合并为ZONE_NORMAL。高端内存ZONE_HIGHEM也是类似。不同的区组成了不同的内存池,内存池的主要作用是减少内存碎片。
内核中区也有对应的结构体(省略部分):
struct zone {
unsigned long watermark[NR_WMARK];
unsigned long percpu_drift_mark;
unsigned long min_unmapped_pages;
unsigned long min_slab_pages;
spinlock_t lock;
struct free_area free_area[MAX_ORDER];
/* Fields commonly accessed by the page reclaim scanner */
spinlock_t lru_lock;
struct zone_lru {
struct list_head list;
} lru[NR_LRU_LISTS];
struct zone_reclaim_stat reclaim_stat;
unsigned long pages_scanned; /* since last reclaim */
unsigned long flags; /* zone flags, see below */
/* Zone statistics */
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
int prev_priority;
unsigned int inactive_ratio;
unsigned long wait_table_hash_nr_entries;
unsigned long wait_table_bits;
struct pglist_data *zone_pgdat;
/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
unsigned long zone_start_pfn;
unsigned long spanned_pages; /* total size, including holes */
unsigned long present_pages; /* amount of memory (excluding holes) */
const char *name;
}
内核中总共就三个区,所以也只有3个这样的结构,name分别为“DMA”、“Normal”、“HighMem”。
页的操作接口:
static inline struct page *alloc_pages(gfp_t gfp_mask, unsigned int order) 请求多个内存页,返回指向第一个页的page结构体
void *page_address(struct page *page) 把给定的页转换成虚拟地址,返回虚拟地址
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order) 相当于上面两个函数一起执行的结果,返回第一个页的逻辑地址
unsigned long get_zeroed_page(gfp_t gfp_mask) 请求填充为0的页
void __free_pages(struct page *page, unsigned int order) 释放页,入参为page
void free_pages(unsigned long addr, unsigned int order) 释放页,入参为逻辑地址
我们常用的是以字节为单位的分配,字节内存的操作接口:
void *kmalloc(size_t size, gfp_t flags) 用来分配物理地址连续的内存。
void kfree(const void *) 用来释放kmalloc函数分配的内存。
void *vmalloc(unsigned long size)用来分配虚拟地址连续的内存,不要求物理地址连续。
void vfree(const void *addr)用于释放vmalloc分配的内存
以上的内存分配接口都涉及到gfp_mask标志,这里不进行细说,例举几个常用的:
标志 | 描述 |
GFP_KERNEL | 常规分配方式,可能会阻塞。这个标志在睡眠安全时用在进程上下文代码中。一般为首选标志。 |
GFP_ATOMIC | 用于中断处理程序、下半部等不能睡眠的地方 |
GFP_DMA | 从ZONE_DMA进行分配,通常与其他标志组合使用 |
针对频繁申请和释放数据结构场景(如文件描述符、进程描述符等)实现了slab分配器,slab分配器将不同的对象划分高速缓存组,每个高速缓存组都存放不同的对象,每种对象类型对应一个高速缓存。kmalloc()接口建立在slab层之上,使用了一组通用高速缓存。每个高速缓存又可分为一个或多个slab,通常一个slab由一个页组成。

每个slab处于三种状态之一:满、部分满、空。slab在内核中也对应着一个结构体:
struct slab {
struct list_head list; //满、部分满或空链表
unsigned long colouroff; //slab着色偏移量
void *s_mem; //在slab中的第一个对象,包含着色偏移
unsigned int inuse; //slab中分配的对象
kmem_bufctl_t free; //slab中第一个空闲的对象
unsigned short nodeid; //内存结点ID,非负时分配器尝试从相同同在结点处分配内存,通常用于NUMA系统
};
slab描述有两种存放的选择:一种是存放在slab自身开始的地方,另一种是在slab之外另行分配。slab分配器创建新的slab时,可以kmem_get_pages()。使用kmem_freepages释放内存。
创建高速缓存接口如下:
struct kmem_cache *kmem_cache_create (const char *name, size_t size, size_t align,unsigned long flags, void (*ctor)(void *))
第一个参数为高速缓存名称,第二个参数高速缓存中每个元素的大小,第三个参数为slab内第一个对象的偏移,通常为0。最后一个是高速缓存的构造函数,新页追加到高速缓存时才被调用。成功返回一个指向高速缓存的指针,失败返回 NULL。
void kmem_cache_destroy(struct kmem_cache *);
撤销高速缓存
void *kmem_cache_alloc(struct kmem_cache *cachep,gfp_t flags)
从高速缓存中分配对象,第二个参数通常是GFP_KERNEL或GFP_ATOMIC
void kmem_cache_free(struct kmem_cache *cachep, void *objp)
释放对象,放回slab中。第一个为高速缓存,第二个为需要释放的对象地址
内核中也存在进程的概念,每个内核进程都有两页的内核栈。所以内核进程中尽量避免静态分配,节省栈空间。
高端内存的页不能永久映射到内核地址空间上,使用alloc_pages()+__GFP_HIGHMEM标志来获得的页不具有虚拟地址,还需要使用kmap()将页映射到内核地址空间,通常是3G~4G。该映射函数可以睡眠,只能用于进程上下文。使用kunmap()可以解除映射。在中断上下文,可用kmap_atomic()来映射高端内存,kunmap_atomic()可以解除映射。
最后,内核实现了一种叫"per cpu"变量,我们称为每CPU变量,实际就是定义了一个数组,长度为CPU数量,每一个元素只能由对应的CPU处理。这样可以避免并发访问的问题,处理器可以不加锁的情况下进行安全访问。如:
long my_percpu[NR_CPUS];
int cpu;
cpu=get_cpu(); //获取当前处理器,并禁止内核抢占
my_percpu[cpu]++;
put_cpu() //激活内核抢占
可以看到,在CPU访问每CPU变量时,禁止了内核抢占,这是为了防止某些CPU可以接触到其他CPU的数据。
涉及到的相关接口如下:
DEFINE_PER_CPU(type,name) 编译时创建类型为type,名字为name的变量
DECLARE_PER_CPU(type,name) 声明每CPU变量
get_cpu_var() 返回当前处理器变量,同时禁止抢占,如:get_cpu_var()++;
put_cpu_var() 激活内核抢占
per_cpu(name,cpu)++ 增加指定CPU上name变量的值
void* alloc_percpu(type) 动态分配type类型的每CPU变量
void* __alloc_percpu(size_t size, size_t align) 增加了字节对齐
void free_percpu(const void *); 释放每CPU变量