内存分配指南¶
Linux 提供了多种用于内存分配的 API。你可以使用 kmalloc 或 kmem_cache_alloc 系列函数分配小块内存,使用 vmalloc 及其派生函数分配大块虚拟连续区域,或者直接使用 alloc_pages 从页分配器请求页面。也可以使用更专门的分配器,例如 cma_alloc 或 zs_malloc。
大多数内存分配 API 使用 GFP 标志来表示内存应如何分配。GFP 缩写代表 “get free pages”(获取空闲页面),这是底层的内存分配函数。
分配 API 的多样性以及众多的 GFP 标志使得“我应该如何分配内存?”这个问题并不容易回答,尽管很可能你应该使用
kzalloc(<size>, GFP_KERNEL);
当然,在某些情况下,必须使用其他分配 API 和不同的 GFP 标志。
Get Free Page 标志¶
GFP 标志控制分配器的行为。它们告诉分配器可以使用哪些内存区域,分配器应该如何努力寻找空闲内存,内存是否可以被用户空间访问等。Documentation/core-api/mm-api.rst 提供了 GFP 标志及其组合的参考文档,这里我们简要概述它们的推荐用法。
大多数时候,
GFP_KERNEL
是你所需要的。内核数据结构、可 DMA 内存、inode 缓存以及许多其他分配类型都可以使用GFP_KERNEL
。请注意,使用GFP_KERNEL
意味着GFP_RECLAIM
,这意味着在内存压力下可能会触发直接回收;调用上下文必须允许睡眠。如果分配是从原子上下文(例如中断处理程序)执行的,请使用
GFP_NOWAIT
。此标志会阻止直接回收以及 I/O 或文件系统操作。因此,在内存压力下,GFP_NOWAIT
分配很可能失败。此标志的用户需要提供合适的备用方案,以应对此类故障(如适用)。如果你认为访问内存储备是合理的,并且除非分配成功,否则内核将面临压力,你可以使用
GFP_ATOMIC
。从用户空间触发的不可信分配应受 kmem 记账约束,并且必须设置
__GFP_ACCOUNT
位。对于需要记账的GFP_KERNEL
分配,有一个方便的GFP_KERNEL_ACCOUNT
快捷方式。用户空间分配应使用
GFP_USER
、GFP_HIGHUSER
或GFP_HIGHUSER_MOVABLE
标志中的任意一个。标志名称越长,其限制越少。
GFP_HIGHUSER_MOVABLE
不要求分配的内存能被内核直接访问,并且意味着数据是可移动的。
GFP_HIGHUSER
意味着分配的内存不可移动,但不需要被内核直接访问。一个例子可能是直接将数据映射到用户空间但没有寻址限制的硬件分配。
GFP_USER
意味着分配的内存不可移动,并且必须被内核直接访问。
你可能会注意到现有代码中有很多分配指定了 GFP_NOIO
或 GFP_NOFS
。历史上,它们用于防止由直接内存回收调用回 FS 或 IO 路径并阻塞已持有的资源所导致的递归死锁。自 4.12 版本以来,解决此问题的首选方法是使用 Documentation/core-api/gfp_mask-from-fs-io.rst 中描述的新作用域 API。
其他遗留的 GFP 标志是 GFP_DMA
和 GFP_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 killer 的受害者目前允许失败)。
GFP_KERNEL | __GFP_NORETRY
- 覆盖默认分配器行为,所有分配请求都会尽早失败,而不是导致破坏性回收(此实现中为一轮回收)。OOM killer 不会被调用。
GFP_KERNEL | __GFP_RETRY_MAYFAIL
- 覆盖默认分配器行为,所有分配请求会非常努力地尝试。如果回收无法取得任何进展,请求将失败。OOM killer 不会被触发。
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 分配的对象可以使用 kfree 或 kvfree 释放。通过 kmem_cache_alloc 分配的对象可以使用 kmem_cache_free、kfree 或 kvfree 释放,其中后两者可能更方便,因为不需要 kmem_cache 指针。
相同的规则适用于释放函数的 _bulk 和 _rcu 变体。
通过 vmalloc 分配的内存可以使用 vfree 或 kvfree 释放。通过 kvmalloc 分配的内存可以使用 kvfree 释放。通过 kmem_cache_create 创建的缓存应在首先释放所有分配的对象后,才能使用 kmem_cache_destroy 释放。