zoukankan      html  css  js  c++  java
  • 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指针值,这样就可以安全删除了。

  • 相关阅读:
    [国家集训队]整数的lqp拆分
    来一波全套向量运算(C++)
    天哪又要搬家啦qvq
    利用输入输出流复制文件
    系统会随机给你两个日期(yyyy-MM-dd),求出两个日期的具体相差天数
    mysql中的连接操作
    java面试题集锦
    Java关于Math类的三个取整方法
    设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。写出程序。
    MySql5.5安装详细说明
  • 原文地址:https://www.cnblogs.com/Arnold-Zhang/p/15267002.html
Copyright © 2011-2022 走看看