背景
最近在抽空看《linux 内核设计与实现》这本经典之作。看到了内核的数据结构一章,发现内核链表的实现确实很优秀。(因为我平时写应用代码也会涉及到链表的操作,相比较而言,就显得我很low,没有对比就没有伤害)。今天就总结一下自己的启发。
工作中的链表使用
我相信做linux 应用开发的工程师,工作中肯定会因为业务需求接触过链表的实现。无论你的链表是用来保存什么数据。它总归会涉及到以下几个接口:
链表的插入:xxx_insert
链表的删除:xxx_delete
链表的遍历:xxx_traverse
因为也就是不同类型的链表,就需要定义不一样的接口,虽然接口内部的逻辑一样,但是形参不一样,还是需要重新定义的。这种的编码方式,我想大部分写应用的工程师都是这样的逻辑。(大牛除外,毕竟山外有山)
但是这样的设计模式,我们就会发现一点不简洁,并且当你链表的类型很多时,就会出现大量的重复代码,十分不简洁。通过内核的链表实现方式,我才受到启发。不得不佩服写内核的大佬们的厉害之处。
内核的链表使用
在内核里面其实也是需要使用到大量的不同类型的链表。但是如果按照我们上面的表述,那么就会有很多的链表操作接口,用来针对不同类型的链表。这样的情况肯定是不会出现在内核里面的(大佬们不允许啊)。于是他们就设计出了一个统一的链表操作接口。任何类型的链表都可以使用。接下来我们就一起来看看是内核是如何实现的。(其中有用到C语言的技巧)
内核里面操作链表的接口,入参都是list_head
类型:
struct list_head {
struct list_head *next;
struct list_head *prev;
};
比如我们现在有一个fox数据结构,其内容如下:
struct fox {
int tail_length; /*尾巴的长度*/
int weight; /*重量*/
}
在我们应用里面,如果使用该数据结构的链表,一般会如下定义:
struct fox {
int tail_length; /*尾巴的长度*/
int weight; /*重量*/
struct fox * next;
struct fox * prev;
}
之后,在根据这个类型定义不同的链表操作接口。
但是内核就会这样定义数据结构。如下:
struct fox {
int tail_length; /*尾巴的长度*/
int weight; /*重量*/
struct list_head list; /*所有fox结构体形成一个链表*/
}
从上面数据结构定义的不同,我们可知,内核它不是将数据结构塞入链表,而是将链表节点塞入数据结构。
内核中提供了很多对链表操作的接口,比如:
1. 项链表中增加一个节点
list_add(struct list_head *new,struct list_head *head)
默认是将新节点插入到head节点的后面。需要注意的是,内核中的链表都是双向循环链表,因此任何一个节点都可以作为head头节点,进行遍历的开始
list_add_tail(struct list_head *new ,struct list_head head)
把节点增加到链表的尾部。即将节点插入到head的前面
2.从链表中删除节点
list_del(struct list_head* entry)
3.遍历链表
list_for_each(p,list)
这个其实是一个宏定义:
#define list_for_each(pos, head) \
for (pos = (head)->next; pos != (head); pos = pos->next)
内核提供的操作接口有很多,我在这里就不一一说明了。
到了这里,我想大家应该对内核实现链表操作的方式都有了一定了解。觉得这种方式很帅气。但是我们使用链表时,都是想要使用节点里面的有效数据。但是内核的这种链表实现方式,怎么来访问有效数据呢?刚开始,我也有这样的疑惑。通过自己的探索,发现另一个厉害之处。内核找到链表的节点之后,一般都会使用到list_entry
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
#define container_of(ptr, type, member) \
(type *)((char *)(ptr) - (char *) &((type *)0)->member)
乍看一下,可能看不懂,如果你仔细分析的话,就会发现他的强大之处:
假设我的们节点类型如下:
struct fox {
int tail_length; /*尾巴的长度*/
int weight; /*重量*/
struct list_head list; /*所有fox结构体形成一个链表*/
}
并且通过链表操作接口找到了一个list_head节点。调用list_entry接口
list_entry(&list,struct fox,list);
其实它返回的时list_head节点所在数据结构的首地址。即,
fox *p = NULL;
p = list_entry(&list,struct fox,list);
之后就可以通过p之中访问fox数据结构中的任意成员了。
这样的方式和数据结构里面有多少成员没有关系,因此她才会有通用性。
通过了解到内核的这种实现方式,我建议以后在应用层,我们也应该这样做,提供一个通用的接口,这样可以减少我们的开发量,对代码的维护也会变得方便
若我的内容对您有所帮助,还请关注我的公众号。不定期分享干活,剖析案例,也可以一起讨论分享。
我的宗旨:
踩完您工作中的所有坑并分享给您,让你的工作无bug,人生尽是坦途