0x00 前言
linux中的双向链表和传统的双向链表不太一样,是注入了抽象封装灵魂的链表,
他不是把将数据结构塞入链表,而是将链表节点塞入数据
之前在windows里就见识过_ETHREAD结构.....的双向链表,但苦于没有源码,所以在linux里好好康一康。
链表的头文件是list.h(include\linux\list.h),我们重此来探究
参考文章:
https://blog.csdn.net/xu_ya_fei/article/details/49744699
https://www.cnblogs.com/wangzahngjun/p/5556448.html
https://wenku.baidu.com/view/5df5736fa0116c175e0e4817.html
https://blog.csdn.net/wanshilun/article/details/79747710
https://www.ibm.com/developerworks/cn/linux/kernel/l-chain/index.html
0x01 传统双向链表
详见此文http://data.biancheng.net/view/8.html
就不在此赘述了。
0x02 基础数据结构
1. 内核链表的定义
struct list_head {
struct list_head *next, *prev;
};
可以看出来,linux内核里并没有数据域,这正是体现抽象思想的表现,把双向链表的共性(前驱指针,后继指针)作为一个基本数据结构。
所以我们就可以如下使用
struct My_List{
void* My_Data;
struct list_head My_List;
};
1) list_head 是一种侵入式链表,数据附加在链表之上,使得 list_head 数据结构是通用的,使用起来就不需要考虑节点数据的类型。
2)list_head结构体可以添加到结构体的任何位置
3)可以为 list_head 命名
4)可以添加多个list_head链表
2.是否有效
#ifdef CONFIG_DEBUG_LIST
extern bool __list_add_valid(struct list_head *new,
struct list_head *prev,
struct list_head *next);
extern bool __list_del_entry_valid(struct list_head *entry);
#else
static inline bool __list_add_valid(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
return true;
}
static inline bool __list_del_entry_valid(struct list_head *entry)
{
return true;
}
#endif
通常在进行插入和删除时会首先判断是否有效
3.WRITE_ONCE && READ_ONCE
在某些情况下CPU对内存中变量读写并不是一次完成的,这可能会出现竞争。而READ_ONCE和WRITE_ONCE实现对变量一次性读取和一次性写入。
详情可以看这篇文章:https://blog.csdn.net/cloudblaze/article/details/51676139
4. offsetof && container_of
在下文 "遍历" 时详细说明。
0x03 基础函数
1. 声明与初始化
上一段描述了链表的链节点,在LIST_HEAD这个宏中描述了头节点
在源码中可以看到俩个宏:
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
第一个:&(name) 前驱指针,后继指针都是指向自己的指针
第二个:调用了第一个的初始化,使用宏 LIST_HEAD_INIT 进行初始化,这会使用变量name 的地址来填充prev和next 结构体的两个变量
两者的使用的不同:第一个宏是初始化,第二个宏是声明+初始化
除了宏以外还有Linux还提供了一个INIT_LIST_HEAD宏用于运行时初始化链表
static inline void
INIT_LIST_HEAD(struct list_head *list)
{
list->next = list->prev = list;
}
2. 插入
在linux内核中一共有俩个插入的函数
static inline void list_add()
static inline void list_add_tail()
list_add在链表头插入,实现栈的功能
list_add_tail在链表尾插入,实现队列的功能
在传统的双向链表中的插入:
前插
int DlinkIns(DoubleList L,int i,ElemType e)
{
DNode *s,*p;
…/*先检查待插入的位置i是否合法(实现方法同单链表的前插操作)*/
…/*若位置i合法,则让指针p指向它*/
s=(DNode *)malloc(sizeof(DNode));
if(s)
{
s->data=e;
s->prior=p->prior;p->prior->next=s;
s->next=p;p->prior=s;
return TRUE;
}
else
teturn fALSE;
}
后插也是一堆,这里略
咱们来看linux内核如何实现:
先设置了一个基础: __list_add
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
if (!__list_add_valid(new, prev, next))
return;
next->prev = new;
new->next = next;
new->prev = prev;
WRITE_ONCE(prev->next, new);
}
而属于前插(栈)还是后插(队列)这由参数来区分:
list_add:
static inline void list_add(struct list_head *new, struct list_head *head)
{
__list_add(new, head, head->next);
}
list_add_tail:
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
__list_add(new, head->prev, head);
}
代码本身没有难度,但是封装的灵魂,让人很惊叹,__list_add作为一个基础操作只管增加链节点,不管插入的位置。
而list_add调用__list_add,并确定位置,插在head和head->next之间,实现栈的操作;
list_add_tail调用__list_add,并确定位置,插在head->prev和head之间,实现队列的操作。
那如何找得将要插入的位置呢?在下文遍历中会详细说明。
3. 删除
同样贯彻封装的思想,先封装删除操作:
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
WRITE_ONCE(prev->next, next);
}
其中的WRITE_ONCE:把 next写入prev->next ,在某些情况下CPU对内存中变量读写并不是一次完成的,这可能会出现竞争。WRITE_ONCE实现对变量一次性写入。
list_del:
static inline void __list_del_entry(struct list_head *entry)
{
if (!__list_del_entry_valid(entry))
return;
__list_del(entry->prev, entry->next);
}
static inline void list_del(struct list_head *entry)
{
__list_del_entry(entry);
entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;
}
注意:其中在使用list_del时,删除操作会把所有要删除的节点(point)的 next 和 prev都指向一个固定的值(LIST_POSITION1和LIST_POSITION2),因为LIST_POSITION1和LIST_POSITION2的宏定义了一个不可访问的地址,所以当使用
point = point->next时会因不可访问而发生页错误。
那为什么不把next 和 prev直接free掉呢?
LIST_POSITION1和LIST_POSITION2指向的是内核中的位置,在用户态给0,这样list.h可以直接移植到用户态来用,笔者这里觉得没有直接释放掉,和便于移植有关。
另外,删除还有另一种情况:删除列表项并清除'prev'指针
static inline void __list_del_clearprev(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
entry->prev = NULL;
}
。这是网络代码中使用的一种特殊用途的列表清除方法,用于按cpu分配的列表,我们不希望产生常规list_del_init()的额外*WRITE ONCE()开销。使用这个*的代码需要检查节点'prev'指针,而不是调用list_empty()。
安全删除
static inline void list_del_init(struct list_head *entry)
{
__list_del_entry(entry);
INIT_LIST_HEAD(entry);
}
static inline void __list_del_entry(struct list_head *entry)
{
if (!__list_del_entry_valid(entry))
return;
__list_del(entry->prev, entry->next);
}
相对于list_del的简单调用,list_del_init,不止调用__list_del,之后还会将它初始化(将entry节点的前继节点和后继节点都指向entry本身)。
4.空 && 第一 && 最后 && 是否单链
这个不多说,看源码,有两种
static inline int list_empty(const struct list_head *head)
{
return READ_ONCE(head->next) == head;
}
上面这种直接检测 后继 是否指向自己 下面这种检查的更多
static inline int list_empty_careful(const struct list_head *head)
{
struct list_head *next = head->next;
return (next == head) && (next == head->prev);
}
不止判断 后继 是否是自己,还判断 前驱 本身和 后继 一不一样。
linux还设置了判断是否是第一个节点的函数:
/**
* list_is_first -- tests whether @list is the first entry in list @head
* @list: the entry to test
* @head: the head of the list
*/
static inline int list_is_first(const struct list_head *list,
const struct list_head *head)
{
return list->prev == head;
}
对应的,判断是否是最后一个节点:
static inline int list_is_last(const struct list_head *list,
const struct list_head *head)
{
return list->next == head;
}
判断是否是单个链:
static inline int list_is_singular(const struct list_head *head)
{
return !list_empty(head) && (head->next == head->prev);
}
5. 移动
把链表A的某节点插入链表B(前插,后插),说白了就是利用了删除与插入:
前插:
static inline void list_move(struct list_head *list, struct list_head *head)
{
__list_del_entry(list);
list_add(list, head);
}
后插:
static inline void list_move_tail(struct list_head *list,
struct list_head *head)
{
__list_del_entry(list);
list_add_tail(list, head);
}
linux也提供了 在一个同一个链表中的移动:把链表的前一部分,放到链表的后部:
static inline void list_bulk_move_tail(struct list_head *head,
struct list_head *first,
struct list_head *last)
{
first->prev->next = last->next;
last->next->prev = first->prev;
head->prev->next = first;
first->prev = head->prev;
last->next = head;
head->prev = last;
}
1)head 参数 指向链表头,
2)first 指向需要移动的多个连续节点的第一个节点,
3)last 指向需要移动的多个 连续节点的最后一个节点。
(环的一部分)
6.替换
list_replace:只把old替换成new
static inline void list_replace(struct list_head *old,
struct list_head *new)
{
new->next = old->next;
new->next->prev = new;
new->prev = old->prev;
new->prev->next = new;
}
list_replace_init:把old替换成new,并初始化old(要被替换的元素)
static inline void list_replace_init(struct list_head *old,
struct list_head *new)
{
list_replace(old, new);
INIT_LIST_HEAD(old);
}
7.交换(两个链表间)
用entry2替换entry1,并在entry2的位置重新添加entry1:
list_swap:
static inline void list_swap(struct list_head *entry1,
struct list_head *entry2)
{
struct list_head *pos = entry2->prev;
list_del(entry2);
list_replace(entry1, entry2);
if (pos == entry1)
pos = entry2;
list_add(entry1, pos);
}
8.反转 (同一个链表内)
不同于上述的交换,反转是同一个链表内,前后节点的交换:
list_rotate_left:(前后相连)
static inline void list_rotate_left(struct list_head *head)
{
struct list_head *first;
if (!list_empty(head)) {
first = head->next;
list_move_tail(first, head);
}
}
list_rotate_to_front:(将链节点提到最前面)
static inline void list_rotate_to_front(struct list_head *list,
struct list_head *head)
{
/*
* Deletes the list head from the list denoted by @head and
* places it as the tail of @list, this effectively rotates the
* list so that @list is at the front.
*/
list_move_tail(head, list);
}
9. 剪切
贯彻抽象思想,暂时不管位置,先实现剪切操作
__list_cut_position:(在entry处剪切,并把entry也剪掉)
static inline void __list_cut_position(struct list_head *list,
struct list_head *head, struct list_head *entry)
{
struct list_head *new_first = entry->next;
list->next = head->next;
list->next->prev = list;
list->prev = entry;
entry->next = list;
head->next = new_first;
new_first->prev = head;
}
设置一个新的链表list,通过head将第一个节点到entry节点(包括它本身),转到新链表list里,再将head 链表重新指向 entry 的下一个节点。
list_cut position:
static inline void list_cut_position(struct list_head *list,
struct list_head *head, struct list_head *entry)
{
if (list_empty(head))
return;
if (list_is_singular(head) &&
(head->next != entry && head != entry))
return;
if (entry == head)
INIT_LIST_HEAD(list);
else
__list_cut_position(list, head, entry);
}
list_cut_position() 函数用于将一个链表切成两个链表。参数 list 指向一个新的链表, 参数 head 指向被拆开的链表 (原始链表),entry 参数指向拆开的位置。函数首先调用 list_empty() 函数确定 head 链表是不是空链表,如果是则直接返回;如果不是空链表, 则调用 list_is_singular() 函数确定 head 链表是不是单一节点的链表,如果是则判断 head 链表的 next 不指向 entry 参数,并且 head 不是 entry,那么直接返回;反之 不是,则判断 entry 与 head 的关系,如果 entry 就是 head,那么代表新链表是空链表, 那么直接调用 INIT_LIST_HEAD() 函数初始化 list 链表;反之调用 __list_cut_position() 将 head 链表拆成两段,第一个节点到 entry 节点的链表使用 list 指定,entry 到最后一个 节点通过 head 指向。
list_cut_before: (在entry处剪切,不把entry剪掉)
static inline void list_cut_before(struct list_head *list,
struct list_head *head,
struct list_head *entry)
{
if (head->next == entry) {
INIT_LIST_HEAD(list);
return;
}
list->next = head->next;
list->next->prev = list;
list->prev = entry->prev;
list->prev->next = list;
head->next = entry;
entry->prev = head;
}
list_cut_before() 函数用于将 head 拆分成两个链表,新链表从 head 的第一个 节点到 entry 的前一个节点,拆分之后的 head 链表的第一个节点变成了 entry 节点。 函数首先检查 head->next 与 entry 之间的关系,如果相等,代表 list 链表是一个 空链表,因此调用 INIT_LIST_HEAD() 初始化 list 链表;如果不相等,就将 entry 之前的链表拆分给 list 链表,head 链表则维护从 entry 之后的链表。
10. 合并
说白了就是把整个链表插入:
基本插入操作(封装的思想)
static inline void __list_splice(const struct list_head *list,
struct list_head *prev,
struct list_head *next)
{
struct list_head *first = list->next;
struct list_head *last = list->prev;
first->prev = prev;
prev->next = first;
last->next = next;
next->prev = last;
}
list_splice:前插
static inline void list_splice(const struct list_head *list,
struct list_head *head)
{
if (!list_empty(list))
__list_splice(list, head, head->next);
}
list_splice_tail:后插
static inline void list_splice_tail(struct list_head *list,
struct list_head *head)
{
if (!list_empty(list))
__list_splice(list, head->prev, head);
}
图解:(一位大佬的图)
当list1被挂接到list2之后,作为原表头指针的list1的next、prev仍然指向原来的节点,为了避免引起混乱,Linux提供了一个list_splice_init()函数
static inline void list_splice_init(struct list_head *list,
struct list_head *head)
{
if (!list_empty(list)) {
__list_splice(list, head, head->next);
INIT_LIST_HEAD(list);
}
}
该函数在将list合并到head链表的基础上,调用INIT_LIST_HEAD(list)将list设置为空链
对应的还有 list_splice_tail_init ,除了变成了后插,没有别的区别。
11. 遍历
对于链表,遍历是很重要的。在上文中,在添加操作时,如何找到将要插入的位置?在这里得到解答,linux中简单的宏实现遍历:
以下是向后遍历
#define list_for_each(pos, head) \
for (pos = (head)->next; pos != (head); pos = pos->next)
当然也可以反向遍历
#define list_for_each_prev(pos, head) \
for (pos = (head)->prev; pos != (head); pos = pos->prev)
pos: 指向list_head的指针。
head :需要遍历的链表的链表头。
那我们可以找到list_head的位置,但怎么找到对应的数据呢?我们需要另一个宏list_entry
list_entry:
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
其中
ptr是指向该数据中list_head成员的指针,也就是存储在链表中的地址值,
type是数据项的类型,
member则是数据项类型定义中list_head成员的变量名
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
其实它的语法很简单,只是一些指针的灵活应用,它分两步:
第一步,首先定义一个临时的数据类型(通过typeof( ((type *)0)->member )获得)与ptr相同的指针变量__mptr,然后用它来保存ptr的值。
第二步,用(char *)__mptr减去member在结构体中的偏移量,得到的值就是整个结构体变量的首地址(整个宏的返回值就是这个首地址)
图解:
ok,那问题来了,这个偏移咋算的,我们跟进offseto().
在include\linux\stddef.h里
那么我们可以假设在0地址分配了一个结构体变量struct TYPE a,然后定义结构体指针变量p并指向a(struct TYPE *p = &a),如此我们就可以通过 &(p->MEMBER) 获得成员MEMBER的地址。由于a的首地址为0x0,所以获取的MEMBER的地址就是在结构体中的偏移,最后在强制转换成(size_t)类型。这样就拿到了list_head真正的偏移。
这样我们一路逆向回去,就可以找到节表的位置(data域开始的位置)。
我们拿代码验证一下:
#include<stdio.h>
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
struct My_Struct {
int data;
char My_list1;
char My_list2;
};
int main()
{
printf("My_list1相对My_Struct的位移:%d",offsetof(struct My_Struct, My_list1));
printf("My_list2相对My_Struct的位移:%d", offsetof(struct My_Struct, My_list2));
return 0;
}
运行结果:
4和5 没问题。
0x04 总结
第一次看内核的源码,感觉好爽,等有时间自己实现一下linux的链表,(写好了再来贴上),有不对的地方望路过的大佬斧正。