Linux内核中的算法和数据结构
算法和数据结构纷繁复杂,但是对于Linux Kernel开发人员来说重点了解Linux内核中使用到的算法和数据结构很有必要。
在一个国外问答平台stackexchange.com的Theoretical Computer Science子板有一篇讨论实际使用中的算法和数据结构,Vijay D做出了详细的解答,其中有一部分是Basic Data Structures and Algorithms in the Linux Kernel对Linux内核中使用到的算法和数据结构做出的归纳整理。详情参考这里。
同时有一篇中文翻译在https://linux.cn/article-2317-1.html可以找到。
下面就以Vijay D的回答作为蓝本进行学习总结。
测试方法准备
由于需要在内核中进行代码测试验证,完整编译安装内核比较耗时耗力。准备采用module形式来验证。
Makefile
obj-m:=linked-list.o KERNELBUILD:=/lib/modules/$(shell uname -r)/build default: |
linked-list.c
#include <linux/module.h> int linked_list_init(void) void linked_list_exit(void) module_init(linked_list_init); |
安装module
sudo insmod linked-list.ko |
查找安装情况
lsmod | grep linked-list |
执行log
<4>[621267.946711] linked_list_init |
删除module
sudo rmmod linked-list |
链表、双向链表、无锁链表
有一篇关于内核链表《深入分析Linux内核链表》值得参考。
链表是一种常用的组织有序数据的数据结构,它通过指针将一系列数据节点连接成一条数据链,是线性表的一种重要实现方式。相对于数组,链表具有更好的动态性,建立链表时无需预先知道数据总量,可以随机分配空间,可以高效地在链表中的任意位置实时插入或删除数据。链表的开销主要是访问的顺序性和组织链的空间损失。
通常链表数据结构至少应包含两个域:数据域和指针域,数据域用于存储数据,指针域用于建立与下一个节点的联系。按照指针域的组织以及各个节点之间的联系形式,链表又可以分为单链表、双链表、循环链表等多种类型.
通过设计前驱和后继两个指针域,双链表可以从两个方向遍历,这是它区别于单链表的地方。如果打乱前驱、后继的依赖关系,就可以构成"二叉树";如果再让首节点的前驱指向链表尾节点、尾节点的后继指向首节点(如图2中虚线部分),就构成了循环链表;如果设计更多的指针域,就可以构成各种复杂的树状数据结构。
循环链表的特点是尾节点的后继指向首节点。前面已经给出了双循环链表的示意图,它的特点是从任意一个节点出发,沿两个方向的任何一个,都能找到链表中的任意一个数据。如果去掉前驱指针,就是单循环链表。
Simple doubly linked list
数据结构:
struct list_head { |
声明和初始化: 在表头插入和在表尾插入: static inline void list_add_tail(struct list_head *entry, struct list_head *head) 删除,被删除的节点prev、next分别被设为LIST_POISON2、LIST_POISON1,当访问此节点时会引起叶故障。保证不在链表中的节点项不可访问。 static inline void list_del_init(struct list_head *entry) 将entry从链表解下来,重新初始化,就可以访问节点。 将节点从一个链表搬移到另一个链表,根据插入表头和表位分两种: static inline void list_move_tail(struct list_head *list, struct list_head *head) 用新节点替换纠结点: 将list插入到head: static inline void list_splice_tail(struct list_head *list, struct list_head *head) static inline void list_splice_init(struct list_head *list, struct list_head *head) 将list设为空链表 static inline void list_splice_tail_init(struct list_head *list, struct list_head *head) 将list设为空链表 static inline void list_cut_position(struct list_head *list, struct list_head *head, struct list_head *entry) 遍历宏: list_first_entry(ptr, type, member) list_last_entry(ptr, type, member) list_next_entry(pos, member) list_prev_entry(pos, member) list_for_each(pos, head) list_for_each_prev(pos, head) 反向操作 list_for_each_safe(pos, n, head) 安全操作 list_for_each_entry(pos, head, member) 遍历链表是获取链表节点 list_for_each_entry_safe(pos, n, head, member) 安全操作 list_for_each_entry_reverse(pos, head, member) 反向操作 判断链表是否为空: |
Doubly linked list with a single pointer list head
linux内核里边除了著名的list双向循环链表以外,还有一个重要的数据结构,就是哈希链表。哈希链表也在很多重要的地方有所使用,比如linux内核的dentry,进程查询,文件系统等,可以说,弄明白hlist对于理解linux内核具有重要的意义。
struct hlist_head { struct hlist_node { |
linux内核的hash链表有两个数据结构组成,一个是hlist_head是hash表的表头,一个是hlist_node是hash标的后续节点。
在使用的时候,一般定义一个struct hlist_head xxx[100]数组(100只是一个代表的数字,视具体情况而定),采取哈希函数来将键值与数组的对应的地址联系起来,如果出现冲突的话,就在hlist_head的后边继续添加。
hlist_head的成员first指针指向后续的第一个节点,如果哈希链表是空的话,就为NULL。
为什么hlist_head不弄成双向链表呢,因为为了节约空间,如果一个指针的话,一个哈希数组的空间消耗就会减半。
hlist_node的成员next指向后续的节点的地址,如果为空就是NULL,另一个成员pprev是二级指针,指向前一个节点的next成员的地址,如果前一个成员是hlist_head的话,pprev的值就是前一个的first指针的地址。
#define HLIST_HEAD(name) struct hlist_head name = { .first = NULL } 定义并且初始化。 #define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL) 在定义之后,需要初始化,不然使用会导致错误。 static inline void INIT_HLIST_NODE(struct hlist_node *h) 初始化node节点 static inline int hlist_empty(const struct hlist_head *h) 判断hash链表是否为空 static inline void hlist_del(struct hlist_node *n) 删除节点,并且将节点next、pprev指针修改为LIST_POSITION1和LIST_POSITION2。 static inline void hlist_del_init(struct hlist_node *n) 此种方法更安全,删除然后再初始化节点。 static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h) 将节点插入到hash链表的头结点后边。 static inline void hlist_add_before(struct hlist_node *n, struct hlist_node *next) 将一个节点插入到next前面。 static inline void hlist_add_behind(struct hlist_node *n, struct hlist_node *prev) 将一个节点插入到prev后面。 遍历访问节点: hlist_for_each(pos, head) hlist_for_each_safe(pos, n, head) #define hlist_entry(ptr, type, member) container_of(ptr,type,member) hlist_entry_safe(ptr, type, member) hlist_for_each_entry(pos, head, member) hlist_for_each_entry_safe(pos, n, head, member) |
Lock-less NULL terminated single linked list
无锁链表定义在include/linux/llist.h。
数据结构如下:
struct llist_head { struct llist_node { |
#define LLIST_HEAD(name) struct llist_head name = LLIST_HEAD_INIT(name) static inline void init_llist_head(struct llist_head *list) llist_entry(ptr, type, member) llist_for_each(pos, node) static inline bool llist_empty(const struct llist_head *head) static inline struct llist_node *llist_next(struct llist_node *node) static inline bool llist_add(struct llist_node *new, struct llist_head *head) bool llist_add_batch(struct llist_node *new_first, struct llist_node *new_last, struct llist_head *head) static inline struct llist_node *llist_del_all(struct llist_head *head) struct llist_node *llist_del_first(struct llist_head *head) |
llist_add、llist_add_batch、llist_del_first都是基于cmpxchg原子操作来实现,整个操作是原子的;llist_del_all是基于xchg来实现的。
cmpxchg(void* ptr, int old, int new),如果ptr和old的值一样,则把new写到ptr内存,否则返回ptr的值,整个操作是原子的。在Intel平台下,会用lock cmpxchg来实现,这里的lock个人理解是锁住内存总线,这样如果有另一个线程想访问ptr的内存,就会被block住。
B+树
B+ Trees with comments telling you what you can't find in the textbooks.
|
关于B树及B树衍生树有篇介绍不错《从B树、B+树、B*树谈到R 树》。
B树诞生的背景:
在大规模数据存储中,实现索引查询这样一个实际背景下,树节点存储的元素数量是有限的,这样就会导致二叉树结构由于树的深度过大而造成磁盘I/O读写过于频繁,进而导致查询效率低下。
那么如何减少树的深度,一个基本的想法是采用多叉树结构。
因为磁盘的操作费时费资源,那么如何提高效率,即如何避免频繁的读取呢?根据磁盘查找存取的次数往往由树的高度决定,所以只要通过较好的结构降低树的高度。根据平衡二叉树的启发,自然就想到平衡多叉树结构。
几个算法时间复杂度度量:
O(n) 表示某函数值(未列出)是 n 的常数倍;亦即他们增长的速度相当.称 大O,big O (发音 "欧" 英文字母 O )
同理:O(logN):是 logN 的常数倍;O(nlogn):是 nlogn 的常数倍
优先排序列表
Priority sorted lists used for mutexes, drivers, etc. |
plist有两个重要结构体struct plist_head和struct plist_node,分别用来表示plist表头和plist节点。
struct plist_head { struct list_head node_list; }; struct plist_node { int prio; struct list_head prio_list; struct list_head node_list; }; |
相关函数:
PLIST_HEAD(head) 初始化plist表头 PLIST_NODE_INIT(node, __prio) 初始化plist节点 static inline void plist_head_init(struct plist_head *head) 初始化plist表头 static inline void plist_node_init(struct plist_node *node, int prio) 初始化plist节点 添加节点、删除节点: 遍历plist: 判断head是否为空: 判断当前node是否在node_list上: 获取前一、后一节点: 获取首节点、尾节点: |
下面是对plist进行的一些验证:
static dump_list(void) printk(KERN_DEBUG "%s start\n", __func__); first_node = plist_first(&test_head); #if 0 static int __init plist_test(void) printk(KERN_DEBUG "start plist test\n"); for (loop = 0; loop < 10; loop++) { dump_list(); for (i = 0; i < ARRAY_SIZE(test_node); i++) { printk(KERN_DEBUG "end plist test\n"); |
通过初始化不超过10个node节点,优先级为0-9。然后查看node_list和prio_list两链表的节点情况:
[22050.404475] start plist test |
可以看出node_list上的节点按照优先级由高到低排序,优先级可能会重复;在prio_list上是不同优先级的节点。如下所示:
* pl:prio_list (only for plist_node) |
红黑树
Red-Black trees are used for scheduling, virtual memory management, to track file descriptors and directory entries,etc. |
Linux内核中的算法和数据结构的更多相关文章
- Linux内核中关于内存的数据结构
物理页面 /* * Try to keep the most commonly accessed fields in single cache lines * here (16 bytes or gr ...
- Linux内核中常用的数据结构和算法(转)
知乎链接:https://zhuanlan.zhihu.com/p/58087261 Linux内核代码中广泛使用了数据结构和算法,其中最常用的两个是链表和红黑树. 链表 Linux内核代码大量使用了 ...
- linux内核中的C语言常规算法(前提:你的编译器要支持typeof和type)
学过C语言的伙伴都知道,曾经比较两个数,输出最大或最小的一个,或者是比较三个数,输出最大或者最小的那个,又或是两个数交换,又或是绝对值等等,其实这些算法在linux内核中通通都有实现,以下的代码是我从 ...
- Linux内核中双向链表的经典实现
概要 前面一章"介绍双向链表并给出了C/C++/Java三种实现",本章继续对双向链表进行探讨,介绍的内容是Linux内核中双向链表的经典实现和用法.其中,也会涉及到Linux内核 ...
- Linux内核中流量控制
linux内核中提供了流量控制的相关处理功能,相关代码在net/sched目录下:而应用层上的控制是通过iproute2软件包中的tc来实现, tc和sched的关系就好象iptables和netfi ...
- Linux内核中Makefile、Kconfig和.config的关系(转)
我们在编译Linux内核时,往往在Linux内核的顶层目录会执行一些命令,这里我以RK3288举例,比如:make firefly-rk3288-linux_defconfig.make menuco ...
- (笔记)Linux内核中内存相关的操作函数
linux内核中内存相关的操作函数 1.kmalloc()/kfree() static __always_inline void *kmalloc(size_t size, gfp_t flags) ...
- TCP/IP协议栈在Linux内核中的运行时序分析
网络程序设计调研报告 TCP/IP协议栈在Linux内核中的运行时序分析 姓名:柴浩宇 学号:SA20225105 班级:软设1班 2021年1月 调研要求 在深入理解Linux内核任务调度(中断处理 ...
- Linux 内核中的 Device Mapper 机制
本文结合具体代码对 Linux 内核中的 device mapper 映射机制进行了介绍.Device mapper 是 Linux 2.6 内核中提供的一种从逻辑设备到物理设备的映射框架机制,在该机 ...
随机推荐
- LeetCode之“链表”:Remove Nth Node From End of List
题目链接 题目要求: Given a linked list, remove the nth node from the end of list and return its head. For ex ...
- Swift之GCD使用指南1
Grand Central Dispatch(GCD)是异步执行任务的技术之一.一般将应用程序中记述的线程管理用的代码在系统级中实现.开发者只需要定义想执行的任务并追加到适当的Dispatch Que ...
- iOS多线程篇:NSThread简单介绍和使用
一.什么是NSThread NSThread是基于线程使用,轻量级的多线程编程方法(相对GCD和NSOperation),一个NSThread对象代表一个线程, 需要手动管理线程的生命周期,处理线程同 ...
- 多Region下HBase写入问题
最近在集群上发现hbase写入性能受到较大下降,测试环境下没有该问题产生.而生产环境和测试环境的区别之一是生产环境的region数量远远多于测试环境,单台regionserver服务了约3500个re ...
- rails项目编写中的一些小技巧小心得
1. 如果form中有数据要传回服务器可以用隐藏属性的控件: form_for(xxx) do |f| f.hidden_field :xxx,value:xxx end 2. 如果你需要一些信息放在 ...
- c# 获取TFS结构 文件
#region 获取最新版本 /// <summary> /// 获取最新版本 /// </summary> /// <param name="server_u ...
- spring boot之入门Controller常用注解
Controller常用注解 @Controller 处理http请求 @RestController Spring4之后新加的注解,原来返回json数据需要@ResponseBody配合@Cont ...
- j2EE经典面试题
1. hibernate中离线查询去除重复项怎么加条件? dc.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY); 2. http协议及端口,sm ...
- 8.2 Query 语句优化基本思路和原则
在分析如何优化MySQL Query 之前,我们需要先了解一下Query 语句优化的基本思路和原则.一般来说,Query 语句的优化思路和原则主要提现在以下几个方面: 1. 优化更需要优化的Query ...
- access登录校验代码二
' 这一段是用来检查有没有输入用户名或密码的If IsNull(Trim(Me.username )) ThenDoCmd.BeepMsgBox ("请输入用户名称! ")Else ...