在解释完内核中的链表基本知识以后,下面解释链表的重要接口操作:

1. 声明和初始化

实际上Linux只定义了链表节点,并没有专门定义链表头,那么一个链表结构是如何建立起来的呢?让我们来看看LIST_HEAD()这个宏:

#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name)

需要注意的是,Linux 的每个双循环链表都有一个链表头,链表头也是一个节点,只不过它不嵌入到宿主数据结构中,即不能利用链表头定位到对应的宿主结构,但可以由之获得虚拟的宿主结构指针。

LIST_HEAD()宏可以同时完成定义链表头,并初始化这个双循环链表为空。

静态定义一个list_head 类型变量,该变量一定为头节点。name为struct list_head{}类型的一个变量,&(name)为该结构体变量的地址。用name结构体变量的始地址将该结构体变量进行初始化。

[cpp] view plaincopy
  1. #define INIT_LIST_HEAD(ptr) do { \
  2. (ptr)->next = (ptr); (ptr)->prev = (ptr); \
  3. } while (0)

动态初始化一个已经存在的list_head对象,ptr为一个结构体的指针,这样可以初始化堆栈以及全局区定义的list_head对象。ptr使用时候,当用括号,(ptr),避免ptr为表达式时宏扩展带来的异常问题。此宏很少用于动态初始化内嵌的list对象,主要是链表合并或者删除后重新初始化头部。若是在堆中申请了这个链表头,调用INIT_LIST_HEAD()宏初始化链表节点,将next和prev指针都指向其自身,我们就构造了一个空的双循环链表。

在宏的下面有一个内联函数:

[cpp] view plaincopy
  1. static inline void INIT_LIST_HEAD(struct list_head *list)
  2. 29 {
  3. 30     list->next = list;
  4. 31     list->prev = list;
  5. 32 }

这样便可初始化头节点

当我们用LIST_HEAD(nf_sockopts)声明一个名为nf_sockopts的链表头时,它的next、prev指针都初始化为指向自己,这样,我们就有了一个空链表,因为Linux用头指针的next是否指向自己来判断链表是否为空:

static inline int list_empty(const struct list_head *head)
{return head->next == head;
}

2. 插入/删除/合并

a) 插入

在上面的设计下,所有链表(包括添加、删除、移动和拼接等)操作都是针对数据结构list_head进行的。提供给用户的的添加链表的操作有两种:表头添加和表尾添加。注意到,Linux双循环链表中有一个链表头,表头添加是指添加到链表头之后,而表尾添加则是添加到链表头的prev所指链表节点之后。

对链表的插入操作有两种:在表头插入和在表尾插入。Linux为此提供了两个接口:

所有链表(包括添加、删除、移动和拼接等)操作都是针对数据结构list_head进行的。提供给用户的的添加链表的操作有两种:表头添加和表尾添加。注意到,Linux双循环链表中有一个链表头,表头添加是指添加到链表头之后,而表尾添加则是添加到链表头的prev所指链表节点之后。

static inline void list_add(struct list_head *new, struct list_head *head);
static inline void list_add_tail(struct list_head *new, struct list_head *head);

因为Linux链表是循环表,且表头的next、prev分别指向链表中的第一个和最末一个节点,所以,list_add和list_add_tail的区别并不大,实际上,Linux分别用

__list_add(new, head, head->next);

__list_add(new, head->prev, head);

来实现两个接口,可见,在表头插入是插入在head之后,而在表尾插入是插入在head->prev之后。

[cpp] view plaincopy
  1. static inline void __list_add(struct list_head *new, struct list_head *prev,  struct list_head *next)
  2. {
  3. next->prev = new;
  4. new->next = next;
  5. new->prev = prev;
  6. prev->next = new;
  7. }

普通的在两个非空结点中插入一个结点,注意new、prev、next都不能是空值。

Prev可以等于next,此时在只含头节点的链表中插入新节点。

[cpp] view plaincopy
  1. static inline void list_add(struct list_head *new, struct  list_head *head)
  2. {
  3. __list_add(new, head, head->next);
  4. }

在head和head->next两指针所指向的结点之间插入new所指向的结点。

即:在head指针后面插入new所指向的结点。Head并非一定为头结点。

当现有链表只含有一个头节点时,上述__list_add(new, head, head->next)仍然成立。

[cpp] view plaincopy
  1. static inline void list_add_tail(struct list_head *new, struct list_head *head)
  2. {
  3. __list_add(new, head->prev, head);
  4. }

在结点指针head所指向结点的前面插入new所指向的结点。当head指向头节点时,也相当于在尾结点后面增加一个new所指向的结点。

注意:

head->prev不能为空,即若head为头结点,其head->prev当指向一个数值,一般为指向尾结点,构成循环链表。

上述三个函数实现了添加一个节点的任务,其中__list_add()为底层函数,“__”通常表示该函数是底层函数,供其他模块调用,此处实现了较好的代码复用,list_add和list_add_tail虽然原型一样,但调用底层函数__list_add时传递了不同的参数,从而实现了在head指向节点之前或之后添加新的对象。

b) 删除

static inline void list_del(struct list_head *entry);

当我们需要删除nf_sockopts链表中添加的new_sockopt项时,我们这么操作:

list_del(&new_sockopt.list);

如果要从链表中删除某个链表节点,则可以调用list_del或list_del_init。

需要注意的是,上述操作均仅仅是把节点从双循环链表中拿掉,用户需要自己负责释放该节点对应的数据结构所占用的空间,而这个空间本来就是用户分配的。

[cpp] view plaincopy
  1. static inline void __list_del(struct list_head * prev, struct list_head * next)
  2. {
  3. next->prev = prev;
  4. prev->next = next;
  5. }

在prev和next指针所指向的结点之间,两者互相所指。在后面会看到:prev为待删除的结点的前面一个结点,next为待删除的结点的后面一个结点。(看样子想要删除某一个节点,需要通过应用程序先找到这个节点,然后调用这个函数进行删除!)

[cpp] view plaincopy
  1. static inline void list_del(struct list_head *entry)
  2. {
  3. __list_del(entry->prev, entry->next);
  4. entry->next = LIST_POISON1;
  5. entry->prev = LIST_POISON2;
  6. }

删除entry所指的结点,同时将entry所指向的结点指针域封死。

对LIST_POISON1,LIST_POISON2的解释说明:

Linux 内核中解释:These are non-NULL pointers that will result in page faults under normal circumstances, used to verify that nobody uses  non-initialized list entries.

#define LIST_POISON1  ((void *) 0x00100100)

#define LIST_POISON2  ((void *) 0x00200200)

常规思想是:entry->next = NULL; entry->prev = NULL; 保证不可通过该节点进行访问。

[cpp] view plaincopy
  1. static inline void list_del_init(struct list_head *entry)
  2. {
  3. __list_del(entry->prev, entry->next);
  4. INIT_LIST_HEAD(entry);
  5. }

删除entry所指向的结点,同时调用LIST_INIT_HEAD()把被删除节点为作为链表头构建一个新的空双循环链表。

c) 搬移

Linux提供了将原本属于一个链表的节点移动到另一个链表的操作,并根据插入到新链表的位置分为两类:

static inline void list_move(struct list_head *list, struct list_head *head);
static inline void list_move_tail(struct list_head *list, struct list_head *head);

[cpp] view plaincopy
  1. static inline void list_move(struct list_head *list, struct list_head *head)
  2. {
  3. __list_del(list->prev, list->next);
  4. list_add(list, head);
  5. }

将list结点前后两个结点互相指向彼此,删除list指针所指向的结点,再将此结点插入head,和head->next两个指针所指向的结点之间。

即:将list所指向的结点移动到head所指向的结点的后面。

[cpp] view plaincopy
  1. tatic inline void list_move_tail(struct list_head *list,    struct list_head *head)
  2. {
  3. __list_del(list->prev, list->next);
  4. list_add_tail(list, head);
  5. }

删除了list所指向的结点,将其插入到head所指向的结点的前面,如果head->prev指向链表的尾结点的话,就是将list所指向的结点插入到链表的结尾。

d) 合并

除了针对节点的插入、删除操作,Linux链表还提供了整个链表的插入功能:

static inline void list_splice(struct list_head *list, struct list_head *head);

static inline void list_splice_init(struct list_head *list,struct list_head *head)

[cpp] view plaincopy
  1. static inline void __list_splice(struct list_head *list,      struct list_head *head)
  2. {
  3. struct list_head *first = list->next;
  4. struct list_head *last  = list->prev;
  5. struct list_head *at    = head->next;
  6. first->prev = head;
  7. head->next = first;
  8. last->next = at;
  9. at->prev = last;
  10. }

将一个非空链表插入到另外一个链表中。不作链表是否为空的检查,由调用者默认保证。因为每个链表只有一个头节点,将空链表插入到另外一个链表中是没有意义的。但被插入的链表可以是空的。

[cpp] view plaincopy
  1. --------------------list_splice()----------------
  2. /**
  3. * list_splice - join two lists
  4. * @list: 被合并的链表的头节点.
  5. * @head: the place to add it in the first list.
  6. */
  7. static inline void list_splice(struct list_head *list, struct list_head *head)
  8. {
  9. if (!list_empty(list))
  10. __list_splice(list, head);
  11. }

这种情况会丢弃list所指向的头结点,这是特意设计的,因为两个链表有两个头结点,要去掉一个头结点。只要list非空链,head无任何限制,该程序都可以实现链表合并。

[cpp] view plaincopy
  1. --------------------list_splice_init()-----------------------------------
  2. /**
  3. * list_splice_init - join two lists and reinitialise the emptied list.
  4. * @list: the new list to add.
  5. * @head: the place to add it in the first list.
  6. *
  7. * The list at @list is reinitialised
  8. */
  9. static inline void list_splice_init(struct list_head *list,
  10. struct list_head *head)
  11. {
  12. if (!list_empty(list)) 0
  13. {
  14. __list_splice(list, head);
  15. INIT_LIST_HEAD(list);
  16. }
  17. }
  18. 将一个链表的有效信息合并到另外一个链表后,重新初始化空的链表头。

假设当前有两个链表,表头分别是list1和list2(都是struct list_head变量),当调用list_splice(&list1,&list2)时,只要list1非空,list1链表的内容将被挂接在list2链表上,位于list2和list2.next(原list2表的第一个节点)之间。新list2链表将以原list1表的第一个节点为首节点,而尾节点不变。如图(虚箭头为next指针):

图4 链表合并list_splice(&list1,&list2)

当list1被挂接到list2之后,作为原表头指针的list1的next、prev仍然指向原来的节点,为了避免引起混乱,Linux提供了一个list_splice_init()函数:

static inline void list_splice_init(struct list_head *list, struct list_head *head);

该函数在将list合并到head链表的基础上,调用INIT_LIST_HEAD(list)将list设置为空链。

e)链表判空

由list-head构成的双向循环链表中,通常有一个头节点,其不含有有效信息,初始化时prev和next都指向自身。判空操作是判断除了头节点外是否有其他节点。

[cpp] view plaincopy
  1. static inline int list_empty(const struct list_head *head)
  2. {
  3. return head->next == head;
  4. }

测试链表是否为空,如果是只有一个结点,head,head->next,head->prev都指向同一个结点,则这里会返回1,表示空;但这个空不是没有任何结点,而是只有一个头结点,因为头节点只是纯粹的list节点,没有有效信息,故认为为空。

[cpp] view plaincopy
  1. static inline int list_empty_careful(const struct list_head *head)
  2. {
  3. struct list_head *next = head->next;
  4. return (next == head) && (next == head->prev);
  5. }

1.只有一个头结点head,这时head指向这个头结点,head->next,head->prev指向head,即:head==head->next==head->prev,这时候list_empty_careful()函数返回1。

2.有两个结点,head指向头结点,head->next,head->prev均指向后面那个结点,即:head->next==head->prev,而head!=head->next,head!=head->prev.所以函数将返回0

3.有三个及三个以上的结点,这是一般的情况,自己容易分析了。

注意:这里empty list是指只有一个空的头结点,而不是毫无任何结点。并且该头结点必须其head->next==head->prev==head

3. 遍历

遍历是链表最经常的操作之一,为了方便核心应用遍历链表,Linux链表将遍历操作抽象成几个宏。在介绍遍历宏之前,我们先看看如何从链表中访问到我们真正需要的数据项。

a) 由链表节点到数据项变量

我们知道,Linux链表中仅保存了数据项结构中list_head成员变量的地址,那么我们如何通过这个list_head成员访问到作为它的所有者的节点数据呢?Linux为此提供了一个list_entry(ptr,type,member)宏,其中ptr是指向该数据中list_head成员的指针,也就是存储在链表中的地址值,type是数据项的类型,member则是数据项类型定义中list_head成员的变量名,例如,我们要访问nf_sockopts链表中首个nf_sockopt_ops变量,则如此调用:

list_entry(nf_sockopts->next, struct nf_sockopt_ops, list);

这里"list"正是nf_sockopt_ops结构中定义的用于链表操作的节点成员变量名。

list_entry的使用相当简单,相比之下,它的实现则有一些难懂:

#define list_entry(ptr, type, member) container_of(ptr, type, member)
container_of宏定义在[include/linux/kernel.h]中:
#define container_of(ptr, type, member) ({          \const typeof( ((type *)0)->member ) *__mptr = (ptr);   \(type *)( (char *)__mptr - offsetof(type,member) );})
offsetof宏定义在[include/linux/stddef.h]中:
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)

size_t最终定义为unsigned int(i386)。

这里使用的是一个利用编译器技术的小技巧,即先求得结构成员在与结构中的偏移量,然后根据成员变量的地址反过来得出属主结构变量的地址。

container_of()和offsetof()并不仅用于链表操作,这里最有趣的地方是((type *)0)->member,它将0地址强制"转换"为type结构的指针,再访问到type结构中的member成员。在container_of宏中,它用来给typeof()提供参数(typeof()是gcc的扩展,和sizeof()类似),以获得member成员的数据类型;在offsetof()中,这个member成员的地址实际上就是type数据结构中member成员相对于结构变量的偏移量。

如果这么说还不好理解的话,不妨看看下面这张图:

图5 offsetof()宏的原理

对于给定一个结构,offsetof(type,member)是一个常量,list_entry()正是利用这个不变的偏移量来求得链表数据项的变量地址。

b) 遍历宏

这一部分的核心部分在上篇文章中已经讲解,只是没有说明具体的内容!

在[net/core/netfilter.c]的nf_register_sockopt()函数中有这么一段话:

        ……
struct list_head *i;
……list_for_each(i, &nf_sockopts) {struct nf_sockopt_ops *ops = (struct nf_sockopt_ops *)i;……}……

函数首先定义一个(struct list_head *)指针变量i,然后调用list_for_each(i,&nf_sockopts)进行遍历。在[include/linux/list.h]中,list_for_each()宏是这么定义的:

          #define list_for_each(pos, head) \for (pos = (head)->next, prefetch(pos->next); pos != (head); \pos = pos->next, prefetch(pos->next))

它实际上是一个for循环,利用传入的pos作为循环变量,从表头head开始,逐项向后(next方向)移动pos,直至又回到head(prefetch()可以不考虑,用于预取以提高遍历速度)。

那么在nf_register_sockopt()中实际上就是遍历nf_sockopts链表。为什么能直接将获得的list_head成员变量地址当成struct nf_sockopt_ops数据项变量的地址呢?我们注意到在struct nf_sockopt_ops结构中,list是其中的第一项成员,因此,它的地址也就是结构变量的地址。更规范的获得数据变量地址的用法应该是:

struct nf_sockopt_ops *ops = list_entry(i, struct nf_sockopt_ops, list);

大多数情况下,遍历链表的时候都需要获得链表节点数据项,也就是说list_for_each()和list_entry()总是同时使用。对此Linux给出了一个list_for_each_entry()宏:

#define list_for_each_entry(pos, head, member)      ……

与list_for_each()不同,这里的pos是数据项结构指针类型,而不是(struct list_head *)。nf_register_sockopt()函数可以利用这个宏而设计得更简单:

……
struct nf_sockopt_ops *ops;
list_for_each_entry(ops,&nf_sockopts,list){……
}
……

某些应用需要反向遍历链表,Linux提供了list_for_each_prev()和list_for_each_entry_reverse()来完成这一操作,使用方法和上面介绍的list_for_each()、list_for_each_entry()完全相同。

如果遍历不是从链表头开始,而是从已知的某个节点pos开始,则可以使用list_for_each_entry_continue(pos,head,member)。有时还会出现这种需求,即经过一系列计算后,如果pos有值,则从pos开始遍历,如果没有,则从链表头开始,为此,Linux专门提供了一个list_prepare_entry(pos,head,member)宏,将它的返回值作为list_for_each_entry_continue()的pos参数,就可以满足这一要求。

4. 安全性考虑

在并发执行的环境下,链表操作通常都应该考虑同步安全性问题,为了方便,Linux将这一操作留给应用自己处理。Linux链表自己考虑的安全性主要有两个方面:

a) list_empty()判断

基本的list_empty()仅以头指针的next是否指向自己来判断链表是否为空,Linux链表另行提供了一个list_empty_careful()宏,它同时判断头指针的next和prev,仅当两者都指向自己时才返回真。这主要是为了应付另一个cpu正在处理同一个链表而造成next、prev不一致的情况。但代码注释也承认,这一安全保障能力有限:除非其他cpu的链表操作只有list_del_init(),否则仍然不能保证安全,也就是说,还是需要加锁保护。

b) 遍历时节点删除

前面介绍了用于链表遍历的几个宏,它们都是通过移动pos指针来达到遍历的目的。但如果遍历的操作中包含删除pos指针所指向的节点,pos指针的移动就会被中断,因为list_del(pos)将把pos的next、prev置成LIST_POSITION2和LIST_POSITION1的特殊值。

当然,调用者完全可以自己缓存next指针使遍历操作能够连贯起来,但为了编程的一致性,Linux链表仍然提供了两个对应于基本遍历操作的"_safe"接口:list_for_each_safe(pos, n, head)、list_for_each_entry_safe(pos, n, head, member),它们要求调用者另外提供一个与pos同类型的指针n,在for循环中暂存pos下一个节点的地址,避免因pos节点被释放而造成的断链。

另一种关于0强制转化为指针的解释:

1.(type *)0->member为设计一个type类型的结构体,起始地址为0,编译器将结构体的起始的地址加上此结构体成员变量的偏移得到此结构体成员变量的地址,由于结构体起始地址为0,所以此结构体成员变量的偏移地址就等于其成员变量在结构体内距离结构体开始部分的偏移量。即:&(type *)0->member就是取出其成员变量的偏移地址。而其等于其在结构体内的偏移量:即为:(size_t)(& ((type *)0)->member)经过size_t的强制类型转换后,其数值为结构体内的偏移量。该偏移量这里由offsetof()求出。

2.typeof( ( (type *)0)->member )为取出member成员的变量类型。用其定义__mptr指针.ptr为指向该成员变量的指针。__mptr为member数据类型的常量指针,其指向ptr所指向的变量处。

3.(char *)__mptr转换为字节型指针。(char *)__mptr - offsetof(type,member) )用来求出结构体起始地址(为char *型指针),然后(type *)( (char *)__mptr - offsetof(type,member) )在(type *)作用下进行将字节型的结构体起始指针转换为type *型的结构体起始指针。

这就是从结构体某成员变量指针来求出该结构体的首指针。指针类型从结构体某成员变量类型转换为该结构体类型。

Linux内核分析--内核中的数据结构双向链表续【转】相关推荐

  1. HashMap,ArrayMap,SparseArray 源码角度分析,Android中的数据结构你该如何去选择?

    table = newTab; 可以看到当我们的table数组存储的节点值大于threshold时,会按我们的当前数组大小的两倍生成一个新的数组,并把旧数组上的数据复制到新数组上这就是我们的HashM ...

  2. linux内核调用( )为进程创建虚存区_Linux内核分析-总结篇(九)

    本次内容作为Linux内核的总结内容,主要涉及对Linux系统的总体的一些理解,同时将之前的一些总结贴出来作为大家的一个索引,希望笔者一样的菜鸟有一些帮助和入门的作用.从一个初学者的角度对Linux有 ...

  3. Linux 0.11 内核解析:中断相关(1)asm.s文件中断处理分析

    0 源代码 有两个版本的,一个是带中文注释,Intel格式的:一个是不带注释是AT&T格式的. Linux 0.11 中文注释版 Linux 0.11 源码,基于<Linux内核完全注释 ...

  4. 庖丁解牛Linux内核分析慕课课程

    本课程从理解计算机硬件的核心工作机制(存储程序计算机和函数调用堆栈)和用户态程序如何通过系统调用陷入内核(中断异常)入手,通过上下两个方向双向夹击的策略,并利用实际可运行程序的反汇编代码从实践的角度理 ...

  5. linux内核中kset是什么意思,Linux内核之设备驱动-底层数据结构kobject/kset

    Linux内核之设备驱动-底层数据结构kobject/kset kobject kobject是组成device.driver.bus.class的基本结构.如果把前者看成基类,则后者均为它的派生产物 ...

  6. 分析Linux内核中进程的调度(时间片轮转)-《Linux内核分析》Week2作业

    1.环境的搭建: 这个可以参考孟宁老师的github:mykernel,这里不再进行赘述.主要是就是下载Linux3.9的代码,然后安装孟宁老师编写的patch,最后进行编译. 2.代码的解读 课上的 ...

  7. Linux 内核中的数据结构:双链表,基数树,位图

    Linux 内核中的数据结构 rtoax 2021年3月 1. 双向链表 Linux 内核自己实现了双向链表,可以在 include/linux/list.h 找到定义.我们将会从双向链表数据结构开始 ...

  8. linux内核教学的全套视频,中科大老师全程讲解Linux内核分析视频教程《附加介绍+总结》共23节课...

    中科大老师全程讲解Linux内核分析视频教程<附加介绍+总结>共23节课" F4 u& {+ T) p5 G' W  ]; o% m 2 q: ]. j8 I; q' D ...

  9. linux路由内核实现分析(二)---FIB相关数据结构(1)

    ------------------------------------------------------------------------------------------ 以下是我根据 li ...

最新文章

  1. 【学术相关】什么是核心期刊?国家级期刊、省级期刊、国际级期刊又是啥?...
  2. 为什么Python是数据科学领域最受欢迎的语言
  3. 【Unity3D基础教程】给初学者看的Unity教程(四):通过制作Flappy Bird了解Native 2D中的RigidBody2D和Collider2D...
  4. 5分钟学习基于Go,go-microservice-template,Minke的微服务
  5. 聊聊spring security的permitAll以及webIgnore
  6. The Joy of Clojure – Clojure philosophy(1)
  7. 包概念与__init__注意事项
  8. 十年积累,5.4万GitHub Star一朝清零:开源史上最大意外损失
  9. Python学习笔记——控制语句
  10. centos usb转网口_centOS安装与配置minicom(串口转USB)
  11. hdu 5288 OO’s Sequence(计数)
  12. 初级工程师该如何去学习,如何去研发开关电源?
  13. 武汉大学计算机考研复试考什么,2018武汉大学计算机考研复试经验贴
  14. 安装了防火墙之后还有必要安装杀毒软件吗
  15. 2015年维多利亚的秘密新晋十位天使
  16. OSChina 周三乱弹 ——找女朋友都是双胞胎
  17. ibm虚拟化 用的服务器品牌,浅析IBM i虚拟化技术
  18. 把我本科2年爬过的坑,送给高考完想要选计算机专业的你,成为人们眼中的大神吧
  19. html表格制作步骤详解,HTML网页表单制作详细讲解
  20. 现有小程序平台有哪些?如何让自己的App运行小程序?

热门文章

  1. eclipse 设置PythonIDE
  2. Homebrew安装(MacOS)
  3. 骨干云池服务器SATA盘的RAID配置,标准互联 美国CN2云池服务器即将开售,欢迎新老客户选购 - Pesyun.com 公告与通知...
  4. MyBatis框架学习 DAY_03:如何解决无法封装问题 / 一对一关联查询 / 一对多关联查询
  5. XamarinSQLite教程下载安装SQLite/SQL Server Compact Toolbox
  6. 伪造服务钓鱼工具Ghost Phisher
  7. 群英服务器网站,群英:域名、DNS及URL功能说明
  8. 高响应比优先算法代码_以梦为码丨让每一行代码都充满温情
  9. vue、cnpm不是内部文件_vue文件通过cnpm install后无法用npm run serve打开
  10. vue树形结构html,怎么在vue中利用递归组件实现一个树形控件