文章目录
第六章 内核数据结构
Linux内核实现的通用数据结构,链表、队列、映射、二叉树等,在开发时进行重用即可。
链表
包含的元素都是动态创建并插入的,有单向链表、双向链表、环形链表等,使用与需要遍历所用数据或动态加入或者删除数据的情况。linux内核的标准链表采用环形双向链表形式实现的,有着最大的灵活性。
内核实现
把链表塞入数据结构中。链表头文件存放与<linux/list.h>中:
struct list_head{
struct list_head *next;
struct list_head *prev;
}
相当于把双向链表中的前后指针做了封装,需要放到自己的数据结构中。与此同时,内核也提供了一组链表操作方法,只接受list_head结构作为参数,使用container_of()宏可以从链表指针中找到父结构的任何变量,因为C语言里,一个给定结构的变量偏移在编译时地址就被ABI固定下来。通过结构中某一个变量可以获取到整个结构的地址。
#define continer_of(ptr,type,member) ({ \
const typeof((type *)0)->member) *__mptr=(ptr);
(type *)((char *)__mptr-offsetof(type,member));})
可以定义一个简单的函数可以返回包含list_head的父类型结构体:
#define list_entry(ptr,type,member) \
contianer_of(ptr,type,member)
依靠list_entry()方法,内核提供了创建、操作以及其它链表管理的各种历程,不需要知道list_head所嵌入的数据结构。
- 定义
list_head本身没有意义,需要嵌入具体的数据结构中才能生效:
struct fox{
unsigned long tail_length;
unsigned long weight;
bool is_fantastic;
struct list_head list;
运行时初始化,动态申请空间并赋值:
struct fox *red_fox;
red_fox=kmalloc(sizeof(*red_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);
}
注意上面两个初始化list_head的方法不相同。
- 由内核链表例程管理
需要一个链表头:
static LIST_HEAD(fox_list);
该函数定义并初始化了一个名为fox_list的链表例程。
操作链表
内联实现,原型在<linux/list.h>。
- 增加节点
list_add(struct list_head * new,struct list_head *head);
向head节点后插入new节点,通常是没有首尾节点的概念,可以把任何一个节点当做head。如果把“最后”一个节点当做head,该函数可以实现一个栈。
把节点增加到链表尾:
list_add_tail(struct list_head *new,struct list_head *head);
可以用来实现一个队列。
实际就是头插法或尾插法建立链表的情况。
- 删除一个节点
list_del(struct list_head *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);
}
从链表删除一个节点并对其初始化:
list_del_init(struct list_head *entry);
- 移动和合并链表
节点从一个链表到其它链表:
/*从一个链表里移除list的项并加入到head的节点后面。*/
list_move(struct list_head *list,struct list_head *head);
/*移到head节点前*/
list_move_tail(struct list_head *list,struct list_head *head);
检查链表是否为空:
/*为空返回非0值,真*/
list_empty(struct list_head *head);
把两个未连接的链表合并在一起:
/*把list指向的链表插入到head元素后面*/
list_splice(struct list_head *list,struct list_head *head);
/*把list指向的链表插入到head元素后面,list指向的链表要被初始化*/
list_splice_init(struct list_head *list,struct list_head *head);
note:
如果碰巧得到了next和prev指针,可以直接调用内部链表函数。内部和外部包装函数同名,仅仅多了两个下划线,如可以用__list_del(prev,next)代替list_del(list)。
遍历链表
- 基本方法
两个参数都是list_head类型的,第一个是必须提供的临时变量
struct list_head *p;
list_for_each(p,list){
}
而我们实际需要的是指向整个结构的指针,而不是指向list_head的指针,可以使用前面定义的list_entry()获取包含list_head的数据结构。
struct list_head *p;
struct fox *f;
list_for_each(p,&fox_list){
f=list_entry(p,struct fox,list);
}
- 可用的方法
/*第1种方法的优化版
*@paran pos 指向包含lsit_head对象的头指针,看做list_entry宏的返回值
*@param head 指向头结点的指针,遍历开始位置
*@param member list_head结构的变量名
*/
list_for_each_entry(pos,head,member);
/*重写之前的遍历代码*/
struct fox *f;
list_for_each_entry(f,&fox_list,list){
}
/*实际例子,来自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(pos,head,member);
- 遍历的同时删除
/*
*@note 可以按list_for_each_entry()的方式使用,需要提供临时变量next指针供程序保存下一项
*/
list_for_each_entry_safe(pos,next,head,member);
/*inotify的例子*/
void inotify_inode_is_dead(struct inode *inode)
{
struct inotify_watch *watch,*next;
mutext_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);
mutex_unlock(&ih->mutex);
}
mutex_unlock(&inode->inotify_mutex);
}
反向遍历并删除:
list_for_each_entry_safe_reverse(pos,n,head,member);
队列
linux内核通用队列称为kfifo,实现在<kernel/kfifo.c>,声明在<kernel/kfifo.h>中。
kfifo
两个主要操作enqueue和dequeue。kfifo对象维护了两个偏移量:入口偏移和出口偏移。入口偏移是指下一次入队列的位置,出口偏移是指下一次出队列的位置。
创建队列
使用kfifo前,首先需要定义和初始化。
动态方法比较普遍:
/*
* 创建并且初始化一个大小为size的kfifo,内核使用gfp_mask标识分配队列。成功返回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;
/*
* 自己分配缓冲,使用buffer指向的size字节大小的内存。
*/
void kfifo_init(struct kfifo *fifo,void *buffer,unsigned int size);
kfifo_init和kfifo_alloc的size必须是2的幂。
/*
* 静态声明,不常用
*/
DECLARE_KFIFO(name,size);
INIT_KFIFO(name);
入队
/*
* 把from指针所指的len字节数据拷贝到fifo所指的队列中,成功返回推入字节数目
*/
unsigned int kfifo_in(struct kfifo *fifo,const void *from,unsigned int len);
出队
/*
* 从队列里拷贝len字节数据到to所指的缓冲区,返回拷贝数据长度,队列里不包含该数据
*/
unsigned int kfifo_out(struct kfifo *fifo,void *to,unsigned int len);
/*
* 仅仅查看数据,并不删除数据。offset指定队列里的哪一个数据,为0查看队列头的数据。
*/
unsigned int kfifo_out_peek(struct kfifo *fifo,void *to,unsigned int len,unsigned offset);
获取队列长度
/* 总体大小,字节 */
static inline unsigned int kfifo_size(struct kfifo *fifo);
/* 已推入数据大小 */
static inline unsigned int kfifo_len(struct kfifo *fifo);
/* 可用空间 */
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);
重置和撤销
/*
* 重置fifo,抛弃所有内容
*/
static inline void kfifo_reset(struct kfifo *fifo);
/*
* 撤销或删除一个使用kfifo_alloc分配的fifo
*/
void kfifo_free(struct kfifo *fifo);
例子
/* 入队元素 */
unsigned int i;
for(i=0;i<32;i++)
kfifo_in(fifo,&i,sizeof(i));
/* 查看首元素 */
unsigned int val;
int ret;
ret=kfifo_out_peek(fifo,&val,sizeof(val),0);
if(ret!=sizeof(val))
return -EINVAL;
printk(KERN_INFO "%u\n",val);
/* 出队元素 */
while(kfifo_avail(fifo)){
unsigned int val;
int ret;
ret=kfifo_out(fifo,&val,sizeof(val));
if(ret != sizeof(val)
return -EINVAL;
printk(KERN_INFO "%u\n",val);
}
映射
也被称为关联数组,一个唯一键关联一个特定的值,至少支持三个操作:
- Add(key,value)
- Remove(key)
- value=Lookup(key)
linux内核提供了简单、有效的映射数据结构,但是它是一个非通用的映射,映射一个唯一的表示数(UID)到一个指针。idr数据结构用于映射用户空间的UID,UID到指针的映射。
初始化idr
void idr_init(struct idr *idp);
/* 先定义,后初始化 */
struct idr id_huh;
idr_init(&id_huh);
分配新的UID
/*
* 调增大小,成功返回1,失败返回0
*/
int idr_pre_get(struct idr *idp,gfp_t gfp_mask);
/*
* 使用idp所指的idr去分配一个新的UID,并且将其关联到ptr上,成功时返回0,错误码-EAGAIN需要再次调用,-ENOSPC表示idr已满
*/
int idr_get_new(struct idr *idp,void *ptr,int *id);
/* 例子:讲一个UID映射到ptr,UID保存在id中 */
int id;
do{
if(!idr_pre_get(&idr_huh,GFP_KERNEL))
return -ENOSPC;
ret=idr_get_new(&idr_huh,ptr,&id);
}while(ret!=-EAGAIN);
/*
* 确保UID大于等于starting_id
*/
int idr_get_new_above(struct idr *idp,void *ptr,int starting_id,int *id);
查找UID
/*
* 调用成功,返回id关联的指针,错误,返回空指针,最好不要将UID映射到空指针上,无法确定是否查找成功。
*/
void *idr_find(struct idr *idp,int id);
/* 例子 */
struct my_struct *ptr=idr_find(&idr_huh,id);
if(!ptr)
return -EINVAL;
删除UID
/*
* 将id和关联的指针一起从映射中删除
*/
void idr_remove(struct idr *idp,int id);
撤销idr
/*
* 只释放未使用的内存
*/
void idr_destroy(struct idr *idp);
/*
* 删除所有的UID
*/
void idr_remove_all(struct idr *idp);
释放idr所有的内存,先调用isr_remove_all再调用idr_remove。
二叉树
二叉搜索树BST
- 根的左分支节点值都小于根节点值
- 右分支节点值都大于根节点值
- 所有的子树也都是二叉搜索树
自平衡二叉搜索树
平衡二叉树是一个所有叶子节点深度不超过1的二叉搜索树。一个自平衡二插搜索树是指其操作都试图维持(半)平衡的二叉搜索树。
- 红黑树
具有特殊的着色属性。 - rbtree
定义在lib/rbtree.c中,声明在<linux/rbtree.h>中。
rbtree的根节点由结构rb_root描述,其它节点由rb_node表示。
struct rb_root root=RB_ROOT;
搜索和插入需要用户实现。
/* 搜索例子,在页告诉缓冲中搜索一个文件区(由一个节点i和文件偏移决定) */
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;
}
/*
* 插入,offset决定搜索的方向,如果已经在缓存中,返回页结构地址。当插入点找到后,调用rb_link_node()在给定位置插入新节点,接着调用rb_insert_color()方法执行复杂的再平衡动作。
*/
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;
}
数据结构和选择
链表:主要操作是遍历,数据项多少不确定。
队列:生产者和消费者模式。
红黑树:存储大量树,检索迅速。
映射:映射一个UID到一个对象。
其它数据结构:基树和位图。
算法复杂度
注意算法的负载和典型输入集合大小的关系。不要为了根本不需要支持的伸缩度要求,盲目的去优化算法。