目录
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;
};
![](/image/aHR0cHM6Ly9pLWJsb2cuY3NkbmltZy5jbi9ibG9nX21pZ3JhdGUvNjA3YmFkZmFlMDUxNTVkOWE3NDljZmEzYzgxZDllMGMucG5n)
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)时间复杂度
常见的复杂度: