#list.h源码阅读
此文章是我阅读list.h后的一些见解,有问题且理解不到位的地方希望大家批评指正。
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
其实可以写成一句
struct list_head name ={&(name),&(name)};
这个结构体在types.中定义了双向链表。
##初始化链表
static inline void INIT_LIST_HEAD(struct list_head *list)
{
WRITE_ONCE(list->next, list);
list->prev = list;
}
将双链表的头尾指向自己(初始化)。
这里有两个问题,1、inline的作用,2、WRITE_ONCE的使用。
static inline bool __list_add_valid(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
return true;
}
static inline bool __list_del_entry_valid(struct list_head *entry)
{
return true;
}
而开启DEBUG模式下的两个函数与这里的区别是无返回值,这里定义他们的类型是bool型,而bool的默认返回值为FALUSE,也就是说DEBUG模块定义了__list_add_valid,与 __list_del_entry_valid,他们的返回值为FALUSE而非DEBUG下定义了__list_add_valid与__list_del_entry_valid,他们的返回值为TRUE。我们目前只要明确这一点就好,接着往下看。
##链表插入节点
内核代码真是精妙无比,我们来看它是怎么做的。
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
if (!__list_add_valid(new, prev, next))
return;
next->prev = new;
new->next = next;
new->prev = prev;
WRITE_ONCE(prev->next, new);
}
首先回去检测我们之前定义的__list_add_valid,如果返回值为TRUE,则继续下面的增加节点,否则就跳出去。在这里我觉得有两种意思,第一,开启DEBUG模式下返回值为FAULSE,那么就跳出去,不会增加新节点。第二,若在定义新节点过程中有特殊情况发生导致定义不成功,那么也会跳出去,所以我觉得一方面是程序需要开启调试模式不需要增加节点,另一方面也保证了它的安全性。
接着进行双链表的增加节点工作可翻译如下:
又看到了熟悉的WRITE_ONCE(),前面讲过这里不多提了。接下来内核做了一件什么事呢?
static inline void list_add(struct list_head *new, struct list_head *head)
{
__list_add(new, head, head->next);
}
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
__list_add(new, head->prev, head);
}
封装!对,它把整个__list_add封装了起来,定义了头插和尾插,同时只给了两个接口,让使用者调用这两个类似于API即可实现双链表的头插和尾插。
##链表删除节点
删除的基本操作就比较简单啦,只需要让前节点的后指针指向后面,后节点的前指针指向前面即可,需要注意的是虽然改变了前后指针指向的地址,但是我们原中间节点的指针指向地址并没有改变,所以我们还可以用此节点做位置记录。
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
WRITE_ONCE(prev->next, next);
}
static inline void __list_del_entry(struct list_head *entry)
{
if (!__list_del_entry_valid(entry))
return;
__list_del(entry->prev, entry->next);
}
static inline void list_del(struct list_head *entry)
{
__list_del_entry(entry);
entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;
}
从上面三步我们可以看出,层层封装,把普通的删除操作加以确认是否DEBUG模式封装为__list_del_entry,最终加以封装为list_del。
entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;
这两句的意思是prev、next指针分别被设为LIST_POSITION2和LIST_POSITION1两个特殊值,LIST_POISON1和LIST_POISON2这两个变量在是poison.h中被设置的,是为了保证不在链表中的节点项不可访问(对LIST_POSITION1和LIST_POSITION2的访问都将引起页故障),同时由于访问了这两个值,它也去掉了__的标志。
##替换与移动等
首先还是基础的替换操作:
static inline void list_replace(struct list_head *old,
struct list_head *new)
{
new->next = old->next;
new->next->prev = new;
new->prev = old->prev;
new->prev->next = new;
}
接着是封装为替换的整体操作:
static inline void list_del_init(struct list_head *entry)
{
__list_del_entry(entry);
INIT_LIST_HEAD(entry);
}
需要注意的是这里还调用了初始化节点的函数,让替换后的节点初始化为空节点。紧随其后的list_del_init也调用了这一函数,让删除后的节点初始化为空节点,这与前面删除后节点两指针指向固定位置不同,系统可分辨出这个节点是删除过后有没有初始化是否可用。
static inline int ist_emplty_careful(const struct list_head *head)
{
struct list_head *next = head->next;
return (next == head) && (next == head->prev);
}
它不但判断链表是否为空,还同时检查了有没有当前正在定义它前后指针的进程,增加了一步保险。在Linux内核代码中我们会经常看到这种写法,list.h中还有一个,我们稍后会看到。
static inline void __list_cut_position(struct list_head *list,
struct list_head *head, struct list_head *entry)
{
struct list_head *new_first = entry->next;
list->next = head->next;
list->next->prev = list;
list->prev = entry;
entry->next = list;
head->next = new_first;
new_first->prev = head;
}
__list_cut_position是把一个链表裁剪成两段新的,具体操作如下:
之后还是一样的封装。
static inline void __list_splice(const struct list_head *list,
struct list_head *prev,
struct list_head *next)
{
struct list_head *first = list->next;
struct list_head *last = list->prev;
first->prev = prev;
prev->next = first;
last->next = next;
next->prev = last;
}
同样通过封装,它也有从头或者从尾合并的两种调用方法。而list_splice_init与list_splice_tail_init则是将list合并到head链表的基础上,调用INIT_LIST_HEAD(list)将list设置为空链。
static inline void list_splice_init(struct list_head *list,
struct list_head *head)
{
if (!list_empty(list)) {
__list_splice(list, head, head->next);
INIT_LIST_HEAD(list);
}
}
##大量的宏定义
接着我们会看到大量的宏定义,通过观察我们可以发现有许多是相同的,只是多了一个safe,那么到底safe在哪里呢?
#define list_for_each_safe(pos, n, head)
for (pos = (head)->next, n = pos->next; pos != (head);
pos = n, n = pos->next)
#define list_for_each_prev_safe(pos, n, head) /
for (pos = (head)->prev, n = pos->prev;
prefetch(pos->prev), pos != (head);
pos = n, n = pos->prev)
发现它总是多定义一个n,相当于一个暂存容器,用来保存当前数据,同时可以在遍历的过程中对节点进行操作并且不会导致数据的丢失。
struct hlist_head {
struct hlist_node *first;
};
struct hlist_node {
struct hlist_node *next, **pprev;
};
我们会立即注意到定义节点的时候它使用了二级指针,它为什么要使用二级指针来定义呢,它在里面怎么用呢?我们接着往下走
###初始化
#define HLIST_HEAD_INIT { .first = NULL }
#define HLIST_HEAD(name) struct hlist_head name = { .first = NULL }
#define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL)
这里用的宏方法与前面双链表类似不再多提。分别是只初始化头节点,声明并初始化头节点,使用指针初始化头节点,可在运行时初始化。
static inline void INIT_HLIST_NODE(struct hlist_node *h)
{
h->next = NULL;
h->pprev = NULL;
}
static inlinkjlke int hlist_unhashed(const struct hlist_node *h)
{
return !h->pprev;
}
static inline int hlist_empty(const struct hlist_head *h)
{
return !READ_ONCE(h->first);
}
这里初始化了哈希表节点,检测节点是否被哈希以及检测哈希表是否为空。
##删除哈希节点
static inline void hlist_del(struct hlist_node *n)
{
__hlist_del(n);
n->next = LIST_POISON1;
n->pprev = LIST_POISON2;
}
static inline void hlist_del_init(struct hlist_node *n)
{
if (!hlist_unhashed(n)) {
__hlist_del(n);
INIT_HLIST_NODE(n);
}
}
在这里我们看到了定义的二级指针。那么为什么不用双链表解决这个问题呢?先看看哈希表的数据结构定义。
假如我们用双链表来实现会有什么问题呢?
static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h)
{
struct hlist_node *first = h->first;
n->next = first;
if (first)
first->pprev = &n->next;
WRITE_ONCE(h->first, n);
n->pprev = &h->first;
}
static inline void hlist_add_before(struct hlist_node *n,
struct hlist_node *next)
{
n->pprev = next->pprev;
n->next = next;
next->pprev = &n->next;
WRITE_ONCE(*(n->pprev), n);
}
static inline void hlist_add_behind(struct hlist_node *n,
struct hlist_node *prev)
{
n->next = prev->next;
WRITE_ONCE(prev->next, n);
n->pprev = &prev->next;
if (n->next)
n->next->pprev = &n->next;
}
hlist_add_head:将结点n插在头结点h之后。
hlist_add_before:将结点n插在next结点的前面,所以需要next节点一定不可以为NULL
hlist_add_after:将结点next插在n之后(n在哈希链表中)
最奇怪的是最后这个hlist_add_fake函数,它将ppev指针指向自己的next指针,称之为虚假添加,同时注释中写到,这样的结点会被hlist_unhashed判为已hash。所以也可以对它调用hlist_del函数,对此函数并未搞懂,可能是没有碰到实际应用的地方。
static inline void hlist_add_fake(struct hlist_node *n)
{
n->pprev = &n->next;
}
###移动Hash等
对哈希表的移动与双链表很类似,这里将本来以old为头结点的链表移动成为以new为头结点的链表。
static inline void hlist_move_list(struct hlist_head *old,
struct hlist_head *new)
{
new->first = old->first;
if (new->first)
new->first->pprev = &new->first;
old->first = NULL;
}
当然Hash里也有检查是否只有头节点的函数:
hlist_is_singular_node(struct hlist_node *n, struct hlist_head *h)
{
return !n->next && n->pprev == &h->first;
}
紧接着后面的大段是哈希表的遍历宏,和前面的双链表类似,也有很多safe的操作,为内核代码提供了强壮性。
##总结
通过阅读List.h掌握到一种很重要的思想就是封装,这让内核代码看起来十分简洁,不断地调用,让简单的功能实现复杂功能的同时给与调用函数的人方便的使用,这样的代码,怪不得说内核代码是宝库,名不虚传,我学到了很多。期待之后阅读带来的惊喜。