目录
概述
Linux 内核中双向循环链表采用将抽象结构和数据分离的特殊方式实现,即将抽象链表 list_head 嵌入到储存具体数据的数据结构中,通过 list_entry 宏(下文有讲解)将链表结构与数据连接起来。
内核中 list_head 的实现
list.h 文件实现了list_head 的所有功能,可在 .../include/linux 中找到(博主用的2.6版本内核)
1.list_head 结构体
struct list_head {
struct list_head *next, *prev;
};
list_head 的结构非常简单, 只储存其前后节点,这符合其抽象的特征。
2.list_head 实现的操作
list.h 内部实现的对链表的操作都是对 list_head 链表自身的,包括初始化、插入、删除、移动等操作,但并不实现对其所嵌入数据结构的操作,篇幅原因这里仅简单例举初始化和插入操作.
初始化
//使用举例 list_head *head = LIST_HEAD_INIT(head)
#define LIST_HEAD_INIT(name) { &(name), &(name) }
//使用举例 list_head *head;
// LIST_Head(head);
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
//使用举例 list_head *head;
// INIT_LIST_HEAD(head);
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}
需要注意的是因为是双向循环链表因此初始化后的头节点的 prev 和 next 均指向 head 自己
插入
//在两个已知节点间插入
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
prev->next = new;
}
//在 head 节点后添加新节点
static inline void list_add(struct list_head *new, struct list_head *head)
{
__list_add(new, head, head->next);
}
//在 head 节点前添加新节点 即插入到尾部
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
__list_add(new, head->prev, head);
}
list.h 中实现了三种插入操作,其中后两种操作便于实现队列。
list.h 中其他函数也并不复杂,请读者自行研究,下面我们来介绍一个重要的宏 list_entry 。
3.list_entry 宏
list_head 结构和储存数据的结构联系起来的关键就是 list_entry 宏,这个宏一般是提供给与其绑定的数据结构使用的,首先我们看其定义:
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
可以看到 list_entry 宏重新封装了另一个宏 container_of ,这个宏在内核中经常用到,定义如下:
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
这个宏比较复杂,让我们来层层拆开来分析,首先我们看三个参数,ptr 为实例化结构体的 member 指针,type 为结构体的类型名称,member 为被嵌入在结构体中的 某成员的名称。
下面让宏的内容更整齐一些
{
const typeof( ((type *)0)->member ) *__mptr = (ptr);
(type *)( (char *)__mptr - offsetof(type, member) );
}
第一行巧妙的将0强制转换为 type 类型指针并指向其 member 成员然后再利用 typeof 获取此member 的类型即 typeof( ((type *)0)->member ) ,然后将 ptr 赋给此类型的临时指针 __mptr ,前置 const 防止 ptr 指向的内容被修改。【1.宏的参数中并未提供 member 的类型 2.宏 typeof 用于获取类型名称】
第二行 offsetof 用于获取 member 成员在数据结构中内存偏移,用 __mptr 指针减去偏移量即可获取 member 所嵌入数据结构的地址,即储存数据的结构的地址,之后将这个地址强转为 type 类型指针即可,宏返回的便是 type 类型的指针。【offsetof 由编译器提供用于计算元素相对于结构体头部的偏移】
下面的图将有助于理解:
用 list_node 的地址减去 data_size 即可取得 实例化的data_struct 的地址,注意图中所标识的地址是相对于结构体内部的。【注意这里的 data_size 为 指针 data 的da'xiao
使用如下:
struct data_struct {
void *data;
list_head list_node;
};
struct data_struct *temp = init();
list_entry(&temp->list_node, struct data_struct, list_node);
list_head 的使用
上面 data_struct 的例子便是使用 list_head 时数据的组织结构,list.h 中封装了对链表的很多操作使用时直接使用这些即可形成链表,而除 data 数据需要另行处理。
//此处援引上文中用到的 data_struct
struct data_struct *head = init();
//插入
struct data_struct *new_1 = init();
list_add(&new->list_node, &head->list_node);
//删除
struct data_struct *new_2 = init();
list_del(&new1->list_node);//仅仅删除抽象链表中的元素,并未清除任何内存
上面删除操作便很好的体现了数据和抽象链表的分离,当我们需要真正删除某一元素时需要自己定义删除函数来实现操作。
void list_del_m(struct data_struct *node)
{
list_del(&node->list_node);
free(node->data);
node->data = NULL;
free(node);
node = NULL;
}
//援引上面代码
list_del_m(new_1);
结语
限于篇幅原因本文先写到这里,对于 list_head 的更多使用可以参考内核中 task 结构体及其相关功能,可以在 .../include/linux/sched.h 找到 task_struct 定义。
最后,非常感谢您能读到这里,有不妥之处敬请指正。