第六章 内核数据结构

第六章 内核数据结构

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所嵌入的数据结构。

  1. 定义
    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的方法不相同。

  1. 由内核链表例程管理
    需要一个链表头:
static LIST_HEAD(fox_list);

该函数定义并初始化了一个名为fox_list的链表例程。

操作链表

内联实现,原型在<linux/list.h>。

  1. 增加节点
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);

可以用来实现一个队列。

实际就是头插法或尾插法建立链表的情况。

  1. 删除一个节点
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);
  1. 移动和合并链表
    节点从一个链表到其它链表:
/*从一个链表里移除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)。

遍历链表

  1. 基本方法
    两个参数都是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. 可用的方法
/*第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;
}
  1. 反向遍历链表
list_for_each_entry_reverse(pos,head,member);
  1. 遍历的同时删除
/*
 *@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);
}

映射

也被称为关联数组,一个唯一键关联一个特定的值,至少支持三个操作:

  1. Add(key,value)
  2. Remove(key)
  3. 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. 根的左分支节点值都小于根节点值
  2. 右分支节点值都大于根节点值
  3. 所有的子树也都是二叉搜索树

自平衡二叉搜索树

平衡二叉树是一个所有叶子节点深度不超过1的二叉搜索树。一个自平衡二插搜索树是指其操作都试图维持(半)平衡的二叉搜索树。

  1. 红黑树
    具有特殊的着色属性。
  2. 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到一个对象。
其它数据结构:基树和位图。

算法复杂度

注意算法的负载和典型输入集合大小的关系。不要为了根本不需要支持的伸缩度要求,盲目的去优化算法。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值