作为数据结构中的基础结构之一,链表的优点就是可以不要求连续的存储空间,但是没有了随机访问,在按索引增删改查,似乎效率没有数组快,但是链表也有它优势的地方,linux内核和企业上,对于链表的时候,和我们一般学的方法有所不同,这里详细讲解一下。
废话说到这里,开始上干货。
内核链表
在Linux内核中使用了大量的链表结构来组织数据,包括设备列表以及各种功能模块中的数据组织。这些链表大多采用在[include/linux/list.h]实现的一个相当精彩的链表数据结构。事实上,内核链表就是采用双循环链表机制。
内核链表有别于传统链表就在节点本身不包含数据域,只包含指针域。故而可以很灵活的拓展数据结构。
头节点
#pragma
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<iostream>
using namespace std;
struct list_head {
struct list_head *prev;
struct list_head *next;
};
struct list_head headNode;
//初始化函数
void INIT_LIST_HEAD(struct list_head *node) {
node->next = node;
node->prev = node;
}
首先,我们可以发现,链表节点只有指针域,没有数据。它相当于把头节点,内嵌到数据域中,就好像晾衣服的挂钩一样,下面会进行实现,这里先看一下链表的基础操作,因为是双向循环链表,所以操作和循环链表类似。
基本操作
void __list_add(struct list_head *new_, struct list_head *prev, struct list_head *next)
{
prev->next = new_;
new_->next = next;
next->prev = new_;
new_->prev = prev;
}
void __list_del(struct list_head *prev, struct list_head *next) {
prev->next = next;
next->prev = prev;
}
//头部添加
void list_add(struct list_head *new_, struct list_head *head){
__list_add(new_, head, head->next);
}
//尾部添加
void list_add_tail(struct list_head *new_, struct list_head *head){
__list_add(new_, head->prev, head);
}
//节点删除
void list_del(struct list_head *entry) {
__list_del(entry->prev, entry->next);
}
主要是三个函数,头部添加,尾部添加,和节点删除,这里内核的写法,是尽可能把冗余得到代码都封装成了函数,整体看起来非常简介,很有借鉴意义。
- 循环链表的头部添加和尾部添加,基本是一样的,需要修改插入位置前驱节点的后继指针,后继节点的前驱指针,以及该节点的前后指针。
- 节点删除,只需要修改该节点前驱节点的后继指针和后继节点的前驱指针即可,这里没有对malloc的节点进行free操作,需要我们自己进行。
循环遍历
#define list_for_each(pos, head) \
for(pos = (head)->next; pos != (head); pos = pos->next) //替换语句
struct list_head *pos = NULL;
list_for_each(pos, &headNode) {
userTmp = container_of(pos, struct userInfo, node);
printf("id = %d name = %s \n", userTmp->id, userTmp->name);
}
这里我们采用宏函数,来进行遍历,宏函数的使用是内核链表一个很精妙的地方,这里我们先从最简单的开始。
这里宏函数就会无脑的替换掉成for循环的语句,然后输出结果
数据域
struct userInfo
{
int id;
char name[10];
struct list_head node;
};
数据的定义如上,这里我们简单定义一个整形和一个字符数组。我们可以发现指针域的定义是放在数据下面的,这样的话,我们如果需要把list_head
指针转换成userInfo
的话,就需要在list_head
指针的地址上减去一个数据的偏移量。那么内核链表核心的代码来了,具体如下所示
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) * __mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); })
上述两个宏函数,分别是
- 偏移量 = 结构体成员地址 - 结构体地址(0)= 结构体成员地址
- 结构体地址 = 结构体成员地址 - 偏移量地址
这里我们拆解来看
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
/*
1. ((struct userInfo*)0) 对地址0强制转换成struct userInfo
2. ((struct userInfo*)0)->node 访问结构体中的node成员
3. &((struct userInfo*)0)->node 获取成员node的地址,表示node成员在结构体中的偏移量
4. ((size_t))&((struct userInfo*)0)->node 在32位系统中,size_t表示无符号int, 在64系统中表示无符号long类型
*/
这就相当于将地址0,强转成结构体userInfo
,然后调用它的成员变量的地址,而这个结构体成员变量的地址,就是偏移量,最后在强转成size_t,就得到了最后的偏移量。
#define container_of(ptr, type, member) ({ \
const typeof(((type *)0)->member) * __mptr = (ptr); \
(type *)((char *)__mptr - offsetof(type, member)); })
/*
1. typeof( ((struct userInfo*)0)->node ) 拿到成员的数据类型
2. const typeof( ((struct userInfo*)0)->node ) *__mptr = &(userInfo->node)
: const struct list_head *__mptr = &(userInfo->node) 创建了一个链表的指针域
3. (char*)__mptr - offserof(struct userInfo, node) 需要对地址做加减,所以需要强制转换为char*类型
4. 这样就获取到了结构体的起始地址。
*/
typeof函数,可以拿到成员的数据类型,用于定义__mptr指针,指向结构体成员的地址,而char*的强转,是因为地址上的加减运算,需要强转。最后减去偏移量,就得到了数据域的地址。
不过这里我们需要注意的是,win上C++是没有typeof函数,同时也不支持({ }) 形式 ,这个要修改.且offset宏定义和编译器再带的宏函数是冲突的,修改情况如下:
// 将offsetof改成了offsetof2
#define offsetof2(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
// C++编译器不支持({ }) 形式 ,这个要修改
#define container_of(ptr, type, member) \
(type*)( (char*)(ptr) - offsetof2(type, member))
接下来,我们在写一个添加节点的代码
void addUser(int id,const char *name)
{
struct userInfo *userTmp = (struct userInfo *)malloc(sizeof(struct userInfo));
userTmp->id = id;
memcpy(userTmp->name, name, strlen(name)+1);
list_add(&(userTmp->node), &headNode);
}
如上所示,传入id和name之后,会自动创建数据域结构体,并增长添加指针域节点即可。
访问的话,如下所示
list_for_each(pos, &headNode) {
userTmp = container_of(pos, struct userInfo, node);
printf("id = %d name = %s \n", userTmp->id, userTmp->name);
}
最后,以下是测试代码
int main() {
INIT_LIST_HEAD(&headNode);
struct list_head *pos = NULL;
struct userInfo *userTmp = NULL;
//因为是全局变量,所以这里直接调用addUser
addUser(1, "fang");
addUser(2, "wangyu");
addUser(3, "hongjie");
list_for_each(pos, &headNode) {
userTmp = container_of(pos, struct userInfo, node);
printf("id = %d name = %s \n", userTmp->id, userTmp->name);
}
return 0;
}
企业链表
我们来看一下企业链表的示意图
大家有发现企业链表和内核链表的区别吗,这里因为没有画内核链表的示意图,但是通过结构体我们可以发现,内核链表的指针域是放在下面的,而数据是放在上面的。而对于企业链表则是指针域在上面,数据域在下面,这样有什么好处呢?
相信大家已经知道了,那就是不需要在计算偏移量了。
这里只需要修改两个东西
struct userInfo
{
struct list_head node;
int id;
char name[10];
};
除此之外,还需要强转指针域的指针,转成数据域
userTmp = (userInfo*)pos;
这样你再次测试,就没有问题了。
你还可以尝试一下,将指针域的定义放在数据域的中间,如果你不使用偏移量,而直接强转的话,你会发现不会得到好的结果。