这段代码展示了 Linux 内核编程中最经典的两个宏:offsetof 和 container_of。它们是 C 语言中实现"逆向寻找结构体入口"的核心工具。
下面我为你逐行拆解这两个宏的工作原理。
1. offsetof 宏:计算成员偏移量
c
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
- 目的 :计算结构体成员
MEMBER距离结构体TYPE起始地址的字节数。 - 拆解 :
(TYPE *)0:将地址0强制转换为指向TYPE结构体的指针。这并不会导致崩溃,因为我们只是在进行地址运算,并没有真正去读取(解引用)这个地址里的内容。((TYPE *)0)->MEMBER:假设结构体从 0 开始,我们要访问其中的MEMBER。&(...):获取这个成员的地址。- 由于起始点是 0,那么成员的地址值(例如 8)刚好就是它相对于起始点的偏移量。
(size_t):最后强制转换为无符号整型。
2. container_of 宏:根据成员指针找结构体首地址
这是内核开发的"黑科技",它能让你在只知道某个成员指针的情况下,反向推导出整个结构体的首地址。
c
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
-
参数说明:
ptr:指向结构体内部成员的指针。type:容器结构体的类型。member:成员在结构体中的变量名。
-
拆解:
- 第一行(类型安全检查) :
typeof( ((type *)0)->member ):通过typeof获取成员的实际类型。- 它定义了一个临时指针
__mptr指向ptr。如果ptr的类型和结构体定义的成员类型不匹配,编译器会报错,保证了程序的健壮性。
- 第二行(核心计算) :
(char *)__mptr:将成员指针强转为char *,因为char的大小是 1 字节,指针加减运算是以字节为单位进行的。- offsetof(type, member):用成员的实际地址 减去成员在结构体中的偏移量。(type *):最后将计算得到的地址强转为结构体类型。
- 第一行(类型安全检查) :
3. 图解与例子
假设我们有如下结构体:
c
struct Person {
int age; // 偏移量 0
char gender; // 偏移量 4
int id; // 偏移量 8
};
struct Person p1;
int *id_ptr = &p1.id; // 我们手里只有指向 id 的指针
如果我们想通过 id_ptr 找到 p1 的起始位置:
offsetof(struct Person, id)的值是 8。container_of(id_ptr, struct Person, id)的逻辑是:- 获取
id_ptr的内存地址(比如0x1008)。 - 计算
0x1008 - 8 = 0x1000。 0x1000就是结构体p1的起始地址。
- 获取
4. 为什么要这么做?
这种设计模式被称为**"侵入式链表(Intrusive Link List)"**。
在传统的 Java 或 C++ 集合类中,是容器包裹数据。但在 Linux 内核中,为了追求极致效率,通常是将"链表节点"嵌入到具体的业务结构体中。当内核遍历链表得到一个节点指针时,它必须通过 container_of 找回包含这个节点的业务结构体,从而访问其数据。
总结:
offsetof:我在结构体的什么位置?container_of:我是谁的成员?(找到"父亲"是谁)
linux内核源码
