内核数据结构之container_of
container_of
宏用于根据结构体某个成员的地址来获取结构体自身的地址,获得结构体自身地址之后可以访问该结构其他字段,定义在<linux/kernel.h>
中。
offsetof
宏用于计算结构体成员到结构体自身地址之间的偏移,定义在<linux/stddef.h>
中。
pragma pack预定义指令用于限制结构体成员的最大对齐数,结构体的对齐数取决于结构体成员的最大对齐数,结构体成员的对齐数取决于其自身大小和编译器设置的参数。
__attribute__((packed))
和__attribute__((aligned(n)))
分别用于GCC不设置对齐和设置以n
字节对齐。
实现
container_of
宏定义在<linux/kernel.h>
头文件中,因此只能用于Linux内核编程,如果想要用在应用层,则需要自己手动定义。
内核中定义如下:
/**
* container_of - cast a member of a structure out to the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*
*/
#define container_of(ptr, type, member) ({ \
void *__mptr = (void *)(ptr); \
BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) && \
!__same_type(*(ptr), void), \
"pointer type mismatch in container_of()"); \
((type *)(__mptr - offsetof(type, member))); })
下面是自己手动实现:
// container_of 宏的实现
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) \
({ \
const typeof(((type *)0)->member) *__mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); \
})
首先可以看到这两个宏都使用了offsetof
宏,该宏的作用是计算结构体成员到结构体基址之间的偏移。
原理
正常来说,要从一个结构体的成员地址获取该结构体地址,那么需要用该成员地址-成员到结构体地址之间的偏移。
而一个给定结构体的中的变量偏移在编译时就已经确定下来了,因此从理论上来说这是可行的。
既然涉及到结构体的偏移就避免不了讨论结构体的对齐,下面先讨论结构体的对齐。
结构体对齐
结构体对齐遵循两条规则:
- 在不同的编译器和平台下,结构体成员的对齐数是由成员自身的大小和编译器指定的对齐参数(
#pragma pack
)共同决定的。一般来说,成员的对齐数取其自身大小和编译器指定对齐参数中的较小值。 - 结构体的整体对齐是取结构体中所有成员的最大对齐数。结构体的总大小必须是这个最大对齐数的整数倍,如果不足则需要进行填充。
- 结构体的连续成员的对齐数之和小于结构体整体对齐数时,该连续成员占用一个整体对齐数大小的空间,如果不能完全占用整体对齐数大小的空间则需要填充。
在编译器不指定的情况下,64位系统中以下类型的结构体成员的对齐数为:
类型 | 大小 |
---|---|
char | 1 |
short | 2 |
int | 4 |
float | 4 |
double | 8 |
long | 8 |
编译器指定结构体对齐参数有以下几种方式:
#pragma pack(n)
预处理指令,用于调整编译器的对齐参数,而该编译器的对齐参数是用来限制结构体成员的最大对齐数,支持gcc和msvc。例如一个结构体中存在char
和double
,而设置#pragma pack(2)
则使得char
的最大对齐数为2然后和自身大小1取最小值则为1,double
自身大小为8和设置的最大对齐数2取最小值则为2。__attribute__((packed))
属性,用于指定结构体不进行字节对齐,即让结构体成员紧密排列,不填充字节,仅支持gcc。__attribute__((aligned(n)))
属性,可以指定结构体按照指定的字节数n
进行对齐,n
必须是2的幂次方。例如__attribute__((aligned(4)))
即以4字节对齐。
下面的示例展示了#pragma pack
指定不同大小的对齐参数时结构体成员的偏移:
pragma pack(1)示例
#pragma pack(1)
将结构体成员的最大对齐数限制为1,因此即使结构体中存在大小超过1字节的成员其对齐数仍旧为1,这进而限制结构体的整体对齐数为1。
#include <stdio.h>
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#pragma pack(1) // 指定编译器的对齐参数为1,用来限制结构体成员的最大对齐数为1
typedef struct ts
{
// 1. 结构体成员的对齐数取自其自身大小和编译器设置的对齐数的最小值
// 2. 结构体整体的对齐数取自结构体成员的对齐数的最大值
int a; // 自身大小4字节 > 1字节,对齐数1字节
int b; // 自身大小4字节 > 1字节,对齐数1字节
char c; // 自身大小1字节 >= 1字节,对齐数1字节
float d; // 自身大小4字节 > 1字节,对齐数1字节
int e; // 自身大小4字节 > 1字节,对齐数1字节
double f; // 自身大小8字节 > 1字节,对齐数1字节
// 整体对齐数为1字节
} data_st;
int main(int argc, char* argv[])
{
printf("%ld\n", offsetof(data_st, a));
printf("%ld\n", offsetof(data_st, b));
printf("%ld\n", offsetof(data_st, c));
printf("%ld\n", offsetof(data_st, d));
printf("%ld\n", offsetof(data_st, e));
printf("%ld\n", offsetof(data_st, f));
return 0;
}
// $ ./align
// 0
// 4
// 8
// 9
// 13
// 17
pragma pack(2)示例
#pragma pack(2)
限制结构体成员的最大对齐数为2,当结构体成员中存在char
时,该成员的对齐数为1(因为和成员大小取最小值),但当该结构体中仍旧存在其他大小大于2的成员时,这些成员的对齐数被限制为2,进而会导致结构体的整体对齐数为2。
如果结构体中仅存在char
成员时,那么所有成员的对齐数都为1,结构体的整体对齐数也为1。
#include <stdio.h>
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#pragma pack(2) // 设置编译器的对齐参数,用于将结构体成员的对齐数限制为2
typedef struct ts
{
// 1. 结构体成员的对齐数取自其自身大小和编译器设置的对齐数的最小值
// 2. 结构体整体的对齐数取自结构体成员的对齐数的最大值
int a; // 自身大小4字节 > 2字节,对齐数2字节
int b; // 自身大小4字节 > 2字节,对齐数2字节
char c; // 自身大小1字节 < 2字节,对齐数1字节
float d; // 自身大小4字节 > 2字节,对齐数2字节
int e; // 自身大小4字节 > 2字节,对齐数2字节
double f; // 自身大小8字节 > 2字节,对齐数2字节
// 整体对齐数为2字节
} data_st;
int main(int argc, char* argv[])
{
printf("%ld\n", offsetof(data_st, a));
printf("%ld\n", offsetof(data_st, b));
printf("%ld\n", offsetof(data_st, c));
printf("%ld\n", offsetof(data_st, d));
printf("%ld\n", offsetof(data_st, e));
printf("%ld\n", offsetof(data_st, f));
return 0;
}
// $ ./align2
// 0
// 4
// 8
// 10
// 14
// 18
pragma pack(4)示例
#pragma pack(4)
时结构体中如果存在自身大小大于或等于4字节的成员时,该成员的对齐数被限制为4字节,同时结构体的整体对齐数也被限制为4字节。
在下面结构体成员中short
和char
类型的对齐数为2和1,其和小于整体对齐数,因此只占用一个对齐数大小的空间:
#include <stdio.h>
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#pragma pack(4)
typedef struct ts
{
// 1. 结构体成员的对齐数取自其自身大小和编译器设置的对齐数的最小值
// 2. 结构体整体的对齐数取自结构体成员的对齐数的最大值
// 3. 连续成员b,c的对齐数之和小于整体对齐数,因此只占用一个对齐数大小的空间,因此d的偏移为8
int a; // 自身大小4字节 <= 4字节,对齐数4字节
short b; // 自身大小2字节 <= 4字节,对齐数2字节
char c; // 自身大小1字节 < 4字节,对齐数1字节
float d; // 自身大小4字节 <= 4字节,对齐数4字节
int e; // 自身大小4字节 <= 4字节,对齐数4字节
double f; // 自身大小8字节 > 4字节,对齐数4字节
// 整体对齐数为4字节
} data_st;
int main(int argc, char* argv[])
{
printf("%ld\n", offsetof(data_st, a));
printf("%ld\n", offsetof(data_st, b));
printf("%ld\n", offsetof(data_st, c));
printf("%ld\n", offsetof(data_st, d));
printf("%ld\n", offsetof(data_st, e));
printf("%ld\n", offsetof(data_st, f));
return 0;
}
// $ ./align3
// 0
// 4
// 6
// 8
// 12
// 16
pragma pack(8)示例
当#pragma pack
设置的对齐参数大于结构体成员中所有参数的自身大小时,则不会对结构体成员的默认对齐规则产生影响。
因为会先与结构体成员大小取较小值得到每个成员的对齐数,再从这些对齐数中取较大值得到结构体的整体对齐数,在这个过程中pragma pack
设置的对齐数是不产生任何影响的。
#include <stdio.h>
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
#pragma pack(8)
typedef struct ts
{
// 1. 结构体成员的对齐数取自其自身大小和编译器设置的对齐数的最小值
// 2. 结构体整体的对齐数取自结构体成员的对齐数的最大值
// 3. 连续成员b,c的对齐数之和小于整体对齐数,因此只占用一个对齐数大小的空间,因此d的偏移为8
int a; // 自身大小4字节 <= 8字节,对齐数4字节
short b; // 自身大小2字节 <= 8字节,对齐数2字节
char c; // 自身大小1字节 < 8字节,对齐数1字节
float d; // 自身大小4字节 <= 8字节,对齐数4字节
int e; // 自身大小4字节 <= 8字节,对齐数4字节
// 整体对齐数为4字节
} data_st;
int main(int argc, char* argv[])
{
printf("%ld\n", offsetof(data_st, a));
printf("%ld\n", offsetof(data_st, b));
printf("%ld\n", offsetof(data_st, c));
printf("%ld\n", offsetof(data_st, d));
printf("%ld\n", offsetof(data_st, e));
return 0;
}
// $ ./align3
// 0
// 4
// 6
// 8
// 12
__attribute__((packed))
使用示例
__attribute__((packed))
属性仅支持GCC,其结果相当于与#pragma pack(1)
,使结构体成员紧密排列,没有任何填充数据。
在使用该属性时需要注意的一点是,当结构体存在嵌套时需要内部结构体也使用该属性,才能保持成员之间的紧密连接。同时内部结构体不能是指针,指针占用的字节数仅于系统寻址位数有关,和指向的结构体内容大小无关。
下面的例子显示了使用该属性之后,结构体成员的偏移量。
#include <stdio.h>
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
typedef struct ts
{
int a;
short b;
char c;
float d;
int e;
} __attribute__((packed)) data_st;
int main(int argc, char* argv[])
{
printf("%ld\n", offsetof(data_st, a));
printf("%ld\n", offsetof(data_st, b));
printf("%ld\n", offsetof(data_st, c));
printf("%ld\n", offsetof(data_st, d));
printf("%ld\n", offsetof(data_st, e));
return 0;
}
// $ ./align5
// 0
// 4
// 6
// 7
// 11
__attribute__((aligned(n)))
使用示例
__attribute__((aligned(n)))
指定结构体的对齐数,下面的示例中指定结构体以2字节对齐,因此成员c
需要填充1个字节,导致后面的成员d
的偏移量为8。
#include <stdio.h>
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
typedef struct ts
{
int a;
short b;
char c;
float d;
int e;
} __attribute__((aligned(2))) data_st;
// __attribute__((aligned(2))) 表示以两字节对齐,此时成员c需要填充一个字节
int main(int argc, char* argv[])
{
printf("%ld\n", offsetof(data_st, a));
printf("%ld\n", offsetof(data_st, b));
printf("%ld\n", offsetof(data_st, c));
printf("%ld\n", offsetof(data_st, d));
printf("%ld\n", offsetof(data_st, e));
return 0;
}
// $ ./align6
// 0
// 4
// 6
// 8
// 12
offsetof
offsetof
宏就是用来获取结构体成员到结构体基址之间的偏移量。
#define offsetof(type, member) ((size_t) &((type *)0)->member)
根据上面的实现可以看到,
(type*)0
是将0强制转换为type
类型的指针,构造出一个指向地址0的type
类型指针。但是这里并没有真的访问地址0。((type*)0)->member
:借助指向地址0的type
类型指针访问type
类型中的成员member
,用来获取对成员的引用,但不产生实际的地址访问。&((type*)0)->member
:获取成员member
的地址。由于结构体的基址为0,那么成员的地址就等同于其相对于结构体起始地址的偏移量。
同时由于结构体成员在内存中是按照对齐规则排列的,因此offsetof
返回的偏移量对齐之后的值。
注意:offsetof
宏从始至终都没有涉及到访问结构体的实例,只是将地址0强制转换为type
类型进行操作,并不涉及实际地址访问。
该宏接收两个参数,分别是结构体的类型名和成员名,返回的是该成员到结构体基址之间的偏移。
用法见上述示例。
container_of
与offsetof
宏不同的是,container_of
宏不仅涉及到结构体类型定义也涉及到结构体实例的具体地址访问。
// container_of 宏的实现
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) \
({ \
const typeof(((type *)0)->member) *__mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); \
})
从上面的实现中看,ptr
是结构体实例中成员的地址,type
表示结构体类型,member
表示ptr
对应的结构体成员。
从实现原理上看很简单,通过ptr
指向的结构体实例成员的地址减去通过offsetof
计算得到的结构体成员的偏移量,即可达到结构体实例的起始地址。
问题1:为什么不将ptr
直接转换为char *
然后减去成员偏移量?
上面的实现中是先将ptr
赋值给新定义的变量__mptr
,其变量类型是通过typeof
函数获取到member
的类型,这里可以通过编译器的类型检查功能确保传入的ptr
和member
类型一致。
比如一个结构体中存在char
和int
两种类型的成员,ptr
指向的是char
成员地址,member
传入的确实int
成员,此处就会报错。
问题2:为什么要将__mptr
转为char *
然后减成员偏移量呢?
因为指针类型的加减规则是基于其自身大小的。比如int *
减1相当于减去4个字节,而char *
减1则只减去1个字节。
contianer_of
的使用示例如下
#include <stdio.h>
/**************************************************************************************************/
/* DEFINES */
// container_of 宏的实现
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) \
({ \
const typeof(((type *)0)->member) *__mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); \
})
/**************************************************************************************************/
typedef struct ts
{
int a; // 偏移为0
int b; // 偏移为4
char c; // 偏移为8
float d; // 偏移为12
int e; // 偏移为16
double f; // 偏移为20
} data_st;
int main()
{
data_st demo = {1, 2, 'c', 3.0, 4, 5.0};
printf("%d\n", container_of(&demo.f, data_st, f)->a);
printf("%d\n", container_of(&demo.f, data_st, f)->b);
printf("%c\n", container_of(&demo.f, data_st, f)->c);
printf("%f\n", container_of(&demo.f, data_st, f)->d);
printf("%d\n", container_of(&demo.f, data_st, f)->e);
printf("%lf\n", container_of(&demo.a, data_st, a)->f);
}
// $ ./container_of
// 1
// 2
// c
// 3.000000
// 4
// 5.000000
参考
- 源码
- 《Linux内核设计与实现》
- 原文作者:生如夏花
- 原文链接:https://blduan.top/post/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/%E5%86%85%E6%A0%B8%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B9%8Bcontainer_of/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。