zoukankan      html  css  js  c++  java
  • list链表

    链表数据结构的定义很简单(节选自[include/linux/list.h],以下所有代码,除非加以说明,其余均取自该文件):

    struct list_head {
      struct list_head *next, *prev;
    };

      list_head结构包含两个指向list_head结构的指针prev和next,由此可见,内核的链表具备双链表功能,实际上,通常它都组织成双循环链表。

      这里的list_head没有数据域。在Linux内核链表中,不是在链表结构中包含数据,而是在数据结构中包含链表节点。

    1. 声明和初始化

    实际上Linux只定义了链表节点,并没有专门定义链表头,那么一个链表结构是如何建立起来的呢?让我们来看看LIST_HEAD()这个宏:

    #define LIST_HEAD_INIT(name) { &(name), &(name) }
    #define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name)

    当我们用LIST_HEAD(nf_sockopts)声明一个名为nf_sockopts的链表头时,它的next、prev指针都初始化为指向自己,

    这样,我们就有了一个空链表,因为Linux用头指针的next是否指向自己来判断链表是否为空:

    static inline int list_empty(const struct list_head *head)
    {
      return head->next == head;
    }

    除了用LIST_HEAD()宏在声明的时候初始化一个链表以外,Linux还提供了一个INIT_LIST_HEAD宏用于运行时初始化链表

    #define INIT_LIST_HEAD(ptr) do { 
      (ptr)->next = (ptr); (ptr)->prev = (ptr); 
    } while (0)

    我们用INIT_LIST_HEAD(&nf_sockopts)来使用它

    2. 插入/删除/合并

    i:插入

    对链表的插入操作有两种:在表头插入和在表尾插入。Linux为此提供了两个接口:

    static inline void list_add(struct list_head *new, struct list_head *head); //将新元素, 放到了链表的头端
    static inline void list_add_tail(struct list_head *new, struct list_head *head);//添加一个到尾部

    因为Linux链表是循环表,且表头的next、prev分别指向链表中的第一个和最末一个节点,

    所以,list_add和list_add_tail的区别并不大,实际上,Linux分别用

    __list_add(new, head, head->next);
    __list_add(new, head->prev, head);

    来实现两个接口,可见,在表头插入是插入在head之后,而在表尾插入是插入在head->prev之后。

    假设有一个新nf_sockopt_ops结构变量new_sockopt需要添加到nf_sockopts链表头,我们应当这样操作:

    list_add(&new_sockopt.list, &nf_sockopts);

    从这里我们看出,nf_sockopts链表中记录的并不是new_sockopt的地址,而是其中的list元素的地址。

    如何通过链表访问到new_sockopt呢?下面会有详细介绍。

    ii:删除

    static inline void list_del(struct list_head *entry);

    当我们需要删除nf_sockopts链表中添加的new_sockopt项时,我们这么操作:

    list_del(&new_sockopt.list);

    被剔除下来的new_sockopt.list , prev、next指针分别被设为LIST_POSITION2和LIST_POSITION1两个特殊值,

    这样设置是为了保证不在链表中的节点项不可访问--对LIST_POSITION1和LIST_POSITION2的访问都将引起页故障。

    与之相对应,list_del_init()函数将节点从链表中解下来之后,调用LIST_INIT_HEAD()将节点置为空链状态。

    iii:搬移

    Linux提供了将原本属于一个链表的节点移动到另一个链表的操作,并根据插入到新链表的位置分为两类:

    static inline void list_move(struct list_head *list, struct list_head *head);
    static inline void list_move_tail(struct list_head *list, struct list_head *head);

    例如list_move(&new_sockopt.list,&nf_sockopts)会把new_sockopt从它所在的链表上删除,并将其再链入nf_sockopts的表头。

    iiii:合并

    除了针对节点的插入、删除操作,Linux链表还提供了整个链表的插入功能:

    static inline void list_splice(struct list_head *list, struct list_head *head)
    {
      if (!list_empty(list))
      __list_splice(list, head);
    }

      假设当前有两个链表,表头分别是list1和list2(都是struct list_head变量),当调用list_splice(&list1,&list2)时,只要list1非空,list1链表的内容

    将被挂接在list2链表上,位于list2和list2.next(原list2表的第一个节点)之间。

    新list2链表将以原list1表的第一个节点为首节点,而尾节点不变。

    当list1被挂接到list2之后,作为原表头指针的list1的next、prev仍然指向原来的节点,为了避免引起混乱,Linux提供了一个list_splice_init()函数:

    static HPT_INLINE void list_splice_init(struct list_head *list, struct list_head *head)
    {
      if (!list_empty(list)) 
      {   __list_splice(list, head);   INIT_LIST_HEAD(list);   } }

    该函数在将list合并到head链表的基础上,调用INIT_LIST_HEAD(list)将list设置为空链。

    3. 遍历

    遍历是链表最经常的操作之一,为了方便核心应用遍历链表,Linux链表将遍历操作抽象成几个宏。

    在介绍遍历宏之前,我们先看看如何从链表中访问到我们真正需要的数据项。

    i:由链表节点到数据项变量
    我们知道,Linux链表中仅保存了数据项结构中list_head成员变量的地址,那么我们如何通过这个list_head成员访问到作为它的所有者的节点数据呢?

    Linux为此提供了一个list_entry(ptr,type,member)宏,其中ptr是指向该数据中list_head成员的指针,也就是存储在链表中的地址值,

    type是数据项的类型,member则是数据项类型定义中list_head成员的变量名,例如,我们要访问nf_sockopts链表中首个nf_sockopt_ops变量,

    则如此调用:

    list_entry(nf_sockopts->next, struct nf_sockopt_ops, list);

    这里"list"正是nf_sockopt_ops结构中定义的用于链表操作的节点成员变量名。

    list_entry的使用相当简单,相比之下,它的实现则有一些难懂:

    #define list_entry(ptr, type, member) container_of(ptr, type, member)

    //container_of宏定义在[include
    /linux/kernel.h]中: #define container_of(ptr, type, member) ({ const typeof( ((type *)0)->member ) *__mptr = (ptr); (type *)( (char *)__mptr - offsetof(type,member) );})
    //offsetof宏定义在[include
    /linux/stddef.h]中: #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

    size_t最终定义为unsigned int(i386)。

    这里使用的是一个利用编译器技术的小技巧,即先求得结构成员在与结构中的偏移量,然后根据成员变量的地址反过来得出属主结构变量的地址。

    ii:遍历宏
    在[net/core/netfilter.c]的nf_register_sockopt()函数中有这么一段话:

    ……
    struct list_head *i;
    ……
    list_for_each(i, &nf_sockopts) {
      struct nf_sockopt_ops *ops = (struct nf_sockopt_ops *)i;
      ……
    }
    ……

    函数首先定义一个(struct list_head *)指针变量i,然后调用list_for_each(i,&nf_sockopts)进行遍历。

    在[include/linux/list.h]中,list_for_each()宏是这么定义的:

    #define list_for_each(pos, head) 
    for (pos = (head)->next, prefetch(pos->next); pos != (head); pos = pos->next, prefetch(pos->next))

    它实际上是一个for循环,利用传入的pos作为循环变量,从表头head开始,逐项向后(next方向)移动pos,

    直至又回到head(prefetch()可以不考虑,用于预取以提高遍历速度)。

    转载自:深入分析 Linux 内核链表

  • 相关阅读:
    渗透测试-实用版
    微观传输原理:协议、端口、套接字接口
    如何暂停和继续运行Linux程序
    QUIC:基于udp的传输新技术
    ops运维工具
    日志监控工具安装:windows上安装elk
    python实现图片隐藏信息技术
    让网站不去请求favicon.ico图标
    nginx 内核优化
    nginx 模块介绍
  • 原文地址:https://www.cnblogs.com/listxue/p/12744669.html
Copyright © 2011-2022 走看看