对齐这个事情在内核中可不是个什么小事,内核中涉及到内存方面的都需要非常的谨慎。
上一篇我们知道了可以通过__attribute__来声明属性,也知道了section这个属性,这篇我们来看看关于内存对齐使用的两个属性–>aligned和packed
地址对齐:aligned
GNU C通过__attribute__来声明aligned和packed属性,指定一个变量或类型的对齐方式。
使用这两个变量告诉编译器,按照我让aligned和packed两同事给你传的信息做事情哦。
举个栗子:在内存中以8字节地址对齐
int a __attribute__((aligned(8)))
这个aligned的参数必须是2的幂次方
但是确实总是可以的专业显示指定变量的对齐方式,会因为边界对齐造成一些内存空洞,浪费内存资源。所以这个操作的必要性在哪里呢?
一个主要原因就是:**这种对齐设置可以简化CPU和内存RAM之间的接口和硬件设计。**一个32位的计算机系统,在CPU读取内存时,硬件设计上可能只支持4字节或4字节倍数对齐的地址访问,CPU每次向内存RAM读写数据时,一个周期可以读写4字节。如果我们把一个int型数据放在4字节对齐的地址上,那么CPU一次就可以把数据读写完毕;如果我们把一个int型数据放在一个非4字节对齐的地址上,那么CPU可能就要分两次才能把这个4字节大小的数据读写完毕。
为了配合计算机的硬件设计,编译器在编译程序时,对于一些基本数据类型,如int、char、short、float等,会按照其数据类型的大小进行地址对齐,按照这种地址对齐方式分配的存储地址,CPU一次就可以读写完毕。虽然边界对齐会造成一些内存空洞,浪费一些内存单元,但是在硬件上的设计却大大简化了。
这个对齐要求不只是针对普通变量,对于复合变量也需要满足地址对齐的要求。
结构体对齐
结构体是复合的数据类型,编译器在分配空间时需要的不仅仅是考虑成员变量的对齐,还要考虑整个结构体的。当老大也是很不容易的。
于是: 为了结构体内各个成员地址对齐,编译器可能会在结构体内填充一些空间;
为了结构体整体对齐,编译器可能会在结构体的末尾填充一些空间。
整个栗子吃吃看:
struct data{
char a;
int b;
short c;
}
因为结构体的成员b需要4字节对齐,所以编译器在给成员a分配完1字节的存储空间后,会空出3字节,在满足4字节对齐的0x0028FF34地址处才给成员b分配4字节的存储空间。接着是short类型的成员c占据2字节的存储空间。三个结构体成员一共占据1+3+4+2=10字节的存储空间。
根据结构体的对齐规则,结构体的整体对齐要按结构体所有成员中最大对齐字节数或其整数倍对齐,或者说结构体的整体长度要为其最大成员字节数的整数倍,如果不是整数倍则要补齐。因为结构体最大成员int为4字节,所以结构体要按4字节对齐,或者说结构体的整体长度要是4的整数倍,要在结构体的末尾补充2字节,最后结构体的大小为12字节。
但是我们换个顺序试试:
struct data{
char a;
short b;
int data;
}
char型变量a和short型变量b,被分配在了结构体前4字节的存储空间中,而且都满足各自的地址对齐方式,整个结构体大小是8字节,只造成1字节的内存空洞。
当然这里我们也可以让这个变成12字节
struct data{
char a;
short b __attribute__((aligned(4)));
int c;
}
也可以指定整个结构体
struct data{
char a;
short b;
int c;
}__attribute__((aligned(16)));
整个结构体的对齐只要按最大成员的对齐字节数对齐即可,结构体整体就以4字节对齐,结构体的整体长度为8字节,但是在这里,显式指定结构体整体以16字节对齐,所以编译器就会在这个结构体的末尾填充8字节以满足16字节对齐的要求,最终导致结构体的总长度变为16字节。
想想这个编译器就说什么听什么?会不会它觉得自己更靠谱。
编译器一定会按照aligned指定的方式对齐吗? NONONO
我们通过这个属性声明,其实只是建议编译器按照这种大小地址对齐,但不能超过编译器允许的最大值。一个编译器,对每个基本数据类型都有默认的最大边界对齐字节数。如果超过了,则编译器只能按照它规定的最大对齐字节数来给变量分配地址。(在我的底线内你随便玩)
char c2 __attribute__((aligned(16))) = 4; //能执行
char c2 __attribute__((aligned(32))) = 4; //nonono
这是因为32字节的对齐方式超过了编译器允许的最大值。
编译器还挺霸道总裁
到这里,关于aligned七七八八了,具体的还需要去内核的源码中遨游学习,在属性中,还有个packed,在上篇文章的例子中有出现过。下面来一起看看。
属性声明:packed
aligned属性一般用来增大变量的地址对齐,元素之间因为地址对齐会造成一定的内存空洞。
packed属性则与之相反,一般用来减少地址对齐,指定变量或类型使用最可能小的地址对齐方式。
话不多说,给大爷们摆上栗子:
struct data{
char a;
short b __attribute__((packed));
int c _attribute__((packed));
};
结构体内各个成员地址的分配,使用最小1字节的对齐方式,没有任何内存空间的浪费,导致整个结构体的大小只有7字节。
上面刚刚说了内存对齐方便读写,这感觉有点矛盾?不是的哈,各个使用的场景不同。
packed这个特性在底层开发驱动还是非常有用:
例如,你想定义一个结构体,封装一个IP控制器的各种寄存器,在ARM芯片中,每一个控制器的寄存器地址空间一般都是连续存在的。如果考虑数据对齐,则结构体内就可能有空洞,就和实际连续的寄存器地址不一致。使用packed可以避免这个问题,结构体的每个成员都紧挨着,依次分配存储地址,这样就避免了各个成员因地址对齐而造成的内存空洞。
于是我们可以对结构体加packed属性,保证每个结构体的成员是连续的。
配合使用packed和aligned
我们可以在结构体内部保持 连续,然后让结构体去对齐,这样既避免了结构体内各成员因地址对齐产生内存空洞,又指定了整个结构体的对齐方式。
struct data{
char a;
short b;
int c;
}__attribute__((packed,aligned(8)));
结构体data虽然使用了packed属性声明,结构体内所有成员所占的存储空间为7字节,但是我们同时使用了aligned(8)指定结构体按8字节地址对齐,所以编译器要在结构体后面填充1字节,这样整个结构体的大小就变为8字节,按8字节地址对齐。
- 资料:《嵌入式C语言自我修养——从芯片、编译器到操作系统》