内核数据结构
Linux内核实现了这些通用数据结构,而且提倡大家在开发时重用。
内核开发者应该尽可能地使用这些数据结构,而不要自作主张的山寨方法。
通用的数据结构有以下几种:链表、队列、映射和二叉树
一、链表
1.1 单向链表和双向链表
链表是Linux中最简单、最普通的数据结构。
最简单的数据结构表示一个链表:
/* 一个链表中的一个元素 */ struct list_element { void *data; /* 有效数据 */ struct list_element *next; /* 指向下一个元素的指针 */ };
然后还有双向链表
/* 一个链表中的一个元素 */ struct list_element { void *data; /* 有效数据 */ struct list_element *next; /* 指向下一个元素的指针 */ struct list_element *prev; /* 指向前一个元素的指针 */ };
1.2 环形链表
通常情况下,链表最后一个元素后面没有元素了,所以将链表元素中的向后指针设置为NULL,以此表明是链表中的最后一个元素。
在有些链表中,链表尾元素指向链表首元素,这种链表首位相连,被称为环形链表。
1.3 沿链表移动
只能是线性移动,先访问某个元素,然后访问下一个元素,不断重复。
如果需要随机访问,一般不使用链表。
有时,首元素会用一个特殊指针表示,该指针称为头指针。
1.4 Linux内核中的实现
linux的内核方式与众不同,它不是将数据结构塞入链表,而是将链表结点塞入数据结构。
链表的数据结构在<linux/list.h>中声明,结构很简单:
struct list_head { struct list_head *next; struct list_head *prev; };
这样子我们可以用这个来实现一个链表了
struct fox { unsigned long tail_length; /* 尾巴长度,以厘米为单位 */ unsigned long weight; /* 重量,以千克为单位 */ bool is_fantasitic /* 这只狐狸奇妙吗? */ struct list_head list; /* 所有fox结构体形成链表 */ };
但是list_head的头链表要找到用户自动义的结构体指针还是要费点功夫
#define container_of(ptr, type, member) ({ const typeof( ((type *)0)->member) *__mptr = (ptr); (type *)( (char *)__mptr - offsetof(type, member) );}) #define list_entry(ptr, type, member) container_of(ptr, type, member)
依靠list_entry()方法,内核提供了创建、操作以及其他链表管理的各种例程。所有这些方法都不需要知道list_head所嵌入对象的数据结构。
1.4.2 定义一个链表
链表需要在使用前初始化,最常见的方式是在运行时初始化链表
struct fox *red_fox; red_fox = kmalloc(sizeof(struct fox), GFP_KERNEL); red_fox->tail_length = 40; red_fox->weight = 6; red_fox->is_fantastic = false; INIT_LIST_HEAD(&red_fox->list);
当然如果一个结构在编译期静态创建,需要在其中给出一个链表的直接引用:
struct fox red_fox = { .tail_length = 40, .weight = 6, .list = LIST_HEAD_INIT(red_fox.list), };
1.4.3 链表头
链表需要一个标准的索引指针指向整个链表,即链表的头文件。
内核链表最杰出的特性就是:任何节点都是无差别的,索引整个链表的节点,也是一个常规的节点。
static LIST_HEAD(fox_list);
该函数定义并初始化了一个名为fox_list的链表例程。
1.5 操作链表
相关的函数都在文件<linux/list.h>中有原型,大多都是以内联函数的形式实现的。
1.5.1 向链表中增加一个节点
给链表增加一个节点,向指定链表的head节点后插入new节点
list_add(struct list_head *new, struct list_head *head);
把节点增加到链表尾,
list_add_tail(struct list_head *new, struct list_head *head);
1.5.2 从链表中删除一个节点
函数从链表中删除entry元素,该操作不会释放entry或释放包含entry的数据结构体所占用的内存。仅仅是将entry元素从链表中一走,调用后通常还需要撤销包含entry的数据结构体和其他的entry项。
list_del(struct list_head *entry)
1.5.3 移动和合并链表节点
/* 把节点从一个链表移到另一个链表 从链表中移除list项,然后将其加入到另一链表的head节点后面 */ list_move(struct list_head *list, struct list_head *head); /* 把节点从一个链表移到另一个链表的末尾 和list_move一样,不过是将list项插入到head前面 */ list_move_tail(struct list_head *list, struct list_head *head); /* 检查链表是否为空,如果链表为空返回非0,否则返回0 */ list_empty(struct list_head *head); /* 把两个未连接的链表合并在一起 将list指向的链表插入到指定链表的head元素后面 */ list_splice(struct list_head *list, struct list_head *head); /* 把两个未连接的链表合并在一起,并重新初始化原来的链表 不同于list_splice,list指向的链表要被重新初始化 */ list_splice_init(struct list_head *list, struct list_head *head);
如果碰巧已经得到了next和prev指针,可以直接调用内部链表函数,从而省下一点时间。获取指针的时间。
1.6 遍历链表
和操作链表不同,链表遍历的复杂度为O(n),n是链表所包含的元素数目
①基本方法
遍历链表最简单的方法是使用list_for_each()宏
/* 需要使用两个list_head类型的参数,第一个指向当前项,临时变量 第二个指向参数是需要遍历的链表以头节点的形式存在的list_head 每次遍历,第一个参数在链表中不断移动,知道访问完所有元素 */ struct list_head *p; list_for_each(p, list) { /* p指向链表中的元素 */ }
不过获得指向链表结构的指针基本没用,需要使用list_entry()宏,获取数据结构的指针。
struct list_head *p; struct fox *f; list_for_each(p, &fox_list) { /* f points to the struct in which the list is embedded */ f = list_entry(p, struct fox, list); }
②可用的方法
上面的写法不够灵活,所以多数内核采用list_for_each_entry()宏遍历链表
/* 这里pos是一个指向包含list_head节点对象的指针,可以看成list_entry的返回值 head是一个指向头节点的指针,即遍历开始位置 member是pos中list_head结构的变量名 */ list_for_each_entry(pos, head, member); /* 一个例子 */ struct fox *f; list_for_each_entry(f, &fox_list, list) { /* on each iteration, 'f' points to the next fox structure ... */ }
在inotify内核文件系统的更新通知机制中,有实际的例子:
static struct inotify_watch *inode_find_handle(struct inode *inode, struct inotify_handle *ih) { struct inotify_watch *watch; list_for_each_entry(watch, &inode->inotify_watches, i_list) { if(watch->ih == ih) return watch; } return NULL; }
③反向遍历链表
宏list_for_each_entry_reverse()和list_for_each_entry()类似,不同的是它是反向遍历
/* 函数不再是沿着next指针遍历,而是沿着prev遍历 用法和list_for_each_entry()相同 */ list_for_each_entry_reverse(pos, head, member);
反向可以组成类似堆的功能
④遍历的同时删除
标准的链表遍历是无法同时删除节点的。
lsit_for_each_entry_safe(pos, next, head, member)
inotify中也有例子:
void inotify_inode_is_dead(struct inode *inode) { struct inotify_watch *watch, *next; mutex_lock(&inode->inotify_mutex); list_for_each_entry_safe(watch, next, &inode->inotify_watches, i_list) { struct inotify_handle *ih = watch->ih; mutex_lock(&ih->mutex); inotify_remove_watch_locked(ih, watch); /* deletes watch */ mutex_unlock(&ih->mutex); } mutex_unlock(&inode->inotify_mutex); }
内核还提供了反向遍历并删除,list_for_each_entry_safe_reverse()
list_for_each_entry_safe_reverse(pos, n, head, member);
剩下的就在<linux/list.h>中。。。
1.7 链表练习的例子
二、队列
实现生产者和消费者最简单的方式是使用队列。
Linux内核通用队列实现称为kfifo。在<kernel/kfifo.h>中声明,在kernel/kfifo.c中实现。使用前请仔细检查文件<linux/kfifo.h>
2.1 kfifo
linux的kfifo和多数其他队列实现类似,提供两个主要操作:
- enqueue(入队列):拷贝数据到队列中的入口偏移位置
- dequeue(出队列):从队列中出口偏移处拷贝数据
kfifo对象维护两个偏移量:
- 入口偏移量:下一次入队列时的位置,入口偏移等于出口偏移时队列为空,入口偏移等于队列长度是满
- 出口偏移量:下一次出队列时的位置,出口偏移总是小于等于入口偏移
2.2 创建队列
使用kfifo前,必须对它进行定义和初始化,有静态和动态分配两种,动态更普遍:
/* size:初始化kfifo的大小 */ /* gfp_mask:表示分配队列,12章详细讨论 */ /* 成功:返回0,错误:返回负数错误码 */ int kfifo_alloc(struct kfifo *fifo, unsigned int size, gfp_t gfp_mask); /* 使用例子 */ struct kfifo fifo; int ret; ret = kfifo_alloc(&fifo, PAGE_SIZE, GFP_KERNEL); if(ret) return ret; /* "fifo"现在代表一个大小为PAGE_SIZE的队列 */ /* 如果自己分配缓冲,可以调用 */ /* 由buffer指定size字节大小的内存,而且提到的size必须是2的幂 */ void kfifo_init(struct kfifo *fifo, void *buffer, unsigned int size);
静态分配不太常用:
/* 创建一个名称为name,大小为size的kfifo对象 */ DECLARE_KFIFO(name, size); INIT_KFIFO(name);
2.3 推入队列数据
当kfifo对象创建和初始化后,推入数据到队列需要通过kfifo_in()方法完成:
/* from指针所指的len字节数据拷贝到fifo所指定的队列中 成功:返回推入数据的字节大小。 如果队列中空闲字节小于len,则最多拷贝可用空间大小。 然后返回值就会小于len,甚至会返回0 unsigned int kfifo_in(struct kfifo *fifo, const void *from, unsigned int len);
2.4 摘取队列数据
摘取数据通过函数kfifo_out()完成。
unsigned int kfifo_out(struct kfifo *fifo, void *to, unsigned int len);
从fifo所指的队列中拷贝出长度为len字节的数据到to所指的缓冲中。
如果只是查看数据内容,而不删除它,可以使用kfifo_out_peek()方法。
unsigned int kfifo_out_peek(struct kfifo *fifo, void *to, unsigned int len, unsigned offset);
该函数出口偏移不增加,下次还能被kfifo_out获得。
2.5 获取队列长度
kfifo相关的有,获取队列空间总体大小、获取队列已推入的数据大小、获取还有多少可用空间、
判断队列空、判断队列满
/* 获取用于存储kfifo队列的空间总体大小 */ static inline unsigned int kfifo_size(struct kfifo *fifo); /* 获取kfifo队列中已推入的数据大小 */ static inline unsigned int kfifo_len(struct kfifo *fifo); /* 获取kfifo队列中还有多少可用空间 */ static inline unsigned int kfifo_avail(struct kfifo *fifo); /* 判断队列是否为空 */ static inline int kfifo_is_empty(struct kfifo *fifo); /* 判断队列是否为满 */ static inline int kfifo_is_full(struct kfifo *fifo);
2.6 重置和撤销队列
如果重置,那么之前的内容会被抛弃掉。如果撤销,需要根据不同初始化情况设置。
static inline void kfifo_reset(struct kfifo *fifo); void kfifo_free(struct kfifo *fifo);
2.7 队列使用举例
内核例程:
#include <linux/init.h> #include <linux/module.h> #include <linux/proc_fs.h> #include <linux/mutex.h> #include <linux/kfifo.h> #define FIFO_SIZE 128 #define PROC_FIFO "record-fifo" static DEFINE_MUTEX(read_lock); static DEFINE_MUTEX(write_lock); #if 0 #define DYNAMIC #endif #ifdef DYNAMIC struct kfifo_rec_ptr_1 test; #else typedef STRUCT_KFIFO_REC_1(FIFO_SIZE) mytest; static mytest test; #endif static const char *expected_result[] = { "a", "bb", "ccc", "dddd", "eeeee", "ffffff", "ggggggg", "hhhhhhhh", "iiiiiiiii", "jjjjjjjjjj", }; static int __init testfunc(void) { char buf[100]; unsigned int i; unsigned int ret; struct { unsigned char buf[6]; } hello = { "hello" }; printk(KERN_INFO "record fifo test start "); kfifo_in(&test, &hello, sizeof(hello)); printk(KERN_INFO "fifo peek len: %u " ,kfifo_peek_len(&test)); for(i=0;i<10;i++) { memset(buf, 'a'+i, i+1); kfifo_in(&test, buf, i+1); } printk(KERN_INFO "skip 1st element "); kfifo_skip(&test); printk(KERN_INFO "fifo len: %u ", kfifo_len(&test)); ret = kfifo_out_peek(&test, buf, sizeof(buf)); if(ret) printk(KERN_INFO "%.*s ", ret, buf); i = 0; while(!kfifo_is_empty(&test)) { ret = kfifo_out(&test, buf, sizeof(buf)); buf[ret] = '