Featured image of post 深入解析 container_of​ 宏:原理、实现与应用

深入解析 container_of​ 宏:原理、实现与应用

一、引言

在 C 语言中,由于其本身缺乏如 C++ 的反射、类继承、成员指针反向查找等高级特性,开发者经常需要借助一些技巧来实现类似“对象关联”或“结构体成员反向定位”的功能。在 Linux 内核以及众多系统级 C 程序中,有一个极为重要且经典的宏,用于解决如下问题:

已知一个结构体中的某个成员的指针,如何获取包含该成员的整个结构体的指针?

这个问题的解决方案就是:container_of

它不仅是 Linux 内核中数据结构(如链表、设备模型等)实现的基础工具,也是理解 C 语言指针运算、宏编程、类型系统和内存模型的极佳案例。


二、container_of 的作用

1. 问题背景

在 C 语言中,我们常常定义如下的结构体:

1struct Person {
2    int age;
3    char name[50];
4    void *private_data;
5};

当我们仅持有一个指向结构体某个成员的指针(例如 &p.private_data)时,标准 C 并没有提供直接的方法来获取该成员所属的整个结构体对象的指针(例如 &p)。

2. container_of 的功能

container_of通过一个结构体成员的指针,计算并返回包含该成员的整个结构体的指针

换句话说,它是:

一种通过“部分”信息(成员指针)反推出“整体”对象(宿主结构体)的机制。


三、container_of 宏的标准实现

在 Linux 内核中,container_of 宏通常定义为如下形式(使用 GCC 扩展语法):

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

⚠️ 注意:此实现使用了 GCC 的语句表达式( ({ ... }) typeof 关键字,属于 GCC 扩展语法,并非标准 C,但在 Linux 内核中被广泛采用。

若需编写更具可移植性、符合标准 C 的版本,可简化为:

1#define container_of(ptr, type, member) \
2    ((type *)((char *)(ptr) - offsetof(type, member)))

下面我们将围绕这个宏,深入讲解它的组成、原理、关键点与实现细节


四、container_of 的组成与工作原理

1. 宏接口说明

1container_of(ptr, type, member)
参数含义
ptr指向结构体中某个成员的指针(例如&p.private_data
type该成员所属的结构体类型(例如struct Person
member该成员在结构体中的名称(例如private_data

2. 宏的工作目标

通过传入的成员指针 ptr,计算并返回包含该成员的整个结构体对象的指针


3. 核心实现原理

container_of 的实现依赖于以下两个关键技术:

  1. offsetof(type, member) :计算成员在结构体中的偏移字节数
  2. 指针运算:通过成员指针减去其偏移量,反推出结构体起始地址

五、offsetof 宏:偏移量计算的核心

1. 什么是 offsetof

offsetof(type, member) 是一个,用于计算结构体 type 中的成员 member 相对于结构体起始地址的偏移字节数

例如,如果结构体起始地址为 0x1000,而成员 private_data 的地址为 0x1036,那么:

1offsetof(struct Person, private_data) == 0x36  // 即 54 字节

2. offsetof 的标准实现

在大多数标准 C 库(如 glibc)和编译器中,offsetof 的实现通常如下:

1#define offsetof(type, member) ((size_t) &((type *)0)->member)

详细解析:

  • (type *)0:将整数值 0 强制转换为一个指向 type 类型的指针。它表示 “假设存在一个 type 类型的对象,其地址为 0”

    ⚠️ 注意:这里并没有真的访问地址 0 的内存,只是构造了一个指针用于类型推导和偏移计算。

  • ((type *)0)->member:访问该(假设)对象的成员 member

  • &((type *)0)->member取该成员的地址。由于结构体假设位于地址 0,因此该地址的值就是成员相对于结构体起始地址的偏移字节数

  • (size_t):将得到的地址值(指针)强制转换为 size_t 类型(一种无符号整型,通常用于表示大小或偏移),最终得到一个整数,表示成员在结构体中的偏移量。


3. 为什么 offsetof 是安全的?

尽管代码中出现了 (type *)0,但由于我们仅用它来计算成员的地址,而并未真正解引用该指针去访问内存,因此:

✅ 它是编译期完成的计算 ✅ 它不会在运行时访问地址 0 的内存 ✅ 它是安全且被标准广泛接受的做法


六、container_of 宏的逐步解析

以如下调用为例:

1struct Person *p_ptr = container_of(&p.private_data, struct Person, private_data);

假设该宏展开后为:

1(type *)((char *)__mptr - offsetof(type, member))

步骤解析:

  1. 输入:

    • ptr 是指向成员的指针,例如 &p.private_data
    • type 是结构体类型,例如 struct Person
    • member 是成员名称,例如 private_data
  2. 计算成员偏移量:

    • 调用 offsetof(type, member),得到成员 private_data 在结构体中的偏移字节数(例如 54)
  3. 将成员指针转换为字节指针:

    • ptr(可能为 void * 类型)强制转换为 char *,因为 char 的大小为 1 字节,便于进行按字节精确计算
  4. 计算结构体起始地址:

    • 用成员指针的地址,减去该成员的偏移量,得到结构体的起始地址
  5. 类型转换:

    • 将计算出的地址强制转换回结构体类型指针,如 (struct Person *),从而得到指向整个结构体的指针

七、完整示例代码

下面是一个完整、可运行的示例程序,演示 container_of 的使用与原理:

 1#include <stdio.h>
 2#include <stddef.h>
 3
 4// 定义 container_of 宏(标准 C 兼容版本)
 5#define container_of(ptr, type, member) \
 6    ((type *)((char *)(ptr) - offsetof(type, member)))
 7
 8// 定义测试结构体
 9struct Person {
10    int age;
11    char name[50];
12    void *private_data;
13};
14
15int main() {
16    struct Person p;
17    p.age = 25;
18    snprintf(p.name, sizeof(p.name), "Alice");
19    p.private_data = (void *)0xDEADBEEF;
20
21    // 获取成员 private_data 的指针
22    void *ptr = &p.private_data;
23
24    // 通过 container_of 获取包含该成员的结构体指针
25    struct Person *p_found = container_of(ptr, struct Person, private_data);
26
27    // 输出验证
28    printf("Original person   : %p\n", &p);
29    printf("Found person      : %p\n", p_found);
30    printf("Member private_data: %p\n", p_found->private_data);
31
32    if (p_found == &p) {
33        printf("✅ container_of 成功找到了原始结构体!\n");
34    } else {
35        printf("❌ 错误:未正确找到结构体\n");
36    }
37
38    return 0;
39}

输出示例:

1Original person   : 0x7ffd12345678
2Found person      : 0x7ffd12345678
3Member private_data: 0xdeadbeef
4✅ container_of 成功找到了原始结构体!

八、关键技术与疑难点解析

1. 为什么需要 container_of

C 语言没有内置的机制可以从成员指针反推宿主结构体指针。而实际开发中(尤其是在实现通用容器、链表、面向对象风格的代码时),这一功能极其重要。


3. offsetof 的实现安全吗?

✅ 是的。虽然它使用了 (type *)0,但仅用于计算成员地址,而并未真正访问该地址的内存,因此是安全的。


4. container_of 返回的是什么?

它返回一个 指向结构体的指针,类型为 type *,即你传入的结构体类型。

不是常数,也不是宏直接替换为值,而是在预处理阶段展开为一段代码,最终在运行时计算并返回一个指针。


5. 为什么使用 typeof(在 GCC 版本中)?

typeof 用于自动推导成员的类型,使得 container_of 宏可以适用于任意结构体和任意成员,具有高度的通用性。标准 C 中可用显式类型替代,但通用性会降低。


九、总结

项目说明
作用通过结构体成员的指针,获取包含该成员的整个结构体的指针
核心原理成员指针 - 成员偏移量 = 结构体起始地址
关键宏offsetof(type, member):计算成员偏移量
实现方式指针运算:(char *)member_ptr - offsetof(...)
典型应用Linux 内核链表、设备模型、通用容器、面向对象风格代码
扩展知识涉及指针类型转换、宏编程、编译期计算、内存模型等 C 语言高级主题