Linux内核链表的设计初衷是为了解决不同数据类型作为链表数据节点对函数接口和封装的影响。比如以struct cat和struct dog分别形成两条链表的话,它们的接口函数(插入、删除、遍历等)都需要依赖它们自己的类型,这使得我们不得不反复的根据类型修改链表的接口函数。
Linux内核链表设计时将链表的链独立出来(struct list_head),链表的接口函数就不会受到具体节点数据影响。比如我们设计一个以struct dog的结构为链表。
常规设计:
struct dog{
unsigned short color;
char name[20];
struct dog *next, *prev;
};
// 在链表开头插入新节点
void insertHead( struct dog **h, struct dog *n )
{
n->next = *h;
if( *h != NULL ){
(*h)->prev = n;
}
*h = n;
}
图 1 常规链表设计
Linux内核链表设计:
struct list_head{
struct list_head *next, *prev;
};
struct dog{
unsigned short color;
char name[20];
struct list_head lists;
};
// 在链表开头插入新节点
void insertHead( struct list_head **h, struct list_head *n )
{
n->next = *h;
if( *h != NULL ){
(*h)->prev = n;
}
*h = n;
}
图 2 内核链表设计
如上示例代码和图可见,常规链表设计节点数据类型和链表操作函数耦合性高,当类型发生变化,链表操作函数必须重新实现。而内核链表设计可以有效避免上述弊端,它的链的操作和具体节点数据类型没有任何关系。内核链表的设计解决的链表接口一致问题,但是却出现的访问问题。从内核链表头指针出发,我们只能访问到struct list_head类型(图2中底色为橘色部分)。为了解决访问问题,我们就设计了一个非常经典的宏定义container_of,具体定义如下:
#define container_of( ptr, type, member )
(type *)( (char *)ptr – (unsigned long)&((type*)0)->member )
container_of的核心含义是:已知结构体对象中任何一个成员的地址,可以找到结构体对象的首地址。所以根据container_of我们可以找到图2中整个节点的首地址。测试代码如下:
int main()
{
struct list_head *head = NULL;
int i = 0;
while( i < 5 ){
struct dog *d = malloc( sizeof( struct dog ) );
d->lists.next = NULL;
d->lists.prev = NULL;
d->color = rand() % 0xffffff;
sprintf( d->name, "wangcai%d", i );
insertHead( &head, &d->lists );
i++;
}
struct list_head *p = head;
while( p != NULL ){
struct dog *t = container_of( d, struct dog, lists );
printf( "t->name = %s, t->color = %x ", t->name, t->color );
p = p->next;
}
return 0;
}