【Linux内核笔记】内核数据结构

本文介绍了Linux内核中常用的数据结构,包括链表、队列FIFO、映射和二叉树。链表是最简单且普遍的数据结构,内核中以双向环形链表为主。队列FIFO适合生产者消费者模型,而映射主要用于UID到内核数据结构的映射。二叉树部分,特别是红黑树,是内核中存储数据的重要结构。理解这些数据结构的特性及其在内核中的应用对于内核编程至关重要。
摘要由CSDN通过智能技术生成


本笔记不详细讲述数据结构。

链表

链表是一种存放和操作可变数量元素(常称为节点)的数据结构

  • 无须在内存中占用连续内存区,是Linux内核中最简单、最普通的数据结构
  • 链表有单向链表、双向链表、环形链表、环形双向链表(Linux内核标准链表)
  • 使用链表存放数据的理想情况是:遍历、动态加入或删除。随机访问一般不使用链表
Linux内核中的实现

先介绍普通的链表实现

//数据结构中嵌入链表指针
struct fox{
	struct fox *next; //下一个fox结构体
	struct fox *prev
};

而内核中是将链表节点塞入数据结构
<linux/list.h>

struct list_head{
	struct list_head *next;//下一个链表节点
	struct list_head *prev;
};
struct fox{
	struct list_head list;//所有fox结构体行成链表
}

上述fox中的 list.next 指向下一个元素

Linux内核链表操作

//不详细讲述功能如何完成
都只接受list_head结构作为参数

list_add()
list_entry()依靠此方法内核提供了创建、操作以及其他链表管理的各种例程。不需要知道list_head所嵌入对象的数据结构
  • list_head本身没有意义——需要被嵌入到数据结构中才生效。
  • 链表需要在使用前初始化、多数元素都是动态创建的,因此最常见的方式是在运行时初始化链表
  • 链表上的插入删除的算法复杂度O(1),与长度无关
  • 操作的一组函数都以C语言的内联函数形式实现,原型文件在<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)
//head节点前插入。若把“第一个”元素当做head,该函数可以用来实现一个队列
list_del(struct list_head *entry)
//从链表中删除entry元素。该操作不释放entry及包含entry数据结构体的内存,仅从链表中移走,所以还需善后函数
list_del_init();
list_del_init(struct list_head *entry)//从链表删除一个节点并对其重新初始化
list_move(struct list_head *list, struct list_head *head)
//把list节点移除,加入到另一个链表head节点后面
list_move_tail(struct list_head *list, struct list_head *head)
//把list移动到另一个链表尾,在head前
list_empty(struct list_head *head)//链表为空返回非0。不为空返回0
list_splice(struct list_head *list, struct list_head *head)
//list链表连接到另一个链表head后面
list_splice_init(struct list_head *list, struct list_head *head)
//同splice,但list指向的链表要被重新初始化
  • dereference
    若已经得到next prev指针,可直接调用内部链表函数,省下提领指针的时间。前面讨论的所有函数其实没有什么特别的操作。仅是找到next和prev指针,再去调用内部函数。内部函数时外部包装函数加两条下划线 如:__list_del(prev,next)代替list_del(list),详看<linux/list.h>接口

遍历链表

  • 复杂度O(n)
list_for_each(p,list)//宏 p不断在list中指向下一个元素
list_for_each_entry(pos,head,member)//多使用这个宏,该宏内部也是用list_entry但是简化遍历过程。pos指向包含list_head节点对象的指针,可看做list_entry宏的返回值。head遍历开始位置。member是pos中list_head结构的变量名
list_for_each_entry_reverse(pos,head,member)//反向遍历链表 
list_for_each_entry_safe(pos,next,head,member)//遍历同时删除,该函数内有对mutex锁操作
list_for_each_entry_safe_reverse(pos,n,head,member)//反向遍历删除

队列FIFO

适用于生产者消费者模型
内核通用队列kfifo 实现在kernel/kfifo.c声明在<linux/kfifo.h>

  • kfifo主要两个主要操作:enqueue(入队列)、dequeue(出队列),kfifo对象维护了两个偏移量:入口偏移(下一次入队的位置)和出口偏移(下一次出队时的位置)。出口偏移总是≤入口偏移。出=入 队列空,入=队列长度,则在重置队列前不可推入新数据
队列操作
int kfifo_alloc(struct kfifo *fifo, unsigned int size, gfp_t gfp_mask)
//创建并初始化一个大小为size的kfifo.内核使用gfp_mask标识分配队列。成功返回0,错误返回负数错误码
void kfifo_init(struct kfifo *fifo, void *buffer, unsigned int size)
//自己分配缓冲。初始化大小为size字节大小的kfifo.对于alloc和init,size大小必须是2的幂
unsigned int kfifo_in(struct kfifo *fifo, const void *from, unsigned int len)//from处拷贝len到fifo.成功返回字节大小len,若无len大小的空闲则返回推入的字节大小或0
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)//从offset位置摘取len到to。出口偏移不变
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)//为空返回非0,否则返回0
static inline int kfifo_is_full(struct kfifo *fifo)//满返回非0,否则返回0
static inline void kfifo_reset(struct kfifo *fifo)//抛弃所有内容
void kfifo_free(struct kfifo *fifo)//撤销使用kfifo_alloc()分配的队列。若使用init函数创建的则需要释放缓冲

映射

一个映射,也常称为关联数组,是一个由唯一键组成的集合,每个键必然关联一个特定的值。这种键到值的关联关系成为映射。映射需要至少支持三个操作:

Add(key,value)
Remove(key)
value=Lookup(key)

散列表也是一种映射,二叉搜索树更优,无需散列函数,只需定义≤操作算子即可
更多时候映射特指使用二叉树

  • Linux内核提供的映射是非通用的映射:映射一个唯一标识数(UID)到一个指针除了提供以上3个操作还在add基础上实现allocate操作。不但向map中加入了键值对,而且还可以产生UID。 idr数据结构用于映射用户空间的UID到内核中相关联的数据结构上。idr映射命名沿袭内核中含糊不清的命名体系。

定义初始化 idr数据结构

struct idr id_huh;
void idr_init(&id_huh);
映射UID操作

① 告诉idr需要分配新的UID,允许在必要时调整后备树的大小
②请求新的UID(涉及在无锁情况下分配内存,所以要允许调整大小)
重点放在idr的使用

int idr_pre_get(struct idr *idp,gfp_t gfp_mask)
//调整由idp指向的idr的大小。若需要调整,则使用gfp标识。与内核其他函数不同,成功时返回1,失败返回0int idr_get_new(struct idr *idp, void *ptr, int *id)
//idp->idr分配一个新UID并关联到ptr上,成功返回0,错误返回-EAGAIN,说明需要再次get()若idr已满 -ENOSPC
int idr_get_new_above(struct idr *idp ,void *ptr ,int starting_id,int *id)
//使用idp指向的idr分配一个≥stating_id的UID。保证UID不会被重用且唯一
void *idr_find(struct idr *idp, int id)
//在idp中返回id相关的指针,错误返回空指针,因此不要将UID映射到空指针上
void idr_remove(struct idr *idp)
//与id关联的指针一起删除,无错误提示
void idr_destory(struct idr *idp)
//只释放idr中未使用的内存,通常内核不撤销idr除非关闭或卸载。且只在没有其他用户(没有更多的UID)时删除
void idr_remove_all(struct idr *idp)//强制删除。释放idr所有内存

二叉树

二叉树介绍的函数不多,更多的结构。此处不细讲

二叉搜索树BST
  • 节点有序
  • 左支小于根节点
  • 右支大于根节点
  • 子树也是二叉搜索树
自平衡二叉树

所有叶子节点深度差不超过1的二叉搜索树

红黑树rbtree

linux主要的自平衡二叉树结构是红黑树,例如与就绪线程存储在rbtree结构中
定义在 lib/rbtree.c 声明<linux/rbtree.h>

  • 所有节点红色或黑色
  • 叶子节点黑色
  • 叶子节点不包含数据
  • 非叶子节点都有两个子节点
  • 若一个节点是红色,则它的子节点都是黑色
  • 一个节点到其叶子节点的最短路径,总是包含相同数据的黑色节点
    根节点rb_root数据结构。
    struct rb_root root=RB_ROOT
    其他节点 rb_node。通过同名节点指针来找到左右子节点。rb_right rb_left

算法复杂度

大O符号

是上限,增长率。f(x)写作O(g(x)),读作 f是g的大O
如f(x)是O(g(x))则
∃ \exists c,x满足f(x)≤c·g(x) ∀ \forall x>x
完成f(x)的时间 ≤ 完成g(x)的时间和任意常量乘积
实际意义是:找到一个函数,它的行为和目前算法一样差或更差。它的无限大输入是目前算法的执行上限。

时间复杂度
O(g(x))名称
1衡量(理想伸缩度)
logn对数的
n线性的
n 2 n^2 n2平方的
n 3 n^3 n3立方的
2 n 2^n 2n指数 最好不要
n ! n! n!阶乘 最好不要

总结

  • Linux内核主要使用双向环形链表、将链表节点list_head塞入数据结构.
  • 队列对象主要操作入队和出队控制两个偏移量
  • 内核的映射主要为用户空间UID映射到内核指针,使用idr数据结构来操作。
  • 内核常用红黑树存储数据,函数较为复杂。没有通用操作,用户自行定义。
  • 算法复杂度是函数的上限,O是评价算法和内核组件在多用户、处理器、进程、网络连接以及其他环境下伸缩度的重要指标
数据结构以及选择

遍历——链表
生产消费者——队列 若大小不明 链表
UID到一个对象——映射
大量数据。检索迅速——红黑树

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值