Linux 内核内存申请相关的接口详解

一、核心内存申请接口分类

内核内存申请接口可以根据分配的内存大小、连续性、上下文以及用途等进行分类。

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: 用于可移动页的区域,有助于减少内存碎片,主要供用户空间使用。

三、选择内存申请接口的关键考虑因素

  1. 分配大小:
  • 小块 (几十字节到几KB): kmalloc 家族。
  • 页整数倍 (4KB 到几 MB): alloc_pages 家族。
  • 大块 (几MB以上,不需要物理连续): vmalloc 家族。
  1. 物理连续性:
  • 需要物理连续: kmalloc, alloc_pages。
  • 不需要物理连续 (虚拟连续即可): vmalloc。
  1. 分配上下文:
  • 进程上下文 (可睡眠): GFP_KERNEL (用于 kmalloc, alloc_pages),vmalloc。
  • 中断/原子上下文 (不可睡眠): GFP_ATOMIC (用于 kmalloc, alloc_pages)。
  1. 生命周期和用途:
  • 通用内核数据: kmalloc。
  • 需要保证分配成功: mempool。
  • 硬件DMA: DMA分配API。
  • 每CPU数据: Per-CPU API。
  1. 是否需要清零:
  • 是: kzalloc, kcalloc, get_zeroed_page, vzalloc (或 vmalloc),或使用 __GFP_ZERO 标志。
  1. 性能:
  • kmalloc 和页分配器通常较快。
  • vmalloc 开销较大。
  1. 错误处理:
  • 所有分配函数都可能失败! 必须总是检查返回值 (NULL 或 0) 并妥善处理错误。

本文版权归原作者zhaofujian所有,采用 CC BY-NC-ND 4.0 协议进行许可,转载请注明出处。

发表评论