linux kernel list,linux内核的链表结构,它使用了最简洁的方式实现了一个几乎是万能的链表。
说起链表,一共有3种:单向链表、双向链表、循环链表。
单向链表是链表中最简单的一种形式,它只有一个后继指针(一般代码中是next)来维护结构。所以,单向链表只能沿着后继指针做向后操作,不能向前操作。单向链表也不能从中间插入或者删除一个元素,因为没有前向指针,所以无法获取前一个元素,从中间删除链表元素会引起断链。单向链表示意图如下:
双向链表:双向链表比单向链表复杂一些,它除了有一个后续指针next指向下一个元素之外还有一个前驱指针prev指向前一个元素。所以,双向链表不仅能向后遍历,还能向前遍历。同时,双向链表也能对链表中的任一元素进行操作,并且在任何位置都可以进行增加和删除操作,因为它不会断链。双向链表的示意图如下:
循环链表:循环链表是双向链表的升级版。相比双向链表,循环列表首尾相连。示意图如下:
本节所要讨论的kernel list是一个循环链表结构。我注意并且运用linux kernel list是从2.6内核开始的。linux内核的链表和我们一般定义的链表最大的差别有2个:
-
结构定义。linux内核的链表定义非常简单明了,并且严格遵守了自定义结构不定义数据的规则。在list.h文件中定义如下:
struct list_node { struct list_node *prev; struct list_node *next;};
在linux的kernel中结构体的名称是list_head,只是因为习惯问题,我们团队觉得list_head使用起来不太顺手,而list_node比较习惯,所以我们在移植的时候统一改了一下结构体的名称。list_node的定义相当清晰:前驱指针和后继指针。list_node的定义和我们通常对于链表结构的定义最大的不同是list_node链表结构没有数据域。通常我们要实现双向链表结构会将本身的链表关系结构和实体数据属性合并到一起。比如有一个学生结构体,属性为学号(id),姓名(name),年龄(age)。那么这样的一个链表定义如下:
struct student { int id; char *name; int age; struct student *prev; struct student *next;};
这个结构体正确,并且可用,但是并不通用。
-
linux kernel list使用的时候需要一个表头。因为它的通用化设计和追求更简单的算法实现,list必须要一个简单而通用的方法通过自身来判断链表是否存在数据、链表是否已经遍历到最后等等。所以linux kernel list不能像我们通常的定义一样来区分,而是通过在使用链表之前置一个链表头来实现。链表头是在链表初始化的时候确定的,所以链表可以通过前驱和后继指针都指向自己来确定链表目前的状态是空的。
linux kernel list使用起来也很简单。当我们使用它来表示链表的时候,需要使用数据包含结构的方式而不是我们上面的结构和数据混合方式。比如同样的定义一个student结构,使用linux kernel list的代码如下:
struct student { struct list_node chain; int id; char *name; int age; };
随后,在使用链表操作之前一定要先初始化链表头,再使用linux kernel list提供的API进行增加、删除、遍历等操作。
初始化
linux kernel list提供了3种初始化链表的方式,前面2种其实是一样的,属于创建型,API如下:
#define LIST_HEAD_INIT(name) { &(name), &(name) }#define LIST_HEAD(name) struct list_node name = LIST_HEAD_INIT(name)
第3种是初始化模式,这对于结构体内嵌链表头比较适合,比如我们上面内存池中的slot_chain、larger_chain和cleanup_chain。
static inline void INIT_LIST_HEAD(struct list_node *list) { list->next = list; list->prev = list; }
不管是那个API初始化链表,初始化的时候始终只在做一件事情:就是把head的prev指针和next指针都指向head本身。初始化后的链表结构如下图:
添加链表项
linux kernel list提供了2个添加链表项的API,分别是在链表的头部添加一个链表项和在链表的尾部添加一个列表项。因为linux kernel list定义链表结构的时候只定义了前向和后继两个指针,添加链表项也只是指针指向的变动;又因为从开始设计linux kernel list的时候就设定链表头是肯定存在的,所以也没有了链表是否为空之类的繁琐判断,整个代码显得非常的简洁。linux kernel list提供的代码如下:
/* * 在两个已知连续的实体间插入一个新实体 * 这个方法仅使用在我们知道实体的prev/next指针的内部链表操作 */ static inline void __list_add(struct list_node *new, struct list_node *prev, struct list_node *next) { next->prev = new; new->next = next; new->prev = prev; prev->next = new; }
注意:这是一个私有函数。在linux kernel list定义中,形如"__XXXX()"的函数,也就是双下划线开始的函数,被内定为内部函数,外部不许调用(下同,不在累述)。 在私有函数中,一般只会进行最简单的操作,而对于边界问题、特殊值等检查和校验都会在对外的接口函数中完成。
/** * list_add - 添加一个实体 * @new: 被添加的新实体 * @head: 链表头部,新实体被添加在这个head后面 * * 在指定的链表头部后面添加一个新实体。 * 这有利于实现栈。 */ static inline void list_add(struct list_node *new, struct list_node *head) { __list_add(new, head, head->next); }
这是一个链表的添加函数,将实体添加到链表的head后面,也就是next指针处。比如初始化链表头后的示意图如上图所示,那么添加实体后的整个链表示意图如下所示:
/** * list_add_tail - 添加一个新实体 * @new: 被添加的新实体 * @head: 链表头部,新实体加被添加在这个head之前 * * 在指定的链表头部之前添加一个新实体。 * 这对于实现队列有用。 */ static inline void list_add_tail(struct list_node *new, struct list_node *head) { __list_add(new, head->prev, head); }
这也是一个链表的添加函数,和上面的函数不同的是此函数将实体添加到链表的末尾,也就是head的prev指针指向处。如果在上图的基础上将实体添加到链表的末尾,添加后的链表示意图如下所示:
删除链表项
删除链表项和添加链表项如出一辙,也是简单的指针操作。相比添加链表项,因为双向链表中的项每个都有前向和后继两个指针,所以删除链表项只需要明确单个链表项即可,并不存在从头部删除或者是从尾部删除的区别。所以,linux kernel list的代码如下:
/* * 通过实体的prev/next指针相互指向对方删除实体 * * 这个方法仅使用在我们知道实体的prev/next指针的内部链表操作 */ static inline void __list_del(struct list_node * prev, struct list_node * next) { next->prev = prev; prev->next = next; } static inline void __list_del_entry(struct list_node *entry) { __list_del(entry->prev, entry->next); }
同上,这2个函数是链表删除的真正操作,因为是"__"双下划线开头,故是一个私有函数。
/** * list_del - 从链表中删除一个实体. * @entry: 从链表中欲删除的实体. * Note: 在entry上执行list_del操作后,对entry执行list_empty操作将不返回true。 * 因为entry属于未定义状态。 */ static inline void list_del(struct list_node *entry) { __list_del(entry->prev, entry->next); entry->next = NULL; entry->prev = NULL; }
上面的函数是删除链表项的对外API。如有原始链表如下图:
执行list_del操作,删除实体2后,链表图示如下:
对于这个函数,我们在移植的过程中进行了更改,原本的代码是这样的:
static inline void list_del(struct list_head *entry) { __list_del(entry->prev, entry->next); entry->next = LIST_POISON1; //注意这里的不同 entry->prev = LIST_POISON2; }
我们更改的是将prev和next指针指向了NULL,而linux kernel是指向了LIST_POISON1和LIST_POISON2两个宏。查看一下这两个宏定义:
#define LIST_POISON1 ((void *) 0x00100100 + POISON_POINTER_DELTA) #define LIST_POISON2 ((void *) 0x00200200 + POISON_POINTER_DELTA)
有点莫名其妙的定义,在往下查,POISON_POINTER_DELTA的宏定义如下:
#ifdef CONFIG_ILLEGAL_POINTER_VALUE #define POISON_POINTER_DELTA _AC(CONFIG_ILLEGAL_POINTER_VALUE, UL) #else #define POISON_POINTER_DELTA 0 #endif
还是没看懂。没看懂也不重要,查了一下linux的官方解析,LIST_POISON是一段内核内的无效的地址区,当开发者误用这个地址时,会发出页错误。我们为了简单起见,将其改成了NULL,也无伤大雅。
linux还提供了一个删除链表项实体的API,如下:
/** * list_del_init - 从链表中删除一个实体,并且重新将该实体初始化为另一链表的head. * @entry: 从链表中欲删除的实体. */ static inline void list_del_init(struct list_node *entry) { __list_del_entry(entry); INIT_LIST_HEAD(entry); }
这个API和上面的删除链表项API的不同是:会将被删除的实体初始化为另一个链表的head。同上一样删除实体2,本API会将实体2初始化为head。示意图如下: