Linux内核链表深度分析
2016-05-07 16:15
489 查看
链表简介:
链表是一种常用的数据结构,它通过指针将一系列数据节点连接成一条数据链。相对于数组,链表具有更好的动态性,建立链表时无需预先知道数据总量,可以随机分配空间,可以高效地在链表中的任意位置实时插入或者删除数据。链表的开销主要是访问的顺序性和组织链的空间损失。
内核链表的好主要体现为两点,1是可扩展性,2是封装。可扩展性肯定是必须的,内核一直都是在发展中的,所以代码都不能写成死代码,要方便修改和追加。将链表常见的操作都进行封装,使用者只关注接口,不需关注实现。分析内核中的链表我们
可以做些什么呢?我觉得可以将其复用到用户态编程中,以后在用户态下编程就不需要写一些关于链表的代码了,直接将内核中list.h中的代码拷贝过来用。也可以整理出my_list.h,在以后的用户态编程中直接将其包含到C文件中。
1. 链表对比
传统链表和内核链表
传统链表:一般指的是单向链表
struct List
{
struct list *next;//链表结点指针域
};
内核链表:双向循环链表 设计初衷是设计出一个通用统一的双向链表!
struct list_head
{
struct list_head *head, *prev;
};
list_head结构包含两个指向list_head结构体的指针
prev和next,由此可见,内核的链表具备双链表功能,实际上,通常它都组织成双向循环链表
2. 内核链表使用
1. INIT_LIST_HEAD:创建链表
2. list_add:在链表头插入节点
3. list_add_tail:在链表尾插入节点
4. list_del:删除节点
5. list_entry:取出节点
6. list_for_each:遍历链表
(如果我们不知道这些函数的参数以及函数内部实现,学习查阅这些函数的参数或者实现代码最好的方法还是直接查看内核源码,结和前面的用sourceInsight工具直接搜索这些函数的名字)
下面举个例子:比如查阅INIT_LIST_HEAD函数,
这个是先将内核源码导入sourceInsight工程里面!源码可以在官网上下载,然后在Linux下解压(文件名Linux分大小写,windows不分大小写),然后通过Samba和映射网络驱动器功能(前面的sourceInsight博文有讲到),点击R图标左边的那个图标(像一个打开的一本书)
这样可以很快的查看到代码实现部分:在内核Mkregtale.c文件中
这个不含数据域的链表,可以嵌入到任何数据结构中,例如可按如下方式定义含有数据域的链表:
上面的红色部分初始化一个已经存在的list_head对象,score_head为一个结构体的指针,这样可以初始化堆栈以及全局区定义的score_head对象。调用INIT_LIST_HEAD()宏初始化链表节点,将next和prev指针都指向其自身,我们就构造了一个空的双循环链表。
初始化一个空链表:其实就是链表头,用来指向第一个结点!定义结点并且初始化!然后双向循环链表就诞生了
static 加在函数前,表示这个函数是静态函数,其实际上是对作用域的限制,指该函数作用域仅局限于本文件。所以说,static 具有信息隐蔽的作用。而函数前加 inline 关键字的函数,叫内联函数,表 示编译程序在调用这个函数时,立即将该函数展开。
list_add:在链表头插入节点
list_add_tail:在链表尾插入节点
用法示例:
struct score
{
int num;
int English;
int math;
struct list_head list;//链表链接域
};
struct list_head score_head;//所建立链表的链表头
//定义三个节点 然后插入到链表中
struct score stu1, stu2, stu3;
list_add_tail(&(stu1.list), &score_head);//使用尾插法
Linux 的每个双循环链表都有一个链表头,链表头也是一个节点,只不过它不嵌入到宿主数据结构中,即不能利用链表头定位到对应的宿主结构,但可以由之获得虚拟的宿主结构指针。
list_del:删除节点
list_entry:取出节点
list_for_each:遍历链表
可以看出,使用了辅助指针pos,pos是从第一节点开始的,并没有访问头节点,直到pos到达头节点指针head的时候结束。
而且 这种遍历仅仅是找到一个个结点的当前位置,那如何通过pos获得起始结点的地址,从而可以引用结点的域?
list.h 中定义了 list_entry 宏:
#define list_entry( ptr, type, member ) \
( (type *) ( (char *) (ptr) - (unsigned long) ( &( (type *)0 ) -> member ) ) )
分析:(unsigned long) ( &( (type *)0 ) -> member ) 把 0 地址转化为 type 结构的指针,然后获取该
结构中 member 域的指针,也就是获得了 member 在type 结构中的偏移量。其中 (char *) (ptr) 求
出的是 ptr 的绝对地址,二者相减,于是得到 type 类型结构体的起始地址,即起始结点的地址。使用方法非常的巧妙!
比如下列用法:
struct score stu1, stu2, stu3;
struct list_head *pos;//定义一个结点指针
struct score *tmp;//定义一个score结构体变量
list_for_each_safe: 链表的释放
3. 内核链表实现分析
4. 移植内核链表(这里先贴出一个使用内核链表的内核模块小例程)
mylist.c文件
Makefile文件
在终端上加载运行内核模块:
这里rmmod 时会有个错误!不过没大事!百度有很多解决方案!
链表是一种常用的数据结构,它通过指针将一系列数据节点连接成一条数据链。相对于数组,链表具有更好的动态性,建立链表时无需预先知道数据总量,可以随机分配空间,可以高效地在链表中的任意位置实时插入或者删除数据。链表的开销主要是访问的顺序性和组织链的空间损失。
内核链表的好主要体现为两点,1是可扩展性,2是封装。可扩展性肯定是必须的,内核一直都是在发展中的,所以代码都不能写成死代码,要方便修改和追加。将链表常见的操作都进行封装,使用者只关注接口,不需关注实现。分析内核中的链表我们
可以做些什么呢?我觉得可以将其复用到用户态编程中,以后在用户态下编程就不需要写一些关于链表的代码了,直接将内核中list.h中的代码拷贝过来用。也可以整理出my_list.h,在以后的用户态编程中直接将其包含到C文件中。
1. 链表对比
传统链表和内核链表
传统链表:一般指的是单向链表
struct List
{
struct list *next;//链表结点指针域
};
内核链表:双向循环链表 设计初衷是设计出一个通用统一的双向链表!
struct list_head
{
struct list_head *head, *prev;
};
list_head结构包含两个指向list_head结构体的指针
prev和next,由此可见,内核的链表具备双链表功能,实际上,通常它都组织成双向循环链表
2. 内核链表使用
1. INIT_LIST_HEAD:创建链表
2. list_add:在链表头插入节点
3. list_add_tail:在链表尾插入节点
4. list_del:删除节点
5. list_entry:取出节点
6. list_for_each:遍历链表
(如果我们不知道这些函数的参数以及函数内部实现,学习查阅这些函数的参数或者实现代码最好的方法还是直接查看内核源码,结和前面的用sourceInsight工具直接搜索这些函数的名字)
下面举个例子:比如查阅INIT_LIST_HEAD函数,
这个是先将内核源码导入sourceInsight工程里面!源码可以在官网上下载,然后在Linux下解压(文件名Linux分大小写,windows不分大小写),然后通过Samba和映射网络驱动器功能(前面的sourceInsight博文有讲到),点击R图标左边的那个图标(像一个打开的一本书)
这样可以很快的查看到代码实现部分:在内核Mkregtale.c文件中
/* * This is a simple doubly linked list implementation that matches the * way the Linux kernel doubly linked list implementation works. */ struct list_head { struct list_head *next; /* next in chain */ struct list_head *prev; /* previous in chain */ };
这个不含数据域的链表,可以嵌入到任何数据结构中,例如可按如下方式定义含有数据域的链表:
struct score { int num; int English; int math; struct list_head list;//链表链接域 }; struct list_head score_head;//所建立链表的链表头INIT_LIST_HEAD(&score_head);//初始化链表头 完成一个双向循环链表的创建
上面的红色部分初始化一个已经存在的list_head对象,score_head为一个结构体的指针,这样可以初始化堆栈以及全局区定义的score_head对象。调用INIT_LIST_HEAD()宏初始化链表节点,将next和prev指针都指向其自身,我们就构造了一个空的双循环链表。
初始化一个空链表:其实就是链表头,用来指向第一个结点!定义结点并且初始化!然后双向循环链表就诞生了
static 加在函数前,表示这个函数是静态函数,其实际上是对作用域的限制,指该函数作用域仅局限于本文件。所以说,static 具有信息隐蔽的作用。而函数前加 inline 关键字的函数,叫内联函数,表 示编译程序在调用这个函数时,立即将该函数展开。
/* Initialise a list head to an empty list */ static inline void INIT_LIST_HEAD(struct list_head *list) { list->next = list; list->prev = list; }
list_add:在链表头插入节点
/** * list_add - add a new entry * @new: new entry to be added * @head: list head to add it after * * Insert a new entry after the specified head. * This is good for implementing stacks. */ static inline void list_add(struct list_head *new, struct list_head *head) { __list_add(new, head, head->next); }
/* * Insert a new entry between two known consecutive entries. * * This is only for internal list manipulation where we know * the prev/next entries already! */ #ifndef CONFIG_DEBUG_LIST static inline void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next) { next->prev = new; new->next = next; new->prev = prev; prev->next = new; } #else extern void __list_add(struct list_head *new, struct list_head *prev, struct list_head *next); #endif
list_add_tail:在链表尾插入节点
/** * list_add_tail - add a new entry * @new: new entry to be added * @head: list head to add it before * * Insert a new entry before the specified head. * This is useful for implementing queues. */ static inline void list_add_tail(struct list_head *new, struct list_head *head) { __list_add(new, head->prev, head); }
用法示例:
struct score
{
int num;
int English;
int math;
struct list_head list;//链表链接域
};
struct list_head score_head;//所建立链表的链表头
//定义三个节点 然后插入到链表中
struct score stu1, stu2, stu3;
list_add_tail(&(stu1.list), &score_head);//使用尾插法
Linux 的每个双循环链表都有一个链表头,链表头也是一个节点,只不过它不嵌入到宿主数据结构中,即不能利用链表头定位到对应的宿主结构,但可以由之获得虚拟的宿主结构指针。
list_del:删除节点
/* Take an element out of its current list, with or without * reinitialising the links.of the entry*/ static inline void list_del(struct list_head *entry) { struct list_head *list_next = entry->next; struct list_head *list_prev = entry->prev; list_next->prev = list_prev; list_prev->next = list_next; }
list_entry:取出节点
/** * list_entry - get the struct for this entry * @ptr:the &struct list_head pointer. * @type:the type of the struct this is embedded in. * @member:the name of the list_struct within the struct. */ #define list_entry(ptr, type, member) \ container_of(ptr, type, member)
/** * container_of - cast a member of a structure out to the containing structure * @ptr: the pointer to the member. * @type: the type of the container struct this is embedded in. * @member: the name of the member within the struct. * */ #define container_of(ptr, type, member) ({ \ const typeof(((type *)0)->member)*__mptr = (ptr); \ (type *)((char *)__mptr - offsetof(type, member)); })
list_for_each:遍历链表
#define list_for_each(pos, head) \ for (pos = (head)->next; prefetch(pos->next), pos != (head); \ pos = pos->next)</span></span>
可以看出,使用了辅助指针pos,pos是从第一节点开始的,并没有访问头节点,直到pos到达头节点指针head的时候结束。
而且 这种遍历仅仅是找到一个个结点的当前位置,那如何通过pos获得起始结点的地址,从而可以引用结点的域?
list.h 中定义了 list_entry 宏:
#define list_entry( ptr, type, member ) \
( (type *) ( (char *) (ptr) - (unsigned long) ( &( (type *)0 ) -> member ) ) )
分析:(unsigned long) ( &( (type *)0 ) -> member ) 把 0 地址转化为 type 结构的指针,然后获取该
结构中 member 域的指针,也就是获得了 member 在type 结构中的偏移量。其中 (char *) (ptr) 求
出的是 ptr 的绝对地址,二者相减,于是得到 type 类型结构体的起始地址,即起始结点的地址。使用方法非常的巧妙!
比如下列用法:
struct score stu1, stu2, stu3;
struct list_head *pos;//定义一个结点指针
struct score *tmp;//定义一个score结构体变量
//遍历整个链表,每次遍历将数据打印出来 list_for_each(pos, &score_head)//这里的pos会自动被赋新值 { tmp = list_entry(pos, struct score, list); printk(KERN_WARNING"num: %d, English: %d, math: %d\n", tmp->num, tmp->English, tmp->math); }
list_for_each_safe: 链表的释放
/** * list_for_each_safe - iterate over a list safe against removal of list entry * @pos:the &struct list_head to use as a loop cursor. * @n:another &struct list_head to use as temporary storage * @head:</span>the head for your list. */ #define list_for_each_safe(pos, n, head) \ for (pos = (head)->next, n = pos->next; pos != (head); \ pos = n, n = pos->next)
3. 内核链表实现分析
4. 移植内核链表(这里先贴出一个使用内核链表的内核模块小例程)
mylist.c文件
#include<linux/module.h>
#include<linux/init.h>
#include<linux/list.h>//包含内核链表头文件
struct score { int num; int English; int math; struct list_head list;//链表链接域 }; struct list_head score_head;//所建立链表的链表头
//定义三个节点 然后插入到链表中
struct score stu1, stu2, stu3;
struct list_head *pos;//定义一个结点指针
struct score *tmp;//定义一个score结构体变量
int mylist_init()
{
INIT_LIST_HEAD(&score_head);//初始化链表头 完成一个双向循环链表的创建
stu1.num = 1;
stu1.English = 59;
stu1.math = 99;
//然后将三个节点插入到链表中
list_add_tail(&(stu1.list), &score_head);//使用尾插法
stu2.num = 2;
stu2.English = 69;
stu2.math = 98;
list_add_tail(&(stu2.list), &score_head);
stu3.num = 3;
stu3.English = 89;
stu3.math = 97;
list_add_tail(&(stu3.list), &score_head);
//遍历整个链表,每次遍历将数据打印出来 list_for_each(pos, &score_head)//这里的pos会自动被赋新值 { tmp = list_entry(pos, struct score, list); printk(KERN_WARNING"num: %d, English: %d, math: %d\n", tmp->num, tmp->English, tmp->math); }
return 0;
}
void mylist_exit()
{
//退出时删除结点
list_del(&(stu1.list));
list_del(&(stu2.list));
printk(KERN_WARNING"mylist exit!\n");
}
module_init(mylist_init);
module_exit(mylist_exit);
Makefile文件
obj-m := mylist.o KDIR := /home/kernel/linux-ok6410 all: make -C $(KDIR) M=$(PWD) modules CROSS_COMPILE=arm-linux- ARCH=arm clean: rm -f *.o *.ko *.order *.symvers
在终端上加载运行内核模块:
这里rmmod 时会有个错误!不过没大事!百度有很多解决方案!
相关文章推荐
- C#数据结构之顺序表(SeqList)实例详解
- Lua教程(七):数据结构详解
- 解析从源码分析常见的基于Array的数据结构动态扩容机制的详解
- C#数据结构之队列(Quene)实例详解
- C#数据结构揭秘一
- C#数据结构之单链表(LinkList)实例详解
- 数据结构之Treap详解
- 用C语言举例讲解数据结构中的算法复杂度结与顺序表
- C#数据结构之堆栈(Stack)实例详解
- C#数据结构之双向链表(DbLinkList)实例详解
- JavaScript数据结构和算法之图和图算法
- Java数据结构及算法实例:冒泡排序 Bubble Sort
- Java数据结构及算法实例:插入排序 Insertion Sort
- Java数据结构及算法实例:考拉兹猜想 Collatz Conjecture
- java数据结构之java实现栈
- java数据结构之实现双向链表的示例
- Java数据结构及算法实例:选择排序 Selection Sort
- Java数据结构及算法实例:朴素字符匹配 Brute Force
- Java数据结构及算法实例:汉诺塔问题 Hanoi
- Java数据结构及算法实例:快速计算二进制数中1的个数(Fast Bit Counting)