linux内核链表分析
一、常用的链表和内核链表的区别
1.1 常规链表结构
通常链表数据结构至少应包含两个域:数据域和指针域,数据域用于存储数据,指针域用于建立与下一个节点的联系。按照指针域的组织以及各个节点之间的联系形式,链表又可以分为单链表、双链表、循环链表等多种类型,下面分别给出这几类常见链表类型的示意图:
单链表:
双链表:
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内核链表与普通链表的示意图:
在数据结构课本中,链表的经典定义方式通常是这样的(以单链表为例):
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\n",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
linux内核链表分析的更多相关文章
- 链表的艺术——Linux内核链表分析
引言: 链表是数据结构中的重要成员之中的一个.因为其结构简单且动态插入.删除节点用时少的长处,链表在开发中的应用场景许多.仅次于数组(越简单应用越广). 可是.正如其长处一样,链表的缺点也是显而易见的 ...
- Linux内核链表深度分析
链表简介:链表是一种常用的数据结构,它通过指针将一系列数据节点连接成一条数据链.相对于数组,链表具有更好的动态性,建立链表时无需预先知道数据总量,可以随机分配空间,可以高效地在链表中的任意位置实时插入 ...
- 深入分析 Linux 内核链表--转
引用地址:http://www.ibm.com/developerworks/cn/linux/kernel/l-chain/index.html 一. 链表数据结构简介 链表是一种常用的组织有序数据 ...
- 深入分析 Linux 内核链表
转载:http://www.ibm.com/developerworks/cn/linux/kernel/l-chain/ 一. 链表数据结构简介 链表是一种常用的组织有序数据的数据结构,它通过指 ...
- 数据结构开发(10):Linux内核链表
0.目录 1.老生常谈的两个宏(Linux) 1.1 offsetof 1.2 container_of 2.Linux内核链表剖析 3.小结 1.老生常谈的两个宏(Linux) Linux 内核中常 ...
- Linux 内核链表的使用及深入分析【转】
转自:http://blog.csdn.net/BoArmy/article/details/8652776 1.内核链表和普通链表的区别 内核链表是一个双向链表,但是与普通的双向链表又有所区别.内核 ...
- Linux 内核链表 list.h 的使用
Linux 内核链表 list.h 的使用 C 语言本身并不自带集合(Collection)工具,当我们需要把结构体(struct)实例串联起来时,就需要在结构体内声明指向下一实例的指针,构成所谓的& ...
- linux内核链表剖析
1.移植linux内核链表,使其适用于非GNU编译器 2.分析linux内核中链表的基本实现 移植时的注意事项 清除文件间的依赖 剥离依赖文件中与链表实现相关的代码 清除平台相关的代码(GNU C) ...
- Linux内核链表复用实现栈
我们当然可以根据栈的特性,向实现链表一样实现栈.但是,如果能够复用已经经过实践证明的可靠数据结构来实现栈,不是可以更加高效吗? so,今天我们就复用Linux内核链表,实现栈这样的数据结构. 要实现的 ...
随机推荐
- 【转载】mysqldump的single-transaction和master-data
原文地址:mysqldump的single-transaction和master-data 作者:myownstars 先看一下--lock-tables和--lock-all-tables --lo ...
- NodeJS - Express 4.0下使用app.dynamicHelpers错误
在NodeJS - Express 4.0下使用app.dynamicHelpers发生错误: app.dynamicHelpers({ ^ TypeError: Object function (r ...
- apache-tomcat-9安装以及与eclipse结合
apache-tomcat-下载:http://tomcat.apache.org/download-90.cgi 安装(转载):http://jingyan.baidu.com/article/60 ...
- [algorithm]求最长公共子序列问题
最直白方法:时间复杂度是O(n3), 空间复杂度是常数 reference:http://blog.csdn.net/monkeyandy/article/details/7957263 /** ** ...
- 【BZOJ】【1026】【SCOI2009】Windy数
数位DP cxlove基础数位DP第三题 = =预处理是个很有用的东西!然后就是分类讨论! /***************************************************** ...
- 并行编译加快 VS C++ 项目的编译速度
最近编译的项目都比较大,话说自己的电脑配置还行,但编译所花的时间还是很长,遇到需要重新编译整个项目的时候真的有回宿舍睡一觉的冲动.昨天一不小心被我发现了一款软件Xoreax IncrediBuild ...
- uva 11609
可以想到 答案为 1*C(1,n)+2*C(2,n)+3*C(3,n)+....+n*C(n,n); 由公式 k*C(k,n) = n*C(k-1,n-1) 所以最终答案 n*2^(n-1) 用到快速 ...
- Install wget in Mac OS X Without Homebrew or MacPorts
May 22, 2012 - 31 Comments The command line tool wget lets you retrieve a group of files from FTP an ...
- What is Object Oriented Design? (OOD)
Object Oriented Design is the concept that forces programmers to plan out their code in order to hav ...
- POJ 3125 Printer Queue(队列,水题)
题意:有多组数据,每组数据给出n,m,n表示需要打印的文件个数,m表示要打印的目标位置(m为0~n-1). 接下来给出n个数,第i个值对应第i-1个位置的优先级大小. 打印规则如下: ...