内核角落 - 在内核中分配内存
对于内核开发者来说不幸的是,在内核中分配内存不像在用户空间中分配内存那么简单。许多因素导致了复杂性,其中包括:
内核被限制在约 1GB 的虚拟和物理内存。
内核的内存不可分页。
内核通常需要物理上连续的内存。
通常,内核必须在不休眠的情况下分配内存。
内核中的错误比其他地方的错误代价更高。
在内核内部分配内存的通用接口是 kmalloc()
#include <linux/slab.h> void * kmalloc(size_t size, int flags);
它应该看起来很熟悉——毕竟它与用户空间的 malloc() 非常相似——除了它接受第二个参数,flags。让我们暂时忽略 flags,看看我们认识什么。首先,size在这里与 malloc() 中的相同——它指定了分配的大小,以字节为单位。成功返回后,kmalloc() 返回一个指向 size 字节内存的指针。分配的内存的对齐方式适合存储和访问任何类型的对象。与 malloc() 一样,kmalloc() 可能会失败,您必须检查其返回值是否为 NULL。让我们看一个例子:
struct falcon *p; p = kmalloc(sizeof (struct falcon), GFP_KERNEL); if (!p) /* the allocation failed - handle appropriately */
flags 字段控制内存分配的行为。我们可以将 flags 分为三组:动作修饰符、区域修饰符和类型。动作修饰符告诉内核如何分配内存。例如,它们指定内核是否可以休眠(即,调用 kmalloc() 是否可以阻塞)以满足分配请求。另一方面,区域修饰符告诉内核应该从哪里满足请求。例如,某些请求可能需要从硬件可以通过直接内存访问 (DMA) 访问的内存中满足。最后,类型标志指定分配的类型。它们将相关的动作和区域修饰符组合成一个助记符。通常,您应该指定单个类型标志,而不是指定多个动作和区域修饰符。
表 1 列出了动作修饰符,表 2 列出了区域修饰符。可以使用许多不同的标志;在内核中分配内存并非易事。可以控制内核中内存分配的许多方面。您的代码应该使用类型标志,而不是单独的动作和区域修饰符。最常见的两个标志是 GFP_ATOMIC 和 GFP_KERNEL。几乎所有内核内存分配都应指定这两个标志之一。
表 1. 动作修饰符
标志 | 描述 |
---|---|
__GFP_COLD | 内核应使用缓存冷页。 |
__GFP_FS | 内核可以启动文件系统 I/O。 |
__GFP_HIGH | 内核可以访问紧急池。 |
__GFP_IO | 内核可以启动磁盘 I/O。 |
__GFP_NOFAIL | 内核可以重复分配。 |
__GFP_NORETRY | 如果分配失败,内核不会重试。 |
__GFP_NOWARN | 内核不打印失败警告。 |
__GFP_REPEAT | 如果分配失败,内核会重复分配。 |
__GFP_WAIT | 内核可以休眠。 |
GFP_ATOMIC 标志指示内存分配器永远不要阻塞。在无法休眠的情况下使用此标志——必须保持原子性——例如中断处理程序、下半部和持有锁的进程上下文代码。由于内核无法阻塞分配并尝试释放足够的内存来满足请求,因此指定 GFP_ATOMIC 的分配比不指定的分配成功机会更小。尽管如此,如果您的当前上下文无法休眠,这是您唯一的选择。使用 GFP_ATOMIC 很简单:
struct wolf *p; p = kmalloc(sizeof (struct wolf), GFP_ATOMIC); if (!p) /* error */
相反,GFP_KERNEL 标志指定正常的内核分配。在进程上下文中执行且不持有任何锁的代码中使用此标志。使用此标志调用 kmalloc() 可以休眠;因此,您必须仅在可以安全地这样做时才使用此标志。内核利用休眠能力来释放内存(如果需要)。因此,指定此标志的分配具有更大的成功机会。例如,如果可用内存不足,内核可以阻塞请求代码并将一些非活动页面交换到磁盘、缩小内存缓存、写出缓冲区等等。
有时,在编写 ISA 设备驱动程序时,您需要确保分配的内存能够进行 DMA。对于 ISA 设备,这是物理内存的前 16MB 中的内存。为了确保内核从这个特定内存中分配,请使用 GFP_DMA 标志。通常,您会将此标志与 GFP_ATOMIC 或 GFP_KERNEL 结合使用;您可以使用二进制 OR 运算组合标志。例如,要指示内核分配支持 DMA 的内存并在需要时休眠,请执行:
char *buf; /* we want DMA-capable memory, * and we can sleep if needed */ buf = kmalloc(BUF_LEN, GFP_DMA | GFP_KERNEL); if (!buf) /* error */
表 3 列出了类型标志,表 4 显示了每个动作和区域修饰符等同于哪个类型标志。头文件 <linux/gfp.h> 定义了所有标志。
表 3. 类型
标志 | 描述 |
---|---|
GFP_ATOMIC | 分配是高优先级的,并且不休眠。这是在中断处理程序、下半部和其他无法休眠的情况下使用的标志。 |
GFP_DMA | 这是分配支持 DMA 的内存。需要支持 DMA 的内存的设备驱动程序使用此标志。 |
GFP_KERNEL | 这是一个正常的分配,可能会阻塞。这是在进程上下文代码中,当可以安全休眠时使用的标志。 |
GFP_NOFS | 此分配可能会阻塞并可能启动磁盘 I/O,但它不会启动文件系统操作。这是在文件系统代码中,当您无法启动另一个文件系统操作时使用的标志。 |
GFP_NOIO | 此分配可能会阻塞,但它不会启动块 I/O。这是在块层代码中,当您无法启动更多块 I/O 时使用的标志。 |
GFP_USER | 这是一个正常的分配,可能会阻塞。此标志用于为用户空间进程分配内存。 |
当您完成访问通过 kmalloc() 分配的内存时,您必须将其返回给内核。这项工作是使用 kfree() 完成的,它是用户空间的 free() 库调用的对应物。kfree() 的原型是:
#include <linux/slab.h> void kfree(const void *objp);
kfree() 的用法与用户空间变体相同。假设 p 是指向通过 kmalloc() 获取的内存块的指针。那么,以下命令将释放该块并将内存返回给内核:
kfree(p);
与用户空间中的 free() 一样,在已释放的内存块上或在不是从 kmalloc() 返回的地址的指针上调用 kfree() 是一个错误,它可能导致内存损坏。始终平衡分配和释放,以确保在正确的指针上恰好调用一次 kfree()。显式检查在 NULL 上调用 kfree() 是安全的,尽管这不一定是一个明智的主意。
让我们看一下完整的分配和释放周期:
struct sausage *s; s = kmalloc(sizeof (struct sausage), GFP_KERNEL); if (!s) return -ENOMEM; /* ... */ kfree(s);
kmalloc() 函数返回物理上和因此虚拟上连续的内存。这与用户空间的 malloc() 函数形成对比,后者返回虚拟上但不一定是物理上连续的内存。物理上连续的内存有两个主要优点。首先,许多硬件设备无法寻址虚拟内存。因此,为了使它们能够访问内存块,该块必须作为物理上连续的内存块存在。其次,物理上连续的内存块可以使用单个大页面映射。这最大限度地减少了寻址内存的转换后备缓冲区 (TLB) 开销,因为只需要单个 TLB 条目。
分配物理上连续的内存有一个缺点:通常很难找到物理上连续的内存块,特别是对于大型分配。分配仅在虚拟上连续的内存具有更大的成功机会。如果您不需要物理上连续的内存,请使用 vmalloc()
#include <linux/vmalloc.h> void * vmalloc(unsigned long size);
然后,您可以使用 vfree() 将使用 vmalloc() 获取的内存返回给系统:
#include <linux/vmalloc.h> void vfree(void *addr);
再次强调,vfree() 的用法与用户空间的 malloc() 和 free() 函数相同:
struct black_bear *p; p = vmalloc(sizeof (struct black_bear)); if (!p) /* error */ /* ... */ vfree(p);
在这种特殊情况下,vmalloc() 可能会休眠。
内核中的许多分配都可以使用 vmalloc(),因为很少有分配需要对硬件设备显得连续。如果您要分配仅软件访问的内存,例如与用户进程关联的数据,则无需使内存物理上连续。尽管如此,内核中很少有分配使用 vmalloc()。大多数选择使用 kmalloc(),即使不需要,部分是出于历史原因,部分是出于性能原因。由于物理上连续页面的 TLB 开销大大降低,因此性能提升通常受到高度赞赏。尽管如此,如果您需要在内核中分配数千万兆字节的内存,vmalloc() 是您的最佳选择。
与用户空间进程不同,在内核中执行的代码既没有大的堆栈,也没有动态增长的堆栈。相反,内核中的每个进程都有一个小的固定大小的堆栈。堆栈的确切大小取决于架构。大多数架构为堆栈分配两个页面,因此在 32 位机器上堆栈为 8KB。
由于堆栈很小,因此不鼓励进行大型、自动和在堆栈上的分配。实际上,您永远不应该在内核代码中看到这样的内容:
#define BUF_LEN 2048 void rabbit_function(void) { char buf[BUF_LEN]; /* ... */ }
相反,以下是首选的:
#define BUF_LEN 2048 void rabbit_function(void) { char *buf; buf = kmalloc(BUF_LEN, GFP_KERNEL); if (!buf) /* error! */ /* ... */ }
您也很少在用户空间中看到这种堆栈的等效项,因为当您在编写代码时知道分配大小时,几乎没有理由执行动态内存分配。但是,在内核中,只要分配大小大于几个字节左右,就应该使用动态内存。这有助于防止堆栈溢出,这会破坏每个人的心情。
稍微理解一下,在内核中获取内存的过程就被揭开了神秘面纱,而且做起来并不比在用户空间中困难多少。一些简单的经验法则可以大有帮助:
确定您是否可以休眠(即,调用 kmalloc() 是否可以阻塞)。如果您在中断处理程序、下半部中,或者如果您持有锁,则您不能休眠。如果您在进程上下文中并且不持有锁,则您可能可以休眠。
如果您可以休眠,请指定 GFP_KERNEL。
如果您无法休眠,请指定 GFP_ATOMIC。
如果您需要支持 DMA 的内存(例如,对于 ISA 或损坏的 PCI 设备),请指定 GFP_DMA。
始终检查并处理来自 kmalloc() 的 NULL 返回值。
不要泄漏内存;确保您在某处调用 kfree()。
确保您不会发生竞争并多次调用 kfree(),并且永远不要在释放内存块后访问它。
Robert Love (rml@tech9.net) 是 MontaVista Software 的内核黑客,也是佛罗里达大学的学生。他是 Linux 内核开发 一书的作者。Robert 喜欢美酒,住在佛罗里达州盖恩斯维尔。