内存分配指南

Linux 提供了多种内存分配 API。您可以使用 kmallockmem_cache_alloc 系列分配小块内存,使用 vmalloc 及其衍生品分配大的虚拟连续区域,或者可以使用 alloc_pages 直接从页面分配器请求页面。也可以使用更专门的分配器,例如 cma_alloczs_malloc

大多数内存分配 API 都使用 GFP 标志来表达应如何分配内存。GFP 缩写代表“获取空闲页面”,这是底层的内存分配函数。

分配 API 的多样性加上众多的 GFP 标志使得“我应该如何分配内存?”这个问题不容易回答,尽管很可能您应该使用

kzalloc(<size>, GFP_KERNEL);

当然,在某些情况下,必须使用其他分配 API 和不同的 GFP 标志。

获取空闲页面标志

GFP 标志控制分配器的行为。它们告知可以使用哪些内存区域,分配器应尝试多努力地查找空闲内存,该内存是否可由用户空间访问等。Documentation/core-api/mm-api.rst 提供了 GFP 标志及其组合的参考文档,这里我们简要概述其推荐用法

  • 大多数情况下,您需要的是 GFP_KERNEL。内核数据结构、可 DMA 内存、inode 缓存以及所有这些和其他许多分配类型都可以使用 GFP_KERNEL。请注意,使用 GFP_KERNEL 意味着 GFP_RECLAIM,这意味着在内存压力下可能会触发直接回收;调用上下文必须允许睡眠。

  • 如果分配是从原子上下文执行的,例如中断处理程序,请使用 GFP_NOWAIT。此标志可防止直接回收以及 IO 或文件系统操作。因此,在内存压力下,GFP_NOWAIT 分配很可能失败。此标志的用户需要提供适当的回退来应对适当的此类故障。

  • 如果您认为访问内存保留是合理的,并且除非分配成功,否则内核会受到压力,则可以使用 GFP_ATOMIC

  • 从用户空间触发的不可信分配应接受 kmem 记帐,并且必须设置 __GFP_ACCOUNT 位。对于应记帐的 GFP_KERNEL 分配,有一个方便的快捷方式 GFP_KERNEL_ACCOUNT

  • 用户空间分配应使用 GFP_USERGFP_HIGHUSERGFP_HIGHUSER_MOVABLE 标志之一。标志名称越长,其限制性就越小。

    GFP_HIGHUSER_MOVABLE 不要求内核直接访问分配的内存,并表示该数据是可移动的。

    GFP_HIGHUSER 表示分配的内存不可移动,但不需要内核直接访问。例如,硬件分配可以直接将数据映射到用户空间,但没有寻址限制。

    GFP_USER 表示分配的内存不可移动,并且必须可由内核直接访问。

您可能会注意到,现有代码中的许多分配指定了 GFP_NOIOGFP_NOFS。从历史上看,它们用于防止直接内存回收调用回 FS 或 IO 路径并阻止已持有的资源而导致的递归死锁。自从 4.12 以来,解决此问题的首选方法是使用 Documentation/core-api/gfp_mask-from-fs-io.rst 中描述的新作用域 API。

其他旧式 GFP 标志是 GFP_DMAGFP_DMA32。它们用于确保硬件可以访问具有有限寻址能力的已分配内存。因此,除非您正在为具有此类限制的设备编写驱动程序,否则请避免使用这些标志。即使对于具有限制的硬件,最好还是使用 dma_alloc* API。

GFP 标志和回收行为

内存分配可能会触发直接或后台回收,并且了解页面分配器将尝试多努力地满足该请求或另一个请求是很有用的。

  • GFP_KERNEL & ~__GFP_RECLAIM - 无任何尝试释放内存的乐观分配。最轻量级的模式,甚至不会启动后台回收。应谨慎使用,因为它可能会耗尽内存,并且下一个用户可能会遇到更积极的回收。

  • GFP_KERNEL & ~__GFP_DIRECT_RECLAIM (或 GFP_NOWAIT) - 无任何尝试从当前上下文释放内存的乐观分配,但如果该区域低于低水位线,则可以唤醒 kswapd 以回收内存。可以从原子上下文中使用,也可以在请求是性能优化并且慢速路径有另一个回退时使用。

  • (GFP_KERNEL|__GFP_HIGH) & ~__GFP_DIRECT_RECLAIM (又名 GFP_ATOMIC) - 具有昂贵回退的非睡眠分配,因此它可以访问部分内存保留。通常从中断/下半部上下文中使用,并具有昂贵的慢速路径回退。

  • GFP_KERNEL - 允许后台回收和直接回收,并使用默认页面分配器行为。这意味着无成本的分配请求基本上不会失败,但是不能保证该行为,因此调用方必须正确检查失败(例如,允许 OOM 杀手受害者目前失败)。

  • GFP_KERNEL | __GFP_NORETRY - 覆盖默认的分配器行为,并且所有分配请求都会尽早失败,而不是导致破坏性的回收(在此实现中为一轮回收)。不会调用 OOM 杀手。

  • GFP_KERNEL | __GFP_RETRY_MAYFAIL - 覆盖默认的分配器行为,并且所有分配请求都会非常努力地尝试。如果回收无法取得任何进展,则请求将失败。不会触发 OOM 杀手。

  • GFP_KERNEL | __GFP_NOFAIL - 覆盖默认的分配器行为,并且所有分配请求都会无限循环,直到它们成功为止。这可能非常危险,特别是对于较大的订单。

选择内存分配器

分配内存的最直接方法是使用 kmalloc() 系列中的函数。为了安全起见,最好使用将内存设置为零的例程,例如 kzalloc()。如果需要为数组分配内存,则可以使用 kmalloc_array()kcalloc() 助手。可以使用助手 struct_size()array_size()array3_size() 来安全地计算对象大小而不会溢出。

可以使用 kmalloc 分配的块的最大大小是有限的。实际限制取决于硬件和内核配置,但是对于小于页面大小的对象使用 kmalloc 是一种好习惯。

使用 kmalloc 分配的内存块的地址至少按照 ARCH_KMALLOC_MINALIGN 字节对齐。对于大小为 2 的幂的内存块,其对齐方式也保证至少为相应的大小。对于其他大小的内存块,其对齐方式保证至少为该大小的最大 2 的幂的除数。

使用 kmalloc() 分配的内存块可以使用 krealloc() 调整大小。类似于 kmalloc_array():提供了用于调整数组大小的辅助函数 krealloc_array()

对于较大的分配,可以使用 vmalloc() 和 vzalloc(),或者直接从页面分配器请求页面。vmalloc 及相关函数分配的内存并非物理上连续的。

如果不确定分配的大小对于 kmalloc 是否过大,可以使用 kvmalloc() 及其派生函数。它会尝试使用 kmalloc 分配内存,如果分配失败,则会使用 vmalloc 重试。对可以与 kvmalloc 一起使用的 GFP 标志有限制;请参阅 kvmalloc_node() 参考文档。请注意,kvmalloc 可能会返回物理上不连续的内存。

如果需要分配许多相同的对象,可以使用 slab 缓存分配器。在使用之前,应该使用 kmem_cache_create()kmem_cache_create_usercopy() 设置缓存。如果缓存的一部分可能被复制到用户空间,则应使用第二个函数。创建缓存后,可以使用 kmem_cache_alloc() 及其便捷的封装器从该缓存中分配内存。

当不再需要分配的内存时,必须将其释放。

使用 kmalloc 分配的对象可以通过 kfreekvfree 释放。使用 kmem_cache_alloc 分配的对象可以使用 kmem_cache_freekfreekvfree 释放,其中后两者可能更方便,因为不需要 kmem_cache 指针。

相同的规则适用于释放函数的 _bulk 和 _rcu 版本。

使用 vmalloc 分配的内存可以使用 vfreekvfree 释放。使用 kvmalloc 分配的内存可以使用 kvfree 释放。使用 kmem_cache_create 创建的缓存只能在释放所有已分配的对象后才能使用 kmem_cache_destroy 释放。