container_of(ptr, type, member) 解析

深入理解container_of宏
本文详细解析了Linux内核中的container_of宏的工作原理,通过实例展示了如何从结构体成员指针回溯到整个结构体指针,适用于内核及复杂系统编程场景。

看代码的时候经常看到contain_of函数的使用,之前也看过其原理,但是没有深入理解,今天有时间就抽空好好研究下其来龙去脉。该宏在include/linux/kernel.h中实现的:

/** 
 * 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) ({          \  
    const typeof( ((type *)0)->member ) *__mptr = (ptr); \  
    (type *)( (char *)__mptr - offsetof(type,member) );})  

#define offsetof(TYPE, MEMBER)  ((size_t)&((TYPE *)0)->MEMBER)

它的作用显而易见,那就是根据一个结构体变量中的一个域成员变量的指针来获取指向整个结构体变量的指针。比如,有一个结构体变量,其定义如下:

struct demo_struct {
           type1 member1;
           type2 member2;
           type3 member3;
           type4 member4;
};     
struct demo_struct demo;

同时,在另一个地方,获得了变量demo中的某一个域成员变量的指针,比如:

type3  *memp = get_member_pointer_from_somewhere();

此时,如果需要获取指向整个结构体变量的指针,而不仅仅只是其某一个域成员变量的指针,我们就可以这么做:

struct demo_struct *demop = container_of(memp, struct demo_struct, member3);

这样,我们就通过一个结构体变量的一个域成员变量的指针获得了整个结构体变量的指针。
下面说一说我对于这个container_of的实现的理解:
首先,我们将container_of(memp, struct demo_struct, type3)根据宏的定义进行展开如下:

      struct demo_struct *demop = ({                      /
         const typeof( ((struct demo_struct *)0)->member3 ) *__mptr = (memp);    /
         (struct demo_struct *)( (char *)__mptr - offsetof(struct demo_struct, member3) );})

其中,typeof是GNU C对标准C的扩展,它的作用是根据变量获取变量的类型。因此,上述代码中的第2行的作用是首先使用typeof获取结构体域变量member3的类型为 type3,然后定义了一个type3指针类型的临时变量__mptr,并将实际结构体变量中的域变量的指针memp的值赋给临时变量__mptr。

(char )__mptr转换为字节型指针。(char )__mptr - offsetof(type,member) )用来求出结构体起始地址(为char 型指针),然后(type )( (char )__mptr - offsetof(type,member) )在(type )作用下进行将字节型的结构体起始指针转换为type *型的结构体起始指针。
假设结构体变量demo在实际内存中的位置如下图所示:
demo
+————-+ 0xA000
| member1 |
+————-+ 0xA004
| member2 |
+————-+ 0xA010
| member3 |
+————-+ 0xA018
| member4 |
+————-+
则,在执行了上述代码的第2行之后__mptr的值即为0xA010。
再看上述代码的第3行,其中需要说明的是offsetof,它定义在include/linux/stddef.h中,其定义如下:

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

先分析一下这个宏的运行机理:

  • ( (TYPE *)0 ) 将零转型为TYPE类型指针;
  • ((TYPE *)0)->MEMBER 访问结构中的数据成员;
  • &( ( (TYPE *)0 )->MEMBER )取出数据成员的地址;
  • (size_t)(&(((TYPE*)0)->MEMBER))结果转换类型。巧妙之处在于将0转换成(TYPE*),结构以内存空间首地址0作为起始地址,则成员地址自然为偏移地址;

    同样,我们将上述的offsetof调用展开,即为:

(struct demo_struct *)( (char *)__mptr - ((size_t) &((struct demo_struct *)0)->member3) );

可见,offsetof的实现原理如上所述,就是取结构体中的域成员相对于地址0的偏移地址,也就是域成员变量相对于结构体变量首地址的偏移。
因此,offsetof(struct demo_struct, member3)调用返回的值就是member3相对于demo变量的偏移。结合上述给出的变量地址分布图可知,offsetof(struct demo_struct, member3)将返回0x10。
于是,由上述分析可知,此时,__mptr==0xA010,offsetof(struct demo_struct, member3)==0x10。
因此, (char )__mptr - ((size_t) &((struct demo_struct )0)->member3) == 0xA010 - 0x10 == 0xA000,也就是结构体变量demo的首地址(如上图所示)。
这就是从结构体某成员变量指针来求出该结构体的首指针。指针类型从结构体某成员变量类型转换为该结构体类型。
由此,container_of实现了根据一个结构体变量中的一个域成员变量的指针来获取指向整个结构体变量的指针的功能。
感谢原作者:https://blog.youkuaiyun.com/yasin_lee/article/details/5700583

<think>我们正在讨论container_of宏,它是在Linux内核中广泛使用的一个宏,用于通过结构体成员的地址来获取整个结构体的地址。 首先,让我们看一下container_of宏的定义(通常位于linux/kernel.h中): ```c #define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type, member) );}) ``` 或者,有时也使用另一种形式(不包含类型检查): ```c #define container_of(ptr, type, member) \ (type *)( (char *)(ptr) - offsetof(type, member) ) ``` 但通常我们使用第一种,因为它有类型检查。 解释: 1. `ptr`:指向结构体成员的指针。 2. `type`:结构体的类型。 3. `member`:结构体中该成员的名称。 宏的作用:给定一个结构体成员的指针(ptr),该成员在结构体(type)中的名字(member),返回该结构体实例的指针。 步骤: a) 首先,使用`typeof`获取成员的类型,并创建一个指向该类型的临时指针`__mptr`,并将其赋值为ptr。这一步主要是为了进行类型检查,确保ptr确实是指向该成员类型的指针。 b) 然后,计算成员在结构体中的偏移量(使用`offsetof`宏)。`offsetof`宏定义在`stddef.h`中,用于计算结构体成员相对于结构体起始地址的偏移量。 c) 最后,将成员指针(转换为char*,以便按字节计算)减去该偏移量,得到结构体的起始地址,然后将其转换为`type*`类型。 举个例子: 假设我们有一个结构体: ```c struct sample { int a; char b; double c; }; ``` 现在,我们有一个指向结构体中成员c的指针:`double *p = &sample_instance.c;` 我们可以通过`container_of(p, struct sample, c)`来得到指向整个结构体实例的指针(即&sample_instance)。 为什么需要这个宏? 在内核中,我们经常需要从结构体的某个成员指针反推回整个结构体的指针。例如,在内核的链表实现中,链表节点被嵌入到各种结构体中,当我们遍历链表时,我们得到的是链表节点的指针,但我们希望得到的是包含该节点的结构体的指针,这样我们就可以访问结构体中的其他数据。 代码实现细节: 1. `offsetof(type, member)`:这个宏通常定义为: ```c #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) ``` 它的原理是:将0地址强制转换为`TYPE*`类型,然后获取其成员`MEMBER`的地址,这个地址就是该成员相对于结构体起始地址的偏移量(因为结构体起始地址为0)。 2. 在container_of宏中,我们首先将ptr赋值给一个临时变量__mptr(并带有类型检查),然后计算偏移量,最后用(char*)ptr减去偏移量得到结构体的地址。 注意:使用`char*`进行指针运算,因为char类型的大小是1字节,这样偏移量就是按字节计算的。 安全性: 第一个版本的container_of宏中,使用`typeof`来检查ptr的类型是否与结构体中成员的类型一致。这可以防止一些类型错误。 使用场景: 在Linux内核中,链表(list_head)、哈希表、红黑树等数据结构都使用了container_of宏。例如,当我们定义一个包含链表节点的结构体: ```c struct my_struct { int data; struct list_head list; }; ``` 当我们遍历链表时,我们得到的是`struct list_head *`,然后通过`container_of`可以获取到包含它的`struct my_struct`的指针。 示例代码片段(内核链表): ```c struct list_head *pos; list_for_each(pos, &my_list) { struct my_struct *entry = container_of(pos, struct my_struct, list); // 现在可以使用entry->data } ``` 总结:container_of宏是Linux内核中一个非常基础且重要的宏,它通过结构体成员的地址计算出结构体的地址,在内核的数据结构管理中起着核心作用。 §§相关问题§§ 1. container_of宏中的`({ ... })`语法是什么? 2. 为什么container_of宏需要临时变量`__mptr`? 3. 在C语言中,`offsetof`宏是如何工作的? 4. 能否举一个使用container_of宏的实际内核代码例子? 5. container_of宏在非Linux环境下是否可以使用?</think>### `container_of`宏的解释 #### 定义和作用 `container_of`宏是Linux内核中广泛使用的核心工具,其作用是通过**结构体成员的地址反推整个结构体的起始地址**。它在内核链表、设备驱动等场景中至关重要,解决了"已知成员指针,如何获取其父结构体"的问题。 #### 代码实现解析 ```c // 标准定义(Linux内核版本) #define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ // 类型安全检查 (type *)( (char *)__mptr - offsetof(type, member) );}) // 地址计算 ``` ##### 关键组成部分: 1. **类型安全检查**(第2行): ```c const typeof( ((type *)0)->member ) *__mptr = (ptr); ``` - `typeof( ((type *)0)->member )`:获取成员`member`的类型 - 创建临时指针`__mptr`并赋值为`ptr`,确保`ptr`的类型与`member`匹配 - 若类型不匹配,编译器会报错 2. **地址计算**(第3行): ```c (type *)( (char *)__mptr - offsetof(type, member) ); ``` - `offsetof(type, member)`:计算成员在结构体中的偏移量 - `(char *)__mptr`:将指针转为字节地址(便于按字节计算偏移) - 成员地址 - 偏移量 = 结构体起始地址 3. **`offsetof`宏原理**: ```c #define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER) ``` - 将地址`0`强制转换为`(TYPE *)`类型 - 获取成员`MEMBER`的地址,其值即偏移量(因结构体起始地址为0) #### 工作示例 假设有以下结构体: ```c struct person { int age; char name[20]; struct list_head node; // 链表节点 }; ``` 当遍历链表时获得`node`指针: ```c struct list_head *curr_node = ...; // 当前节点指针 ``` 通过`container_of`获取父结构体: ```c struct person *p = container_of(curr_node, struct person, node); ``` 计算过程: 1. 计算`node`在`struct person`中的偏移量(如24字节) 2. `(char *)curr_node - 24`得到`struct person`的起始地址 3. 转换为`struct person *`类型 #### 核心用途 1. **内核链表实现** 通过嵌入`struct list_head`成员,实现通用链表操作,避免为每种结构体重写链表逻辑[^1]。 2. **设备驱动模型** 在字符设备驱动中,通过`file->private_data`成员找回设备控制结构体: ```c struct my_device *dev = container_of(file->private_data, struct my_device, cdev); ``` 3. **对象关系映射** 解决"成员指针→父对象"的逆向定位问题,减少冗余指针存储。 #### 注意事项 1. **严格类型匹配** 依赖编译器的类型检查,避免错误使用。 2. **非标准语法** `({...})`是GCC扩展语法(语句表达式),非标准C特性。 3. **内存安全** 需确保`ptr`确实指向目标结构体的成员,否则会导致非法访问。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值