内核双向链表

链表是一种重要的数据结构,应用的非常广泛。链表分为单向链表与双向链表,一般的实现就是在结构体中内嵌指向下一个元素的指针。例如:

  1. struct name {
  2. int num;
  3. ...;
  4. struct name *next;
  5. struct name *prev;

但是linux内核中的实现确有点特殊他是通过独立定义一个链表结构,通过结构体中内嵌这个结构来完成的,这样就实现了链表的定义与结构体的分离。linux内核中广泛的应用了这种链表,可以这么说,如果没有linux list_head链表就不会有现在linux的强大。本来我就以为只有linux内核用这种链表,但是当我分析完lsusb的代码后,发现这个程序也是用内核链表来组织数据结构的,我想Kroah-Hartman不愧为内核的维护者,连应用程序都带着内核的影子。我就试着在应用程序中使用这种链表,发现他的非常的好用,只要包含list.h,然后在结构体重嵌入这种链表,就能方便的实现数据结构的线性链接。下面我就简单的介绍一下这种链表的实现原理。

list.h中首先定义了这样一个结构体,这个是链表的基本结构:

  1. struct list_head {
  2. struct list_head *next, *prev;
  3. };

我们使用的时候,在自己的结构体中内嵌这个结构就行了,如下;

  1. struct my_struct {
  2. int a;
  3. int b;
  4. ...
  5. struct list_head list;
  6. }

这个链表链接起来的不是结构体本身,而是list_head结构。需要一个链表头,这个链表头是list_head结构,不需要内嵌在任何结构中,在使用链表的时候要定义以及初始化这样一个结构。list.h中已经定义了宏用于定义与初始化链表头,如下

  1. #define LIST_HEAD_INIT(name) { &(name), &(name) }
  2. #define LIST_HEAD(name) \
  3. struct list_head name = LIST_HEAD_INIT(name)
  4. #define INIT_LIST_HEAD(ptr) do { \
  5. (ptr)->next = (ptr); (ptr)->prev = (ptr); \
  6. } while (0)

LIST_HEAD(name)宏用来定义一个链表头,使得他的两个指针都指向自己。我们可以直接在程序的变量声明处,直接调用LIST_HEAD(name)宏,来定义并初始化一个名为name的链表,也可以先声明一个链表,调用INIT_LIST_HEAD来初始化这个链表。从宏定义山看不能直接使用LIST_HEAD_INIT,它只适合声明初始化。

定义了一个链表后,最重要的操作是向链表添加元素,与删除元素以及遍历链表。下面先说添加元素的操作:

  1. static inline void __list_add(struct list_head *new,
  2. struct list_head *prev,
  3. struct list_head *next)
  4. {
  5. next->prev = new;
  6. new->next = next;
  7. new->prev = prev;
  8. prev->next = new;
  9. }
  10. static inline void list_add(struct list_head *new, struct list_head *head)
  11. {
  12. __list_add(new, head, head->next);
  13. }
  14. static inline void list_add_tail(struct list_head *new, struct list_head *head)
  15. {
  16. __list_add(new, head->prev, head);
  17. }

向链表添加元素只知道链表的头与新的元素地址就可以了,因为链表是双向循环链表,链表头的前一个元素就是这个链表的最后一个元素。list_add是将新元素添加到链表头的后面,ist_add_tail是将心元素添加到链表的尾部,这两个操作如下图所示:

图 1 list_add操作

图 2 list_add_tail操作

说完了添加元素,下面说一下如何删除链表元素,如下:

  1. static inline void __list_del(struct list_head *prev, struct list_head *next)
  2. {
  3. next->prev = prev;
  4. prev->next = next;
  5. }
  6. static inline void list_del(struct list_head *entry)
  7. {
  8. __list_del(entry->prev, entry->next);
  9. }

删除链表元素也非常简单,只需要找到前一个元素与后一个元素,将他们链接在一起就可以了。

list_head链表最重要的特点就是链表操作与结构体分离,用list_head链接的链表结构上如下图所示:

图 3 list_head链表特点

但是我们使用链表的目的不是链表本身,而是内嵌链表的结构体,我们可以方便的遍历list_head结构,如下:

[cpp] view plaincopy

  1. for ( mylist = mylist_head->next; mylist != mylist_head; mylist = mylist->next);

但是这样的程序没有什么作用,我们需要的是结构体。所以需要一种技术使得由list_head的地址找到内嵌他的结构提的地址,这个技术就是内核中顶顶大名的container_of宏,这个宏就是list_head链表的精髓,它的初衷是为了实现由结构体的元素的地址而找到结构体首地址。借鉴这种技术就可以实现我们的目的。如下:

[cpp] view plaincopy

  1. #define list_entry(ptr, type, member) \
  2. ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))

这个宏实现非常的简单,但是往往伟大的思想就蕴藏在简单的代码中,我们来分析一下这个宏。首先他是一个带参数的宏,有三个参数第一个参数是一个地址,他是结构体中元素的地址,第二个参数是结构体类型,第三个参数是,元素在结构体中的名字。我们以list_head链表为例来说明。例如有如下结构:

  1. struct my_struct {
  2. int a;
  3. char b;
  4. ...
  5. strcut list_head list;
  6. }

我们知道了里面的list_head元素的地址为plist,需要找到类型问struct my_struct结构变量的地址。就需要这样调用宏

struct my_struct *ptr = list_entry(plist, struct my_struct, list);
        list_entry宏实现的非常巧妙,括号比较多,让我们将这个宏分解来看。
((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
        最外面的一层括号可以去掉,这是为了防止宏扩展的,去掉如下:
(type *) ((char *)(ptr)-(unsigned long)(&((type *)0)->member))
        现在就比较清楚了,首先(type *)是C强制转换操作,就是将后面的的数据转化成type结构的指针。而后面的操作可以再分解
(char *)(ptr) - (unsigned long)(&((type *)0)->member)
        这样就是一个减法的操作,前面是一个指针,我们传过去的结构体元素的指针,这里被转化成指向字符的。而后面是一个长整形,可以再分解
(unsigned long) (&((type *)0)->member)
        显然这个长整形是一个指针转化的,而这个指针又可以再分解,
&((type *)0)->member
       可以看出这个指针是一个变量取地址得到的,这个变量又是什么呢
((type *)0)->member 
       看起来有点奇怪,不过这个操作是整个宏中最精妙的,他将地址0转化成type类型,接下来又取得这个结构的member元素,member就是我们传进来的参数:元素在结构体中的命名。其实((type *)0)->member取的变量是内容是什么一点都不重要,重要的我们要取这个变量的地址。取完这个地址将它转换成unsigned long类型,这样这个数据就是((type *)0)->member相对与地址0的偏移。回到上面的那个减法,将结构体中元素的地址与他与结构体首地址的偏移相减,不就得到了结构体的地址了吗。整个操作如下图所示:

时间: 2024-10-27 07:13:38

内核双向链表的相关文章

linux内核-双向链表

linux中的经典宏定义 offsetof 定义:offsetof在linux内核的include/linux/stddef.h中定义. #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) 说明:获得结构体(TYPE)的变量成员(MEMBER)在此结构体中的偏移量. (01) ( (TYPE *)0 ) 将零转型为TYPE类型指针,即TYPE类型的指针的地址是0. (02) ((TYPE *)0)->MEMBER

例说Linux内核链表(三)

经常使用的linux内核双向链表API介绍 linux link list结构图例如以下: 内核双向链表的在linux内核中的位置:/include/linux/list.h 使用双向链表的过程,主要过程包括创建包括struct link_head结构的结构体(item),建立链表头.向链表中加入item(自己定义数据结构.双向链表数据单元).删除链表节点.遍历链表,判空等. 1.建立自己定义链表数据结构 struct kool_list{ int to; struct list_head li

【转载】proc 虚拟文件系统

内核代码中分别找出一处 proc 和 seq_file 的完整使用过程,记录下来 在用户空间进行相应"读"."写" 介绍 Proc 虚拟文件系统 操作 proc 文件: /proc 文件系统是一个虚拟文件系统(没有任何一部分与磁盘相关,只存在内存中,不占用外存空间),包含了一些目录和虚拟文件 通过它可以在 Linux 内核空间和用户空间之间进行通信:可以向用户呈现内核中的一些信息(用 cat.more 等命令查看 /proc 文件中的信息),也可以用作一种从用户空间

第十八天:双向链表与内核链表

对于双向链表就是在单向链表的基础上加了一个向前的结构体指针.对于一些操作的思想更加的简单.麻烦的事情就是要判断的内容更多了.因为内核链表中运用到的都是双向链表.所以要学习内核链表.就先要会对双向链表进行基本的操作.比如新增和显示. 内核链表中的实现思想类似与面向对象的思想,成员和方法分开写.比如下面这个简单的代码: 1 #include<stdio.h> 2 3 struct boy; 4 struct file_operations; 5 6 struct boy{ 7 int age;//

Linux 内核数据结构:双向链表

Linux 内核提供一套双向链表的实现,你可以在 include/linux/list.h 中找到.我们以双向链表着手开始介绍 Linux 内核中的数据结构 ,因为这个是在 Linux 内核中使用最为广泛的数据结构,具体你可以 查看 这里. 首先让我们看一下主要的结构体: struct list_head { struct list_head *next, *prev; }; 你可以看到其与常见的结构体实现有显著不同,比如 glib 中所使用到的双向链表实现. struct GList { gp

Linux 内核数据结构:Linux 双向链表

Linux 内核提供一套双向链表的实现,你可以在 include/linux/list.h 中找到.我们以双向链表着手开始介绍 Linux 内核中的数据结构 ,因为这个是在 Linux 内核中使用最为广泛的数据结构,具体你可以 查看 这里. 首先让我们看一下主要的结构体: struct list_head { struct list_head *next, *prev; }; 你可以看到其与常见的结构体实现有显著不同,比如 glib 中所使用到的双向链表实现. struct GList { gp

linux内核源码“双向链表list_head”

摘要:linux内核源码真是好东东,是众多高手思维的结晶,在linux 源代码中有个头文件为list.h .很多linux 下的源代码都会使用这个头文件,它里面定义了一个结构, 以及定义了和其相关的一组函数,这个结构是这样的: structlist_head{ structlist_head *next, *prev; }; 如果您之前学过双向链表,那么当你看到这个结构的时候,会觉得似曾相识.岂止似曾相识,如果你看过Fio的源码,你会觉得它用的如此广泛,下面我们通过一个实例演示如何使用 一.编写

linux内核源码“双向链表list_head”续

上篇博文<linux内核源码"双向链表list_head">中以一个实例介绍了list_head双向链表的用法,只有实例的代码,并没有list_head链表的代码,考虑到各位好学博友的强烈愿望,今天把list_head的代码即list.h头文件粘贴到此,供各位好学博友使用. 一.list.h头文件源码 [[email protected] cstudy]# cat list.h             #list.h头文件 #ifndef _LINUX_LIST_H #de

linux内核中对双向链表的操作函数

在linux内核中用的非常多的一种链表是:双向链表.内核中对所用的进程的管理就是通过双向链表来实现的.所以对链表的操作非常的常用也非常的重要,因此内核通过提供一个通用的方法来帮助我们方便的对双链表实现各种操作. struct list_head {struct list_head *next, *prev;} 0,对双向链表中的某个项进行初始化操作的函数: INIT_LIST_HEAD(struct list_head *entry) INIT_LIST_HEAD()的实现方法: static