Linux内核数据结构
1. 链表
(1) Linux内核中的实现
Linux内核不是将数据结构塞入链表,而是将链表节点塞入数据结构。
-
链表数据结构
链表代码在头文件<linux/list.h>中声明:
struct list_head {
struct list_head *next;
struct list_head *prev;
};
list_head
本身其实并没有意义,它被嵌入到数据结构中才能生效。
-
链表操作例程
内核提供了一组链表操作例程,这些方法都只接受
list_head
结构作为参数。比如,使用宏container_of()
可以从链表指针找到父结构中包含的变量,因为在C语言中一个给定结构中的变量偏移在编译时地址就被ABI固定下来了。
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
使用container_of()
宏,再定义一个简单的函数便可返回包含list_head
的父类型结构体:
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
- 链表头
static LIST_HEAD(list);
该函数定义并初始化了一个名为list
的链表例程,这些例程大多数只接受一、两个参数:头节点或者头节点加一个特殊链表节点。
(2) 操作链表
内核提供了一组函数来操作链表,这些函数都要使用list_head
结构体指针作参数。这些函数都是用C语言以内联函数形式实现的,它们的原型在文件<linux/list.h>中。所有这些函数的复杂度都为O(1),意味着无论操作的链表大小、参数如何,它们都在恒定时间内完成。
-
向链表中增加一个节点
a. 给链表增加一个节点:
list_add(struct list_head *new, struct list_head *head)
该函数向指定链表的head
节点后插入new
节点。因为链表循环,通常没有首尾节点的概念,所以可以把任何一个节点当成head
。如果把“最后”一个节点当做head
的话,那么该函数可以用来实现一个栈。
b. 把节点增加到链表尾:
list_add_tail(struct list_head *new, struct list_head *head)
该函数向指定链表的head
节点前插入new
节点。如果把“第一个”元素当做head
的话,那么该函数可以用来实现一个队列。
-
从链表中删除一个节点
a. 从链表中删除一个结点:
list_del(struct list_head *entry)
该函数将entry
元素从链表中移走,不会释放entry
或释放包含entry
的数据结构体所占用的内存,所以该函数被调用后,通常还需要再撒销包含entry
的数据结构体和其中的entry
项。该函数的实现如下:
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);
}
b. 从链表中刪除一个节点并对其重新初始化:
list_del_init(struct list_head *entry)
-
移动和合并链表节点
a. 把节点从一个链表移到另一个链表:
list_move(struct list_head *list, struct list_head *head)
该函数从一个链表中移除list
项,然后将其加入到另一链表的head
节点后面。
b. 把节点从一个链表移到另一个链表的末尾:
list_move_tail(struct list_head *list, struct list_head *head)
该函数从一个链表中移除list
项,然后将其插入到另一链表的head
项前。
c. 检查链表是否为空:
list_empty(struct list_head *head)
如果指定的链表为空,该函数返回非0值;否则返回0。
d. 把两个未连接的链表合并在一起:
list_splice(struct list_head *list,struct list_head *head)
该函数合并两个链表,它将list
指向的链表插入到指定链表的head
元素后面。
e. 把两个未连接的链表合并在一起,并重新初始化原来的链表:
list_splice_init(struct list_head *list,struct list_head *head)
-
节约两次提领
如果已经得到(提领)了
next
和prev
指针,可以直接调用内部链表函数。内部函数和外部包装函数同名,仅仅在前面加了两条下划线。比如,可以调用__list_del(prev, next)
函数代替调用list_del(list)
函数。
(3) 遍历链表
遍历链表的复杂度为O(n),n是链表所包含的元素数目。
-
基本方法
a. 遍历链表最简单的方法是使用
list_for_each()
宏,该宏使用两个list_head
类型的参数,第一个参数指向当前项,第二个参数是需要遍历的链表的以头节点形式存在的list_head
。用法如下:
struct list_head *p;
list_for_each(p, list){
/*p指向链表中的元素*/
}
b. 采用list_for_each_entry()
宏遍历链表。该宏内部也使用list_entry()
宏,但简化了遍历过程:
list_for_each_entry(pos, head, member)
pos
是一个指向包含list_head
节点对象的指针,可看做是list_entry
宏的返回值;head
是一个指向头节点的指针,即遍历开始位置。
c. 反向遍历链表:
list_for_each_entry_reverse(pos, head, member)
-
遍历的同时删除
a. 遍历链表的同时删除
list_for_each_entry_safe(pos, next, head, member)
next
指针和pos
是同样的类型,next
指针将下一项存进表中,以使得能安全删除当前项。
b. 在反向遍历链表的同时删除:
list_for_each_entry_safe_reverse(pos, n, head, member)
c. list_for_each_entry()
的安全版本只能保护在循环体中从链表中删除数据。如果这时有可能从其他地方并发进行删除,或者有任何其他并发的链表操作,就需要锁定链表。
2. 队列
Linux内核通用队列实现称为kfifo,它实现在文件kernel/kfifo.c中,声明在文件<linux/kfifo.h>中。
(1) kfifo
Linux的kfifo提供了两个主要操作:enqueue(入队列)和dequeue(出队列)。kfifo对象维护了两个偏移量:入口偏移和出口偏移。入口偏移是指下一次入队列时的位置;出口偏移是指下一次出队列时的位置,出口偏移总是小于等于人口偏移。
(2) 创建队列
- 使用kfifo前,首先必须对它进行定义和初始化,动态方法更为普遍:
int kfifo_alloc(struct kfifo *fifo, unsigned int size, gfp_t gfp_mask);
该函数创建并且初始化一个大小为size
的kfifo,size
必须是2的幂。内核使用gfp_mask
标识分配队列。如果成功返回0;错误则返回一个负数错误码。
- 要想自己分配缓冲,可以调用:
void kfifo_init(struct kfifo *fifo, void *buffer, unsigned int size);
该函数创建并初始化一个kfifo对象,它将使用由buffer
指向的size
字节大小的内存,size
必须是2的幂。
- 静态声明kfifo更简单,但不大常用:
DECLARE_KFIFO(name, size);
INIT_KFIFO(name) ;
该方法会创建一个名称为name
、大小为size
的kfifo对象,size
必须是2的幂。
(3) 推入队列数据
kfifo对象创建和初始化后,需要推入数据到队列:
unsigned int kfifo_in(struct kfifo *fifo, const void *from, unsigned int len);
该函数把from
指针所指的len
字节数据拷贝到fifo所指的队列中,如果成功,则返回推入数据的字节大小。
(4) 摘取队列数据
- 摘取数据:
unsigned int kfifo_out(struct kfifo *fifo, void *to, unsigned int len);
该函数从fifo所指向的队列中拷贝出长度为len
字节的数据到to
所指的缓冲中。如果成功,该函数则返回拷贝的数据长度。
- 如果仅仅想“偷窥"队列中的数据,而不真想删除,
offset
指向队列中的索引位置:
unsigned int kfifo_out_peek(struct kfifo *fifo, void *to, unsigned int len, unsigned offset);
(5) 获取队列长度
返回用于存储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);
判断kfifo是否为空或者满,如果是,则返回非0值;如果返回0,则相反:
static inline int kfifo_is_empty(struct kfifo *fifo);
static inline int kfifo_is_full(struct kfifo *fifo);
(6) 重置和撤销队列
- 重置kfifo(抛弃所有队列中的内容):
static inline void kfifo_reset(struct kfifo *fifo);
- 撤销一个使用
kfifo_alloc()
分配的队列
void kfifo_free(struct kfifo *fifo);
如果是使用kfifo_init()
方法创建的队列,那么需要再释放相关的缓冲。
3. 映射
-
键到值的关联关系称为映射,也常称为关联数组,更多时候特指使用二叉树而非散列表实现的关联数组,比如C++的STL容器
std::map
。映射要至少支持三个操作:Add (key, value);Remove (key);Value = Lookup (key)。 -
Linux内核提供的映射数据结构映射一个唯一的标识数(UID)到一个指针。除了提供三个标准的映射操作外,还在add操作基础上实现了
allocate
操作。allocate
操作不但向map中加入了键值对,而且还可产生UID。 -
idr数据结构用于映射用户空间的UID,比如将
inodify watch
的描述符或者POSIX的定时器ID映射到内核中相关联的数据结构上,如inotify_watch
或者k_itimer
结构体。
(1) 初始化一个idr
建立一个idr首先需要静态定义或者动态分配一个idr数据结构,然后调用idr_init()
:
void idr_init(struct idr *idp);
(2) 分配一个新的UID
建立了idr就可以分配新的UID了,这个过程分两步完成:第一步,告诉idr需要分配新的UID,允许其在必要时调整后备树的大小;第二步,请求新的UID。分成两步是因为要允许调整初始大小涉及在无锁情况下分配内存的场景。
- 第一个调整后备树大小的方法:
int idr_pre_get(struct idr *idp, gfp_t gfp_mask);
该函数将在需要时进行UID的分配工作:调整由idp
指向的idr的大小。如果需要调整大小,则内存分配例程使用gfp标识:gfp_mask
,不需要对并发访问该方法进行同步保护。该函数成功返回1,失败返回0。
- 获取新的UID,并且将其加到idr:
int idr_get_new(struct idr *idp, void *ptr, int *id);
该方法使用idp
所指向的idr分配一个新的UID,并且将其关联到指针ptr
上。成功返回0,并将新的UID存于id;错误返回非0的错误码。错误码是-EAGAIN
时说明需要(再次)调用idr_pre_get();如果idr已满,错误码是-ENOSPC
。
- 调用者需要指定一个最小的UID返回值:
int idr_get_new_above(struct idr *idp, void *ptr, int starting_id, int *id);
该函数的作用和idr_get_new()相同,除了它确保新的UID大于或等于starting_id
外。
(3) 查找UID
在一个idr中查找UID:
void *idr_find(struct idr *idp, int id);
成功则返回id关联的指针;错误则返回空指针。
(4) 删除UID
从idr中删除UID:
void idr_remove(struct idr *idp,int id);
成功则将id关联的指针一起从映射中删除,但是该函数不提示任何错误(比如,如果id不在idp中)。
(5) 撤销idr
- 撤销一个idr:
void idr_destroy(struct idr *idp);
成功则只释放idr中未使用的内存。它不释放当前分配给UID使用的任何内存。通常内核代码不会撒销idr,除非关闭或者卸载,且只有在没有其他用户(也就没有更多的UID)时才能删除。
- 强制删除所有的UID:
void idr_remove_all(struct idr *idp);
- 先对
idp
指向的idr调用idr_remove_all()
,再调用idr_destroy()
,就能使idr占用的内存都被释放。
4. 红黑树
-
Linux的rbtree定义在lib/rbtree.c中,声明在<linux/rbtree.h>中。rbtree的插入效率和树中节点数目呈对数关系。
-
rbtree的根节点由数据结构
rb_root
描述。创建一个红黑树要分配一个新的rb_root
结构,并且需要初始化为特殊值RB_ROOT
:
struct rb_root root = RB_ROOT;
树里的其他节点由结构rb_node
描述。给定一个rb_node
,可以通过跟踪同名节点指针来找到它的左右子节点。
- rbtree的实现没有提供搜索和插入例程,这些例程由rbtree的用户定义,因为C语言不容易进行泛型编程。可以使用rbtree提供的辅助函数,但要自己实现比较操作算子。