阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

Linux内核之数据双链表

252次阅读
没有评论

共计 4402 个字符,预计需要花费 12 分钟才能阅读完成。

导读 Linux 内核中自己实现了双向链表,可以在 include/linux/list.h 找到定义。我们将会首先从双向链表数据结构开始介绍内核里的数据结构。为什么?因为它在内核里使用的很广泛,你只需要在 free-electrons.com 检索一下就知道了。

Linux 内核之数据双链表

首先让我们看一下在 include/linux/types.h 里的主结构体:
struct list_head {struct list_head *next, *prev;};
你可能注意到这和你以前见过的双向链表的实现方法是不同的。

举个例子来说,在 glib 库里是这样实现的:

struct GList {
gpointer data;
GList *next;
GList *prev;
};
通常来说一个链表结构会包含一个指向某个项目的指针。

但是 Linux 内核中的链表实现并没有这样做。所以问题来了:链表在哪里保存数据呢?实际上,内核里实现的链表是侵入式链表(Intrusive list)。侵入式链表并不在节点内保存数据 - 它的节点仅仅包含指向前后节点的指针,以及指向链表节点数据部分的指针——数据就是这样附加在链表上的。这就使得这个数据结构是通用的,使用起来就不需要考虑节点数据的类型了。

比如:
struct nmi_desc {
spinlock_t lock;
struct list_head head;
};
让我们看几个例子来理解一下在内核里是如何使用 list_head 的。

如上所述,在内核里有很多很多不同的地方都用到了链表。我们来看一个在杂项字符驱动里面的使用的例子。在 drivers/char/misc.c 的杂项字符驱动 API 被用来编写处理小型硬件或虚拟设备的小驱动。这些驱动共享相同的主设备号:

#define MISC_MAJOR 10
但是都有各自不同的次设备号。
比如:
ls -l /dev | grep 10
crw------- 1 root root 10, 235 Mar 21 12:01 autofs
drwxr-xr-x 10 root root 200 Mar 21 12:01 cpu
crw------- 1 root root 10, 62 Mar 21 12:01 cpu_dma_latency
crw------- 1 root root 10, 203 Mar 21 12:01 cuse
drwxr-xr-x 2 root root 100 Mar 21 12:01 dri
crw-rw-rw- 1 root root 10, 229 Mar 21 12:01 fuse
crw------- 1 root root 10, 228 Mar 21 12:01 hpet
crw------- 1 root root 10, 183 Mar 21 12:01 hwrng
crw-rw----+ 1 root kvm 10, 232 Mar 21 12:01 kvm
crw-rw---- 1 root disk 10, 237 Mar 21 12:01 loop-control
crw------- 1 root root 10, 227 Mar 21 12:01 mcelog
crw------- 1 root root 10, 59 Mar 21 12:01 memory_bandwidth
crw------- 1 root root 10, 61 Mar 21 12:01 network_latency
crw------- 1 root root 10, 60 Mar 21 12:01 network_throughput
crw-r----- 1 root kmem 10, 144 Mar 21 12:01 nvram
brw-rw---- 1 root disk 1, 10 Mar 21 12:01 ram10
crw--w---- 1 root tty 4, 10 Mar 21 12:01 tty10
crw-rw---- 1 root dialout 4, 74 Mar 21 12:01 ttyS10
crw------- 1 root root 10, 63 Mar 21 12:01 vga_arbiter
crw------- 1 root root 10, 137 Mar 21 12:01 vhci
现在让我们看看它是如何使用链表的。首先看一下结构体 miscdevice:
struct miscdevice
{
int minor;
const char *name;
const struct file_operations *fops;
struct list_head list;
struct device *parent;
struct device *this_device;
const char *nodename;
mode_t mode;
};
可以看到结构体 miscdevice 的第四个变量 list 是所有注册过的设备的链表。

在源代码文件的开始可以看到这个链表的定义:

static LIST_HEAD(misc_list);

它实际上是对用 list_head 类型定义的变量的扩展。

#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
然后使用宏 LIST_HEAD_INIT 进行初始化,

这会使用变量 name 的地址来填充 prev 和 next 结构体的两个变量。

#define LIST_HEAD_INIT(name) {&(name), &(name) }
现在来看看注册杂项设备的函数 misc_register。

它在一开始就用函数 INIT_LIST_HEAD 初始化了 miscdevice->list。

INIT_LIST_HEAD(&misc->list);
作用和宏 LIST_HEAD_INIT 一样。
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}
接下来,在函数 device_create 创建了设备后,

我们就用下面的语句将设备添加到设备链表:

list_add(&misc->list, &misc_list);
内核文件 list.h 提供了向链表添加新项的 API 接口。

我们来看看它的实现:

static inline void list_add(struct list_head *new, struct list_head *head)
{__list_add(new, head, head->next);
}
实际上就是使用 3 个指定的参数来调用了内部函数__list_add:

new – 新项。
head – 新项将会插在 head 的后面
head->next – 插入前,head 后面的项。
__list_add 的实现非常简单:

static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
prev->next = new;
}
这里,我们在 prev 和 next 之间添加了一个新项。

所以我们开始时用宏 LIST_HEAD_INIT 定义的 misc 链表会包含指向 miscdevice->list 的向前指针和向后指针。
这儿还有一个问题:如何得到列表的内容呢?这里有一个特殊的宏:

#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
使用了三个参数:

ptr – 指向结构 list_head 的指针;
type – 结构体类型;
member – 在结构体内类型为 list_head 的变量的名字;

比如:
const struct miscdevice *p = list_entry(v, struct miscdevice, list)

然后我们就可以使用 p ->minor 或者 p->name 来访问 miscdevice。让我们来看看 list_entry 的实现:

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

如我们所见,它仅仅使用相同的参数调用了宏 container_of。初看这个宏挺奇怪的:

#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member ) *__mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type,member) );})
首先你可以注意到花括号内包含两个表达式。

编译器会执行花括号内的全部语句,然后返回最后的表达式的值。

比如:
#include <stdio.h>
int main() {
int i = 0;
printf("i = %d\n", ({++i; ++i;}));
return 0;
}

最终会打印出 2。

下一点就是 typeof, 它也很简单。

就如你从名字所理解的,它仅仅返回了给定变量的类型。当我第一次看到宏 container_of 的实现时,让我觉得最奇怪的就是表达式 ((type *)0) 中的 0。实际上这个指针巧妙的计算了从结构体特定变量的偏移,这里的 0 刚好就是位宽里的零偏移。

比如:
#include <stdio.h>
struct s {
int field1;
char field2;
char field3;
};
int main() {printf("%p\n", &((struct s*)0)->field3);
return 0;
}

结果显示 0x5。

下一个宏 offsetof 会计算从结构体起始地址到某个给定结构字段的偏移。

它的实现和上面类似:
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
现在我们来总结一下宏 container_of。只需给定结构体中 list_head 类型 字段的地址、名字和结构体容器的类型,它就可以返回结构体的起始地址。在宏定义的第一行,声明了一个指向结构体成员变量 ptr 的指针__mptr,并且把 ptr 的地址赋给它。现在 ptr 和__mptr 指向了同一个地址。从技术上讲我们并不需要这一行,但是它可以方便地进行类型检查。第一行保证了特定的结构体(参数 type)包含成员变量 member。第二行代码会用宏 offsetof 计算成员变量相对于结构体起始地址的偏移,然后从结构体的地址减去这个偏移,最后就得到了结构体。

当然了 list_add 和 list_entry 不是 <linux/list.h>

提供的唯一功能。双向链表的实现还提供了如下 API:

list_add
list_add_tail
list_del
list_replace
list_move
list_is_last
list_empty
list_cut_position
list_splice
list_for_each
list_for_each_entry

等等很多其它 API。

阿里云 2 核 2G 服务器 3M 带宽 61 元 1 年,有高配

腾讯云新客低至 82 元 / 年,老客户 99 元 / 年

代金券:在阿里云专用满减优惠券

正文完
星哥玩云-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-07-24发表,共计4402字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中

星哥玩云

星哥玩云
星哥玩云
分享互联网知识
用户数
4
文章数
19351
评论数
4
阅读量
7979402
文章搜索
热门文章
星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛 NAS-6:抖音视频同步工具,视频下载自动下载保存 前言 各位玩 NAS 的朋友好,我是星哥!...
星哥带你玩飞牛NAS-3:安装飞牛NAS后的很有必要的操作

星哥带你玩飞牛NAS-3:安装飞牛NAS后的很有必要的操作

星哥带你玩飞牛 NAS-3:安装飞牛 NAS 后的很有必要的操作 前言 如果你已经有了飞牛 NAS 系统,之前...
我把用了20年的360安全卫士卸载了

我把用了20年的360安全卫士卸载了

我把用了 20 年的 360 安全卫士卸载了 是的,正如标题你看到的。 原因 偷摸安装自家的软件 莫名其妙安装...
再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见 zabbix!轻量级自建服务器监控神器在 Linux 的完整部署指南 在日常运维中,服务器监控是绕不开的...
飞牛NAS中安装Navidrome音乐文件中文标签乱码问题解决、安装FntermX终端

飞牛NAS中安装Navidrome音乐文件中文标签乱码问题解决、安装FntermX终端

飞牛 NAS 中安装 Navidrome 音乐文件中文标签乱码问题解决、安装 FntermX 终端 问题背景 ...
阿里云CDN
阿里云CDN-提高用户访问的响应速度和成功率
随机文章
星哥带你玩飞牛NAS-2:飞牛配置RAID磁盘阵列

星哥带你玩飞牛NAS-2:飞牛配置RAID磁盘阵列

星哥带你玩飞牛 NAS-2:飞牛配置 RAID 磁盘阵列 前言 大家好,我是星哥之前星哥写了《星哥带你玩飞牛 ...
多服务器管理神器 Nexterm 横空出世!NAS/Win/Linux 通吃,SSH/VNC/RDP 一站式搞定

多服务器管理神器 Nexterm 横空出世!NAS/Win/Linux 通吃,SSH/VNC/RDP 一站式搞定

多服务器管理神器 Nexterm 横空出世!NAS/Win/Linux 通吃,SSH/VNC/RDP 一站式搞...
星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛 NAS-6:抖音视频同步工具,视频下载自动下载保存 前言 各位玩 NAS 的朋友好,我是星哥!...
安装Black群晖DSM7.2系统安装教程(在Vmware虚拟机中、实体机均可)!

安装Black群晖DSM7.2系统安装教程(在Vmware虚拟机中、实体机均可)!

安装 Black 群晖 DSM7.2 系统安装教程(在 Vmware 虚拟机中、实体机均可)! 前言 大家好,...
升级自动部署更新SSL证书系统、申请godaddy的APIKEY

升级自动部署更新SSL证书系统、申请godaddy的APIKEY

升级自动部署更新 SSL 证书系统、申请 godaddy 的 APIKEY 公司之前花钱购买的 ssl 证书快...

免费图片视频管理工具让灵感库告别混乱

一言一句话
-「
手气不错
自己手撸一个AI智能体—跟创业大佬对话

自己手撸一个AI智能体—跟创业大佬对话

自己手撸一个 AI 智能体 — 跟创业大佬对话 前言 智能体(Agent)已经成为创业者和技术人绕...
如何安装2026年最强个人助理ClawdBot、完整安装教程

如何安装2026年最强个人助理ClawdBot、完整安装教程

如何安装 2026 年最强个人助理 ClawdBot、完整安装教程 一、前言 学不完,根本学不完!近期,一款名...
12.2K Star 爆火!开源免费的 FileConverter:右键一键搞定音视频 / 图片 / 文档转换,告别多工具切换

12.2K Star 爆火!开源免费的 FileConverter:右键一键搞定音视频 / 图片 / 文档转换,告别多工具切换

12.2K Star 爆火!开源免费的 FileConverter:右键一键搞定音视频 / 图片 / 文档转换...
浏览器自动化工具!开源 AI 浏览器助手让你效率翻倍

浏览器自动化工具!开源 AI 浏览器助手让你效率翻倍

浏览器自动化工具!开源 AI 浏览器助手让你效率翻倍 前言 在 AI 自动化快速发展的当下,浏览器早已不再只是...
把小米云笔记搬回家:飞牛 NAS 一键部署,小米云笔记自动同步到本地

把小米云笔记搬回家:飞牛 NAS 一键部署,小米云笔记自动同步到本地

把小米云笔记搬回家:飞牛 NAS 一键部署,小米云笔记自动同步到本地 大家好,我是星哥,今天教大家在飞牛 NA...