未对齐的内存访问¶
- 作者:
Daniel Drake <dsd@gentoo.org>,
- 作者:
Johannes Berg <johannes@sipsolutions.net>
- 感谢以下人员的帮助:
Alan Cox, Avuton Olrich, Heikki Orsila, Jan Engelhardt, Kyle McMartin, Kyle Moffett, Randy Dunlap, Robert Hancock, Uli Kunitz, Vadim Lobanov
Linux 运行在各种架构上,这些架构在内存访问方面具有不同的行为。本文档介绍了关于未对齐访问的一些细节,为什么您需要编写不会导致它们的代码,以及如何编写这样的代码!
未对齐访问的定义¶
当您尝试从一个不能被 N 整除的地址(即 addr % N != 0)开始读取 N 个字节的数据时,会发生未对齐的内存访问。例如,从地址 0x10004 读取 4 个字节的数据是可以的,但是从地址 0x10005 读取 4 个字节的数据将是未对齐的内存访问。
上面的内容可能有点模糊,因为内存访问可以以不同的方式发生。这里的上下文是在机器代码级别:某些指令读取或写入一定数量的字节到内存或从内存中读取(例如,x86 汇编中的 movb、movw、movl)。正如将要清楚地看到的那样,很容易发现 C 语句会编译为多字节内存访问指令,即在处理 u16、u32 和 u64 等类型时。
自然对齐¶
上面提到的规则构成了我们所说的自然对齐:当访问 N 个字节的内存时,基址必须可以被 N 整除,即 addr % N == 0。
在编写代码时,假设目标架构具有自然对齐要求。
实际上,只有少数架构要求对所有大小的内存访问进行自然对齐。但是,我们必须考虑所有支持的架构;编写满足自然对齐要求的代码是实现完全可移植性的最简单方法。
为什么未对齐访问不好¶
执行未对齐的内存访问的效果因架构而异。在这里写一篇关于差异的完整文档很容易;下面总结了常见的情况
一些架构能够透明地执行未对齐的内存访问,但通常会产生显著的性能成本。
当发生未对齐的访问时,一些架构会引发处理器异常。异常处理程序能够纠正未对齐的访问,但这会大大降低性能。
当发生未对齐的访问时,一些架构会引发处理器异常,但异常不包含足够的信息来纠正未对齐的访问。
一些架构不能进行未对齐的内存访问,但会静默地执行与请求不同的内存访问,从而导致难以检测的细微代码错误!
从上面应该很明显,如果您的代码导致发生未对齐的内存访问,您的代码在某些平台上将无法正常工作,并且会在其他平台上导致性能问题。
不会导致未对齐访问的代码¶
首先,上面的概念可能看起来有点难以与实际编码实践联系起来。毕竟,您无法很好地控制某些变量的内存地址等。
幸运的是,事情并不太复杂,因为在大多数情况下,编译器会确保一切正常工作。例如,采用以下结构
struct foo {
u16 field1;
u32 field2;
u8 field3;
};
让我们假设上述结构的实例驻留在从地址 0x10000 开始的内存中。在基本理解的程度上,期望访问 field2 会导致未对齐的访问并非不合理。您会期望 field2 位于结构中的偏移量为 2 个字节处,即地址 0x10002,但该地址不能被 4 整除(请记住,我们在这里读取的是 4 个字节的值)。
幸运的是,编译器理解对齐约束,因此在上述情况下,它会在 field1 和 field2 之间插入 2 个字节的填充。因此,对于标准结构类型,您可以始终依赖编译器来填充结构,以便对字段的访问进行适当的对齐(假设您没有将字段强制转换为不同长度的类型)。
同样,您也可以依赖编译器来根据变量类型的大小,将变量和函数参数对齐到自然对齐方案。
此时,应该清楚的是,访问单个字节(u8 或 char)永远不会导致未对齐的访问,因为所有内存地址都可以被 1 整除。
在相关主题上,考虑到上述考虑因素,您可能会注意到,您可以重新排序结构中的字段,以便将字段放置在否则会插入填充的位置,从而减少结构实例的整体驻留内存大小。上述示例的最佳布局是
struct foo {
u32 field2;
u16 field1;
u8 field3;
};
对于自然对齐方案,编译器只需在结构末尾添加一个字节的填充。添加此填充是为了满足这些结构数组的对齐约束。
另一个值得一提的点是在结构类型上使用 __attribute__((packed))。这个特定于 GCC 的属性告诉编译器永远不要在结构中插入任何填充,当您想使用 C 结构来表示以固定排列“在导线上”出现的一些数据时,这非常有用。
您可能倾向于认为,当访问不满足架构对齐要求的字段时,使用此属性很容易导致未对齐的访问。但是,编译器再次意识到对齐约束,并且会生成额外的指令以执行内存访问,而不会导致未对齐的访问。当然,与非打包情况相比,额外的指令显然会导致性能损失,因此只有在避免结构填充很重要时才应使用 packed 属性。
导致未对齐访问的代码¶
考虑到以上内容,让我们转到一个可能导致未对齐内存访问的函数的真实示例。以下函数取自 include/linux/etherdevice.h,是一个优化的例程,用于比较两个以太网 MAC 地址是否相等
bool ether_addr_equal(const u8 *addr1, const u8 *addr2)
{
#ifdef CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS
u32 fold = ((*(const u32 *)addr1) ^ (*(const u32 *)addr2)) |
((*(const u16 *)(addr1 + 4)) ^ (*(const u16 *)(addr2 + 4)));
return fold == 0;
#else
const u16 *a = (const u16 *)addr1;
const u16 *b = (const u16 *)addr2;
return ((a[0] ^ b[0]) | (a[1] ^ b[1]) | (a[2] ^ b[2])) == 0;
#endif
}
在上面的函数中,当硬件具有高效的未对齐访问能力时,此代码没有问题。但是,当硬件无法访问任意边界上的内存时,对 a[0] 的引用会导致从地址 addr1 开始从内存中读取 2 个字节(16 位)。
想想如果 addr1 是一个奇数地址(例如 0x10003)会发生什么。(提示:这将是未对齐的访问。)
尽管上述函数存在潜在的未对齐访问问题,但它仍然包含在内核中,但被理解为仅在 16 位对齐的地址上正常工作。由调用者确保此对齐或根本不使用此函数。此对齐不安全的函数仍然有用,因为当您可以确保对齐时,它是对以太网网络上下文中几乎所有时间都是如此的一种体面的优化。
这是另一个可能导致未对齐访问的代码示例
void myfunc(u8 *data, u32 value)
{
[...]
*((u32 *) data) = cpu_to_le32(value);
[...]
}
每次 data 参数指向一个不能被 4 整除的地址时,此代码都会导致未对齐的访问。
总之,您可能遇到的未对齐访问问题的 2 种主要情况涉及
将变量强制转换为不同长度的类型
指针运算,然后访问至少 2 个字节的数据
避免未对齐的访问¶
避免未对齐访问的最简单方法是使用 <linux/unaligned.h> 头文件提供的 get_unaligned() 和 put_unaligned() 宏。
回到前面一个可能导致未对齐访问的代码示例
void myfunc(u8 *data, u32 value)
{
[...]
*((u32 *) data) = cpu_to_le32(value);
[...]
}
为了避免未对齐的内存访问,您将按如下方式重写它
void myfunc(u8 *data, u32 value)
{
[...]
value = cpu_to_le32(value);
put_unaligned(value, (u32 *) data);
[...]
}
get_unaligned() 宏的工作方式类似。假设“data”是指向内存的指针,并且您希望避免未对齐的访问,它的用法如下
u32 value = get_unaligned((u32 *) data);
这些宏适用于任何长度的内存访问(不仅是上面示例中的 32 位)。请注意,与对齐内存的标准访问相比,使用这些宏来访问未对齐内存可能会在性能方面付出高昂的代价。
如果使用此类宏不方便,另一种选择是使用 memcpy()
,其中源或目标(或两者)的类型为 u8* 或 unsigned char*。由于此操作的按字节性质,可以避免未对齐的访问。
对齐与网络¶
在需要对齐加载的架构上,网络要求 IP 标头在四字节边界上对齐以优化 IP 堆栈。对于常规以太网硬件,使用常量 NET_IP_ALIGN。在大多数架构上,此常量的值为 2,因为正常的以太网标头为 14 个字节长,因此为了获得正确的对齐,需要 DMA 到可以表示为 4*n + 2 的地址。这里的一个值得注意的例外是 powerpc,它将 NET_IP_ALIGN 定义为 0,因为 DMA 到未对齐的地址可能非常昂贵,并且会使未对齐加载的成本相形见绌。
对于某些无法 DMA 到未对齐地址(如 4*n+2)的以太网硬件或非以太网硬件,这可能是一个问题,因此需要将传入的帧复制到对齐的缓冲区中。因为在可以进行未对齐访问的架构上这是不必要的,所以代码可以像这样依赖于 CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS
#ifdef CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS
skb = original skb
#else
skb = copy skb
#endif