所谓自然对齐(Natural Alignmnet),是指从一个地址读N个字节,而这个地址可以被N整除(addr % N == 0)。比如在一个以4字节为boundary的系统里,读取地址0x1008开始的4个字节就是对齐的,而读取0x1009开始的4个字节就不是。
对于非自然对齐的数据访问,不同架构的处理器会有不同的应对策略:
- 有的会透明地处理掉(比如x86架构),软件感觉不到,但是会影响性能(performance penalty)。
- 有的不会报告硬件异常,但继续运行可能出现问题。
- 有的虽然会报告异常,但提供的信息不充分,可能看不出是非对齐的访问造成的。
会填坑的编译器
那什么情况下会出现非对齐的访问呢?来看下面这样一个结构体:
struct foo {
uint8 field1;
uint16 field2;
uint8 field3;
};
对结构体中"field2"的访问就是非对齐的,我们平时代码中会看到不少这样的结构体,但是好像并没有出现过什么问题?这是因为编译器能够识别这种非对齐的情况,自动地加上padding,比如这个结构体的地址是0x1000,那么field2的地址不会是0x1001,而是0x1002,至多就是浪费点存储空间而已。
struct foo {
uint8 field1;
#padding
uint16 field2;
uint8 field3;
#padding
}
如果用"sizeof(struct foo)"看一下,得出的大小将是6个字节。
如果我们手动地调整一下结构体中元素的顺序,像这样:
struct foo {
uint16 field2;
uint8 field1;
uint8 field3;
};
那么此时结构体中所有元素的访问都是对齐的,编译器不需要添加padding了。再用"sizeof()"看一下,得出的大小将是4个字节。
结构体reorder
可是这里有一个问题,举个I2C总线的例子,对I2C设备的访问依次是给出设备的地址、设备内寄存器的地址、寄存器的值,用结构体"foo"来封装的话,那么它们将分别对应"u8"的"field1","u16"的"field2"和"u8"的"field3"。如果调整了"field1"和"field2"的顺序,存储空间是节省了,但结构体表达的语义就不那么清晰了,程序的可读性就变差了。
针对这种情况,要是编译器可以在编译的时候,对结构体内的元素做reorder操作,自动地调换"field1"和"field2"的顺序,岂不是既可以节约空间,又不影响代码的可读性?
编译器说:我没问题啊,小菜一碟嘛。可惜,C语言并不允许编译器这样做。之所以做出这种看似「自废武功」的限制也是有道理的,因为有一些结构体所表达的对象,其在内存中的分布顺序是有「讲究」的。
最典型的例子就是一些和网络协议相关的结构体,因为协议是通信双方约定好的,每个字节代表什么都是事先确定的,如果一方在发送的时候做出reorder,另一方在接收之后就无法正确解析了。
紧凑型分布
面对网络协议,不光reorder不行,编译器的padding也不行,把内存中加了padding的数据包直接发出去,也会让对方「凌乱」的。但是编译器咋知道你这个结构体是用于网络通信的呢。没办法,只有在定义结构体的时候,显示地加上"__attribute__((packed))"。
这个"attribute"传递给编译器的意思就是,不要加padding,让结构体中的元素紧紧地挨在一起。还是上面那个例子,加上这个"attribute"之后,再用"sizeof()"看一下,得出的大小将是4个字节。
struct foo {
uint8 field1;
uint16 field2;
uint8 field3;
}__attribute__((packed));
可是,不加padding就是非对齐访问啊,不会触发硬件异常吗?作为很「懂」硬件的编译器,它会自信地告诉你:不会。因为它的家族里有面向不同处理器架构的成员,比如针对ARM的GCC版本,它就很清楚ARM在非对齐访问方面的限制,所以它会通过添加额外的指令,来避免非对齐访问的出现。
指针强制转换
看来好像不管怎么折腾,聪明的编译器都能给你「找补」回来,所以编程人员其实完全不用考虑非自然对齐的问题?非也,来看下面这个例子:
void func(u8 *data, u32 value)
{
...
*((u32 *) data) = cpu_to_le32(value);
...
}
这里有一个指针的强制转换,假设"data"指针里存储的值是0x1001,那么将它强制转换为指向"u32"数据类型的指针后,接下来的访问就是以4字节为单位的,发生在"0x1001"这个地址处,就是非对齐的。
这种场景已经超过了编译器的「智力」范围,此时要想避免非对齐访问的发生,需要显示地加上get_unaligned()或者put_unaligned(),像这样:
value = cpu_to_le32(value);
put_unaligned(value, (u32 *) data);
DMA的非对齐
还有一种情况是在TCP/IP包的传输中,因为MAC header是14个字节,如果在接收的时候不作任何处理,那么在软件解析IP header的时候就不是自然对齐的。
一个解决办法就是把接收的目标地址右移2个字节,设计为"4n+2"的形式:
在Linux中其对应的实现是使用skb_reserve():
skb_reserve(skb, NET_IP_ALIGN);
其中"NET_IP_ALIGN"的默认值为2(定义在include/linux/skbuff.h)。
但是由于接收网络数据包通常采用DMA的形式,这又会造成的DMA地址的unalignment,在某些架构上,DMA unalignment的损害甚至会超过IP header对齐带来的性能增益。两害相权取其轻,PowerPC和x86目前都将"NET_IP_ALIGN"的值设为0,也就是不进行DMA目标地址的偏移。
参考:
内核文档 Documentation/unaligned-memory-access.txt
Struct Reordering by compiler
On the alignment of IP packets
原创文章,转载请注明出处。