Linux源码中include/linux/list.h封装了双向链表的数据结构。
网上实现双向链表的方式大抵相同,都是定义一个struct Node表示链表的节点,Node结构体包含指向节点数据的指针,和指向节点的next和pre指针:
1 struct Node { 2 void *data; 3 Node *next; 4 Node *pre; 5 };
这种方式将存放的数据保存在链表的节点内部,结构图如下:
list.h的实现思路很有趣。上图中存放的数据(绿色部分),被Node节点封装,耦合度较高。存放的数据一定要保存在Node节点中,而且访问数据时需要经历一次Node *到Data *的跳转,可能会破坏cache,进行两次内存寻址。
而在list.h中,将链表最基本的数据成员(next和pre指针)封装在list_head结构体中。
struct list_head { struct list_head *next, *prev; };
存放数据的结构体只需要将list_head结构体引入,即可作为链表的节点:
struct Data { ... // 存放的数据 struct list_head list; };
这样数据即可脱离上个方式中Node的限制,只需要在结构体中存储next和pre指针即可作为链表的节点,达到解耦的目的。如下图(绿色部分是存储的数据):
当然实现这种存储方式也要解决一个问题:如何通过next指针或pre指针获取数据?
假设我们定义了以下结构体封装一个人的基本信息:
struct people { char name[20]; int age; struct list_head list; }; struct people p1,p2 ; strcpy(p1.name, "Jack"); p1.age = 20; strcpy(p2.name, "Judy"); p2.age = 18; p1->pre = &p2; p1->next = &p2; p2->next = &p1; p2->pre = &p1;
从p1开始遍历这个链表时,我们怎么获取p2的数据呢?
首先,我们通过p1->next得到了p2中list_head成员的地址。但是无法获取p2其他成员变量的地址。
list.h是通过offsetof和container_of宏来获取list_head的父结构体成员变量地址的:
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) #define container_of(ptr, type, member) ({ const typeof( ((type *)0)->member ) *__mptr = (ptr); (type *)( (char *)__mptr - offsetof(type,member) );})
offsetof:获取成员变量相对于结构体的偏移量
如offsetof(struct people, age),展开为:((size_t)&((struct people *)0)->age)
假设struct people的地址为0,那(struct people*)0->age的地址就是age相对于struct people的偏移量。
container_of:给定指向成员变量member的指针ptr,获取父结构体的地址
父结构体的地址其实就是 ptr - offsetof(TYPE, MEMBER)
首先,定义一个__mptr=ptr,__mptr的类型为const typeof(((type *)0)->member) *
再获取父结构体的地址:(type *) ((char *)__mptr = offsetof(type, number))
在list.h中,定义了list_entry宏,以此通过list_head地址获取父结构体的地址:
/** * list_entry - get the struct for this entry * @ptr: the &struct list_head pointer. * @type: the type of the struct this is embedded in. * @member: the name of the list_struct within the struct. */ #define list_entry(ptr, type, member) container_of(ptr, type, member)