1.双链表的演化
- 在C语言中,一个基本的双向链表定义如下:
struct my_list {
void * mydata;
struct my_list *next;
struct my_list *prev;
};
- 图解如下
- prev称为前驱指针,next称为后继指针。这种结构可以从两个方向遍历链表从而提高效率。
- 为什么要对双链表作为重点介绍?对于一个双链表
- 如果减少一个指针域就可以退化成一个单链表
- 如果我们只能对链表的首尾进行插入和删除就可以退化成一个队结构
- 如果我们只能对链表的头进行操作就可以退化成一个栈结构
- 如果前驱和后继表示左右孩子化就可以退化成一个二叉树
2. Linux内核双链表
2.1 定义
- Linux内核对链表的实现方式与众不同,在链表中并不包含数据,其具体的定义如下:
struct list_head {
struct list_head *next, *prev;
};
- 这个链表结构常常被嵌入到其他结构体中,比如:
struct my_list {
void * mydata;
struct list_head list;
};
- 说明:list域隐藏了链表的指针特性以struct list_head为基本对象,可以对链表进行插入、删除、合并以及遍历等各种操作,这些操作位于内核的头文件include/linux/list.h中
2.2 链表的声明和初始化
- 内核代码list.h中定义了两个宏:
#define LIST_HEAD_INIT(name) { &(name), &(name) }/*仅初始化*/
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name) /*声明并初始化*/
2.3 链表中增加一个节点
- 源码存放在include/linux/list.h中
static inline void list_add();
static inline void list_add_tail();
- list_add()和list_add_tail()均调用__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;
}
- 图解如下:
2.4 遍历链表
#define list_for_each(pos, head) \
for (pos = (head)->next; pos != (head); pos = pos->next)
2.5 通过成员偏移获得结构体的首地址
list_entry(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
简化后
list_entry(ptr, type, member) \
((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member)))
-
ptr代表成员member所在的位置
-
type代表这个结构体的类型
-
member是这个结构体的一个成员
-
通过这个宏我们可以获得这个结构体的起始地址
-
如下图
-
list_entry应该这么理解呢,它是如何计算出结构体头的地址的呢?
-
首先要摒弃指针地址加减没有意义的概念。这样我们就可以得到一个公式。
-
结构体首地址的绝对地址
=任意结构体成员的绝对地址
-任意结构体成员的相对地址
-
我们可以很容易的看出(char *)ptr是
绝对地址
,问题就在于相对地址这么求了。 -
可以这么想如果把这个结构体放到
0地址
的地方去算成员的偏移,此时的偏移地址既是绝对地址
又是相对地址
,此时相对地址等于绝对地址,因为相对地址 +0地址
= 绝对地址。 -
所以&((type *)0->member)就是相对地址。