Linux4.1内核常见数据结构
Linux内核为开发者提供了很多实用的内建数据结构,这些结构可以帮助开发者节省开发设计个性化方法的时间,并且这些数据结构的功能丰富强大,本文主要以4.1为例
常用的几个有:链表 队列 红黑树 映射
1. 链表
链表是Linux内核中最简单最普通的数据结构,它是一种存放和操作可变数量节点的数据结构,与静态数组不同之处在于,它的所有节点是动态创建 插入 删除的,编译时不需要知道具体有多少数量,创建时间也各不相同,不占用连续内存空间。
Linux内核链表的独特性:过去使用链表的时候,人们通常是在结构体中添加一个指向前一个节点的指针,和指向后一个节点的指针。例如
struct dog{
unsigned int age;
unsigned int weight;
struct dog *next;
struct dog *prev;
}
这种方式虽然很常见但是不够通用,每次创造一个新的结构体链表都要为此配套,以相应的链表和结构体为参数操作方法。Linux内核给出了一种通用的链表构造和相应的链表操作方法。
链表的结构体代码在<linux/list.h>文件中,结构如下:
struct list_head{
struct list_head *next;
struct list_head *prev;
}
似乎看起来和之前没什么区别,但是它的神奇之处在于它可以直接嵌入到任何结构体中,从而生成一个链表,下面看看它是如何使用的。
struct dog{
unsigned int age;
unsigned int weight;
struct list_head list;
}
接下来,所有的数据结构只需要使用操作结构体list_head的链表操作方法就可以实现链表的所有操作了。在讲链表的操作之前,我们需要先解释一下一些同学的疑问。链表的操作的结构体是list_head,但是这一部分不是我们工作需要的节点信息啊。
C语言在编译的时候,一个给定的结构体的变量偏移是固定的,意思就是说,结构体dog的指针和结构体list_head的指针之间的偏移是固定的,因此,我们拿到一个list_head的指针就可以找到当前节点所属的dog结构体的指针,具体执行方法使用container_of如下,
#define container_of(ptr, type,member) \
{
const typeof( ((type *)0)->member) *__mptr = (ptr); \
//声明了一个type结构体中member成员类型的指针__mptr
(type*)( (char*)__mptr - offsetof(type,member))
//__mptr指针按字节减去 结构体type指针和其成员member指针的偏移 ,得到ptr所在结构体的指针
}
Linux定义了一个宏函数来返回包含list_head结构体的父类型结构体指针的函数,依靠该方法,内核提供了创建操作以及链表管理的各种例程而不需要关注list_head嵌入的对象的数据结构。
#define list_entry(ptr,type,member) \
container_of(ptr,type,member)
链表的初始化:因为大多数节点是动态创建的,所以最常见的方式是在运行时初始化链表。初始化链表有下面几种方法
链表头的指针作为参数初始化
#define INIT_LIST_HEAD(list); {list->next = list; list->prev = list;}
//头结点的前后指针指向的自己
struct dog *yellow_dog;
INIT_LIST_HEAD(&yellow_dog->list);
对结构体成员初始化
struct dog yellow_dog{
.age = 4,
.weight = 6,
.list = LIST_HEAD_INIT(yellow_dog.list),
};
#define LIST_HEAD_INIT(yellow_dog.list) \
{
&(yellow_dog.list),&(yellow_dog.list)
}
//链表头节点的前后指针 指向自己
相信大家看到这里应该明白,最先初始化的是链表头,到这里又有人疑问了:不是只有线性链表需要链表头吗?环形双向链表为什么也需要?
虽然说上述的Linux内核给出的方式中,各个节点都可以作为链表头开始遍历整个链表,但是有时也确实需要一个特殊的链表头,例如,在使用Linux内核中对链表配套的例程中需要一个标准的索引指针指向链表。
static LIST_HEAD(dog_list);
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
- 链表的操作
对于链表的操作,最基础的莫过于 添加节点 删除节点 链表之间的对接 向前遍历 向后遍历等等。我们在这里继续使用上面给出的例子,假设链表头结点是dog_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;
}
list_add(struct list_head *new, struct list_head *head);
{
__list_add(new, head, head->next);
}
此函数是在head节点的后面插入一个new节点,因为环形链表没有真正固定的head,如果总是以最后一个插入的节点作为head的话,那么使用该函数可以做成一个栈结构。(使用的时候可能需要一个总是指向最后一个节点的temp指针作为辅助)
list_add_tail(struct list_head *new, struct list_head *head);
{
__list_add(new, head->prev, head);
}
此函数是在head节点的前面插入一个new节点,和list_add类似,如果总是把第一个插入的元素当成head,那么该函数可以实现一个队列。
链表节点的删除
除添加链表节点外,最重要的操作就是从链表中删除一个节点,调用list_del函数,该函数可以从链表中移走节点但是不会释放节点,也就是说,被删除节点的前后指针依然指向链表中的节点。所以该函数被调用后,通常要再初始化被删除节点的指针。
static inline void __list_del(struct list_head* prev, struct list_head *next)
{
next->prev = prev;
prev->next = next;
}
static inline void list_del(struct list_head* entry)
{
__list_del(entry->prev,entry->next);
}
static inline void list_del_init(struct list_head* entry)
{
__list_del(entry->prev,entry->next);
INIT_LIST_HEAD(entry);
}
移动和合并链表节点
把节点从一个链表移除,并加到另一个链表head节点后面,list_move,或者把节点从一个链表移除,并加到另一个链表head节点前面list_move_tail;
list_move(struct list_head *list, struct list_head *head);
list_move_tail(struct list_head *list, struct list_head *head);
这两个函数的实现是先移除节点再把移除的节点添加到head节点前后
检查链表是否为空
list_empty(struct list_head *head);
该函数的实现为 return head->next==head,若链表为空,返回非零,否则返回0;
链表合并
list_splice(struct list_head *list, struct list_head *head);
该函数可以把两个链表合并,list指向的链表插入到head节点后面
static inline void __list_splice(const struct list_head *list, struct list_head
*prev, struct list_head *next)
{
struct list_head *first = list->next;
struct list_head *last = list->prev;
first->prev = prev;
prev->next = first;
last->next = next;
next->prev = last;
}
static inline void list_splice(const struct list_head *list,struct list_head *head)
{
if (!list_empty(list))
__list_splice(list, head, head->next);
}
另外,还有可以初始化原来链表的函数
static inline void list_splice_init(struct list_head *list, struct list_head *head)
{
if (!list_empty(list)) {
__list_splice(list, head, head->next);
INIT_LIST_HEAD(list);
}
}
遍历链表
链表仅仅是一个能够存储数据的容器,如果不能访问到所需要的数据,这些就没有意义了,因此,我们必须能够利用链表移动并访问包含所需数据的结构体。
遍历链表最基本的方法是使用list_for_each宏
#define list_for_each(pos,head) \
for(pos = (head)->next; pos!=(head);pos = pos->next)
但是,指向链表结构体list_head的指针通常是无用的,我们所用的通常是一个指向包含链表结构list_head的结构体指针,因此,内核使用之前提高的list_entry宏提供了一个遍历宏。
#define list_for_each_entry(pos, head, member) \
for (pos = list_first_entry(head, typeof(*pos), member); \
&pos->member != (head); \
pos = list_next_entry(pos, member))
#define list_first_entry(ptr, type, member) \
list_entry((ptr)->next, type, member)
#define list_next_entry(pos, member) \
list_entry((pos)->member.next, typeof(*(pos)), member)
pos是指向包含list_head节点对象的指针,它应当是list_entry的返回值,head是一个指向头结点的指针,member是pos中list_head结构体的变量名。
反向遍历链表宏为list_for_each_entry_reserve的工作与上述相似,不同的是它是反向遍历的
#define list_for_each_entry_reverse(pos, head, member) \
for (pos = list_last_entry(head, typeof(*pos), member); \
&pos->member != (head); \
pos = list_prev_entry(pos, member))
如果你想遍历的同时删除节点是不行的,因为前面的链表遍历方法建立在你不会修改链表的基础上的,如果当前项在遍历时被删除,那么接下来的遍历就无法获得next或者prev指针。比如使用list_for_each_entry时,因为在删除完当前节点后,需要继续访问下一个节点。而在此时,需要被撤销的节点,这样就无法完成遍历任务。通常开发人员会在潜在的删除操作之前存储next或者prev指针到临时变量中。Linux内核已经提供了这个例程
list_for_each_entry_safe(pos,next,head,member);
该函数可以按照上述例程使用,但是要额外提供一个next指针,next指针和pos指针一致类型,用来存储下一项,使得能够安全删除当前项
#define list_for_each_entry_safe(pos, n, head, member) \
for(pos = list_first_entry(head, typeof(*pos), member), \
n = list_next_entry(pos, member); \
&pos->member != (head); \
pos = n, n = list_next_entry(n, member))
同样,反向遍历链表的同时删除节点的话,内核提供了list_for_each_entry_safe_reserve。注意,安全版本的遍历只能保护在循环体中从链表删除数据,如果这时有其他地方并发删除的话,也不安全,所以要锁定链表。
其他链表方法都可以在<linux/list.h>中找到
2. 队列
队列是任何操作系统都离不开的一种数据结构。例如在处理网络数据包时,需要按照顺序以此对数据进行处理,数据的顺序非常重要,第一个到达的数据包要第一个处理,最后一个到达的数据包要最后一个处理。这正是队列的特点。FIFO,first in first out
Linux内核中通用队列的相关方法结构体,实现在 kfifo.c中,声明以及宏定义在 kfifo.h中,2.6内核和4.1内核的api不同,具体版本的使用具体查看代码,但是思路上是一脉相承的,具有参考意义,本小节以4.1内核为准。
-
kfifo
Linux的kfifo提供了两个主要操作,入队和出队。kfifo对象维护了两个偏移量:入口偏移和出口偏移。入口偏移量是指下一次入队时的位置,出口偏移量是下一次出队列的位置。
正常情况下,出口偏移总是小于等于入口偏移,否则没有意义。
struct __kfifo {
unsigned int in;
unsigned int out;
unsigned int mask;
unsigned int esize; //size的单位大小,一般是unsigned char的大小
void *data;
};
struct kfifo {
union {
struct __kfifo kfifo;
unsigned char *type;
const unsigned char *const_type;
char (*rectype)[0];
void *ptr;
void const *ptr_const;
}
unsigned char buf[0];
}
上面是根据4.1内核中的kfifo.h文件中整理出的结构体,实际代码中,kfifo是一个可动态申请类型的
使用分层宏定义的手法简洁高效的满足动态修改数据类型和指针类型的需求
struct kfifo __STRUCT_KFIFO_PTR(unsigned char, 0, void);
#define __STRUCT_KFIFO_PTR(type, recsize, ptrtype) \
{ \
__STRUCT_KFIFO_COMMON(type, recsize, ptrtype); \
type buf[0]; \
}
#define __STRUCT_KFIFO_COMMON(datatype, recsize, ptrtype) \
union { \
struct __kfifo kfifo; \
datatype *type; \
const datatype *const_type; \
char (*rectype)[recsize]; \
ptrtype *ptr; \
ptrtype const *ptr_const; \
}
创建队列
使用kfifo之前,必须对它进行定义和初始化。分为静态方法和动态方法,动态方法更加普遍。
动态方法使用kfifo_alloc宏
#define kfifo_alloc(fifo, size, gfp_mask) \
__kfifo_int_must_check_helper( \
({ \
typeof((fifo) + 1) __tmp = (fifo); \
struct __kfifo *__kfifo = &__tmp->kfifo; \
__is_kfifo_ptr(__tmp) ? \
__kfifo_alloc(__kfifo, size, sizeof(*__tmp->type), gfp_mask) : \
-EINVAL; \
}) \
)
该宏有三个参数,第一个参数为 结构体kfifo的指针,第二个参数size是你想创建并初始化的队列的大小,第三个参数为 内存分配的标识,暂且按下不表。如果申请成功则返回0,否则返回一个负数。
这个宏的使用是基于kfifo结构体中的联合体是__kfifo才能正常使用,对指针+1可以保证传入的是队列指针而不是某个数组首地址。调用__kfifo_alloc函数去申请空间,该函数中sizeof(*__tmp->type)体现了上面结构体中type buf[0];的作用,单位空间大小。kfifo结构体更加灵活
下面正常使用该宏的一个例子:
struct kfifo fifo;
int ret = 0;
ret = kfifo_alloc(&fifo, PAGE_SIZE, GFP_KERNEL);
if(ret){
return ret;
}
现在fifo代表一个大小为PAGE_SIZE的队列
第二种方法如下,
分配缓冲的函数kfifo_init可以创建并初始化一个kfifo对象,并且使用由buffer指针指向的大小为size字节大小的内存空间,这个函数允许用户自由分配内存
#define kfifo_init(fifo, buffer, size) \
({ \
typeof((fifo) + 1) __tmp = (fifo); \ 防止误传其他类型,必须是指针
struct __kfifo *__kfifo = &__tmp->kfifo; \
__is_kfifo_ptr(__tmp) ? \
__kfifo_init(__kfifo, buffer, size, sizeof(*__tmp->type)) : \
-EINVAL; \
})
另外还有静态声明kfifo的方法
DECLARE_KFIFO(name,size);
INIT_KFIFO(name);
这两个宏要配合使用,初始化结束之后,kfifo的缓冲区在结构体kfifo中的buf数组,大小和之前一样为2的幂次方。为什么要是2的幂次方,mask为什么是size - 1?
因为这是环形队列,将来求模的时候,使用mask直接逻辑与上去就得到了以size为模的结果了,提高速度。
推入队列数据
当kfifo对象创建和初始化后,推入数据到队列需要通过方法完成。
4.1内核中使用了宏kfifo_in(fifo, buf, n),2.6内核中使用的是一个入队函数
unsigned int kfifo_in(struct kfifo *fifo, const void *from, unsigned int len);
第一个参数为入队的队列对象指针fifo,第二个参数是要入队数据的起始指针,第三个参数len是要把from指针指向的len字节数据拷贝到队列中,如果成功返回入队数据的字节大小。如果队列中的空闲空间小于len,则最多拷贝队列可用空间大小的数据,这样的话返回值可能小于len甚至为0,此时意味着没有数据入队。
#define kfifo_in(fifo, buf, n) \
({ \
typeof((fifo) + 1) __tmp = (fifo); \
typeof(__tmp->ptr_const) __buf = (buf); \
unsigned long __n = (n); \
const size_t __recsize = sizeof(*__tmp->rectype); \
struct __kfifo *__kfifo = &__tmp->kfifo; \
(__recsize) ?\
__kfifo_in_r(__kfifo, __buf, __n, __recsize) : \
__kfifo_in(__kfifo, __buf, __n); \
})
判断剩余未使用空间够不够len,然后调用kfifo_copy_in入队
unsigned int __kfifo_in(struct __kfifo *fifo, const void *buf, unsigned int len)
{
unsigned int l;
l = kfifo_unused(fifo);
if (len > l)
len = l;
kfifo_copy_in(fifo, buf, len, fifo->in);
fifo->in += len;
return len;
}
__kfifo_poke_n internal helper function for storeing the length of the record into the fifo
unsigned int __kfifo_in_r(struct __kfifo *fifo, const void *buf,
unsigned int len, size_t recsize)
{
if (len + recsize > kfifo_unused(fifo))
return 0;
__kfifo_poke_n(fifo, len, recsize);
kfifo_copy_in(fifo, buf, len, fifo->in + recsize);
fifo->in += len + recsize;
return len;
}
static void kfifo_copy_in(struct __kfifo *fifo, const void *src,
unsigned int len, unsigned int off)
{
unsigned int size = fifo->mask + 1;
unsigned int esize = fifo->esize;
unsigned int l;
off &= fifo->mask;
if (esize != 1) {
off *= esize;
size *= esize;
len *= esize;
}
l = min(len, size - off);
memcpy(fifo->data + off, src, l);
memcpy(fifo->data, src + l, len - l);
/*
* make sure that the data in the fifo is up to date before
* incrementing the fifo->in index counter
*/
smp_wmb(); //内存屏障
}
同样另一个入队的方法为kfifo_put,没有第三个参数,默认为1
通过 smp_wmb() 保证先向缓冲区写入数据后才修改 in 索引
#define kfifo_put(fifo, val) \
({ \
typeof((fifo) + 1) __tmp = (fifo); \
typeof(*__tmp->const_type) __val = (val); \
unsigned int __ret; \
size_t __recsize = sizeof(*__tmp->rectype); \
struct __kfifo *__kfifo = &__tmp->kfifo; \
if (__recsize) \
__ret = __kfifo_in_r(__kfifo, &__val, sizeof(__val), \
__recsize); \
else { \
__ret = !kfifo_is_full(__tmp); \
if (__ret) { \
(__is_kfifo_ptr(__tmp) ? \
((typeof(__tmp->type))__kfifo->data) : \
(__tmp->buf) \
)[__kfifo->in & __tmp->kfifo.mask] = \
(typeof(*__tmp->type))__val; \
smp_wmb(); \
__kfifo->in++; \
} \
} \
__ret; \
})
摘取队列数据
摘取数据使用kfifo_out(struct kfifo* fifo, void* to, unsigned int len),该函数从fifo所指向的队列中拷贝出长度为len的数据到to所指向的缓冲中,如果成功则返回拷贝长度,如果队列中数据大小小于len,则拷贝出的数据的数量必定小于len。
#define kfifo_out(fifo, buf, n) \
__kfifo_uint_must_check_helper( \
({ \
typeof((fifo) + 1) __tmp = (fifo); \ \\临时指向队列结构体的指针
typeof(__tmp->ptr) __buf = (buf); \ \\临时指向目的缓冲的指针
unsigned long __n = (n); \ \\摘取数据的长度
const size_t __recsize = sizeof(*__tmp->rectype); \
struct __kfifo *__kfifo = &__tmp->kfifo; \
(__recsize) ?\
__kfifo_out_r(__kfifo, __buf, __n, __recsize) : \
__kfifo_out(__kfifo, __buf, __n); \
}) \
)
依据__recsize判断执行那个函数
unsigned int __kfifo_out_r(struct __kfifo *fifo, void *buf,
unsigned int len, size_t recsize)
{
unsigned int n;
if (fifo->in == fifo->out)
return 0;
len = kfifo_out_copy_r(fifo, buf, len, recsize, &n);
fifo->out += n + recsize;
return len;
}
static unsigned int kfifo_out_copy_r(struct __kfifo *fifo,
void *buf, unsigned int len, size_t recsize, unsigned int *n)
{
*n = __kfifo_peek_n(fifo, recsize);
if (len > *n)
len = *n;
kfifo_copy_out(fifo, buf, len, fifo->out + recsize);
return len;
}
/*
* __kfifo_peek_n internal helper function for determinate the length of
* the next record in the fifo
*/
static unsigned int __kfifo_peek_n(struct __kfifo *fifo, size_t recsize)
{
unsigned int l;
unsigned int mask = fifo->mask;
unsigned char *data = fifo->data;
l = __KFIFO_PEEK(data, fifo->out, mask);
if (--recsize)
l |= __KFIFO_PEEK(data, fifo->out + 1, mask) << 8;
return l;
}
static void kfifo_copy_out (struct __kfifo *fifo, void *dst,
unsigned int len, unsigned int off)
{
unsigned int size = fifo->mask + 1;
unsigned int esize = fifo->esize;
unsigned int l;
off &= fifo->mask;
if (esize != 1) {
off *= esize;
size *= esize;
len *= esize;
}
l = min(len, size - off);
memcpy(dst, fifo->data + off, l);
memcpy(dst + l, fifo->data, len - l);
/*
* make sure that the data is copied before
* incrementing the fifo->out index counter
*/
smp_wmb();
}
单个值的出队
#define kfifo_get(fifo, val) \
__kfifo_uint_must_check_helper( \
({ \
typeof((fifo) + 1) __tmp = (fifo); \
typeof(__tmp->ptr) __val = (val); \
unsigned int __ret; \
const size_t __recsize = sizeof(*__tmp->rectype); \
struct __kfifo *__kfifo = &__tmp->kfifo; \
if (__recsize) \
__ret = __kfifo_out_r(__kfifo, __val, sizeof(*__val), \
__recsize); \
else { \
__ret = !kfifo_is_empty(__tmp); \
if (__ret) { \
*(typeof(__tmp->type))__val = \
(__is_kfifo_ptr(__tmp) ? \
((typeof(__tmp->type))__kfifo->data) : \
(__tmp->buf) \
)[__kfifo->out & __tmp->kfifo.mask]; \
smp_wmb(); \
__kfifo->out++; \
} \
} \
__ret; \
}) \
)
获取队列长度
调用kfifo_size方法可以获得用于存储kfifo队列的空间的总体大小,以字节为单位
#define kfifo_size(fifo) ((fifo)->kfifo.mask + 1)
调用kfifo_len方法返回已推入队列数据大小
#define kfifo_len(fifo) \
({ \
typeof((fifo) + 1) __tmpl = (fifo); \
__tmpl->kfifo.in - __tmpl->kfifo.out; \
})
如果想要知道kfifo队列中还有多少可用空间,则要调用方法
#define>kfifo_avail(fifo) \↩
__kfifo_uint_must_check_helper( \↩
({ \↩
typeof((fifo) + 1) __tmpq = (fifo); \↩
const size_t __recsize = sizeof(*__tmpq->rectype); \↩
unsigned int __avail = kfifo_size(__tmpq) - kfifo_len(__tmpq); \↩
(__recsize) ? ((__avail <= __recsize) ? 0 : \↩
__kfifo_max_r(__avail - __recsize, __recsize)) : \↩
__avail; \↩
}) \↩
)↩
判断队列为空或者为满的方法
#define kfifo_is_empty(fifo) \
({ \
typeof((fifo) + 1) __tmpq = (fifo); \
__tmpq->kfifo.in == __tmpq->kfifo.out; \
})
#define>kfifo_is_full(fifo) \
({ \
typeof((fifo) + 1) __tmpq = (fifo); \
kfifo_len(__tmpq) > __tmpq->kfifo.mask; \
})
重置和撤销队列
重置队列意味着清除目前队列中的所有内容,调用kfifo_reset方法
/* kfifo_reset - removes the entire fifo content↩
* @fifo: address of the fifo to be used↩
*
* Note: usage of kfifo_reset() is dangerous. It should be only called when the↩
* fifo is exclusived locked or when it is secured that no other thread is↩
* accessing the fifo.↩要不被锁要不没有其他进程访问
*/
#define kfifo_reset(fifo) \
(void)({ \↩
typeof((fifo) + 1) __tmp = (fifo); \↩
__tmp->kfifo.in = __tmp->kfifo.out = 0; \↩
})↩
/**↩
* kfifo_reset_out - skip fifo content↩
* @fifo: address of the fifo to be used↩
*↩
* Note: The usage of kfifo_reset_out() is safe until it will be only called↩
* from the reader thread and there is only one concurrent reader. Otherwise↩
* it is dangerous and must be handled in the same way as kfifo_reset().↩
*/↩只要是线性读取器读取且只有一个并发读取器
#define kfifo_reset_out(fifo)>--\↩
(void)({ \↩
typeof((fifo) + 1) __tmp = (fifo); \↩
__tmp->kfifo.out = __tmp->kfifo.in; \↩
})↩
撤销一个使用kfifo_alloc分配的队列,调用kfifo_free方法,如果使用的是kfifo_init方法创建的队列,那么需要释放相关的缓存。
#define kfifo_free(fifo) \
({ \↩
typeof((fifo) + 1) __tmp = (fifo); \↩
struct __kfifo *__kfifo = &__tmp->kfifo; \↩
if (__is_kfifo_ptr(__tmp)) \↩
__kfifo_free(__kfifo); \↩
})
void __kfifo_free(struct __kfifo *fifo)↩
{
kfree(fifo->data);↩
fifo->in = 0;↩
fifo->out = 0;↩
fifo->esize = 0;↩
fifo->data = NULL;↩
fifo->mask = 0;
}↩
3. 二叉树
树结构在数学意义上是一个无环 连接 有向图,其中每个节点都有0个或者多个出边或者 0个或者1个入边,二叉树就是每个节点最多有两个出边,也就是0个1个或2个子节点
二叉搜索树
二叉搜索树 BST(Binary Search Tree)作为一种经典的数据结构,它既有链表的快速插入与删除操作的特点,又有数组快速查找的优势;所以应用十分广泛,例如在文件系统和数据库系统一般会采用这种数据结构进行高效率的排序与检索操作。
在二叉搜索树中:
1.若任意结点的左子树不空,则左子树上所有结点的值均不大于它的根结点的值。
2.若任意结点的右子树不空,则右子树上所有结点的值均不小于它的根结点的值。
3.任意结点的左、右子树也分别为二叉搜索树。
不论是查找 删除 插入哪一种操作,所花的时间都和树的高度成正比。因此,如果共有n个元素,那么平均每次操作需要O(log(n))的时间
自平衡二叉搜索树
平衡二叉树具有如下几个性质:
1.可以是空树;
2.假如不是空树,任何一个节点的左子树和右子树都是平衡二叉树,并且高度之差的绝对值不超过1。
也可以是 所有叶子节点的深度差的绝对值不超过1的二叉搜索树
自平衡二叉搜索树是指其 在树上的所有操作都试图维持平衡或半平衡的二叉搜索树
红黑树
红黑树是一种自平衡二叉搜索树,Linux主要的平衡二叉树数据结构就是红黑树,红黑树有以下6个特性:
- 所有节点的颜色只有两种,要么红色 要么黑色;
- 根节点是黑色的
- 所有叶子结点都是黑色;
- 叶子结点不包含数据;
- 所有非叶子节点都有两个子节点;
- 如果一个节点是红色的,则他的子节点都是黑色的;
- 在一个节点到其叶子节点的路径中,如果总是包含同样数目的黑色节点,则该路径相比其他路径最短。
这是一个半平衡的二叉树,最深的叶子结点深度不会超过两倍的最浅的叶子结点深度
解析:
- 显然
- 显然
- 叶子结点为黑色,通常记为NULL节点
- 所有除了叶子结点的节点存储数据
- 每次增加删除带有数据的节点(非叶子节点)时候,NULL节点自动补充
- 显然
- 另一种定义为 从任一节结点其每个叶子的简单路径都包含相同数目的黑色结点,具体这条性质很复杂,且与LInux内核数据结构关系不大,此处省略。
rbtree
Linux内核实现的红黑树叫rbtree,定义在文件lib/rbtree.c中,声明在rbtree.h中。除了一定的优化以外,rbtree类似于前面描述的红黑树,保持了一定的平衡性,其插入效率与树中的节点数目呈对数关系,O(log(n))
Linux内核rbtree的实现并没有提供搜索和插入例程,这些例程希望由rbtree的用户自己定义,这是因为C语言不太容易进行泛型编程,同时Linux内核开发者们相信最有效的搜索和插入方法需要用户自己去实现,你可以使用rbtree提供的辅助函数,但是你要自己实现比较操作的算子。
rbtree的根节点由数据结构rb_root描述,创建一个红黑树,我们需要新建一个新的rb_root并初始化为特殊值RB_ROOT:
struct rb_root root = RB_ROOT;
struct rb_root {
struct rb_node *rb_node;
};
#define RB_ROOT (struct rb_root) { NULL, }
树里的其他节点由rb_node结构描述
struct rb_node {
unsigned long __rb_parent_color;
struct rb_node *rb_right;
struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
举出一个实际场景,在页高速缓存中搜索一个文件区(由一个i节点和一个偏移量共同描述),每个i节点都有自己的rbtree已关联在文件中的页偏移。
static inline struct page * rb_search_page_cache(struct inode * inode,
unsigned long offset)
{
struct rb_node * n = inode->i_rb_page_cache.rb_node;↩
struct page * page;↩
while (n)↩
{
page = rb_entry(n, struct page, rb_page_cache);↩
if (offset < page->offset)↩
n = n->rb_left;↩
else if (offset > page->offset)↩
n = n->rb_right;↩
else↩
return page;↩
}
return NULL;↩
}
#define rb_entry(ptr, type, member) container_of(ptr, type, member)
static inline struct page * rb_insert_page_cache(struct inode * inode,
unsigned long offset,struct rb_node * node)
{↩
struct rb_node ** p = &inode->i_rb_page_cache.rb_node;↩
struct rb_node * parent = NULL;↩
struct page * page;↩
while(*p){
parent = *p;
page = rb_entry(parent, struct page, rb_page_cache);
if (offset < page->offset)
p = &(*p)->rb_left;
else if (offset > page->offset)
p = &(*p)->rb_right;
else↩
return page;
}
rb_link_node(node, parent, p);
rb_insert_color(node,&inode->i_rb_page_cache);
return NULL;
}
上面是某个以前版本Linux内核中页高速缓存中搜索一个文件或者插入一个节点的操作,4.1内核已经使用更新的方法,这里用该例子简单说明rbtree的使用即可。
首先分析,inode结构体中包含的i_rb_page_cache结构体中有红黑树节点结构体rb_node;page结构体中有rb_page_chache;
其次,使用rb_entry方法,从成员结构体rb_page_chache和page的关系,利用成员结构体的指针n,反推出page结构体的指针,因为接下来要使用页偏移offset来找到对应的page结构体。插入也是同理
rb_node结构体的使用其实和链表头list_head结构体的使用方法类似:
将list_head插入需要加入链表的结构体后,使用内核提供的使用list_head对节点的方法进行 添加 删除 遍历 移动 和并,等等;然后从list_head指针反推到原本的结构体指针。链表上重要的是节点信息的使用,而不是仅仅是节点的操作,那么对list_head的操作实际上可以传递到节点的信息。同样,在处理某些计算机信息时,如果需要使用红黑树结构体来对这些信息进行预处理,那么同样可以在构建结构体时,向里面添加rb_node结构体,对该结构体使用设计好的搜索 添加 删除 或者内核自带的 左旋右旋等等操作来维护红黑树,在实际处理业务时,通过红黑树节点找到对应业务信息来提高处理效率。
4. 映射
映射,也常称为关联数组,是一个由唯一键组成的集合,而且每一个键必然关联一个特定的值。这种键到值的关联关系称为映射。
映射要至少支持三个操作:
*Add (key,value)
*Remove(key)
*value=Lookup(key)
Linux内核提供了简单有效的映射数据结构。但是它并非是一个通用的映射。
它的目标是:映射一个唯一的标识数(UID)到一个指针。
除了三种标准操作之外,Linux还在add的基础上提供了一个allocate操作,该操作不仅可以在map中加入键值对,而且还能产生UID。
idr数据结构用于映射用户空间的UID
初始化一个idr
建立一个idr很简单,首先静态定义或者动态分配一个idr数据结构,然后调用idr_init函数;
struct idr id_huh;
idr_init(&id_huh);
struct idr {
struct idr_layer __rcu *hint; /* the last layer allocated from */
struct idr_layer __rcu *top;
int layers;/* only valid w/o concurrent changes */
int cur;/* current pos for cyclic allocation */
spinlock_t lock;
int id_free_cnt;
struct idr_layer *id_free;
};
void idr_init(struct idr *idp)
{
memset(idp, 0, sizeof(struct idr));
spin_lock_init(&idp->lock);
}
分配一个新的UID
建立好idr后,就可以分配新的UID了,分两步完成:
第一步,告诉idr需要分配新的UID,允许在必要时调整后备树的大小;第二步,真正其你去新的UID。
第一个调整后备树大小的方法是idr_pre_get():
第二个函数,获取新的UID,并添加到idr的方法 idr_get_new();
int idr_get_new( struct idr* idp, void *ptr, int *id);
该方法使用idp所指向的idr分配一个UID并将其关联到指针ptr上,成功时返回0,并将新的UID存于id。错误时,返回非0错误码,若错误码是-EAGAIN,说明需要再次调用idr_pre_get,如果返回错误码-ENOSPC,说明idr已满;
下面是一个具体的例子
int id;
do{
if(!idr_pre_get(&idr_huh,GFP_KERNEL))
return -ENOSPC;
ret = idr_get_new(&idr_huh,ptr,&id);
}while(ret == -EAGAIN)
函数idr_get_new_above()使得调用者可指定一个最小的UID返回值,该函数的作用和idr_get_new相同,除了它确保新的UID大于或者等于一个指定的值,使用这个变种方法可以确保UID不会被重复使用,其值不但在当前分配的ID中唯一,而且还保证在系统的整个运行期间唯一。
比如:
int id;
do{
if(!idr_pre_get(&idr_huh,GFP_KERNEL))
return -ENOSPC;
ret = idr_get_new(&idr_huh,ptr,next_id,&id);
}while(ret == -EAGAIN)
if(!ret)
next_id = id + 1;
查找UID
当我们在一个idr中分配一些UID时,我们在需要时就要查找他们:调用者要给出UID,idr则返回对应的指针。查找自然比分配新UID要简单,直接调用idr_find方法即可:
void *idr_find(struct idr *idp, int id)
该函数如果调用成功则返回对应关联的指针,否则返回空指针。因此,如果之前调用idr_get_new或者idr_get_new_above将空指针映射给UID,那么即使该函数执行成功也会返回NULL指针,使用方法比较简单
struct my_struct *ptr = idr_find(&idr_huh,id);
if(!ptr)
return -EINVAL;
删除UID
从idr中删除UID的方法是idr_remove();
void idr_remove(struct idr* idp, int id);
如果idr_remove成功,则将id关联的指针一起从映射中删除,但是没有办法返回错误码。比如id不在idp中。
撤销UID
撤销一个idr的操作很简单,调用idr_destroy函数即可,如果该方法成功,则只释放idr中未使用的内存。它并不释放当前分配给UID使用的任何内存。
如果该方法成功,则释放idr中未使用的内存,但并不释放当前分配给UID使用的任何内存。通常,内核代码不会直接撤销idr,除非关闭和卸载,而且只有在没有其他用户时才能删除,但是可以调用idr_remove_all方法强制删除所有UID,再调用idr_destroy的方法将idr占用的内存都释放。
5. 数据结构的选择
如果对数据集合的主要操作是遍历数据就使用链表。没有其他数据结构可以提供比线性算法复杂度更好的算法遍历元素,所以应当选择最简单的数据结构来完成简单的工作。另外,当性能并非首要的考虑因素时,或者当需要存储的数据项相对较少时,或者当你需要和内核中其他使用链表的代码交互时,也应该选择链表。
如果你的代码符合生产者和消费者模型,则使用队列,特别是想要一个定长的缓冲。队列会使得添加和删除项的工作简单有效,同事队列也提供了FIFO语义,这也正是生产者消费者模式的普遍需求。但是如果你要存储一个大小不明的数据集合,最好使用链表,动态的添加删除数据。
如果你需要映射一个UID到一个对象,就使用映射。映射结构是的映射工作简单有效,而且映射可以帮你维护和分配UID,Linux的映射接口是针对UID到指针的映射,并不适合其他场景。如果你在处理发给用户空间的描述符,就考虑使用映射吧。
如果需要存储大量的数据,并且检索迅速,那就红黑树。红黑树可以确保搜索时间复杂度是对数时间复杂度,勇士也能保证按序遍历的时间复杂度是线性时间复杂度,虽然它比较复杂,但是内存开销情况并不是很差。但是如果没有执行太多次时间紧迫的查找那就使用链表。
要是上述数据结构,都不能满足需求,内核还实现了一些较少使用的,数据结构,比如基树和位图。只要当寻遍所有的内核提供的数据结构后都不能满足需求时,才自己设计。经常在独立的源文件中实现的一种常见的数据结构是,散列表。因为,散列表无非是一些桶和一个散列函数。
6. 算法复杂度
大O符号和大Θ符号,一个是上限,一个是上下限
f(n)是O(g(n)) 表示 存在一个常数c,对于任意的n,总有f(n)<=c*g(n)
f(n)是Θ(g(n)) 表示 存在两个常数a,b,对于任意的n,总有a*g(n)<=f(n)<=b*g(n)
O(1) O(log(n)) O(n) O(n^2) O(n^3) O(2^n) O(n!)
显然,大O符号的使用并不能像大Θ符号一样准确,因为它并不刻画算法时间复杂度的下限。但通常实际中我们使用大O符号时,是当成大Θ符号使用。
虽然我们不赞成使用时间复杂度高的算法,但是要时刻注意算法的负载和典型输入集合大小的关系,不要为了不需要支持的伸缩度要求,盲目优化算法。这一点上是要综合考虑时间复杂度和空间复杂度的。有可能时间复杂度为O(1)的算法无论输入多大总是花几个小时,这个不一定比复杂度为O(n)但输入很少的算法好。
参考资料:
1. 《Linux内核设计与实现》第三版 美Robert Love著 陈莉君 康华译;
2. Linux 4.1.25内核
Linux内核设计理念精深,覆盖范围浩如烟海,知识点更是多如牛毛。学习Linux内核还是要从积累 分享做起,作为新人,希望有前辈多多指点,提出令人振聋发聩、耳目一新的评论观点。
附录
附录中收集了前面部分的一些函数,如果放在前面会影响阅读体验,因此我将一些零碎的相关代码放到了附录中。
int __kfifo_alloc(struct __kfifo *fifo, unsigned int size,size_t esize, gfp_t gfp_mask)
{
/*
* round down to the next power of 2, since our 'let the indices
* wrap' technique works only in this case.
*/
size = roundup_pow_of_two(size);//不小于size的2的幂次方
fifo->in = 0;
fifo->out = 0;
fifo->esize = esize;
if (size < 2) {
fifo->data = NULL;
fifo->mask = 0;
return -EINVAL;
}
fifo->data = kmalloc(size * esize, gfp_mask);
if (!fifo->data) {
fifo->mask = 0;
return -ENOMEM;
}
fifo->mask = size - 1;
return 0;
}
#define roundup_pow_of_two(n) \
( \
__builtin_constant_p(n) ? ( \
(n == 1) ? 1 : \
(1UL << (ilog2((n) - 1) + 1)) \
) : \
__roundup_pow_of_two(n) \
)
该函数是获取不小于n的一个2的幂次方的数字,内建函数的两条路径的作用是一样的。
Gcc的内建函数__builtin_constant_p(n)的作用是判断一个值编译时是否为常数,如果参数的值是常数,函数返回 1,否则返回 0。
unsigned long __roundup_pow_of_two(unsigned long n)
{
return 1UL << fls_long(n - 1);
}
static inline unsigned fls_long(unsigned long l)
{
if (sizeof(l) == 4)
return fls(l);
return fls64(l);
}
这个函数相当于一个接口,根据不同的系统,long类型的数据有的是4有的是8
static __always_inline int fls(int x)
{
int r = 32;
if (!x)
return 0;
if (!(x & 0xffff0000u)) {
x <<= 16;
r -= 16;
}
if (!(x & 0xff000000u)) {
x <<= 8;
r -= 8;
}
if (!(x & 0xf0000000u)) {
x <<= 4;
r -= 4;
}
if (!(x & 0xc0000000u)) {
x <<= 2;
r -= 2;
}
if (!(x & 0x80000000u)) {
x <<= 1;
r -= 1;
}
return r;
}
该函数的作用的返回入参的初始有效位,32位bit中,从右往左数,起始位是1而不是0.
#define DECLARE_KFIFO(fifo, type, size) STRUCT_KFIFO(type, size) fifo
#define STRUCT_KFIFO(type, size) struct __STRUCT_KFIFO(type, size, 0, type)
#define __STRUCT_KFIFO(type, size, recsize, ptrtype) \
{ \
__STRUCT_KFIFO_COMMON(type, recsize, ptrtype); \
type buf[((size < 2) || (size & (size - 1))) ? -1 : size]; \
}
#define __STRUCT_KFIFO_COMMON(datatype, recsize, ptrtype) \
union { \
struct __kfifo kfifo; \
datatype *type; \
const datatype *const_type; \
char (*rectype)[recsize]; \
ptrtype *ptr; \
ptrtype const *ptr_const; \
}
#define kfifo_in(fifo, buf, n) \
({ \
typeof((fifo) + 1) __tmp = (fifo); \
typeof(__tmp->ptr_const) __buf = (buf); \
unsigned long __n = (n); \
const size_t __recsize = sizeof(*__tmp->rectype); \
struct __kfifo *__kfifo = &__tmp->kfifo; \
(__recsize) ?\
__kfifo_in_r(__kfifo, __buf, __n, __recsize) : \
__kfifo_in(__kfifo, __buf, __n); \
})
#define __KFIFO_PEEK(data, out, mask) ((data)[(out) & (mask)])