一文带你了解。你不知道的Linux内核中的算法和数据结构。

测试方法准备

  • 由于需要在内核中进行代码测试验证,完整编译安装内核比较耗时耗力。准备采用module形式来验证。

Makefile

obj-m:=linked-list.o

KERNELBUILD:=/lib/modules/$(shell uname -r)/build

default:
        make -C ${KERNELBUILD} M=$(shell pwd) modules
clean:
        rm -rf *.o *.cmd *.ko *.mod.c .tmp_versions

linked-list.c

#include <linux/module.h>
#include <linux/init.h>
#include <linux/list.h>

int linked_list_init(void)
{
    printk("%s\n", __func__);
    return 0;
}

void linked_list_exit(void)
{
    printk("%s\n", __func__);
}

module_init(linked_list_init);
module_exit(linked_list_exit);
MODULE_AUTHOR("Arnold Lu");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Linked list test");

安装module

sudo insmod linked-list.ko

查找安装情况

lsmod | grep linked-list

执行log

<4>[621267.946711] linked_list_init
<4>[621397.154534] linked_list_exit

删除module

sudo rmmod linked-list

链表、双向链表、无锁链表

Linked list, doubly linked list, lock-free linked list.
  • 链表是一种常用的组织有序数据的数据结构,它通过指针将一系列数据节点连接成一条数据链,是线性表的一种重要实现方式。相对于数组,链表具有更好的动态性,建立链表时无需预先知道数据总量,可以随机分配空间,可以高效地在链表中的任意位置实时插入或删除数据。链表的开销主要是访问的顺序性和组织链的空间损失。

  • 通常链表数据结构至少应包含两个域:数据域和指针域,数据域用于存储数据,指针域用于建立与下一个节点的联系。按照指针域的组织以及各个节点之间的联系形式,链表又可以分为单链表、双链表、循环链表等多种类型.

  • 通过设计前驱和后继两个指针域,双链表可以从两个方向遍历,这是它区别于单链表的地方。如果打乱前驱、后继的依赖关系,就可以构成"二叉树";如果再让首节点的前驱指向链表尾节点、尾节点的后继指向首节点(如图2中虚线部分),就构成了循环链表;如果设计更多的指针域,就可以构成各种复杂的树状数据结构。

  • 循环链表的特点是尾节点的后继指向首节点。前面已经给出了双循环链表的示意图,它的特点是从任意一个节点出发,沿两个方向的任何一个,都能找到链表中的任意一个数据。如果去掉前驱指针,就是单循环链表。

 资料直通车:Linux内核源码技术学习路线+视频教程内核源码

学习直通车:Linux内核源码内存调优文件系统进程管理设备驱动/网络协议栈

Simple doubly linked list

数据结构:

struct list_head {
    struct list_head *next, *prev;
};

声明和初始化:

  • static inline void INIT_LIST_HEAD(struct list_head *list)

在表头插入和在表尾插入:

  1. static inline void list_add(struct list_head *new, struct list_head *head)

  2. static inline void list_add_tail(struct list_head *entry, struct list_head *head)

  • 删除,被删除的节点prev、next分别被设为LIST_POISON2、LIST_POISON1,当访问此节点时会引起叶故障。保证不在链表中的节点项不可访问。

  1. static inline void list_del(struct list_head *entry)

  2. static inline void list_del_init(struct list_head *entry) 将entry从链表解下来,重新初始化,就可以访问节点。

  • 将节点从一个链表搬移到另一个链表,根据插入表头和表位分两种:

  1. static inline void list_move(struct list_head *list, struct list_head *head)

  2. static inline void list_move_tail(struct list_head *list, struct list_head *head)

用新节点替换纠结点:

  • static inline void list_replace(struct list_head *old, struct list_head *new)

将list插入到head:

  1. static inline void list_splice(const struct list_head *list, struct list_head *head)

  2. static inline void list_splice_tail(struct list_head *list, struct list_head *head)

  3. static inline void list_splice_init(struct list_head *list, struct list_head *head) 将list设为空链表

  4. 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)

遍历宏:

  1. list_entry(ptr, type, member)

  2. list_first_entry(ptr, type, member)

  3. list_last_entry(ptr, type, member)

  4. list_next_entry(pos, member)

  5. list_prev_entry(pos, member)

  6. list_for_each(pos, head)

  7. list_for_each_prev(pos, head) 反向操作

  8. list_for_each_safe(pos, n, head) 安全操作

  9. list_for_each_entry(pos, head, member) 遍历链表是获取链表节点

  10. list_for_each_entry_safe(pos, n, head, member) 安全操作

  11. list_for_each_entry_reverse(pos, head, member) 反向操作

判断链表是否为空:

  • static inline int list_empty(const struct list_head *head)

Doubly linked list with a single pointer list head

  • linux内核里边除了著名的list双向循环链表以外,还有一个重要的数据结构,就是哈希链表。哈希链表也在很多重要的地方有所使用,比如linux内核的dentry,进程查询,文件系统等,可以说,弄明白hlist对于理解linux内核具有重要的意义。

struct hlist_head {
    struct hlist_node *first;
};

struct hlist_node {
    struct hlist_node *next, **pprev;
};
  • 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

  • 数据结构如下:

struct llist_head {
    struct llist_node *first;
};

struct llist_node {
    struct llist_node *next;
};

#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+树

A relatively simple B+Tree implementation. I have written it as a learning exercise to understand how 

B+Trees work. Turned out to be useful as well. ... A tricks was used that is not commonly found in textbooks. The lowest values are to the right, not to the left.

 All used slots within a node are on the left, all unused slots contain NUL values. Most operations 

simply loop once over all slots and terminate on the first NUL.

B树诞生的背景:

在大规模数据存储中,实现索引查询这样一个实际背景下,树节点存储的元素数量是有限的,这样就会导致二叉树结构由于树的深度过大而造成磁盘I/O读写过于频繁,进而导致查询效率低下。 那么如何减少树的深度,一个基本的想法是采用多叉树结构。 因为磁盘的操作费时费资源,那么如何提高效率,即如何避免频繁的读取呢?根据磁盘查找存取的次数往往由树的高度决定,所以只要通过较好的结构降低树的高度。根据平衡二叉树的启发,自然就想到平衡多叉树结构。

几个算法时间复杂度度量:

O(n) 表示某函数值(未列出)是 n 的常数倍;亦即他们增长的速度相当.称 大O,big O (发音 "欧" 英文字母 O ) 同理:O(logN):是 logN 的常数倍;O(nlogn):是 nlogn 的常数倍

优先排序列表

  • 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;
};

相关函数:

  1. PLIST_HEAD(head) 初始化plist表头

  2. PLIST_NODE_INIT(node, __prio) 初始化plist节点

  3. static inline void plist_head_init(struct plist_head *head) 初始化plist表头

  4. static inline void plist_node_init(struct plist_node *node, int prio) 初始化plist节点

添加节点、删除节点:

  1. extern void plist_add(struct plist_node *node, struct plist_head *head); 通过plist_add添加到head的node是按照prio优先级由高到低顺序在node_list上排列。

  2. extern void plist_del(struct plist_node *node, struct plist_head *head);

  3. extern void plist_requeue(struct plist_node *node, struct plist_head *head); 是plist_del的优化版本

遍历plist:

  • plist_for_each(pos, head)

判断head是否为空:

  • static inline int plist_head_empty(const struct plist_head *head)

判断当前node是否在node_list上:

  • static inline int plist_node_empty(const struct plist_node *node)

获取前一、后一节点:

  1. plist_next(pos)

  2. plist_prev(pos)

获取首节点、尾节点:

  1. static inline struct plist_node *plist_first(const struct plist_head *head)

  2. static inline struct plist_node *plist_last(const struct plist_head *head)

下面是对plist进行的一些验证:

static dump_list(void)
{
    struct plist_node *node_pos, *first_node, *last_node;
    int i;

    printk(KERN_DEBUG "%s start\n", __func__);
    printk("node_list: ");
    list_for_each_entry(node_pos, &test_head.node_list, node_list) {
        printk("%d ", node_pos->prio);
    }
    printk("\n");

    first_node = plist_first(&test_head);
    last_node = plist_last(&test_head);
    printk("prio_list: %d ", first_node->prio);
    list_for_each_entry(node_pos, &first_node->prio_list, prio_list) {
        printk("%d ", node_pos->prio);
    }
    printk("\n");

#if 0
    for (i = 0; i < ARRAY_SIZE(test_node); i++) {
        if(!plist_node_empty(test_node+i))
            printk(KERN_DEBUG "(test_node+%d)->prio=%d\n", i, (test_node+i)->prio);
    }
#endif
    printk(KERN_DEBUG "MIN(prio)=%d MAX(prio)=%d\n", first_node->prio, last_node->prio);
    printk(KERN_DEBUG "%s end\n", __func__);
}

static int  __init plist_test(void)
{
    int nr_expect = 0, i, loop;
    unsigned int r = local_clock();

    printk(KERN_DEBUG "start plist test\n");
    plist_head_init(&test_head);
    for (i = 0; i < ARRAY_SIZE(test_node); i++)
        plist_node_init(test_node + i, 0);

    for (loop = 0; loop < 10; loop++) {
        r = r * 193939 % 47629;
        i = r % ARRAY_SIZE(test_node);
        if (plist_node_empty(test_node + i)) {
            r = r * 193939 % 47629;
            test_node[i].prio = r % 10;
            plist_add(test_node + i, &test_head);
            nr_expect++;
        } else {
            plist_del(test_node + i, &test_head);
            nr_expect--;
        }
        plist_test_check(nr_expect);
        if (!plist_node_empty(test_node + i)) {
            plist_test_requeue(test_node + i);
            plist_test_check(nr_expect);
        }
    }

    dump_list();

    for (i = 0; i < ARRAY_SIZE(test_node); i++) {
        if (plist_node_empty(test_node + i))
            continue;
        plist_del(test_node + i, &test_head);
        nr_expect--;
        plist_test_check(nr_expect);
    }

    printk(KERN_DEBUG "end plist test\n");
    return 0;
}
  • 通过初始化不超过10个node节点,优先级为0-9。然后查看node_list和prio_list两链表的节点情况:

[22050.404475] start plist test 

[22050.404481] dump_list start 

[22050.404482] node_list: 0 0 1 1 2 6 8 8 9 9

 [22050.404486] prio_list: 0 1 2 6 8 9 

[22050.404488] MIN(prio)=0 MAX(prio)=9 

[22050.404489] dump_list end 

[22050.404491] end plist test 

[22050.947810] start plist test 

[22050.947816] dump_list start 

[22050.947817] node_list: 0 1 1 2 2 3 3 3 8 8

[22050.947820] prio_list: 0 1 2 3 8 

[22050.947822] MIN(prio)=0 MAX(prio)=8 

[22050.947823] dump_list end 

[22050.947825] end plist test 

[22051.491245] start plist test 

[22051.491254] dump_list start 

[22051.491256] node_list: 0 1 2 3 3 3 6 9 9 9 

[22051.491262] prio_list: 0 1 2 3 6 9 

[22051.491266] MIN(prio)=0 MAX(prio)=9 

[22051.491267] dump_list end 

[22051.491271] end plist test

  • 可以看出node_list上的节点按照优先级由高到低排序,优先级可能会重复;在prio_list上是不同优先级的节点。如下所示:

* pl:prio_list (only for plist_node) 

* nl:node_list *HEAD| NODE(S) 

* | * ||------------------------------------| 

* ||->|pl|<->|pl|<--------------->|pl|<-| 

* | |10| |21| |21| |21| |40| (prio) 

* | | | | | | | | | | | 

* | | | | | | | | | | | 

* | ->|nl|<->|nl|<->|nl|<->|nl|<->|nl|<->|nl|<-| 

* |-------------------------------------------------|

红黑树

Red-Black treesareusedfor scheduling, virtual memory management, to track file descriptors and directory entries,etc.

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Linux内核是一种开源的操作系统内核,是Linux操作系统的核心组成部分。它提供了操作系统与硬件之间的抽象层,负责管理系统的资源、调度任务、提供驱动程序等功能。 Linux内核采用分层的架构,包括硬件抽象层、系统调用层、进程管理层、文件系统层和网络层等。硬件抽象层负责将不同硬件设备的接口统一起来,使得上层的软件可以方便地与硬件进行通信。系统调用层提供了一组API供用户进程调用,如文件操作、网络通信等。进程管理层负责进程的创建、销毁以及调度等任务。文件系统层负责文件的管理和存储。网络层负责网络协议的实现和网络通信。 Linux内核的工作原理可以简单概括为以下几个关键步骤。首先,当一台计算机启动时,BIOS会加载内核映像到内存,并执行启动代码。然后,内核初始化各种数据结构、驱动程序和关键服务。接下来,内核创建一个初始的用户空间进程,称为init进程。init进程是所有其他进程的祖先进程。在此之后,内核根据调度算法来决定哪个进程可以使用CPU,并依次执行。同时,内核会提供一个断机制,以便处理硬件事件的优先级。 内核还提供了许多系统调用供用户进程调用,以实现对各种功能的访问。当用户进程需要操作文件、创建进程或进行网络通信时,会通过系统调用将请求传递给内核,由内核代表用户进程执行相应的操作。内核通过调度算法来分配CPU时间片,并通过虚拟内存管理来管理内存资源的分配和回收。 总而言之,Linux内核是一个高度可配置和模块化的操作系统内核,通过分层架构和系统调用机制实现了对硬件的抽象和对用户进程的管理。了解Linux内核架构和工作原理,有助于深入理解Linux操作系统以及开发和调试相关应用程序。 ### 回答2: Linux是一种开源的操作系统内核,其设计目标是为了在不同的计算机硬件平台上提供高效的、稳定的和安全的操作系统服务。 Linux内核架构可以分为三个主要部分:进程管理、内存管理和文件系统管理。 在进程管理方面,Linux内核使用了多任务处理技术,可以同时运行多个进程。每个进程都有独立的地址空间和资源,通过调度算法可以合理分配CPU时间片,优化系统的响应速度和资源利用率。 在内存管理方面,Linux内核使用了虚拟内存技术,将物理内存和逻辑内存进行了映射,使得每个进程都有独立的地址空间。当物理内存不足时,Linux内核会通过页面置换算法将暂时不使用的页写入磁盘交换空间,以释放物理内存供其他进程使用。 在文件系统管理方面,Linux内核支持多种文件系统,包括传统的ext3和ext4文件系统,以及现代的Btrfs和XFS文件系统。它负责文件的读写操作,以及文件的权限控制和磁盘空间的管理。 Linux内核的工作原理可以简单概括为以下几个步骤:首先,启动引导程序将内核加载到内存,并进行初始化。然后,内核分配一部分内存作为内核空间,用于存放内核代码和数据结构。接着,内核根据系统的硬件配置进行设备的初始化和驱动程序的加载。之后,内核根据系统的启动参数和配置文件进行一系列的初始化工作,包括启动系统服务和加载用户程序。最后,内核进入主循环,不断地处理断、调度进程、管理内存和文件系统,以提供稳定的操作系统服务。 总之,Linux内核是一个复杂而高效的软件系统,它通过进程管理、内存管理和文件系统管理等功能,实现了操作系统的基本功能。了解Linux内核架构和工作原理,有助于我们更好地理解和使用这个优秀的开源操作系统。 ### 回答3: Linux内核是一个开放源代码的操作系统内核,由一个核心程序和一组通用的系统工具组成。它是Linux操作系统的核心,负责处理硬件设备、管理系统资源、实现进程管理、文件系统和网络功能等。 Linux内核架构可以分为两个层次:用户空间和内核空间。用户空间包括用户应用程序,如图形界面、终端程序等,它们通过系统调用接口与内核进行通信。内核空间包括内核核心的数据结构和程序,用于管理和控制硬件资源。 Linux内核的工作原理可以概括为以下几个方面: 1. 进程管理:内核负责创建、调度和终止进程。它使用进程描述符(task_struct)来跟踪进程的状态和资源使用情况,并根据调度算法分配CPU时间片给不同的进程。 2. 内存管理:内核负责管理系统的物理内存和虚拟内存。物理内存管理包括内存分配和释放,虚拟内存管理包括页面置换和页面回写等策略,以优化内存的使用效率。 3. 文件系统:内核提供文件系统接口,管理文件和目录的创建、读写和删除等操作。它通过虚拟文件系统层(VFS)将不同的文件系统统一管理,如ext4、NTFS等。 4. 设备驱动:内核提供了访问硬件设备的接口,通过设备驱动程序与硬件交互。不同的硬件设备需要不同的驱动程序,如网卡、显卡、声卡等。 5. 网络功能:内核提供TCP/IP协议栈和网络设备驱动程序,用于实现网络通信功能。它提供网络连接的建立、数据传输和断开等功能,支持各种网络协议,如HTTP、FTP、SSH等。 总的来说,Linux内核是一个非常复杂且功能强大的软件,它负责管理计算机的各种资源和提供操作系统的各种功能。通过深入理解其架构和工作原理,我们可以更好地理解和使用Linux操作系统。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值