作者:陈莉君 来自:
对齐是跟数据块在内存中的位置相关的话题。如果一个变量的内存地址正好是它长度的整数倍,它就被称作是自然对齐的。举例来说,对于一个32位类型的数据,如果它在内存中的地址刚好可以被4整除(也就最低两位为0),那它就是自然对齐的。也就是说,一个大小为2n字节的数据类型n,它地址的最低有效位的后n位都应该为0。
一些体系结构对对齐的要求非常严格。通常象基于RISC的系统,载入未对齐的数据会导致处理器陷入(一种可处理的错误)。还有一些系统可以访问没有对齐的数据,只不过性能会下降。编写可移植性高的代码要避免对齐问题,保证所有的类型都能够自然对齐。
1. 避免对齐引发的问题
编译器通常会通过让所有的数据自然对齐来避免引发对齐问题。实际上,内核开发者在对齐上不用花费太大心思—只有搞gcc的那些老兄才应该为此犯愁呢。可是,当程序员使用 指针太多,对数据的访问方式超出编译器的预期时,就会引发问题了。
一个数据类型长度较小,它本来是对齐的,如果你用一个指针进行类型转换,并且转换后的类型长度较大,那么通过改指针进行数据访问时就会引发对齐问题(无论如何,对于某些体系结构会存在这种问题)。也就是说,下面的代码是错误的:
char dog[10];
char *p = &dog[1];
unsigned long l = *(unsigned long *)p;
这个例子将一个指向char型的指针当作指向unsigned long型的指针来用,这会引起问题,因为此时会试图从一个并不能被4整除的内存地址上载入32位的unsigned long型数据。
如果你能想到“我会在现实中这么做吗?”,你基本上就不会有问题了。无论如何,这种错误出现了,并且它还会发生,所以应该小心。实际编程时错误可能不会像这个例子中这么明显。
2. 非标准类型的对齐
前面提到了,对于标准数据类型来说,它的地址只要是其长度的整数倍就对齐了。而非标准的(复合的)C数据类型按照下列原则对齐:
* 对于数组,只要按照基本数据类型进行对齐就可了(其实随后的所有元素自然能够对齐)。
* 对于联合,只要它包含的长度最大的数据类型能够对齐就可以了。
* 对于结构体,只要它包含的长度最大的数据类型能够对齐就可以了。
结构体还要引入填补机制,这会引出下一个问题。
3. 结构体填补
为了保证结构体中每一个成员都能够自然对齐,结构体要被填补。这点确保了当处理器访问结构中一个给定元素时,元素本身被对齐。This ensures that when the processor accesses a given element in the structure, that element itself is aligned.举个例子,下面是一个在32位机上的结构体:
struct animal_struct {
char dog; /* 1字节*/
unsigned long cat; /* 4字节*/
unsigned short pig; /* 2字节 */
char fox; /* 1字节*/
};
由于该结构不能准确地满足各个成员自然对齐,所以它在内存中可不是按照原样存放的。编译器会在内存中创建一个类似下面给出的结构体:
struct animal _struct {
char dog; /* 1字节*/
u8 __pad0[3]; /* 3字节*/
unsigned long cat; /* 4字节*/
unsigned short pig; /* 2字节 */
char fox; /* 1字节*/
u8 __pad1; /* 1字节*/
};
填补的变量都是为了能够让数据自然对齐而加入的。第一个填充物占用了3个字节的空间,保证cat可以按照4字节对齐。这也自动使其他小的对象都被对齐了,因为它们长度都比cat要小。第二个也是最后的填充是为了填补struct本身的大小。额外的这个填补使结构体的长度能够被4整除,这样,在由该结构体构成的数组中,每个数组项也就会自然对齐了。
注意,在大部分32位系统上,对于任何一个这样的结构体,sizeof(animal_struct)都会返回12。C编译器自动进行填补以保证自然对齐。
通常你可以通过重新排列结构中的对象来避免填充。这样既可以得到一个较小的结构体,又能保证无需填补它也是自然对齐的。
struct animal _struct {
unsigned long cat; /* 4字节*/
unsigned short pig; /* 2字节 */
char dog; /* 1字节*/
char fox; /* 1字节*/
};
现在这个结构体只有8字节大小了。不过,不是任何时候都可以这样对结构体进行调整的。举个例子,如果该结构体是为某个标准的一部分,或者它是现有代码的一部分,那么它的成员次序就已经被定死了,虽然在内核中(缺少一个正式的ABI)相比用户空间来说,这种需求要少的多。还有些时候,你因为一些原因必须使用某种固定的次序—比如说,为了提高高速缓存的命中率进行优化时设定的变量次序。注意,ANSI C明确规定不允许编译器改变结构体内成员对象的次序——它总是由你,程序员来决定。虽然编译器可以帮助你做填充,但是,如果使用-Wpadded标志,那么将使gcc在发现结构体被填充时产生警告。
内核开发者需要注意结构体填补问题,特别是在整体使用时—这是指当需要通过网络发送它们或需要将它们写入文件的时候,因为不同体系结构之间所需要的填补也不尽相同。这也是为什么C没有提供一个内建的结构体比较操作符的原因之一。结构体内的填充字节中可能会包含垃圾信息,所以在结构体之间进行一字节一字节的比较就不大可能实现了。C语言的设计者(正确的)感觉到最好还是由程序员自己为不同的情况编写比较函数,这样才能利用到结构体次序信息。
如果让编译器随心所欲地改变结构体中各个对象地位置的话,现存的程序大部分都会崩溃。在C语言中,函数往往通过在结构体地址上加上偏移量来计算变量的位置。
阅读(309) | 评论(0) | 转发(0) |