container_of()详解


​原地址:http://radek.io/2012/11/10/magical-container_of-macro/

当你开始内核编程的时候,你会随便看看代码,那么你可能很快就会碰到这个神奇的预处理代码结构。
它是用来干嘛的?正如它的名称那样,它获取的是它的容器的地址。该函数需要三个变量--指针,容器的类型,指向成员的指针。(译者注:最终返回的是,该成员所处的结构体的指针。知道成员的指针,根据结构体的size就可以算出来了,具体的算法还要看下面)该宏将返回包含对应成员的容器的指针地址。它确实是一个实现巧妙的宏,但是原理是什么呢?下面让我们来看看。

第一幅插图显示了宏container_of(ptr, type, member) 的原理。一图胜前言,基本上就不需要上面啰嗦的描述了。




container_of宏的工作原理插图


下面是Linux内核中该宏的具体实现:


#define container_of(ptr, type, member) ({              \         
const typeof( ((type *)0)->member ) *__mptr = (ptr);    \         

(type *)( (char *)__mptr - offsetof(type,member) );})


第一眼看上去感觉就像表演魔法一样,下面就让我们一步一步的来解析这个魔法。(译者注:确实是非常优雅的实现。再次感叹。)



表达式中的语句

第一眼看上去,是不是觉得整个表达式的结构很奇怪,不是应该返回一个指针吗,但是为毛看起来只有两个类似{}的表达式在里面。好吧,其实很简单,这个表达式是C语言的GNU扩展(braced-group within expression,不知道怎么翻译,只需要知道是扩展就好)。编译器会执行整个语句块,并且将第二个语句的结果作为最终的结果返回。比如下面的语句将打印5:

int x = ({1; 2;}) + 3; printf("%d\n", x);


typeof()

这个是一个非标准的GNU C扩展。获取参数x的类型。精确的语义在gcc文档中有详细的说明。


int x = 5; typeof(x) y = 6; printf("%d %d\n", x, y);

0指针的间接引用

但是为毛出现了一个0指针啊!!好吧,这个实际上是获取成员类型的小魔法。它不会崩溃,因为这个表达式本身并不会被求值。编译器在乎的指示它的类型。为了避免我们要求地址,同样的状况也会发生(求大神翻译:The same situation occurs in case we ask back for the address. )。重复一遍,编译器并不关心0指针的值,编译器会在分配给结构体的地址加上该成员的偏移,并且返回加上偏移之后的新地址。
struct s {         char m1;         char m2; }; /* This will print 1 */ printf("%d\n", &((struct s*)0)->m2);
注意,下面的两个定义是等价的:
typeof(((struct s *)0)->m2) c; char c;
offsetof(st, m)

这个宏会返回成员在相对结构体首地址的偏移量字节。它甚至作为了标准库的一部分(在stddef.h中可用)。并不是运行在内核空间中,标准C库并没有在这个地方出现。这个有点像我们前面提到的0指针引用,现在的编译器通常会提供一个内建的函数来替换。下面是一个复杂版本(来自内核):
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
It returns an address of a member called MEMBER of a structure of type TYPE that is stored in memory from address 0 (which happens to be the offset we’re looking for).
它会返回TYPE类型结构体中一个名为MEMBER成员的地址,这个地址是内存中从0地址开始计算的地址(这个地址其实也恰好我们需要的偏移量。注:这个0地址其实也就是结构体的首地址)。


将所有的结合起来


#define container_of(ptr, type, member) ({                      \

        const typeof( ((type *)0)->member ) *__mptr = (ptr);    \

         (type *)( (char *)__mptr - offsetof(type,member) );})


当你仔细看看文章开始给出的原始定义的时候,你可能会想,第一行代码真的符合所有情况吗?
你没有猜错。第一行代码说对这个宏的返回结果并不会产生本质上的影响,但是它的作用只作为类型检查的目的。第二行是干嘛的呢?我们提供了结构体中某个成员的地址,然后减去该成员在结构体中的偏移,就的到了包含它的结构体的地址(译者注:这样就可以访问结构体中的其他成员了,牛b吧!在Linux代码中经常看到结构体中包含一个包含struct list_head *next,*prev;的语句,通过遍历这个就可以获取结构体的首地址了,可以很方便的通过包含这个来实现链表,而不是包含该结构体的前后指针,万一这个结构体很复杂呢。Linux中可是有很多复杂的结构体的)。
当我们一点点剥离魔法操作符(0指针),构造和小技巧之后,发现它就是这么简单:-).

(译者注:该宏的总点是对于结构体中每个成员在内存中地址的分配和结构体地址的关系。仅此而已!)

引用

container_of()  explained by Greg KH
container_of() definition in Linux Kernel
Statements and Declarations in Expressions
Referring to a Type with  typeof
offsetof()


<think>我们被要求解释container_of宏的用法和原理。根据提供的引用资料,我们知道container_of宏在Linux内核中非常重要,它用于通过结构体中的某个成员变量的地址推导出整个结构体的起始地址。这在处理链表等数据结构时尤其有用。首先,我们回顾一下结构体在内存中的存储。结构体中的成员在内存中是连续存储的(尽管可能有对齐填充)。因此,如果我们知道结构体中某个成员的地址,我们可以通过减去该成员在结构体中的偏移量来得到结构体的起始地址。container_of宏的定义通常如下(取自Linux内核): ```c#define container_of(ptr,type,member)({ \consttypeof(((type*)0)->member) *__mptr =(ptr); \(type *)( (char *)__mptr -offsetof(type,member) );}) ```或者,另一种常见的写法(使用offsetof): ```c#define container_of(ptr,type,member)\(type *)( (char *)(ptr) -offsetof(type,member)) ```这里我们解释一下:1.`ptr`:指向结构体成员的指针。 2. `type`:结构体的类型。 3. `member`:结构体中该成员的名字。 宏的功能:给定一个指向结构体成员的指针`ptr`,它所属的结构体类型`type`,以及成员名`member`,返回指向整个结构体的指针。原理:-首先,我们需要计算成员`member`在结构体`type`中的偏移量。这可以通过`offsetof(type, member)`获得。`offsetof`是一个标准宏(在stddef.h中定义),用于计算结构体成员相对于结构体起始地址的偏移量。-然后,我们将给定的成员指针`ptr`(它是一个地址)减去这个偏移量,就得到了结构体的起始地址。 -注意:因为指针运算时,加减的值会根据指针类型进行调整(比如,如果指针是int*,那么加1就前进4个字节),所以我们需要先将`ptr`转换为`char*`,因为`char*`的加减操作是以字节为单位的。 另外,在第一个定义中,有一行:```cconsttypeof(((type*)0)->member) *__mptr =(ptr); ```这行代码有两个作用:1.检查`ptr`的类型是否与`type`结构体中`member`的类型一致(通过声明一个同类型的指针并赋值)。2.避免`ptr`是一个表达式(比如函数调用)时被多次求值(因为它被赋值给了一个变量__mptr)。但是,在简化版本中,我们通常只使用第二行,因为`offsetof`已经可以完成工作。 现在,我们举一个例子来说明: 假设我们有一个结构体:```cstructmy_struct {inta;char b;double c; };``` 我们有一个指向成员`c`的指针:`double*ptr= &my_struct_instance.c;`,我们想得到整个结构体的地址。 首先,计算`c`在结构体中的偏移量:`offsetof(structmy_struct, c)`。假设在某个系统上,`int`占4字节,`char`占1字节,然后有3字节填充(为了对齐`double`),那么`c`的偏移量就是8(4+1+3=8)。然后,将`ptr`(即`&c`)减去8个字节,就得到了结构体的起始地址。 使用container_of宏: ```cstruct my_struct *p =container_of(ptr, structmy_struct, c); ```这样,`p`就指向了包含这个`c`的结构体实例。 应用场景:在Linux内核中,链表(list_head)的实现就大量使用了container_of宏。链表节点通常被嵌入到其他结构体中,当我们遍历链表时,每个节点是一个list_head结构,但我们需要访问包含该节点的整个结构体,这时就用container_of宏。例如:```cstructlist_head{struct list_head *next,*prev; };struct my_struct {intdata;structlist_headlist;};//假设我们有一个指向list_head的指针posstructlist_head *pos;// ...在遍历链表时,pos指向当前节点的list_head//通过pos获取包含它的my_structstruct my_struct *item =container_of(pos,struct my_struct,list);``` 这样,我们就可以通过链表节点访问到我们自定义结构体中的数据(如`data`)。 注意事项: -使用container_of宏时,必须确保传入的指针确实是该结构体成员的正确指针。 -在用户空间也可以使用container_of,但需要自己定义offsetof(如果不在标准头文件中)和container_of宏。总结:container_of宏的核心原理就是通过成员指针减去该成员在结构体中的偏移量得到结构体首地址。它利用了结构体在内存中连续存储的特性。 相关问题: 1.为什么在container_of宏中需要将指针转换为char*类型?2.如何自己实现offsetof宏? 3.在Linux内核中,除了链表,还有哪些地方使用了container_of宏?</think>### container_of详解 #### 核心功能 `container_of` 宏用于**通过结构体成员的地址反推整个结构体的起始地址**。其函数签名为: ```c container_of(ptr, type, member) ``` - `ptr`:指向结构体成员的指针 - `type`:目标结构体类型 - `member`:成员在结构体中的名称 #### 实现原理 标准实现(简化版): ```c #define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type, member) );}) ``` **关键步骤解析**: 1. **类型安全校验**(第一行): ```c const typeof( ((type *)0)->member ) *__mptr = (ptr); ``` - `typeof( ((type *)0)->member )` 获取成员类型 - 声明临时指针 `__mptr` 并赋值,验证 `ptr` 的类型正确性 2. **地址计算**(第二行): ```c (type *)( (char *)__mptr - offsetof(type, member) ); ``` - `offsetof(type, member)` 计算成员在结构体中的偏移量 - 将成员指针转为 `char*`(确保字节级计算) - 减去偏移量得到结构体起始地址 > **关键机制**:结构体在内存中连续存储,成员地址 = 结构体首地址 + 成员偏移量[^1]。 #### 典型应用场景 1. **Linux 内核链表操作**: ```c struct list_head { struct list_head *next, *prev; }; struct task { int pid; struct list_head node; // 嵌入链表节点 }; // 通过链表节点获取宿主结构体 struct task *get_task(struct list_head *ptr) { return container_of(ptr, struct task, node); } ``` 2. **设备驱动开发**: ```c struct usb_device { int vendor_id; struct usb_endpoint ep; // 端点成员 }; void handle_endpoint(struct usb_endpoint *ep_ptr) { struct usb_device *dev = container_of(ep_ptr, struct usb_device, ep); // 通过端点操作整个设备 } ``` #### 使用注意事项 1. **指针有效性**:`ptr` 必须指向有效的结构体成员 2. **类型匹配**:`type` 和 `member` 需与实际定义严格一致 3. **内存对齐**:结构体需满足内存对齐规则,否则偏移量计算错误 4. **跨平台移植**:用户空间使用时需自行实现 `offsetof`(标准库通常提供) > **为什么用 `char*` 转换**? > 避免指针算术运算的类型自动缩放(`char*` 以字节为单位计算偏移量)[^2]。 #### 实战示例 ```c #include <stddef.h> // 定义 offsetof // 自定义结构体 struct student { int id; char name[20]; float gpa; // 目标成员 }; int main() { struct student s = {1, "Alice", 3.8}; float *pgpa = &s.gpa; // 成员指针 // 通过 gpa 成员反推 student 结构体地址 struct student *ps = container_of(pgpa, struct student, gpa); // 验证结果 printf("Original: %p\nRecovered: %p\n", &s, ps); // 输出相同地址(如 0x7ffd42a1b6c0) return 0; } ``` ### 总结要点 | 特性 | 说明 | |--------------|----------------------------------------------------------------------| | **核心目的** | 通过成员地址获取宿主结构体地址 | | **底层依赖** | 结构体连续存储原理 + 成员偏移量计算 | | **关键操作** | `offsetof` 获取偏移量,`char*` 类型转换确保字节计算 | | **典型场景** | Linux 内核链表、设备驱动、嵌入式数据结构操作 | | **优势** | 避免冗余指针存储,实现零开销数据结构嵌入 |
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值