非对齐内存访问

=========================

非对齐内存访问

:作者: 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。

在编写代码时,需要假设目标架构有自然的对齐要求。

实际上,只有少数架构需要对所有大小的内存访问进行自然对齐。 但是,我们必须考虑所有支持的架构; 编写满足自然对齐要求的代码是实现完全可移植性的最简单方法。

为什么非对齐访问不好

执行未对齐内存访问的效果因架构而异。 在这里写一篇关于差异的完整文档会很容易;常见情况概述如下:

  • Some architectures are able to perform unaligned memory accesses
    transparently, but there is usually a significant performance cost.
  • 某些体系结构能够透明地执行未对齐的内存访问,但通常会产生显着的性能成本。
  • 当发生未对齐的访问时,某些架构会引发处理器异常。异常处理程序能够纠正未对齐的访问,但对性能的影响很大。
  • 某些架构在发生未对齐访问时会引发处理器异常,但这些异常没有包含足够的信息来纠正未对齐访问。
  • 某些架构无法进行未对齐的内存访问,但会默默地对请求的内存执行不同的内存访问,从而导致难以检测的微妙的代码错误!

从上面应该可以明显看出,如果你的代码导致发生未对齐的内存访问,您的代码将无法在某些平台上正常工作,并且会在其他平台上导致性能问题。

不会引起非对齐访问的代码

乍一看,上面的概念似乎与实际的编码实践联系起来有点困难。 毕竟,您对某些变量的内存地址等没有太多控制权。

幸运的是那并不是很复杂,就像在大多数场景下,编译器会确保这些对你有用。例如,采用以下结构:

struct foo {
	u16 field1;
	u32 field2;
	u8 field3;
};

假设有一个上面结构的实例驻留在以0x10000开始的地址。一个基本的理解是,访问filed2会导致非对齐访问是正常的。你期望 field2 位于结构中偏移 2 个字节处,即地址 0x10002,但该地址不能被 4 整除(请记住,我们在这里读取的是 4 个字节的值)。

幸运的是,编译器理解对齐限制,所以在上面的场景中它会在field1和field2之间插入两个字节。因此,对于标准结构类型,你始终可以依靠编译器来填充结构,以便对字段的访问适当对齐(假设您没有将字段强制转换为不同长度的类型)。

同样,你也可以依靠编译器根据变量类型的大小将变量和函数参数对齐到自然对齐的方案。

在这点上,不言而喻,访问单个字节(u8或者char)永远不会造成非对齐,因为所有的内存地址是可以被1整除的。

在一个相关主题上,考虑到上述注意事项,你可以观察到,你可以对结构中的字段重新排序,以便将字段放置会插入填充的位置,从而减少结构实例的整体驻留内存大小。 的最佳布局
上面的例子是::

struct foo {
	u32 field2;
	u16 field1;
	u8 field3;
};

对于自然对齐方案,编译器只需在结构末尾添加一个字节的填充。添加此填充是为了满足这些结构的数组的对齐约束。

另一个值得考虑的点是在一个结构类型中__ attribute__((packed))的使用。这个特定于 GCC 的属性告诉编译器永远不要在结构中插入任何填充,当您想使用 C 结构来表示一些固定顺序排列的数据时非常有用。

您可能会认为,在访问不满足体系结构对齐要求的字段时,使用此属性很容易导致未对齐的访问。 然而,同样,编译器知道对齐约束,并将生成额外的指令来以不会导致未对齐访问的方式执行内存访问。 当然,与非压缩情况相比,额外的指令显然会导致性能损失,因此只有在避免结构填充很重要时才应使用压缩属性。

会造成非对齐访问的代码

考虑到上述情况,让我们进入一个可能导致未对齐内存访问的函数的真实示例。 以下来自 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 位)。

Think about what would happen if addr1 was an odd address such as 0x10003.
(Hint: it’d be an unaligned access.)

想想如果addr1是一个奇数地址比如0x10003,会发生什么(提示:这是一个非对齐访问)。

尽管上述函数存在潜在的未对齐访问问题,但无论如何它都包含在内核中,但被理解为只能在 16 位对齐地址上正常工作。 由调用者来确保这种对齐或根本不使用此功能。 这种对齐不安全的功能仍然很有用,因为它对于可以确保对齐的情况来说是一个不错的优化,在以太网网络环境中几乎所有场景都是如此。

下面是一些可能导致未对齐访问的代码示例:

void myfunc(u8 *data, u32 value)
{
	[...]
	*((u32 *) data) = cpu_to_le32(value);
	[...]
}

每当 data 参数指向一个不能被 4 整除的地址时,此代码将导致未对齐的访问。

总之,您可能会遇到未对齐访问问题的 2 个主要场景包括:

  1. 将变量转换为不同长度的类型
  2. 指针运算,然后访问至少 2 个字节的数据

避免非对齐访问

避免未对齐访问的最简单方法是使用 <asm/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

译注:
其他非对齐访问资料:谈谈内存对齐

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值