zoukankan      html  css  js  c++  java
  • linux内核链表分析

    一、常用的链表和内核链表的区别

    1.1  常规链表结构

           通常链表数据结构至少应包含两个域:数据域和指针域,数据域用于存储数据,指针域用于建立与下一个节点的联系。按照指针域的组织以及各个节点之间的联系形式,链表又可以分为单链表、双链表、循环链表等多种类型,下面分别给出这几类常见链表类型的示意图:

    单链表:

    图1 单链表

    双链表:

    图2 双链表

    1.2  Linux 2.6内核链表数据结构

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

    struct list_head { struct list_head *next, *prev;};  list_head结构包含两个指向list_head结构的指针prev和next,由此可见,内核的链表具备双链表功能,实际上,通常它都组织成双循环链表。list_head从字面上理解,好像是头结点的意思。但从这里的代码来看却是普通结点的结构体。在后面的代码中将list_head当成普通的结点来处理。
    和第一节介绍的双链表结构模型不同,这里的list_head没有数据域。在Linux内核链表中,不是在链表结构中包含数据,而是在数据结构中包含链表节点。

    linux内核链表与普通链表的示意图:

    linux内核链表 - 红娃子 - 红娃子

     

    linux内核链表 - 红娃子 - 红娃子

    在数据结构课本中,链表的经典定义方式通常是这样的(以单链表为例):

    struct list_node {struct list_node *next;ElemTypedata;};
    因为ElemType的缘故,对每一种数据项类型都需要定义各自的链表结构。并且对于每种的数据结构还要定义相应的操作函数,比如插入、删除、排序等(这正是linux内核数据结构所要避免的)。

    二、  linux内核链表的常用操作函数
    linux内核链表的好处:

        设计思想:尽可能的代码重用,化大堆的链表设计为单个链表。
        链表的构造:如果需要构造某类对象的特定列表,则在其结构中定义一个类型为list_head指针的成员,通过这个成员将这类对象连 接起来,形成所需列表,并通过通用链表函数对其进行操作。其优点是只需编写通用链表函数,即可构造和操作不同对象的列表,而无需为每类对象的每种列表编写专用函数,实现了代码的重用。如果想对某种类型创建链表,就把一个list_head类型的变量嵌入到该类型中,用list_head中的成员和相对应的处理函数来对链表进行遍历。如果想得到相应的结构的指针,使用list_entry可以算出来。

    2.1  新建一个链表

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

    #define LIST_HEAD_INIT(name)      { &(name), &(name) }

    #define LIST_HEAD(name)      struct list_head name = LIST_HEAD_INIT(name)

    其中的name是struct list_head结构的变量的地址,而不是包含struct list_head的数据结构的变量的地址

    2.2  插入删除搬移合并

    a)插入

    对链表的插入操作有两种:在表头插入和在表尾插入。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)

    b) 删除

    static inline void list_del(struct list_head *entry);

    c) 搬移

    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);

    d) 合并

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

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

    2.3  遍历
          遍历是链表最经常的操作之一,为了方便核心应用遍历链表,Linux链表将遍历操作抽象成几个宏。在介绍遍历宏之前,我们先看看如何从链表中访问到我们真正需要的数据项。

    a) 由链表节点到数据项变量

    list_entry宏是用来根据list_head指针查找链表所嵌入的结构体的地址,具体实现是依赖宏container_of:

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

    #define container_of(ptr, type, member)      ({ const typeof( ((type *)0)->member ) *__mptr = (ptr);  
                                                                             (type *)( (char *)__mptr - offsetof(type,member) );})

    #define offsetof(type,  member)    ((size_t) &((type *)0)-> member)

          container_of有三个参数, ptr是成员变量的指针, type是指结构体的类型, member是成员变量的名字。 container_of 的作用就是在已知某一个成员变量的名字、指针和结构体类型的情况下,计算结构体的指针,也就是计算结构体的起始地址。 计算的方法其实很简单,就是用该成员变量的指针减去它于type结构体起始位置的偏移量。在这个定义中,typeof( ((type *)0)->member ) 就是获得 member 的类型, 然后定义了一个临时的常量指针 __mptr, 指向 member 变量。 把 __mptr 转换成 char * 类型, 因为 offsetof 得到的偏移量是以字节为单位。 两者相减得到结构体的起始位置, 再强制转换成 type 类型。

         offsetof在这里,TYPE表示一个结构体的类型,MEMBER是结构体中的一个成员变量的名字。offsetof 宏的作用是计算成员变量 MEMBER 相对于结构体起始位置的内存偏移量,以字节(Byte)为单位。
    b) 遍历宏

    函数首先定义一个(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()可以不考虑,用于预取以提高遍历速度)。

    大多数情况下,遍历链表的时候都需要获得链表节点数据项,也就是说list_for_each()和list_entry()总是同时使用。对此Linux给出了一个list_for_each_entry()宏,与list_for_each()不同,这里的pos是数据项结构指针类型,而不是(struct list_head *)。  

    #define list_for_each_entry(pos, head, member)……

    某些应用需要反向遍历链表,Linux提供了list_for_each_prev()和list_for_each_entry_reverse()来完成这一操作,使用方法和上面介绍的list_for_each()、list_for_each_entry()完全相同。  

    三、 内核链表应用举例

    双循环链表是linux内核常用的数据结构,这也是linux链表的一个非常有特色的地方。而涉及到链表的函数有链表的定义、链表头的初始化、链表的插入、链表的遍历、链表的删除和链表的回收。下面通过一个内核模块来说明链表的相关操作。

    #include <linux/kernel.h>
    #include <linux/module.h>
    #include <linux/init.h>
    #include <linux/slab.h>
    #include <linux/list.h>

    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("David Xie");
    MODULE_DESCRIPTION("List Module");
    MODULE_ALIAS("List module");

    //以上为内核模块的的头文件和模式固定的部分

    struct student
    {
       char name[100];
       int num;
       struct list_head list;
    };

    //以上是定义包含有的struct list_head 结构的数据结构

    struct student *pstudent;//定义一个结构数组,用来存放数据,注意这里pstudent是数组指针,数组的大小由后面的kmalloc来分配!

    struct student *tmp_student;//遍历时临时用来存放指向pstudent[i]的指针

    struct list_head student_list;//定义链表头(是一个节点)

    struct list_head *pos;//指向头结点的一个指针,会在list_for_each中说明

    int mylist_init(void)
    {
       int i = 0;
       INIT_LIST_HEAD(&student_list);//初始化链表头,注意参数是一个指针,用了&符号

       pstudent = kmalloc(sizeof(struct student)*5,GFP_KERNEL);//为结构体数组分配空间,共有5个数组成员
       memset(pstudent,0,sizeof(struct student)*5);//初始化结构体数组
       for(i=0;i<5;i++)//建立链表
       {
          sprintf(pstudent[i].name,"Student%d",i+1);//初始化并显示学生姓名
          pstudent[i].num = i+1; //初始化学生号码
          list_add&(pstudent[i].list), &student_list);//将pstudent[i].list节点插入到student_list链表中,注意这里是从头结点处插入的,最后顺序为   5、4、3、2、1
       }
    list_for_each(pos,&student_list)//遍历链表,此函数指明pos是一个指向节点头的指针,前面已经定义了它的类型。遍历函数相当于一个for循环,{ }内为循环操作,没循环一次pos=&student_list+1!
    {
       tmp_student = list_entry(pos,struct student,list);//list_entry(提取数据结构)指针pos指向结构体struct student中的成员list,返回值为指向list所在的结构体的指针(起始地址)
       printk("<0>student %d name: %s ",tmp_student->num,tmp_student->name);
    }//输出此结构体(结构数组其中的一个成员)的数据信息
       return 0;
    }

    void mylist_exit(void)//删除节点

       int i ;
       for(i=0;i<5;i++)
    {
       list_del(&(pstudent[i].list) );
    }
       kfree(pstudent);//释放分配的内存
    }

    module_init(mylist_init);//内核模块模式固定的部分
    module_exit(mylist_exit);//内核模块模式固定的部分

    声明:本文是参考了网络上大量的文章写成的!!!

    http://www.cnblogs.com/leon19870907/articles/2180529.html

    http://hi.baidu.com/pleasehyj/item/918186b851ee55f063388ecd

    http://qing.weibo.com/tj/6468794333000x2f.html

    http://www.ibm.com/developerworks/cn/linux/kernel/l-chain/index.html

  • 相关阅读:
    http 请求头设置缓存
    手把手教你开发chrome扩展一:开发Chrome Extenstion其实很简单
    django如何用邮箱代替用户名登录
    python函数式编程学习之map,reduce,filter,sorted
    python traceback学习(转)
    python logging模块学习(转)
    python pip安装lxml失败(转)
    python下性能提示
    python移植性提示
    python测试与调试提示
  • 原文地址:https://www.cnblogs.com/Daniel-G/p/3305834.html
Copyright © 2011-2022 走看看