//xk> 引子
首先看一个简单的面试题:定义一个宏FIND(stru, e),求结构体stru中某个成员e相对于stru的偏移量。
题目的解答很简单:
#define FIND(stru, e) &(((stru *)0)->e)
将常量0强制类型转化为stru *类型的指针。因为结构体的首地址为0,所以其成员的地址即为相对于结构体的偏移量。
顺便提一点,C++中不能直接用&对一个类的成员函数取地址,实现这个目的的技巧参见《C++编程思想》。C++中可以直接用&对一个类的数据成员取地址,因为数据成员和成员函数存放在不同的内存区。
//xk> 正文
Linux中广泛运用容器机制。如果struct A中嵌入包含一个子结构B,则称A是B的一个容器。
比如作为Linux标准数据结构的循环双链表,是由一个一个的表头连接起来的,而这些表头就嵌入在双链表所管理的各种类型的对象之中。注意这里的表头不是指链表的表头,而是链表的元素,是被管理对象在链表中的引子,很多时候我们称被管理对象为链表元素。这样做有两个好处:1. 被管理的对象可能类型迥异,而表头的结构统一而简单。2. 双链表应用非常广泛,因为表头结构统一,内核可以提供一套标准的循环双链表操作。
通过循环双链表很容易找到链表上的某个表头,我们关注的是怎样通过嵌入在容器中的表头,找到被管理的那个对象的首地址。
以循环双链表为例,list_entry宏提供了这个功能
// list.h
#define list_entry(ptr, type, member) container_of(ptr, type, member)
而container_of宏则是内核真正用来实现该目的的宏:根据嵌入的子结构成员返回容器首地址。
根据成员返回结构首地址,简言之:成员的地址 - 成员相对于结构首地址的便宜量 = 结构首地址。linux内核提供offsetof宏来做这个事情
// kernel.h
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) // 访问结构指针成员操作符-> 的优先级高于 取地址操作符&
注意操作符优先级:访问结构指针成员操作符-> 高于 取地址操作符& 高于 强制类型转换操作符()
到这里都很简单,不过下一步形式有点复杂:
// kernel.h
/**
* container_of 从结构的成员来获得容器实例
* @ptr: 指向成员数据的指针
* @type: 容器结构的类型
* @member: 成员在容器内的名称
*/
#define container_of(ptr, type, member) \
({ \
const typeof( ((type *)0)->member) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type, member) ); \
})
其核心依然很简单,两步走:
(1) 创建一个指针__mptr,其值等于ptr。其类型为member的类型。(《深入linux内核架构》说其类型为type,还举了个例子,应该是错的,指针不能隐式类型转换,所以__mptr与ptr类型相同才能赋值)
(2) __mptr - 偏移量即得到首地址。强制类型转换(char *)是为了保证指针运算以字节为单位。通常指针运算表示指向数组的下一个元素,因此指针 + 1的结果与指针的类型相关。强制类型转换(type *)是因为这个宏的结果应该是容器结构的类型type,返回的是容器结构首地址嘛。
为啥不直接用ptr,而要生成一个const的__mptr呢?难道container_of宏可能修改ptr?