6.系统内核数据结构

目录

链表

Linux实现

操作!

遍历

队列

API

二叉树

创建一颗红黑树

在一颗红黑树中搜索值

在一颗红黑树中插入数据

在一颗红黑树中删除或替换已经存在的数据

(按排序的顺序)遍历存储在红黑树中的元素

带缓存的红黑树

Reference


我们下面聊聊数据结构的事情。在内核中,常见的数据结构如下:

链表(双向循环链表)

队列

二叉树

链表

在Linux内核中这玩意最常见了。比起来要求是连续分配的数组,它则可以更加零散的分散在内存和磁盘当中。那么,我们如何指代它的下一个呢?答案是使用一个标识来指代下一个:

struct list_elements /*Simple list's elements*/
{
    void* data;
    struct list_elements* next;
};

不知道你想象到了吗:

当然,我们可以扩展,构成一个更加完备的节点::也就是标识之既有前驱又有后驱的节点:

struct list_elements /*Simple list's elements*/
{
    void* data;
    struct list_elements* next;
    struct list_elements* prev;
};

可以理解为这个节点存储着前一个节点的地址和后一个节点的地址。在特殊些:对于第一个节点,我们看可以引导它的前驱指向最后一个节点,引导最后一个节点的后驱指向第一个节点,这样的话,我们就可以发现,这个链表循环起来:

好了这大概就是一个双循环链表

Linux实现

我们重点关心的是Linux如何实现这个骚操作的。它的实现很抽象:他将链表嵌入了结构体内:

struct list_head {
    struct list_head* prev;
    struct list_head* next;
}
​
struct MyData{
    void* welp;
    int someDatas;
    struct list_head lists;
}

如何访问的呢:答案是使用Linux自己的宏实现:比如说这里给出了一个叫做container_of的宏:即通过成员反向定位这个结构体(顺藤摸瓜摸出来地址):

container_of需要传入三个参数,第一个参数是一个指针,第二个参数是结构体类型,第三个是对应第二个参数里面的结构体里面的成员。

container_of(ptr, type, member)

#define container_of(ptr, type, member) ({
    const typeof(  ((type*)0)->member  )* _mptr = (ptr);
    (type*)( (char*)_mptr - offsetof(type, member) );
})
  • ptr:表示结构体中member的地址

  • type:表示结构体类型

  • member:表示结构体中的成员

  • 返回结构体的首地址

所以,这个链表的入口就是在封装一下container_of

#define list_entry(ptr, type, member)  container_of(ptr, type, member)

想要初始化一个链表,你需要使用的宏是:INIT_LIST_HEAD表示初始化

操作!

添加一个节点:list_add(struct list_head* new. struct list_head* head)

MyData a;
MyData head;
...
list_add(&a->list, &head);

删除一个节点:list_del(struct list_head* entry)

这个API将会删除我们上一次加入的节点。实际上就是通过调整前驱和后驱的指针

移动到给定节点的后头:list_move(struct list_head* list. struct list_head* head)这个将会将lists移除添加到head的后端。

可以查看是否为空:list_empty(struct list_head* entry)

遍历

我们创建一个链表的核心目的就是管理了这些数据,以便我们进行访问

#define list_for_each(pos, head) \
    for (pos = (head)->next; pos != (head); pos = pos->next)

你会发现我们拿到的就是list_head没啥意义,所以有一个更加好的版本:

#define list_for_each_entry(pos, head, member)                \
    for (pos = list_entry((head)->next, typeof(*pos), member);&pos->member != (head);   \

它可以直接被拿来用:

static struct inotify_watch* inode_find_handle(struct inode* inode, struct inotify_handle* ih){
	struct inotify_watch* watch;
    list_for_each_entry(watch, &inode->inootify_watches, i_list){
        if(watch->ih == ih)
            return ih;
    }
    return NULL;
}

还有一个安全一点的版本:list_for_each_entry_safe这段代码是一个宏定义,用于遍历一个链表中所有的元素,并且在遍历过程中可以安全地删除元素。具体来说,这个宏定义的功能是:遍历链表中所有的元素,从头节点开始,直到尾节点结束。对于每个元素,使用给定的结构体成员变量名找到它所属的结构体对象,并且将该对象的指针赋值给给定的变量名。在遍历过程中,可以安全地删除当前元素,因为它在删除前会先保存下一个元素的指针,保证不会影响遍历的正确性。

队列

队列(Queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。允许插入的端是队尾,允许删除的端是队头。所以说队列是一个先进先出的线性表,相应的也有顺序存储和链式存储两种方式。

(1)顺序存储就是用数组实现,比如有一个n个元素的队列,数组下标0的一端是队头,入队操作就是通过数组下标一个个顺序追加,不需要移动元素,但是如果删除队头元素,后面的元素就要往前移动,对应的时间复杂度就是O(n),性能自然不高。

(2)为了提高出队的性能,就有了循环队列,什么是循环队列呢?就是有两个指针,front指向队头,rear指向对尾元素的下一个位置,元素出队时front往后移动,如果到了对尾则转到头部,同理入队时rear后移,如果到了对尾则转到头部,这样通过下标front出队时,就不需要移动元素了。

这里常见的应用就是生产者和消费者了

kfifo是linux内核的对队列功能的实现。在内核中,它被称为无锁环形队列。所谓无锁,就是当只有一个生产者和只有一个消费者时,操作fifo不需要加锁。这是因为kfifo出队和入队时,不会改动到相同的变量。

kfifo使用了in和out两个变量分别作为入队和出队的索引:

in&out索引

  • 入队n个数据时,in变量就+n

  • 出队k个数据时,out变量就+k

  • out不允许大于in(out等于in时表示fifo为空)

  • in不允许比out大超过fifo空间(比如上图,in最多比out多8,此时表示fifo已满)

如果in和out大于fifo空间了,比如上图中的8,会减去8后重新开始吗?

不,这两个索引会一直往前加,不轻易回头,为出入队操作省下了几个指令周期。

那入队和出队的数据从哪里开始存储/读取呢,我们第一时间会想到,把 in/out 用“%”对fifo大小取余就行了,是吧?

不,取余这种耗费资源的运算,内核开发者怎会轻易采用呢,kfifo的办法是,把 in/out 与上fifo->mask。这个mask等于fifo的空间大小减一(其要求fifo的空间必须是2的次方大小)。这个“与”操作可比取余操作快得多了。由此,kfifo就实现了“无锁”“环形”队列。

了解了上述原理,我们就能意识到,这个无锁只是针对“单生产者-单消费者”而言的。“多生产者”时,则需要对入队操作进行加锁;同样的,“多消费者”时需要对出队操作进行加锁。

API

使用 kfifo_alloc 申请内存空间;分别使用kfifo_in、kfifo_out执行入队、出队的操作;不再使用kfifo时,使用 kfifo_free 释放申请的内存。想要查看fifo大小就是使用kfifo_size

我们的图在系统架构中的一个代表性应用就是用来映射和分配UID。这里先给出使用办法:

struct idr id_huh; // 图
// 使用API:idr_init来初始化一个图
idr_init(&id_huh);	

之后就是一些API:

idr.h - include/linux/idr.h - Linux source code (v6.8.7) - Bootlin

void *idr_find_slowpath(struct idr *idp, int id);
void idr_preload(gfp_t gfp_mask);
int idr_alloc(struct idr *idp, void *ptr, int start, int end, gfp_t gfp_mask);
int idr_alloc_cyclic(struct idr *idr, void *ptr, int start, int end, gfp_t gfp_mask);
// 遍历
int idr_for_each(struct idr *idp,
		 int (*fn)(int id, void *p, void *data), void *data);
void *idr_get_next(struct idr *idp, int *nextid);
void *idr_replace(struct idr *idp, void *ptr, int id);
void idr_remove(struct idr *idp, int id);
void idr_destroy(struct idr *idp);
bool idr_is_empty(struct idr *idp);

二叉树

在内核中更多使用的是红黑树和二叉搜索树。二叉搜索树(Binary Search Tree, BST)保证了节点按照中序遍历是有序的,其中,为了保证二叉搜索树的平衡我们引入了红黑树,也就是现在Linux使用的二叉树。

一棵合法的红黑树必须遵循以下四条性质:

  1. 节点为红色或黑色

  2. NIL 节点(空叶子节点)为黑色

  3. 红色节点的子节点为黑色

  4. 从根节点到 NIL 节点的每条路径上的黑色节点数量相同

关于红黑树的一个漫画:漫画:什么是红黑树? - 知乎 (zhihu.com)

创建一颗红黑树

红黑树中的数据结点是包含rb_node结构体成员的结构体:

struct mytype {
      struct rb_node node;
      char *keystring;
};

当处理一个指向内嵌rb_node结构体的指针时,包住rb_node的结构体可用标准的container_of() 宏访问。此外,个体成员可直接用rb_entry(node, type, member)访问。

每颗红黑树的根是一个rb_root数据结构,它由以下方式初始化为空:

struct rb_root mytree = RB_ROOT;

在一颗红黑树中搜索值

撸一个搜索函数:从树根开始,比较每个值,然后根据需要继续前往左边或 右边的分支。

示例:

struct mytype *my_search(struct rb_root *root, char *string)
{
      struct rb_node *node = root->rb_node;

      while (node) {
              struct mytype *data = container_of(node, struct mytype, node);
              int result;

              result = strcmp(string, data->keystring);

			// 有序的,搜索的时候决定向哪个根寻找			
              if (result < 0)
                      node = node->rb_left;
              else if (result > 0)
                      node = node->rb_right;
              else
                      return data;
      }
      return NULL;
}

在一颗红黑树中插入数据

在树中插入数据的步骤包括:首先搜索插入新结点的位置,然后插入结点并对树再平衡 (”recoloring”)。

插入的搜索和上文的搜索不同,它要找到嫁接新结点的位置。新结点也需要一个指向它的父节点 的链接,以达到再平衡的目的。

示例:

int my_insert(struct rb_root *root, struct mytype *data)
{
      struct rb_node **new = &(root->rb_node), *parent = NULL;

      /* Figure out where to put new node */
      while (*new) {
              struct mytype *this = container_of(*new, struct mytype, node);
              int result = strcmp(data->keystring, this->keystring);

              parent = *new;
              if (result < 0)
                      new = &((*new)->rb_left);
              else if (result > 0)
                      new = &((*new)->rb_right);
              else
                      return FALSE;
      }

      /* Add new node and rebalance tree. */
      rb_link_node(&data->node, parent, new);
      rb_insert_color(&data->node, root);

      return TRUE;
}

在一颗红黑树中删除或替换已经存在的数据

若要从树中删除一个已经存在的结点,调用:

void rb_erase(struct rb_node *victim, struct rb_root *tree);

示例:

struct mytype *data = mysearch(&mytree, "walrus");

if (data) {
      rb_erase(&data->node, &mytree);
      myfree(data);
}

若要用一个新结点替换树中一个已经存在的键值相同的结点,调用:

void rb_replace_node(struct rb_node *old, struct rb_node *new,
                      struct rb_root *tree);

通过这种方式替换结点不会对树做重排序:如果新结点的键值和旧结点不同,红黑树可能被 破坏。

(按排序的顺序)遍历存储在红黑树中的元素

我们提供了四个函数,用于以排序的方式遍历一颗红黑树的内容。这些函数可以在任意红黑树 上工作,并且不需要被修改或包装(除非加锁的目的):

struct rb_node *rb_first(struct rb_root *tree);
struct rb_node *rb_last(struct rb_root *tree);
struct rb_node *rb_next(struct rb_node *node);
struct rb_node *rb_prev(struct rb_node *node);

要开始迭代,需要使用一个指向树根的指针调用rb_first()或rb_last(),它将返回一个指向 树中第一个或最后一个元素所包含的节点结构的指针。要继续的话,可以在当前结点上调用 rb_next()或rb_prev()来获取下一个或上一个结点。当没有剩余的结点时,将返回NULL。

迭代器函数返回一个指向被嵌入的rb_node结构体的指针,由此,包住rb_node的结构体可用 标准的container_of()宏访问。此外,个体成员可直接用rb_entry(node, type, member) 访问。

示例:

struct rb_node *node;
for (node = rb_first(&mytree); node; node = rb_next(node))
      printk("key=%s\n", rb_entry(node, struct mytype, node)->keystring);

带缓存的红黑树

计算最左边(最小的)结点是二叉搜索树的一个相当常见的任务,例如用于遍历,或用户根据 他们自己的逻辑依赖一个特定的顺序。为此,用户可以使用’struct rb_root_cached’来优化 时间复杂度为O(logN)的rb_first()的调用,以简单地获取指针,避免了潜在的昂贵的树迭代。 维护操作的额外运行时间开销可忽略,不过内存占用较大。

和rb_root结构体类似,带缓存的红黑树由以下方式初始化为空:

struct rb_root_cached mytree = RB_ROOT_CACHED;

带缓存的红黑树只是一个常规的rb_root,加上一个额外的指针来缓存最左边的节点。这使得 rb_root_cached可以存在于rb_root存在的任何地方,并且只需增加几个接口来支持带缓存的 树:

struct rb_node *rb_first_cached(struct rb_root_cached *tree);
void rb_insert_color_cached(struct rb_node *, struct rb_root_cached *, bool);
void rb_erase_cached(struct rb_node *node, struct rb_root_cached *);

操作和删除也有对应的带缓存的树的调用:

void rb_insert_augmented_cached(struct rb_node *node, struct rb_root_cached *,
                                bool, struct rb_augment_callbacks *);
void rb_erase_augmented_cached(struct rb_node *, struct rb_root_cached *,
                               struct rb_augment_callbacks *);

Reference

kfifo(linux kernel 无锁队列) - 知乎 (zhihu.com)

Linux内核-数据结构系列(队列的基本操作) - 知乎 (zhihu.com)

Linux中的红黑树(rbtree) — The Linux Kernel documentation

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值