Linux内核设计与实现 第六章 内核数据结构

目录

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)时间复杂度

常见的复杂度:

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值