目录
1)单向链表和双向链表
?编辑
?编辑
2)环形链表
3)沿链表移动
4)Linux内核中的实现
5)操作链表
6)遍历链表
6.2队列
1)kfifo
2)创建队列
3)推入队列数据
4)摘取队列数据
5)获取队列数据
6)重置和撤销队列
6.3映射
1)初始化一个idr
?编辑2)分配一个新的UID
3)查找UID
4)删除UID
5)撤销idr
6.4二叉树
1)二叉搜索树
2)自平衡二叉搜索树
6.5数据结构以及选择
6.6算法复杂度
1)算法
2)大O符号
3)大θ符号
4)时间复杂度
6.1链表
链表和静态数组的不同之处在于,它所包含的元素都是动态创建并插入链表的,在编译时不必知道具体需要创建多少元素。
1)单向链表和双向链表
2)环形链表
?
3)沿链表移动
沿链表移动是先访问某个元素,然后沿该元素的向前或向后指针访问下一个元素。
链表存放数据的理想情况是:需要遍历所有数据或需要动态加入和删除数据时。
4)Linux内核中的实现
在Linux2.1时官方明确了内核链表的实现,从此在Linux内核只能用官方实现的针对链表的一组函数。
//list_head与一个具体的数据结构fox:
struct list_head{
struct list_head *next;
struct list_head *prev;
}
struct fox{
unsigned long tail_length;//尾巴长度
unsigned long weight;//重量
bool is_fantastic;//是否是狐狸
struct list_head list;
}
//list_head嵌入一个具体的数据结构fox:
struct fox{
unsigned long tail_length;//尾巴长度
unsigned long weight;//重量
bool is_fantastic;//是否是狐狸
struct list_head list;
}
链表头:指链表的头指针
5)操作链表
内核提供了一组函数来操作链表。
list_add(要插入的节点的指针,链表头)//向链表中加入一个节点。
list_del(要删除的节点的指针)//从链表中删除一个节点。(节点结构体有链表头数据)
list_move(源链表的链表头,目的链表的链表头)//把节点从一个链表的链首移动到另一个链表的链首。
list_move_tail(源链表的链表头,目的链表的链表头)//把节点从一个链表的链尾移动到另一个链表的链尾。
list_empty(链表头)//检查链表是否为空
list(源链表的链表头,目的链表的链表头)//把源链表插入到目的链表的链表头后面
6)遍历链表
list_for_each(临时指针变量,遍历的链表的链表头)//运行list_for_each()临时指针变量会从链表头,向下移动到链表尾。临时指针变量就像光标
list_for_each_entry(临时指针变量,遍历的起始位置,遍历的链表的链表头)//从的链表的起始位置正向的遍历
list_for_each_entry_reverse(临时指针变量,遍历的起始位置,遍历的链表的链表头)//从的链表的起始位置反向的遍历
list_for_each_entry(临时指针变量,要删除的节点的指针,遍历的起始位置,遍历的链表的链表头)//从的链表的起始位置正向的遍历,同时删除指定节点
如果出现并发进行删除链表或者并发进行链表操作,就需要锁定链表。第9章和第10章会讨论同步和锁
更多Linux提供的链表操作去看看头文件<linux/list.h>
6.2队列
任何操作系统内核都少不了一种编程模型:生产者和消费者。
生产者将数据推入队列,消费者从队列中摘取出数据。原则是先进先出first-in first-out(FIFO)
Linux内核通用队列实现称为kfifo(kernel first-in first-out)。
1)kfifo
Linux内核通用队列kfifo,维护了两个偏移量:a)入口偏移:指下一次入队列的位置 b)出口偏移:指下一次出队列时的位置。
enqueue操作拷贝数据到队列中的入口偏移位置。
dequeue操作从队列中出口偏移出拷贝数据。
出口偏移总是小于等于入口偏移,否则无意义,因为那样说明要出队列的元素根本还没有入队列。
2)创建队列
//动态创建并初始化
int kfifo_alloc(struct kfifo,unsigned int size,gfp_t gfp_mask)//创建并初始化一个大小为size的kfifo,内核使用gfp_mask标识分配队列
//静态创建并初始化
DECLARE_KFIFO(name,size)//创建一个名称为name,大小为size的Linux内核通用队列kfifo。
INIT_KFIFO(name) //初始化名称为name的Linux内核通用队列kfifo。
3)推入队列数据
unsigned int kfifo_in(struct kfifo *fifo,const *from,unsigned int len)//把form指针所指的len个字节的数据拷贝到fifo所指的队列中
4)摘取队列数据
//kfifo对象维护了两个offset:in offset和out offset。in offset指示了下次入队的位置,而out offset指示了下次出队的位置。
unsigned int kfifo_out(struct kfifo *fifo,void *to,unsigned int len)//从fifo所指向的队列中拷贝出长度为len字节的数据到to所指的缓冲中。
unsigned int kfifo_out_peek(struct kfifo *fifo,void *to,unsigned int len,unsigned offset)//从fifo所指向的队列中拷贝出长度为len字节的数据到to所指的缓冲中,out offset没有被更改。
5)获取队列数据
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)//该函数测试kfifo是否为空.
static inline int kfifo_is_full(struct kfifo *fifo)//该函数测试kfifo是否已满.
6)重置和撤销队列
static inline void kfifo_reset(struct kfifo *fifo)//该函数重置队列.
void kfifo_free(struct kfifo *fifo)//该函数销毁用kfifo_alloc()创建的队列.
6.3映射
IDR机制原理:
IDR机制适用在那些需要把某个整数和特定指针关联在一起的地方。例如,在IIC总线中,每个设备都有自己的地址,要想在总线上找到特定的设备,就必须要先发送设备的地址。当适配器要访问总线上的IIC设备时,首先要知道它们的ID号,同时要在内核中建立一个用于描述该设备的结构体,和驱动程序
将ID号和设备结构体结合起来,如果使用数组进行索引,一旦ID 号很大,则用数组索引会占据大量内存空间。这显然不可能。或者用链表,但是,如果总线中实际存在的设备很多,则链表的查询效率会很低。
此时,IDR机制应运而生。该机制内部采用红黑树实现,可以很方便的将整数和指针关联起来,并且具有很高的搜索效率
一个映射,也常称为关联数组,其实是一个由唯一键组成的集合,而每个键必然关联一个特定的值。即键到值的关联关系称为映射。值=f(键)
三个标准的映射操作: Add(key,value) Remove(key) value=Lookup(key)//look up v.查阅;查找 映射可以通过散列表和平衡二叉树搜索树实现。
Linux在add基础上实现了allocate(内存分配)操作,allocate(内存分配)操作向map中加入键值对,产生标识数UID。linux映射标识数UID到一个指针。
idr数据结构,用于映射用户空间的标识数UID。
1)初始化一个idr
struct idr {
struct idr_layer __rcu *top; /*根节点*/
struct idr_layer *id_free; /*空闲节点*/
int layers; /*树的高度*/
int id_free_cnt; /*空闲节点数*/
spinlock_t lock;
};
struct idr_layer {
unsigned long bitmap; /*位图*/
struct idr_layer __rcu *ary[1<<IDR_BITS]; /*孩子节点或者地址数据*/
int count; /*ary已存放数*/
int layer; /* 相对叶子节点的高度 */
struct rcu_head rcu_head;
};
2)分配一个新的UID
//看看如何使用idr:
int idr_pre_get(struct idr,gfp_t gfp_mask)//分配存放ID号的内存:每次通过IDR获得UID号之前 ,需要为UID号先分配内存。分配内存的函数是idr_pre_get().成功返回1,失败放回0。第一个参数是指向IDR的指针,第二个参数是内存分配标志。
int idr_get_new(struct idr *idp, void *ptr, int *id)//使用idp所指向的idr去分配一个新的UID,并且将其关联到指针ptr上。成功返回0,并将新的UID存于id。失败返回错误码-EAGAIN。
3)查找UID
void *idr_find(struct idr *idp, int id)//在红黑树中找到指定的id
4)删除UID
void idr_remove(struct idr *idp, int id)//在红黑树中删除指定的id,当然id关联的指针也会删除
5)撤销idr
void idr_destroy(struct idr *idp)
//如果成功,则只释放idr中未使用的内存。它并不释放当前分配给UID使用的任何内存。通常,内核代码不会撤销idr,除非关闭或者卸载,而且只用在没有其他用户(也就没有更多的UID)时才能删除。
//当然可以调用void idr_remove_all(struct idr *idp)强制删除所有的UID
6.4二叉树
1)二叉搜索树
略,看算法追逐之路https://blog.csdn.net/weixin_55255438/article/details/125030571?spm=1001.2014.3001.5502
2)自平衡二叉搜索树
略,看算法追逐之路https://blog.csdn.net/weixin_55255438/article/details/125030571?spm=1001.2014.3001.5502
6.5数据结构以及选择
如果对数据集合的主要操作是遍历数据,就使用链表。
如果你的代码符合生产者/消费者模式,则使用队列。
如果你需要映射一个UID到一个对象,就使用映射。
如果你需要存储大量数据,并且检索迅速,那么红黑树最好。
6.6算法复杂度
1)算法
算法就是一系列指令
2)大O符号
一种很有用的渐近表示法,它是一个函数,此函数增长等于或快于我们研究的函数,大O符号用来描述这种增长率。
3)大θ符号
Knuth教授把上述大O符号,称为大θ符号
4)时间复杂度
常见的复杂度:
|