776

阿木币

1

精华

1377 小时

在线时间

管理员

Rank: 9Rank: 9Rank: 9

发表于 2022-3-24 11:00:42 1508 浏览 0 回复

[算法之美] 充电站 | 内核链表的妙用

相信接触过C语言的同学对链表这个数据结构都不陌生,链表作为C语言的一种常用数据结构,它的实现原理大家也已了如指掌,但不知大家是否了解内核中链表是如何实现的。没有对Linux内核有过深入接触的同学可能不会了解内核链表,也不会知道它的实现是有多么的巧妙。下面我就给大家一呈其中妙处。

相比于普通的链表实现方式,Linux内核的实现可以说是独树一帜的。传统的链表要通过数据内部添加一个指向数据的next节点指针,才能串联在链表中。比如,假定一个cat数据结构来描述猫科动物中的一员。

struct cat
{
unsiged long tail_length;/*尾巴长度*/
unsiged long weight;/*重量*/
bool is_fantastic;/*这只猫奇妙吗?*/
}

存储这个结构到链表里的方法通常是在数据结构中嵌入一个链表指针,比如:

struct cat
{
unsiged long tail_length;/*尾巴长度*/
unsiged long weight;/*重量*/
bool is_fantastic;/*这只猫奇妙吗?*/
struct cat *next;/*指向下一只猫*/
struct cat *prev; /*指向上一只猫*/
}

但显然如果Linux内核若采用这种实现方式,因为内核中各个设备的数据类型都不相同,将导致内核无法统一的管理各种设备,并且这样的实现方式也会极大的增加代码的重复性,这显然与聚内核的理念相违背。

故而在内核2.1版本官方引入了内核链表实现,用一个既简单又高效的链表来统一其它链表,其数据结构如下:

/**
 * 双向循环链表的节点结构
 */
struct list_head 
{
struct list_head *next, *prev;
};

可以看到链表中每个节点内都没有数据域,也就是说无论数据结构有多复杂,它在链表中只有前后级指针。而当一个数据结构(即是描述设备的设备结构体)想用通用链表管理,只需在结构体内加入包含节点的字段即可。

struct cat
{
unsiged long tail_length;/*尾巴长度*/
unsiged long weight;/*重量*/
bool is_fantastic;/*这只猫奇妙吗?*/
struct list_head list;/*所以cat结构体形参链表*/
}

并且双向链表可以从任意一个节点的前后遍历整个链表,遍历非常方便。使用循环链表使得可以不断地循环遍历管理节点,像进程的调度:操作系统会把就绪的进程放在一个管理进程的就绪队列的通用链表中管理起来,循环不断地,为他们分配时间片,获得cpu进行周而复始的进程调度。

现在链表已经能用了但显然不够方便。因此内核又提供了一组链表操作例程,如list_add()方法加入一个新节点到链表,而这些方法有个统一的特点:它们只接受list_head结构作为参数。

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

通过实现这种只有节点的链表,便可以统一的管理内核中设备结构体。但是还需要解决的问题便是如何通过链表节点得到设备结构体的首地址,而这才是内核链表实现最巧妙的地方。

#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})

内核链表获得设备结构体的首地址是通过container_of这个宏,我们来具体分析一下这个宏的实现,首先是它的三个参数ptr是成员变量的指针,type是结构体的类型,member是成员变量的名字。container _of宏的作用是通过结构体内某个成员变量的地址和该变量名,以及结构体类型。找到该结构体变量的地址。这里使用的是一个利用编译器技术的小技巧,即先求得结构成员在结构中的偏移量,然后根据成员变量的地址反过来得出主结构变量的地址。下面具体分析各个部分:

Typeof:

是用于返回一个变量的类型,这是GCC编译器的一个拓展功能,即typeof是编译器相关的,不是c语言中的某个标准。

(((type*)0)->member):

((type*)0)将0地址转换为type类型的结构体指针,也就是让编译器认为这个结构体是开始于程序段起始位置0,开始于0地址的话,我们得到的成员变量地址就直接相当于成员变量的偏移地址了。

(((type*)0)->member)引用结构体中member成员

const typeof( ((type *)0)->member ) *__mptr = (ptr);

这里的意思是用typeof()获取结构体内member成员属性的类型,然后定义一个该类型的临时指针变量_mptr,将ptr所指向的member地址赋给_mptr,作用是防止对ptr和ptr指向的内容造成破坏。

其中用到的offsetof(type,member)

#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

这个宏的意思就是求出member相对于0地址的偏移量。

(type *)( (char *)__mptr - offsetof(type,member) );})

这句话的意思是,把_mptr转换城char*类型,因为offsetof得到的偏移量是以字节为单位的。两者相减得到结构体的起始位置,再强转为type类型。自此我们便能得到设备结构体的首地址。

使用container_of宏,我们定义一个简单的函数便可返回包含list_head的父类型结构体。依靠list_entry方法,内核提供了创建,操作以及其他链表管理方法,你甚至不需要知道list_head所嵌入的对象数据结构。

可以看到Linux内核链表的实现不可谓不巧妙,通过对内存地址的通透理解,来实现通用的链表结构,才能更方便统一的管理内核中繁杂的设备。而内核中还有许多其它的巧妙实现思路,多多阅读并学习大牛的实现思路和巧妙方法,是能够显著提升我们日常开发效率的,同时也是对思维的一种提升。


扫一扫浏览分享
回复

使用道具 举报

返回列表
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表