1. 前言

今天在看 Linux 内核源码中有关链表数据结构时,遇到到 container 宏和 offsetof 宏,然后跳转到其定义处,发觉它的定义的形式好陌生并难以理解,所以就作此笔记记录一下。

2. offsetof 宏

2.1 宏的功能

offsetof 宏的功能就是获得结构体变量中的指定成员变量相对于结构体变量起始地址的偏移量。

2.2 宏的定义

offsetof 宏的定义如下所示:

// 代码所在路径:/linux-3.18.6/include/linux/stddef.h
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

我们可以分 4 步来对这个宏进行解析:

  • step 1:((TYPE *)0) 0 地址强制 “转换” 为 TYPE 结构类型的指针;
  • step 2:((TYPE *)0)->MEMBER 访问 TYPE 结构中的 MEMBER 数据成员;
  • step 3:&(((TYPE *)0 )->MEMBER) 取出 TYPE 结构中的数据成员 MEMBER 的地址;
  • step 4:(size_t)(&(((TYPE*)0)->MEMBER)) 结果转换为 size_t 类型。

在这里,我以前有个想不明白的地方就是我们最后从 step 3 获得的不是成员变量 MEMBER 的地址嘛,怎么会最终成了 MEMBER 相对于结构体变量的首地址了呢?

最终通过参考文章 1,我才恍然大悟领略到 offsetof 宏的巧妙之处。它通过将 0 地址强制转换为 TYPE 结构类型的指针,TYPE 结构体以内存空间首地址 0 作为起始地址,则成员地址自然就是偏移地址了。

2.3 使用例子

#include <stdio.h>
#include <stddef.h>

struct S {
    char c;
    int i;
};

int main() {

    printf("the first element is at offset %zu\n", offsetof(struct S, c));
    printf("the double is at offset %zu\n", offsetof(struct S, i));
    return 0;
}

编译运行得到的结果是:

$ gcc offsetof_macro_test.c -o offsetof_macro_test
$ ./offsetof_macro_test 
the first element is at offset 0
the double is at offset 4

3. container_of 宏

3.1 宏的功能

container_of 宏是一个函数宏,它的功能就是通过一个结构变量中一个成员的地址找到这个结构体变量的首地址。下面我们看一个例子:

struct test
{
int i;
int j;
char k;
};

struct test temp;

现在如果我们想通过 temp.j 的地址找到 temp 的首地址就可以使用 container_of(&temp.j,struct test,j)来实现这一功能。

3.2 宏的定义

下面我们就来具体分析一下 container_of 宏的定义。

container_of() 宏的定义如下:

// 代码所在路径:/linux-3.18.6/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) );})

首先,我们看一下函数宏中括号内传入的参数的含义分别是:

  • ptr: 一个指针,指向结构体中的成员变量
  • type: 结构体的类型
  • member: 结构体中成员变量,即 ptr 所指向的成员变量

现在我们根据把上面例子中的 struct test temp 变量来展开 container_of() 定义中的第 10 行代码,替换后的结果如下所示:

const typeof(((struct test *)0)->j) * __mptr = (&temp.j);

中,typeof 是 GNU C 对标准 C 的扩展,它的作用是根据变量获取变量的类型。因此,上述代码的作用是首先使用 typeof 获取结构体成员 j 的类型为 int,然后顶一个 int 指针类型的临时变量 __mptr ,并将结构体变量中的成员的地址赋给临时变量 __mptr 。

下面我们接着来分析 container_of() 中第 12 行代码的含义。同样以上面的 struct test 为例,我们展开代码后得到的结果如下所示:

(struct test *)((char *)__mptr - offsetof(struct test,j));

在前面我们已经知道 offsetof 也是一个函数宏,它的作用就是获得结构体变量中的指定成员变量相对于结构体变量起始地址的偏移量。所以上面展开的代码可以分为下面 3 个步骤来理解:

  • step 1:将通过 container_of 宏获得结构体成员变量 j 的内存地址__mptr强制转换成 (char*) 类型。
  • step 2:采用 offsetof 宏来获得结构体成员变量 j 在结构体中的偏移量。
  • step 3:然后将结构体成员变量 j 的内存地址__mptr减去结构体成员变量 j 在结构体中的偏移量,这样就得到的就是结构体变量的起始地址。最后,通过一个强制转换将结果转换成 (struct test *)型的指针。

3.3 使用例子

#include <stdio.h>
#include <stddef.h>

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

struct test
{
    int i;
    int j;
    char k;
};

int main() {

    struct test temp;
    printf("&temp = %p\n", &temp);
    printf("&temp.j = %p\n", &temp.j);
    printf("container_of(&temp.j,struct test,j) = %p\n", container_of(&temp.j, struct test, j));

    return 0;
}

编译运行后的结果如下所示:

$ gcc container_of_macro_test.c -o container_of_macro_test
$ ./container_of_macro_test 
&temp = 0x7fff25b39000
&temp.j = 0x7fff25b39004
container_of(&temp.j,struct test,j) = 0x7fff25b39000

参考文章

  1. offset宏的讲解
  2. container_of分析
  3. 揭开linux内核中container_of的神秘面纱