内存分配

作者:Michael K. Johnson

Linux 内核中的内存分配非常复杂,因为其中涉及重要的约束——而且不同的内存分配方式有不同的约束。这意味着任何编写 Linux 内核代码的人都需要了解各种内存分配方式,包括其中涉及的权衡。这使得内存和 CPU 时间的使用更加高效——您可以准确指定您需要的——但也使得编程更具挑战性。

内核中基本上有五种不同的内存分配方式。这是一个善意的谎言,但对于任何需要阅读本文以了解内核内存分配的人来说,这已经足够接近真相了。其中三种(提供动态分配)通常很有用,而两种(提供静态分配)已被弃用,并且主要是历史遗留物,不应使用。我们将首先讨论有用方法的优点和局限性,并在本文末尾仅简要提及两种已弃用的方法,以便您知道要避免什么。

内存分配策略

无论您尝试进行何种形式的动态内核内存分配,都有一些规则适用。无论何时您尝试在内核空间中分配内存,您必须准备好应对分配错误。始终检查从分配函数返回的值,如果为 0,您将需要以某种方式干净利落地处理它。如果用户空间代码忽略内存分配错误,可能会因段错误而终止,但内核很容易崩溃,导致整个系统崩溃。

有几种常见的错误处理策略。一种策略是在函数顶部尝试分配关键内存,这样您不太可能陷入困境,并且更有可能干净利落地返回错误。这通常是处理此问题的最佳方法。

另一种策略,通常与在函数顶部进行分配一起使用,是为内存管理系统分配“容易”数量的内存,然后在函数生命周期内将其分配用于各种目的,从而有效地进行自己的内存管理。内核中的几个子系统都这样做,例如高级 SCSI 驱动程序和网络代码。两者都包含特殊的内存分配函数,这些函数仅应在这些子系统中使用。此处未对此进行记录,假设这些子系统的文档应记录子系统特定的内存分配例程。

另一种策略,仅在您不在代码的“关键”部分时才有效,是允许内核通过调用 schedule() 来调度另一个进程,然后在 schedule 返回时稍后重试。请注意,某些类型的分配即使在关键代码中调用一次也是不安全的;这将在我们讨论各个函数时介绍。

基本规则是不编写在未保证完成所需的资源的情况下就致力于完成的算法。内存是最稀缺且最常用的必须保证的资源之一,而保证内存可用的唯一方法是分配它。

Kmalloc

kmalloc() 函数在两个级别分配内存:它使用“bucket”系统分配长度接近一页(在 i86 上为 4Kb)的内存单元,并在不同大小的连续内存块列表上使用“buddy”系统分配长度高达 128Kb(在 i86 上)的内存单元。仅在最近的内核中,它才能够分配长度超过 4Kb 的内存单元,并且使用 kmalloc 分配大量内存很可能失败,尤其是在低内存情况下,尤其是在内存较少的机器上。

Kmalloc 非常灵活,正如其调用约定所证明的那样

void * kmalloc(unsigned int size, int priority);

请注意 priority 参数:这就是使 kmalloc 如此灵活的原因;可以在非常受限的情况下使用 kmalloc,例如从中断处理程序中使用。中断驱动的代码或无法被抢占但仍需要分配内存的代码可以使用 GFP_ATOMIC 优先级调用 kmalloc。这更可能失败,因为它无法交换或执行任何其他可能导致隐式或显式 I/O 的操作。具有宽松要求的代码,可以合法地被抢占,则应使用 GFP_KERNEL 优先级调用 kmalloc。这可能会导致分页并可能导致调用 schedule(),但成功的机会更高。

为了动态分配可以通过 DMA 访问的内存,应使用 GFP_DMA 优先级。它确实会给内存系统带来压力,特别是当请求大量内存时,并且很可能失败。请重试。应该注意的是,GFP_DMA 仅在 DMA 传输严重受限的系统上才可能失败——例如使用通用 ISA 总线的计算机。并非所有平台都受到此问题的影响。

使用 kmalloc() 分配的内存使用 kfree()(或 kfree_s())释放。

Vmalloc

对于分配大面积的虚拟连续内存,这些内存不必是物理连续的,以便与硬件接口,新的 vmalloc() 函数(具有与传统 malloc() 相同的调用约定)将对内存子系统造成的压力较小。它分配可能不连续的空闲内存块,并将它们映射到高内存中的一个连续空间中。在许多情况下,它比 kmalloc 更不容易失败。它不接受像 kmalloc 那样的优先级。不能从中断中调用它,并且它可能隐式地导致抢占发生。

使用 vmalloc 分配的内存不可 DMA,即使在没有 DMA 限制的系统上也是如此,因为 Linux 下的 DMA 假定 1-1 逻辑-物理页面映射。这在几个方面简化了内存管理,并且不是一个严重的限制,因为 kmalloc 提供了一种获取 DMA 功能内存的方法。

仅仅因为它是在虚拟地址空间中寻址的,并不意味着该内存会受到分页到磁盘的影响,尽管有相反的传言。“vmalloc”中的“虚拟”仅指寻址,这与内核的其余部分不同,它不是从虚拟地址空间到物理地址空间的 1-1 映射。可能会启动交换以在调用 vmalloc() 期间提供内存,但随后 vmalloc 分配的内存不会被换出。

使用 vmalloc() 分配的内存使用 vfree() 释放。

get_free_pages

现在我们了解上面的 GFP 代表什么:get_free_page(或者,也许是 __get_free_pages),它只是指定此函数如何尝试查找空闲内存页。正如您可能猜到的,相同的 GFP_* 值也用于这些函数。

这是请求内存子系统容易分配的内存量的方法。这是动态分配内存的最低级别——因此也是开销最低的方式。如果您需要一块大于半页但小于一页的内存(在决定这一点时,请注意页面大小因架构而异;例如,在 i86 上为 4Kb,在 DEC Alpha 上为 8Kb),特别是如果您只需要在当前过程的持续时间内使用它,那么这可能是正确的方法。此外,如果您正在处理子系统特定的内存管理,您几乎肯定希望以这种方式分配内存。

如果您只需要一页,请调用 get_free_page(priority),其中 priorityGFP_* 值之一。当然,关于哪个 GFP_* 值是正确的规则与 kmalloc 相同。如果您只需要一页并且不介意它是否已被清除(设置为所有零值),请改用 __get_free_page(priority),因为使用 get_free_page 分配页的大部分开销都用于清除页面。

如果您需要分配多个连续页,您可以这样做,尽管这比分配单页更可能失败,并且您希望分配的页数越多,您成功的可能性就越小。您只能分配页数为 2 的幂。__get_free_pages(priority, order) 使用相同的 priority 参数调用;order 参数根据以下公式给出大小:PAGE_SIZE<\!s>*<\!s>2<+>order<+> 因此,order 为 0 时给出一页,为 1 时给出两页,为 2 时给出四页,依此类推(至少在当前内核中)最多为 5,这给出 32 页,在 i86 架构上为 128Kb。正如您可能猜到的,PAGE_SIZE 是页面中字节数的标准宏。

__get_dma_pages() 函数的工作方式与 __get_free_pages() 完全相同,不同之处在于它分配的页面能够用于 DMA,并且它给内存分配系统带来更大的压力。

使用 get_free_page()__get_free_page() 分配的页面使用 free_page() 释放,而使用 __get_free_pages()__get_dma_pages() 分配的页面使用 free_pages() 释放。

设备初始化

现在我们来讨论已弃用的策略。它们在某些情况下可能有用,主要是在它们是使驱动程序工作的“简单方法”的情况下。在这些情况下,最好最终找到另一种方法来做同样的事情,因为这两种策略都不太灵活。这两种策略都不适用于可加载模块。

当编译到内核中的设备初始化时,会向其传递一个指向可用内存的指针。然后需要返回一个指针。如果它返回的指针高于它获得的指针,则两个指针之间的内存将保留给设备。该内存将位于内存的前几个兆字节中。确切位置将取决于内核的启动方式。这(可能不幸的是...)记录在Linux Kernel Hackers' Guide中。

一旦分配,该内存就无法释放。

内存初始化

这种特殊方法已被严重弃用,并且也依赖于架构。可以在 mem_init() 的主体中添加函数调用,对于 i86 平台,该函数位于文件 arch/i386/mm/init.c 中。在此函数的中间,提供了两个用于初始化 SCSI 和声卡驱动程序内存的函数。此外,arch/i386/kernel/head.S 提供了另一种平台相关的内存分配方式。这是设置初始内存管理的地方。

如果您对这些有足够的了解以至于可以随意修改它们,那么您不需要我的帮助。这些是内存分配的最后手段,在考虑这些“hack”之前,您需要确切地知道您需要做什么,以及为什么动态分配策略对您不起作用。

Michael K. JohnsonLinux Journal 的编辑,并在业余时间假装自己是 Linux 大师。可以通过电子邮件 johnsonm@ssc.com 与他联系。

加载 Disqus 评论