Linux container_of详解

avatar
@github.com/NasdaqGodzilla

container_of简介

container_of是很重要的一个功能宏,它可以基于一个成员来取得成员所在的结构体。

image.png

container_of定义和使用

/**
 * 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))); })
  • 参数1:成员的指针
  • 参数2:成员所在的结构体名字
  • 参数3:该成员在结构体里面的名字
  • 返回值:返回所在结构体的指针,类型是参数2指定的

使用示例:

struct a_dev {
    struct device* dev;
    int flag;
    struct cdev c_dev;
}

struct cdev *my_cdev = ...;
struct a_dev *res = container_of(my_cdev, a_dev, c_dev);

container_of详解

container_of分为三个主要的组成部分,分别是小括号+花括号组成的赋值语句、offsetof语句、指针运算语句。

整体赋值

container_of本身是通过一对小括号和一对花括号组成的复合赋值语句。可以看到宏定义里面有三个语句(分号分割),然后用小括号({})包起来返回给调用者。伪代码表示的逻辑如下。

auto result = container_of(成员指针,结构体名称,成员在结构体中的名称);

#define container_of(成员指针,结构体名称,成员名称)
    ({
        语句1-成员指针类型转为万能指针;
        语句2-合法性判断;
        语句3-offset计算和指针地址运算;
    })

从C语言的({})赋值方法知道,({})的返回值就是最后一个语句的值,因此container_of宏的整体意义就是把运算后的指针返回给调用者。

offsetof计算成员到结构体头的偏移

offsetof的定义如下。

#undef offsetof
#ifdef __compiler_offsetof
#define offsetof(TYPE, MEMBER)  __compiler_offsetof(TYPE, MEMBER)
#else
#define offsetof(TYPE, MEMBER)  ((size_t)&((TYPE *)0)->MEMBER)
#endif

其核心定义是((size_t)&((TYPE *)0)->MEMBER)。该语句分解如下。

  1. ((TYPE *)0):把地址0当作存储了一个TYPE,取为指针
  2. ((TYPE *)0)->MEMBER:struct结构体访问成员的编译原理实际上是指针偏移,这里利用->来“访问”MEMBER
  3. 加一个&:前面已经访问到了MEMBER,对它加上&,实际上等同于计算了地址0按struct TYPE头地址相对MEMBER的偏移,相当于假设地址0有一个TYPE数据,这一步就取到了MEMBER成员的地址,即MEMBER相对于TYPE头的偏移
  4. (size_t):进行强制类型转换,把偏移变成“大小”

选用0地址的原因是如果结构体头地址是0,那么其成员的地址就是偏移的大小

对0地址做了"->调用",这里利用了编译器进行的地址加减偏移,实际上编译器编译后就是数值0的加减,并未发生调用,不会产生空指针问题。

image.png

指针运算

第二步我们已经获取到了MEMBER相对于TYPE的偏移大小,而我们本身就知道了MEMBER的地址,那么把MEMBER抵消掉这段偏移,得到的就是TYPE的地址。再做一个强制类型转换,就取得了MEMBER所在的TYPE了。

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