内核常用的数据结构有:
-
链表
-
队列
-
映射
-
二叉树
链表
链表是Linux内核中最简单、最普通的数据结构。链表是一种存放和操作可变数t元素〔常称为节点)的数据结构。
单向链表 双向链表 循环链表
/* an element in a linked list */
struct list_element {
void data; /* the payload */
struct list_element *next; /*pointer to the next element */
};
/* an element in a linked list */
struct list_element {
void *data; /* the payload */
struct list_element *next; /* pointer to the next element */
struct list_element *prev; /* pointer to the previous element */
};
循环链表:
因为环形双向链表提供了最大的灵活性,所以L I}UI}内核的标准链表就是采用环形双向链表形式实现的。
有时,首元素会用一个特殊指针表示—该指针称为头指针,利用头指针可方便、快速地找到链表的“起始端”。在非环形链表里,向后指针指向NULL的元素是尾元素,而在环形链表里向后指针指向头元素的元素是尾元素。
Linux内核中的实现
一般的实现是把数据和指针封装到一个结构体中:
struct fox {
unsigned long tail_length; /* length in centimeters of tail */
unsigned long weight; /* weight in kilograms */
bool is_fantastic; /* is this fox fantastic? */
};
struct fox {
unsigned long tail_length; /* length in centimeters of tail */
unsigned long weight; /* weight in kilograms */
bool is_fantastic; /* is this fox fantastic? */
struct fox *next; /* next fox in linked list */
struct fox *prev; /* previous fox in linked list */
};
Linux反其道而行之,把链表节点放入数据结构中去。
struct list_head {
struct list_head *next
struct list_head *prev;
};
struct fox {
unsigned long tail_length; /* length in centimeters of tail */
unsigned long weight; /* weight in kilograms */
bool is_fantastic; /* is this fox fantastic? */
struct list_head list; /* list of all fox structures */
};
通过链表结构就能把其串联起来。为了找到fox的位置使用container_of()。
该宏定义通过list在fox的位置,以及fox起始位置和list的偏移来计算fox的位置。type_of 定义了mptr类型,把相应的地址值ptr赋值给__mptr.再通过offset宏计算两者差值,得到fox的位置。
具体的函数API不做描述。
队列
任何操作系统内核都少不了一种编程模型:生产者和消费者。在该模式中,生产者创造数据(比如说需要读取的错误信息或者需要处理的网络包),而消费者则反过来,读取消息和处理包,或者以其他方式消费这些数据。实现该模型的最简单的方式无非是使用队列。
内核的通用队列实现称之为kfifo.
主要操作有:入队,出队两种。kfifo维护两个偏移量,入口偏移和出口偏移。Linux中的kfifo可以实现循环缓存,其在内存中是连续的。
//根据给定buffer创建一个kfifo
struct kfifo *kfifo_init(unsigned char *buffer, unsigned int size,
gfp_t gfp_mask, spinlock_t *lock);
//给定size分配buffer和kfifo
struct kfifo *kfifo_alloc(unsigned int size, gfp_t gfp_mask,
spinlock_t *lock);
//释放kfifo空间
void kfifo_free(struct kfifo *fifo)
//向kfifo中添加数据
unsigned int kfifo_put(struct kfifo *fifo,
const unsigned char *buffer, unsigned int len)
//从kfifo中取数据
unsigned int kfifo_put(struct kfifo *fifo,
const unsigned char *buffer, unsigned int len)
//获取kfifo中有数据的buffer大小
unsigned int kfifo_len(struct kfifo *fifo)
//获取kfifo总大小buffer大小
unsigned int kfifo_size(struct kfifo *fifo)
//获取kfifo中还有多少可以分配
unsigned int kfifo_avail(struct kfifo *fifo)
映射
映射也称为关联数组,具体实现以下三个函数。
-
Add (key, value)
-
Remove (key)
-
value = Lookup (key)
虽然散列表是一种映射,但并非所有的映射都需要通过散列表实现。除了使用散列表外,映射也’可以通过自平衡二叉搜索树存储数据。虽然散列表能提供更好的平均的渐近复杂度(请看本章后面关于算法复杂度的讨论),但是二叉搜素树在最坏情况下能有更好的表现。
Linux内核提供了简单、有效的映射数据结构。但是它井非一个通用的映射。 目标是:映射一个唯一的标志数(UID)
到一个指针。
-
Init
struct idr id_huh; /* statically define idr structure */ idr_init(&id_huh); /* initialize provided idr structure */
-
get
idr_pre_get()是防止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);
-
find
struct my_struct *ptr = idr_find(&idr_huh, id); if (!ptr) return -EINVAL; /* error */
-
remove
void idr_remove(struct idr *idp, int id);
二叉树
树结构是一个能提供分层的树型数据结构的特定数据结构。在数学意义上,树是一个无环的、连接的有向图.其中任何一个顶点(在树里叫节点)具有0个或者多个出边以及0个或者1个入边。一个二叉树是每个节点最多只有两个出边的树—也就是,一个树,其节点具有0个、1个或者2个子节点。
二叉树定义:
-
根的左分支节点值都小于根节点值。
-
右分支节点值都大于根节点值。
-
所有的子树也都是二叉搜索树。
自平衡二叉搜索树
平衡二叉搜索树:所有叶子节点深度的差值不超过1的二叉搜索树。
自平衡二叉树:指其操作都试图维持〔半)平衡的二叉搜索树。
-
红黑树
红黑树是一种自平衡二叉搜索树。Linux主要的平衡二叉树数据结构就是红黑树.红黑树具有特殊的着色属性,或红色或黑色。红黑树因遵循下面六个属性,所以能维持半平衡结构:
-
所有的节点要么着红色,要么着黑色。
-
叶子节点都是黑色。
-
叶子节点不包含数据。
-
所有非叶子节点都有两个子节点。
-
如果一个节点是红色,则它的子节点都是黑色。(全黑最短)
-
在一个节点到其叶子节点的路径中,如果总是只有黑色节点.则该路径相比其他路径是最短的。
上述条件,保证了最深的叶子节点的深度不会大于两倍的最浅叶子节点的深度。所以,红黑树总是半平衡的。为什么它具有如此神奇的特点呢?首先,第五个属性,一个红色节点不能是其他红色节点的子节点或者父节点。而第六个属性保证了,从树的任何节点到其叶子节点的路径都具有相同数目的黑色节点,树里的最长路径则是红黑交的节点路径,所以最短路径必然是具有相同数量黑色节点的一直包含黑色节点的路径。于是从根节点到叶子节点的的最长路径不会超过最短路径的两倍。
-
-
rbtree
rbtree的根节点由rb_root结构体描述,初始化为特殊值RB_ROOT。
struct rb_root root = RB_ROOT;
搜索操作和插人操作最好的范例就是展示一个实际场景:我们先来看搜素,下面的函数实现了在页高速缓存中搜素一个文件区(由一个i节点和一个偏移量共同描述)。每个i节点都有自己的rbtree,以关联在文件中的页偏移。该函数将搜索给定1节点的rbtree,以寻找匹配的偏移值:
struct page * rb_search_page_cache(struct inode *inode,
unsigned long offset)
{
struct rb_node *n = inode->i_rb_page_cache.rb_node;
while (n) {
struct page *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;
}
擂入操作要相对复杂一些,因为必须实现搜索和插入逻辑。下面并非一个了不起的函数,但可以作为你实现自己的插入操作的一个指导:
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;
}
三个步骤:
-
查找位置。
-
插入节点。
-
着色。
关于红黑树STL,java虚拟机,Linux内核均有用到,是重要的数据结构,必须掌握。
参考资料:
Linux内核设计与实现