一、核心内存申请接口分类
内核内存申请接口可以根据分配的内存大小、连续性、上下文以及用途等进行分类。
1. Slab 分配器 (kmalloc 家族) – 针对字节大小的连续物理内存
Slab 分配器是内核中最常用的内存分配机制,用于分配较小的、大小固定的、物理上连续的内存块。它通过维护不同大小对象的缓存池(slabs)来减少碎片并提高分配和释放的效率。
- void *kmalloc(size_t size, gfp_t flags)
- 作用: 分配 size 字节的物理连续内存。返回指向内存块的内核虚拟地址。
- size: 请求的字节数。Slab 分配器会将其向上舍入到最接近的可用缓存大小。最大可分配大小通常受限于几个页(例如 128KB 或 4MB,取决于架构和配置)。
- flags (Get Free Pages flags): 这是非常重要的参数,用于控制分配行为和上下文。常用标志包括:
- GFP_KERNEL: 最常用的标志。允许睡眠(阻塞)。适用于进程上下文,当内存不足时,调用者可能会被阻塞,直到有足够的内存可用。可以进行 I/O 操作和页交换。
- GFP_ATOMIC: 用于原子上下文(中断处理程序、软中断、持有自旋锁时)。不允许睡眠。分配器会尽力分配,但如果内存不足,会立即失败。通常用于分配较小的内存。
- GFP_NOWAIT: 类似于 GFP_ATOMIC,不允许睡眠,但可以用于更广泛的场景,不仅仅是严格的原子上下文。如果内存不足,立即失败。
- GFP_NOIO: 允许睡眠,但不允许执行任何磁盘 I/O 操作来回收内存。
- GFP_NOFS: 允许睡眠和 I/O,但不允许执行任何文件系统相关的操作。
- __GFP_ZERO (与其他 GFP_ 标志组合使用,如 GFP_KERNEL | __GFP_ZERO): 分配内存并将其内容清零。
- __GFP_HIGHMEM: 从高端内存区分配(如果系统有并且请求的是页)。kmalloc 通常不直接使用此标志,因为它分配的是线性映射的内存。
- GFP_DMA / GFP_DMA32: 从适合 DMA 的内存区域分配。
- 返回值: 成功时返回指向分配内存的指针,失败时返回 NULL。必须检查返回值。
- 使用场景: 驱动程序中分配描述符、小缓冲区、内核数据结构等。
- void *kzalloc(size_t size, gfp_t flags)
- 作用: 等同于 kmalloc(size, flags | __GFP_ZERO)。分配内存并将其初始化为零。
- 使用场景: 当需要一块清零的内存时,使用此函数更简洁。
- void *kcalloc(size_t n, size_t size, gfp_t flags)
- 作用: 分配 n 个 size 大小的对象组成的数组,并将所有字节清零。总分配大小为 n * size。
- 使用场景: 分配清零的对象数组。
- void *krealloc(const void *p, size_t new_size, gfp_t flags)
- 作用: 重新调整指针 p 指向的内存块的大小为 new_size。
- 如果 new_size 为 0,效果等同于 kfree(p) 并返回 NULL。
- 如果 p 为 NULL,效果等同于 kmalloc(new_size, flags)。
- 如果 new_size 小于原大小,内存块可能会被截断。
- 如果 new_size 大于原大小,可能会在原地扩展(如果空间足够),或者分配新的内存块,将旧内容拷贝到新块,然后释放旧块。
- 返回值: 成功时返回指向新内存块的指针,失败时返回 NULL (原内存块 p 保持不变)。
- 注意: krealloc 可能返回与原指针不同的新指针,使用时需更新所有对该内存的引用。
- void kfree(const void *objp)
- 作用: 释放之前通过 kmalloc、kzalloc、kcalloc 或 krealloc 分配的内存。
- objp: 要释放的内存块指针。如果 objp 为 NULL,kfree 不执行任何操作。
- 注意: 传递给 kfree 的指针必须是之前这些分配函数返回的原始指针。重复释放或释放无效指针会导致内核崩溃。
2. 页分配器 (alloc_pages 家族) – 针对页大小的连续物理内存
页分配器是内核内存管理的核心,用于分配一个或多个连续的物理页框。Slab 分配器本身也是构建在页分配器之上的。
- struct page *alloc_pages(gfp_t gfp_mask, unsigned int order)
- 作用: 分配 2^order 个物理上连续的页框。
- gfp_mask: 与 kmalloc 中的 flags 类似,控制分配行为和内存区域。
- order: 分配阶数。0 表示分配 1 页,1 表示 2 页,2 表示 4 页,依此类推。MAX_ORDER 定义了最大阶数(通常为 10 或 11,即 1024 或 2048 页,对应 4MB 或 8MB)。
- 返回值: 成功时返回指向第一个页的 struct page 描述符的指针,失败时返回 NULL。
- 注意: 返回的是 struct page 指针,而不是可直接访问的内核虚拟地址。需要使用 page_address() 将其转换为内核虚拟地址。
- struct page *alloc_page(gfp_t gfp_mask)
- 作用: alloc_pages(gfp_mask, 0) 的简写,即分配单个页。
- unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
- 作用: 类似于 alloc_pages,但直接返回分配到的内存区域的内核虚拟地址。
- 返回值: 成功时返回分配内存的起始虚拟地址,失败时返回 0 (NULL)。
- unsigned long get_zeroed_page(gfp_t gfp_mask)
- 作用: 分配单个页并将其内容清零。等价于 __get_free_pages(gfp_mask | __GFP_ZERO, 0)。
- void free_pages(unsigned long addr, unsigned int order)
- 作用: 释放由 __get_free_pages 或 get_zeroed_page 分配的 2^order 个页。
- addr: 要释放内存的起始虚拟地址。
- order: 分配时的阶数。
- void __free_pages(struct page *page, unsigned int order)
- 作用: 释放由 alloc_pages 分配的 2^order 个页。
- page: 指向第一个页的 struct page 描述符。
- order: 分配时的阶数。
- void put_page(struct page *page) 和 get_page(struct page *page)
- 作用: 管理 struct page 的引用计数。get_page 增加引用计数,put_page 减少引用计数。当引用计数降为零时,如果该页不再被其他地方使用(例如页缓存),它可能会被释放回页分配器。
- 注意: alloc_pages 返回的页引用计数为1,使用完毕后通常应使用 __free_pages 或对应的 put_page 逻辑(如果页被共享或缓存)。
3. vmalloc 家族 – 针对大块的虚拟连续内存
vmalloc 用于分配大块的、在虚拟地址空间中连续,但在物理地址空间中不必连续的内存。它通过逐页映射物理上不连续的页到虚拟地址空间来实现。
- void *vmalloc(unsigned long size)
- 作用: 分配 size 字节的虚拟连续内存。内存会被清零。
- size: 请求的字节数。会被向上舍入到页大小的整数倍。
- 返回值: 成功时返回指向分配内存的内核虚拟地址,失败时返回 NULL。
- 上下文: 必须在进程上下文调用,因为它可能睡眠。
- 性能: vmalloc 的开销比 kmalloc 大,因为它需要建立和维护页表映射。访问 vmalloc 分配的内存也可能比访问 kmalloc 分配的内存慢(因为物理不连续可能导致TLB miss增加)。
- void *vzalloc(unsigned long size)
- 作用: 等同于 vmalloc(size),因为 vmalloc 默认就会清零。在较新内核中,vmalloc 内部可能调用 __vmalloc_node_flags 并传入 __GFP_ZERO。
- void *vmalloc_user(unsigned long size)
- 作用: 类似于 vmalloc,但分配的内存适合映射到用户空间。内存内容未定义(不会自动清零)。
- void *vmalloc_node(unsigned long size, int node, gfp_t gfp_mask)
- 作用: 在指定的 NUMA 节点 node 上分配虚拟连续内存。
- gfp_mask: 可以包含 __GFP_ZERO 来清零内存。
- void vfree(const void *addr)
- 作用: 释放由 vmalloc、vzalloc 或 vmalloc_node 分配的内存。
- addr: 要释放内存的起始虚拟地址。
- 使用场景:
- 当需要非常大的内存块(大于 kmalloc 的限制)时。
- 当不需要物理连续性,但需要虚拟地址连续性时(例如,内核模块加载时的某些部分)。
- 由于性能开销,应避免在性能敏感路径上频繁使用 vmalloc。
4. Per-CPU 变量 (每CPU变量)
Per-CPU 变量为系统中的每个 CPU 都提供一个独立的变量副本。这是一种避免锁和缓存争用的有效方法。
- 静态 Per-CPU 变量:
- DEFINE_PER_CPU(type, name): 定义一个静态的 Per-CPU 变量 name,类型为 type。
- DECLARE_PER_CPU(type, name): 在头文件中声明一个外部静态 Per-CPU 变量。
- 访问:
- get_cpu_var(name): 获取当前 CPU 的变量副本指针,并禁止抢占。
- put_cpu_var(name): 允许抢占,与 get_cpu_var 配对使用。
- this_cpu_ptr(per_cpu_var): 获取当前CPU的变量副本指针(不禁止抢占,需要调用者保证安全)。
- per_cpu(name, cpu_id): 获取指定 cpu_id 的变量副本(需要小心同步)。
- 动态 Per-CPU 变量:
- void __percpu *alloc_percpu(size_t size, size_t align): 动态分配 Per-CPU 数据区。返回一个特殊的 “percpu” 指针。
- void free_percpu(void __percpu *__pdata): 释放动态分配的 Per-CPU 数据。
- 访问:
- per_cpu_ptr(__pdata, cpu_id): 获取动态分配的 Per-CPU 数据在指定 cpu_id 上的指针。
- 通常与 get_cpu() 和 put_cpu() 结合使用来安全访问当前 CPU 的副本。
- 使用场景: 统计计数器、线程局部数据(内核态)、缓存等,以减少跨 CPU 同步。
5. 内存池 (mempool_t)
内存池用于预分配一定数量的内存对象,以确保在紧急情况下(如内存严重不足时)仍能分配到内存。常用于 I/O 操作,如块设备请求。
- mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, mempool_free_t *free_fn, void *pool_data)
- 作用: 创建一个内存池。
- min_nr: 池中保留的最小对象数量。
- alloc_fn: 自定义的分配函数,用于从池中分配对象(如果池为空,则尝试从系统分配)。
- free_fn: 自定义的释放函数。
- pool_data: 传递给 alloc_fn 和 free_fn 的私有数据。
- 内核也提供了基于 kmalloc 和 kmem_cache_alloc (slab缓存) 的通用内存池创建函数:
- mempool_create_kmalloc_pool(int min_nr, size_t size)
- mempool_create_slab_pool(int min_nr, struct kmem_cache *kc)
- void *mempool_alloc(mempool_t *pool, gfp_t gfp_mask)
- 作用: 从内存池中分配一个对象。如果池中有预分配的对象,则直接返回;否则,尝试使用 alloc_fn 分配。如果 gfp_mask 允许睡眠,且 alloc_fn 失败,可能会睡眠等待池中对象被释放。
- gfp_mask: 传递给底层分配函数的标志(如果需要分配新对象)。通常使用 GFP_NOIO 或 GFP_ATOMIC。
- void mempool_free(void *element, mempool_t *pool)
- 作用: 将对象 element 释放回内存池 pool。如果池中对象数量未达到 min_nr,则对象被保留在池中;否则,使用 free_fn 释放。
- void mempool_destroy(mempool_t *pool)
- 作用: 销毁内存池,释放所有预分配的对象。
- 使用场景: 必须保证分配成功的关键路径,如块设备 I/O 请求 (struct bio) 的分配。
6. DMA 内存分配
DMA (Direct Memory Access) 允许硬件设备直接读写主内存,而无需 CPU 干预。内核提供专门的 API 来分配适合 DMA 的内存区域。
- 一致性映射 (Coherent/Consistent Mappings): CPU 和设备对这块内存的视图是一致的,不需要显式的缓存同步操作。
- void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t flag)
- 作用: 分配 size 字节的 DMA 内存,该内存对 CPU 和设备 dev 都是一致的。
- dev: 指向设备的 struct device。
- dma_handle: 输出参数,返回设备可用的总线地址 (物理地址)。
- flag: GFP_KERNEL 或 GFP_ATOMIC 等。
- 返回值: CPU 可用的内核虚拟地址,失败返回 NULL。
- void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t dma_handle)
- 作用: 释放由 dma_alloc_coherent 分配的内存。参数必须与分配时一致。
- 流式映射 (Streaming Mappings): 用于临时的、单向的 DMA 传输。CPU 在传输前后可能需要显式同步缓存。
- dma_addr_t dma_map_single(struct device *dev, void *cpu_addr, size_t size, enum dma_data_direction dir)
- 作用: 将 cpu_addr 指向的、大小为 size 的已分配内存区域(如 kmalloc 分配的)映射给设备 dev 进行 DMA。
- dir: 数据传输方向 (DMA_TO_DEVICE, DMA_FROM_DEVICE, DMA_BIDIRECTIONAL)。这会影响缓存同步操作。
- 返回值: 设备可用的总线地址。
- void dma_unmap_single(struct device *dev, dma_addr_t dma_handle, size_t size, enum dma_data_direction dir)
- 作用: 取消 dma_map_single 创建的映射。
- dma_map_page(…), dma_unmap_page(…): 类似于 _single 版本,但操作对象是 struct page。
- dma_sync_single_for_cpu(…), dma_sync_single_for_device(…): 在流式映射中,用于在 CPU 访问前或设备访问前同步缓存。
- DMA 池 (dma_pool): 类似于 mempool,但用于分配小的、固定大小的、一致性的 DMA 内存块。
- struct dma_pool *dma_pool_create(const char *name, struct device *dev, size_t size, size_t align, size_t boundary)
- void *dma_pool_alloc(struct dma_pool *pool, gfp_t mem_flags, dma_addr_t *handle)
- void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t addr)
- void dma_pool_destroy(struct dma_pool *pool)
- 使用场景: 设备驱动程序中,当硬件需要直接访问内存时(例如网卡的数据包缓冲区、存储控制器的命令块)。
二、内存区域 (Memory Zones)
内核将物理内存划分为不同的区域,GFP_ 标志中的某些位(如 __GFP_DMA, __GFP_HIGHMEM)会影响从哪个区域分配内存。
- ZONE_DMA: 包含可以直接进行 DMA 操作的内存页。通常是物理内存的低端部分(例如,ISA设备只能访问前16MB)。
- ZONE_DMA32: 在64位系统上,用于32位设备可以进行 DMA 的内存区域(通常是物理内存的前4GB)。
- ZONE_NORMAL: 内核常规使用的内存区域,直接映射到内核虚拟地址空间。在32位系统上,这部分内存有限(例如,低于896MB的“低端内存”)。在64位系统上,大部分物理内存都属于 ZONE_NORMAL。
- ZONE_HIGHMEM (仅32位系统): 高端内存。物理内存中超出内核直接映射范围的部分(例如,32位系统上超过约896MB的部分)。访问高端内存页需要显式映射/取消映射到内核地址空间 (kmap/kunmap)。kmalloc 通常不分配高端内存。alloc_pages 可以。
- ZONE_MOVABLE: 用于可移动页的区域,有助于减少内存碎片,主要供用户空间使用。
三、选择内存申请接口的关键考虑因素
- 分配大小:
- 小块 (几十字节到几KB): kmalloc 家族。
- 页整数倍 (4KB 到几 MB): alloc_pages 家族。
- 大块 (几MB以上,不需要物理连续): vmalloc 家族。
- 物理连续性:
- 需要物理连续: kmalloc, alloc_pages。
- 不需要物理连续 (虚拟连续即可): vmalloc。
- 分配上下文:
- 进程上下文 (可睡眠): GFP_KERNEL (用于 kmalloc, alloc_pages),vmalloc。
- 中断/原子上下文 (不可睡眠): GFP_ATOMIC (用于 kmalloc, alloc_pages)。
- 生命周期和用途:
- 通用内核数据: kmalloc。
- 需要保证分配成功: mempool。
- 硬件DMA: DMA分配API。
- 每CPU数据: Per-CPU API。
- 是否需要清零:
- 是: kzalloc, kcalloc, get_zeroed_page, vzalloc (或 vmalloc),或使用 __GFP_ZERO 标志。
- 性能:
- kmalloc 和页分配器通常较快。
- vmalloc 开销较大。
- 错误处理:
- 所有分配函数都可能失败! 必须总是检查返回值 (NULL 或 0) 并妥善处理错误。
本文版权归原作者zhaofujian所有,采用 CC BY-NC-ND 4.0 协议进行许可,转载请注明出处。