内存分配指南 【ChatGPT】

内存分配指南

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标志。

获取空闲页标志

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

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

  • 如果分配是从原子上下文执行的,例如中断处理程序,请使用GFP_NOWAIT。此标志会阻止直接回收和IO或文件系统操作。因此,在内存压力下,GFP_NOWAIT分配可能会失败。具有合理回退的分配应该使用GFP_NOWARN。

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

  • 从用户空间触发的不受信任的分配应该受到kmem记账的约束,并且必须设置__GFP_ACCOUNT位。有一个方便的GFP_KERNEL_ACCOUNT快捷方式,用于应该记账的GFP_KERNEL分配。

  • 用户空间分配应该使用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的幂的大小,也保证至少对齐到相应的大小。

使用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释放。

posted @ 2023-12-09 16:04  摩斯电码  阅读(38)  评论(0编辑  收藏  举报