内存管理
1. 内核如何管理内存
1.1 页
内核用struct page结构代表系统中的每个物理页:linux/mm_types.h
|
|
1.2 区
硬件有一些限制,比如某些页位于内存中的特定物理地址上,所以不能将其用于一些特定的任务。因此内核又把页划分为不同的区(zone),内核使用区将相似属性的页进程分组。
Linux必须处理如下两种硬件引起的内存寻址问题:
- 一些硬件只能在某些特定内存地址上执行DMA
- 一些体系结构的内存的物理寻址范围比虚拟寻址范围大,导致一些内存不能永久映射到内核空间
Linux因此将页分成四种区:
- ZONE_DMA 包含能执行DMA的页
- ZONE_DMA32 支持32位设备
- ZONE_NORMAL 所以能正常映射的页
- ZONE_HIGHEM 包含”高端内存“,其中的页不能永久映射到内核地址空间
区由结构体struct zone表示:linux/mmzone.h,初始化在:
1.3 获取页API
内核提供一种请求内存的底层机制,以页为单位分配内存,相关接口定义在linux/gfp.h
- struct page* alloc_pages(gfp_t gfp_mask, unsigned int order):分配(1<<order)个连续的物理页,返回指向第一页页结构的指针
- void page_address(struct page page):转换为逻辑地址
- __get_free_pages(gfp_mask, order):分配(1<<order)页,返回指向第一页逻辑地址的指针
- alloc_page(gfp_mask)
- __get_free_page(gfp_mask)
- get_zeroed_page(gfp_mask)
- void __free_pages(struct page *page, unsigned int order)
- void free_pages(unsigned long addr, unsigned int order)
- void free_page(unsigned long addr)
2. kmalloc()
内核提供kmalloc()函数来分配以字节为单位的内存,调用它可以获得以字节为单位的一块内核内存
kmalloc()在linux/slab.h
- void *kmalloc(size_t size, gfp_t flags):分配在物理上连续的大小为size的内存块
2.1 gfp_mask标志
- 行为修饰符:表示内核应当如何分配所需内存
- 区修饰符:表示从哪儿分配内存
- 类型:组合行为修饰符和区修饰符,得到一个类型标志,方便使用
这些标志声明在:linxu/gfp.h
标志 | 修饰符 | 描述 |
---|---|---|
GFP_ATOMIC | __GFP_HIGH | 用在中断处理程序、下半部、持有自旋锁以及其他不能睡眠的地方 |
GFP_NOWAIT | 0 | 与GFP_ATOMIC类似,但是调用不会退给紧急内存池,增加了分配失败的可能性 |
GFP_NOIO | __GFP_WAIT | 这种分配可以阻塞,但不会启动磁盘I/O。这个标志在不能引发更多磁盘I/O时能阻塞I/O代码,有递归风险 |
GFP_NOFS | __GFP_WAIT|__GFP_IO | 这种分配在必要时可能阻塞,也可能启动磁盘I/O,但不会启动文件系统操作。这个标志在不能再启动另一个文件系统操作时,用在文件系统代码中 |
GFP_KERNEL | __GFP_WAIT |__GFP_IO | __GFP_FS | 常用的方式,可能会阻塞。这个标志在睡眠安全时用在进程上下文代码中 |
GFP_USER | __GFP_WAIT|__GFP_IO|__GFP_FS | 常用方式,可能会阻塞。这个标志用于用户空间分配内存 |
GFP_HIGHUSER | __GFP_WAIT|__GFP_IO| __GFP_FS|__GFP_HIGHMEM |
从ZONE_HIGHMEM进行分配,可能会阻塞。这个标志也用于为用户空间进程分配内存 |
GFP_DMA | __GFP_DMA | 从ZONE_DMA进行分配。需要获取能供DMA使用的内存的设备驱动程序使用这个标志,通常与以上的某个标志组合 |
2.2 kfree()
kfree声明在
kfree()释放由kmalloc()分配的内存块,注意分配和回收要配对使用,调用kfree(NULL)是安全的。
2.3 vmalloc()
vmalloc()类似于kmalloc(),只是vmalloc()分配的虚拟地址连续,但是物理地址可能不连续。这是用户空间分配函数的工作方式:由malloc()返回的页在进程的虚拟地址空间内连续,但是不能保证其在物理内存也连续。kmalloc()确保页在物理地址上连续。
vmallo()声明在linux/vmalloc.h,定义在mm/vmalloc.c,函数可能睡眠:
|
|
3. slab分配器
内存的分配和释放是非常普遍的,频繁的分配和释放消耗很大,Linux提供了slab分配器管理内存。关于slab分配器的原则:
- 频繁使用的数据结构也会频繁分配和释放,因此应该缓存它们
- 频繁分配和释放会导致内存碎片,因此空闲链表的缓存应该连续存放。
- 回收的对象可以立刻投入下一次分配,因此,提供了频繁的分配释放的性能
- 如果知道对象大小、页大小和总的高速缓存大小,分配器可以做出更明智的决策
- 如果让部分缓存专属于单个处理器,那么分配和释放就可以不加SMP锁
- 如果分配器与NUMA相关,它就可以从相同的内存节点为请求者分配
- 对存放的对象进行着色,防止多个对象映射到相同的高速缓存行(cache line)
学习资料:
- Linux slab 分配器剖析
- Overview of Linux Memory Management Concepts: Slabs
- The Linux Kernel-Memory
- Kernel dynamic memory analysis
- Linux Memory Management
- http://phrack.org/issues/66/15.html#article
- http://phrack.org/issues/66/8.html#article
每个高速缓存使用kmem_cache结构表示,包含三个链表:slabs_full、slabs_partial、slabs_empty,slab实现的文件:linux/slab.h、mm
/slab.c
slab申请的内存释放:
- 当可用内存变得紧缺时,系统试图释放出更多内存以供使用
- 当高速缓存显式被撤销时
4. 在栈上静态分配
内核对进程的栈有限制:内核栈小而且固定。
进程的内核栈通常是两页的大小,32位和64位体系结构的页为4Kb和8kb,所以它们的内核栈大小分别为8kb和16kb。
可能设置单页的内核栈,此时中断处理程序不和内核进程共享同一个栈了,内核为它提供自己的中断栈。
- Virtually mapped kernel stacks-1
- Virtually mapped kernel stacks-2
- Linux Kernel Stack Overflow/Linux 内核栈溢出
- Kernel stack overflows (basics)
5. 高端内存映射
高端内存中的页不能永久映射到内核地址空间,因此通过alloc_pages()以__GFP_HIGHMEM标志获得的页不可能有逻辑地址。
永久映射
要映射一个给定的page结构到内核地址空间,可以使用kmap()函数,定义在linux/highmem.h
|
|
临时映射
当必须创建一个映射而当前上下文不能睡眠时,内核提供了临时映射(原子映射),通过一组保留的映射完成。
|
|
km_type描述了临时映射的目的