1. list_head
linux
的内核实现中使用了很多数据结构,如链表,队列,红黑树等。其中链表的实现很独特,它将链表的前后指针提了出来,定义为一个结构体,然后包含该结构体的数据结构就成为了链表上的一员。即实现了链表指针与数据的分离,使其更具有通用性。其数据结构如下:
struct list_head{
struct list_head *pre;
struct list_head *next;
}
例如,在通常版本中,如果定义一个列车的链表,是这样的:
struct train{
char *color;
int capacity;
//...other member
struct train *pre;
struct train *next;
}
很明显,它在某个特定的结构体(train
)当中定义了指向该类型结构体的指针(train *
)。而如果使用linux
内核的做法,是这样的:
struct train{
char *color;
int capacity;
//...other memner
list_head *node;
}
node
中存放着链表的前后指针,而不代表任何具体的数据类型,它不关心节点上的数据是什么,只关注前后节点,这种抽象是很独特的且很巧妙的。
2. 获取数据
那么问题可能来了,如果链表的指针只是单纯地指向list_head
这样的数据结构,它如何访问链表节点上的数据呢,在上例中,就是如何访问train
的成员呢?
显然linux考虑过这个问题,于是使用了两个更为巧妙的宏来实现对节点数据的访问:offsetof
和container_of
。
2.1 offsetof
offsetof
用来获得结构体中成员的偏移量,宏定义如下:
//定义在 stddef.h 中
#define offsetof(type, member) (size_t)&(((type*)0)->member)
很巧妙吧,它将地址0强制转换成type
类型的指针,也就是说该结构体从地址0处开始存放,然后访问其成员member
,并对其取地址,取得的地址就是该成员member
相对0地址的偏移量。
2.2 container_of
container_of
提供了从成员变量地址获得结构体地址的方法,宏定义如下:
//定义在 linux/kernel.h 中
#define container_of(ptr, type, member)({ \
const typeof(((type *)0)->member) *__mptr = (ptr); \
(type *) ((char *)__mptr - offsetof(type, member));})
依然很巧妙,使用一个member
类型的指针__mptr
保存member
的实际地址,然后用该实际地址减去member
在结构体type
中的偏移量,即可获得结构体的实际地址。其中typeof
操作符提供了编译时的多态,它是gcc的一个扩展,用于获取指定表达式结果的类型[1]。
3. 待续
好了,现在我们拥有了独立于数据之外的链表,以及能够从成员获取结构体地址的宏,之前的问题也可以解决了:linux
通过获取list_head
的地址,可以得到结构体的地址,进而顺利访问结构体的成员。
那么还有一个疑问:结构体的偏移量是在编译时期就确定的吗?回答是是的,要不然这些宏不可能起作用,事实上是在ABI
阶段就确定了。
链表的添加和删除操作等以后有机会在看一下,毕竟这是链表最重要的操作了。
参考资料
[1] gcc typeof在kernel中的使用
[2]《linux内核设计与实现》
[3] offsetof与container_of宏