前面分析wayland的代码时,发现其使用了双向链表这个结构,后来发现和内核中的list基本一模一样,所以这里单开一个文章分析一下内核中的环形双向链表的使用。
先回忆一下我们日常定义的链表,基本上都是结构体A里包含一个指向next的A的指针。这样带来的一个最大的问题就是不通用。也就是明明链表的增,删,查,改操作基本都是类似的,但是换个类型B, 上述操作就是要重新实现一遍。这样搞的话,内核指定会有很多冗余的代码,这显然不是内核开发者们所能容忍的事情。于是大神们想到一种方法,巧妙的解决了这个问题,他们定义了一个独特的结构, 我们称之为链表节点:
struct list_head { struct list_head* next; struct list_head* prev; };
现在,我们的做法是,把链表节点加入到要形成链表的结构体中:
struct fox { int weight; int tail_lenth; struct list_head list; //list.next指向下一个fox, list.prev指向上一个fox };
然后我们看一下该如何使用它,首先,我们肯定里不开初始化,这里链表的第一个节点是有点特殊的:
首先看下动态创建的方法
struct fox* red_fox = kmalloc(sizeof(*red_fox), GFP_KERNEL); red_fox->weight = 10; red_fox->tail_length = 20; INIT_LIST_HEAD(&(red_fox->list));
对于静态的创建方法:
struct fox red_fox = { .weight = 20, .tail_length = 10, .list = LIST_HEAD_INIT(red_fox.list), // prev 和 next都指向自己 }
其中
#define LIST_HEAD_INIT(name) { &(name), &(name) } #define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name) static inline void INIT_LIST_HEAD(struct list_head *list) { WRITE_ONCE(list->next, list); list->prev = list; }
上图中其它的我们都好理解,有一个地方有点疑惑:那就是WRITE_ONCE
#define WRITE_ONCE(var, val) (*((volatile typeof(val) *)(&(var))) = (val))
volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略, 然后也和缓存一致性有关,这里就不展开了。
这里补充一嘴,注意到上面有个LIST_HEAD宏,这个是很有用的操作,一般我们通过这种方式定义一个链表头,后面遍历的时候就可以基于此方便的操作了 e.g. static LIST_HEAD(fox_list)
好的,数据结构有了,我们来看看内核为我们提供的操作方法:【记住一点,均是操作list_head指针】
static inline void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); } 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函数,这个函数就是把new插入到prev和next之间。这么做的结果就是,这个链表用起来就像一个栈,新加入的元素总是在head处。同理我们可以用list_del 去删除指定的list_head, 就好像是出栈一样。
static inline void list_del(struct list_head *entry) { __list_del_entry(entry); entry->next = LIST_POISON1; //删除后,该选节点已不在链表当中,因此不会再使用。LIST_POISON1/2是两个不会存在于内核空间的地址, entry->prev = LIST_POISON2; //如果使用,就会报错。因此,设置指针,是强制禁用该节点。 } 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); }
这里一个博主说了这么一句话:
利用list_del(struct list_head *entry) 接口就可以删除链表中的任意节点了,但需注意,前提条件是这个节点是已知的,既在链表中真实存在,切prev,next指针都不为NULL。
我觉得prev和next不会是NULL,因为是双向环形链表,即使只有链表头了,自己指向自己,还是不会为NULL的。
然后内核还有一个list_add_tail的接口,这里其实就是把new插入到head的prev和head之间,也就是尾插
static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); }
这么搞起来就像是一个队列,新来的元素在尾部,也就是在链表头结点的prev的地方。
接下来,我们看一下链表遍历,内核提供了一些宏,如下:
#define list_for_each(pos, head) for (pos = (head)->next; pos != (head); pos = pos->next) #define list_for_each_prev(pos, head) for (pos = (head)->prev; pos != (head); pos = pos->prev)
分别是前向遍历和后向遍历,注意这里是从head的next开始,到head结束;以及从head的prev开始,到head结束。
#define list_entry(ptr, type, member) container_of(ptr, type, member)
这个玩意算出的结果其实就是struct list_head的ptr 减去其在结构体type中的偏移量,就得到了结构体的首地址。有了这个方法,我们就可以实现对宿主结构的遍历了。
#define list_for_each_entry(pos, head, member)
for (pos = list_first_entry(head, typeof(*pos), member);
&pos->member != (head);
pos = list_next_entry(pos, member))
#define list_first_entry(ptr, type, member) list_entry((ptr)->next, type, member)
#define list_next_entry(pos, member) list_entry((pos)->member.next, typeof(*(pos)), member)
以fox_list为例,看一下如何使用这个东西
struct fox* iter = NULL; list_for_each_entry(iter, &(fox_list.list), list) { //now iter pointer to the 1st fox doSomething(iter);
//now iter pointer to the next fox }
这里还有一个值得一提的事情,我们是不能在遍历的doSomething中执行删除节点的操作的,这样会导致我们后面无法获得正常的next。这点和C++容器库中遍历删除是类似的原因,这样做破坏了遍历得以正常进行的大前提。内核提供了另外一个方法专门用于此目的:
list_for_each_entry_safe(pos, next, head, member)
这里我们需要一个临时变量next存储要被删掉的pos的next指针值,这样就可以安全删除了。