container_of
是 Linux 内核中一个非常重要且广泛使用的宏。它允许你从一个指向结构体成员的指针反向获取整个结构体的指针。下面来详细解释它:
container_of
宏的定义 通常定义在内核源码的这个头文件 <linux/kernel.h>
中
#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))); })
让我们逐行解析这个宏:
#define container_of(ptr, type, member) ({ ... })
- 这是一个宏定义,它接受三个参数:
ptr
: 指向结构体成员的指针。type
: 包含该成员的结构体的类型。member
:ptr
所指向的成员在结构体type
中的名称。
({...})
:这是 GCC 的一个扩展,称为“语句表达式 (Statement Expression)”。它允许一个代码块像一个表达式一样返回一个值,这个值就是代码块中最后一条语句的值。这使得宏可以更安全地引入局部变量并返回计算结果。
- 这是一个宏定义,它接受三个参数:
void *__mptr = (void *)(ptr);
ptr
: 这是传入的指向成员的指针。(void *)(ptr)
: 将ptr
强制转换为void *
类型。这样做是为了进行通用的指针运算,避免编译器因为类型不匹配而发出警告或错误。void *
是一种通用指针,可以指向任何类型的数据。void *__mptr
: 声明一个名为__mptr
的void *
类型的临时指针变量,并用转换后的ptr
初始化它。变量名前的__
(双下划线) 是一种常见的约定,用于表示这是宏内部使用的临时变量,以避免与用户代码中的变量名冲突。
BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) && !__same_type(*(ptr), void), "pointer type mismatch in container_of()");
- 这是一个编译时检查,用于确保类型的正确性,是
container_of
宏安全性的关键。 __same_type(A, B)
: 这通常是内核内部实现的另一个宏或编译器内置功能,用于检查类型A
和类型B
是否完全相同。如果相同,返回 1 (true),否则返回 0 (false)。*(ptr)
: 对ptr
指针进行解引用,获取该成员本身。这主要用于类型检查。((type *)0)->member
: 这是一个巧妙的技巧,用于获取结构体type
中成员member
的类型,而无需一个实际的结构体实例。(type *)0
: 将整数0
(空指针) 强制转换为指向type
类型的指针。->member
: 通过这个空指针访问成员member
。编译器在编译时知道member
的类型。
!__same_type(*(ptr), ((type *)0)->member)
: 检查ptr
指向的实际类型(通过*(ptr)
获取)是否与type
结构体中member
成员的声明类型不同。!__same_type(*(ptr), void)
: 允许ptr
本身是一个void*
。如果ptr
是void*
,那么*(ptr)
的类型也是void
,这个检查确保如果ptr
是void*
,则不会触发类型不匹配错误(因为__mptr
已经将其转换为void*
)。主要目的是第一个类型检查。&&
: 逻辑与操作。BUILD_BUG_ON_MSG(condition, message)
: 如果condition
为真 (即类型不匹配,并且ptr
指向的类型也不是void
),这个宏会在编译时产生一个错误,并显示message
(“pointer type mismatch in container_of()”)。这有助于在早期发现潜在的编程错误。
- 这是一个编译时检查,用于确保类型的正确性,是
((type *)(__mptr - offsetof(type, member)));
- 这是宏的核心计算部分,用于获取容器结构体的起始地址。
offsetof(type, member)
: 这是一个标准的 C 宏 (定义在<stddef.h>
中,内核有自己的实现),它返回成员member
在结构体type
中的偏移量 (以字节为单位),即从结构体起始地址到该成员起始地址的字节数。__mptr - offsetof(type, member)
: 进行指针运算。__mptr
指向成员的起始地址。从这个地址减去该成员在结构体中的偏移量,得到的就是整个结构体的起始地址。由于__mptr
是void *
,指针运算通常按字节 (char *) 进行。(type *)
: 将计算得到的地址 (它实际上是一个void *
或char *
类型) 强制转换为指向type
类型的指针。- 这个表达式的值就是整个
container_of
宏的返回值,即指向包含ptr
所指成员的type
类型结构体的指针。
使用场景
container_of
在 Linux 内核中被广泛使用,尤其是在处理链表、回调函数和面向对象风格的编程时:
- 链表操作:
- 内核中的链表 (
struct list_head
) 通常是嵌入到其他结构体中的。当你遍历链表并获得一个list_head
成员的指针时,你需要使用container_of
来获取包含该list_head
的整个数据结构。 - 例如,
list_for_each_entry(pos, head, member)
宏内部就依赖container_of
。
- 内核中的链表 (
- 回调函数:
- 当注册一个回调函数时,有时你可能会传递一个指向某个结构体成员的指针作为上下文。在回调函数被调用时,如果需要访问整个结构体,就可以使用
container_of
。
- 当注册一个回调函数时,有时你可能会传递一个指向某个结构体成员的指针作为上下文。在回调函数被调用时,如果需要访问整个结构体,就可以使用
- 面向对象设计:
- 在 C 语言中模拟面向对象编程时,一个通用的“基类”结构体可能被嵌入到一个更具体的“派生类”结构体中。如果你有一个指向基类部分的指针,可以使用
container_of
来获取整个派生类结构体的指针。例如,内核中的kobject
系统。
- 在 C 语言中模拟面向对象编程时,一个通用的“基类”结构体可能被嵌入到一个更具体的“派生类”结构体中。如果你有一个指向基类部分的指针,可以使用
- 设备驱动程序:
- 驱动程序中常常会将一个通用的设备结构体 (如
struct device
) 嵌入到特定设备的结构体中。通过通用结构体的指针,可以用container_of
找到特定设备结构体的指针。
- 驱动程序中常常会将一个通用的设备结构体 (如
重要性和优势
- 代码复用和模块化:
- 允许编写通用的数据结构操作代码 (如链表处理函数),这些函数只需要知道成员的类型和名称,而不需要知道包含该成员的特定结构体类型。这极大地增强了代码的复用性。
- 类型安全:
- 通过
BUILD_BUG_ON_MSG
和__same_type
检查,container_of
在编译时就能捕捉到许多类型不匹配的错误。这比在运行时发生难以追踪的内存损坏要好得多。
- 通过
- 代码清晰度和可读性:
container_of(ptr, type, member)
的意图非常明确:从成员指针获取容器指针。这比手动进行指针运算和偏移量计算要清晰得多,也更不容易出错。
- 灵活性:
- 使得在结构体中嵌入通用组件 (如锁、引用计数器、链表头) 变得非常方便,同时还能轻松地从这些组件的指针回到主结构体。
- 效率:
- 由于它是一个宏,并且主要进行编译时检查和简单的指针运算,所以几乎没有运行时开销。计算结果在编译时就已确定或能高效计算。
本文版权归原作者zhaofujian所有,采用 CC BY-NC-ND 4.0 协议进行许可,转载请注明出处。