Linux内核之双向循环链表

前面分析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指针】

void list_add(struct list_head *new, struct list_head *head)
 
这个方法是在指定链表的head节点后插入new节点,看一下其内部实现:
 
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结束。

还有一个比较关键的问题,我们该如何找出宿主结构,上面我们一直在操作链表,压根就没有看到,怎么从这里访问到链表节点的所在宿主结构的其它成员?答案就是container_of宏和offset宏
#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指针值,这样就可以安全删除了。

 

posted on 2021-10-29 11:15  疾速瓜牛  阅读(294)  评论(0编辑  收藏  举报

导航