上一篇在分析Linux虚拟文件系统的过程中,我注意到了一个反复出现的数据结构——struct list_head,通过名称就可以确定这是一种链表数据结构,今天我们就以此为切入点,对内核数据结构的特点做一个简单分析。
既然是链表,就应该至少包括两个方面的内容,一个是链表节点中所包含的数据;另一个就是指向下一个节点的指针,同时指针并不唯一,双向链表就同时具有指向前一个节点的指针与指向后一个节点的指针。2.1版本之后的内核,在实现链表时并没有采用这一方法,而是采用一种更加通用性更强,使用也更加简便的实现方法。
具体定义如下,位于/include/linux/types.h,这一文件被包含于/include/linux/list.h中。
struct list_head {
struct list_head *next, *prev;
};
通过定义可以发现,链表类型中只有两个数据成员,分别是前一个元素的指针与指向后一个元素的指针。在使用时将这一类型的数据结构放入需要使用链表的数据结构中,而后将next、prev指向正确的元素,通过这一简单的步骤,就把自定义的数据结构组织成为一个链表。
仅定义了链表还不够,还需要配合相应的操作。首先是初始化,在list.h文件中定义了三种初始化方法,其一是某个节点的运行时初始化。
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}
通过对源码进行分析可以发现,INIT_LIST_HEAD就是将list的next、prev指针指向list自身。
其二是链表头的初始化。
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
又涉及到了一个宏,这也就是我们的第三点,链表节点的编译期静态创建。
#define LIST_HEAD_INIT(name) { &(name), &(name) }
要把这两个宏展开来看:
#define LIST_HEAD(name) \
struct list_head name = { &(name), &(name) }
这一句的作用还是一样的,就是把name的prev、next指针分别指向自身,通过这一方法就定义一个单独的链表节点。
看过了初始化,再来看看添加操作,实质很简单,就是一系列赋值操作,由调用者给出正确的参数。
static inline void list_add(struct list_head *new, struct list_head *head)
{
__list_add(new, head, head->next);
}
在prev与next之间插入new
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;
}
#else
extern void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next);
将结点插入链表尾部:
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
__list_add(new, head->prev, head);
}
其实就是在头节点与尾节点之间插入new节点。
删除节点:
被删除节点位于prev与next之间。删除函数并不真正的释放空间,只是将节点从链表中移除
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
prev->next = next;
}
//该函数与下一个函数的功能相同,但是并没有设置LIST_POISON1与LIST_POISON2
static inline void __list_del_entry(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
}
调用该函数删除某个节点时,将要删除的节点传入,通过prev与next指针,函数可以定位位于entry之前与之后的节点
static inline void list_del(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
entry->next = LIST_POISON1;
entry->prev = LIST_POISON2;
}
extern void __list_del_entry(struct list_head *entry);
extern void list_del(struct list_head *entry);
#define LIST_POISON1 ((void *) 0x00100100 + POISON_POINTER_DELTA) //位于/include/linux/poison.h
#define LIST_POISON2 ((void *) 0x00200200 + 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
以下内容引用自这篇blog:
https://www.ibm.com/developerworks/cn/linux/kernel/l-chain/
prev、next指针分别被设为LIST_POSITION2和LIST_POSITION1两个特殊值,这样设置是为了保证不在链表中的节点项不可访问--对LIST_POSITION1和LIST_POSITION2的访问都将引起页故障。
原因为何,我现在还不清楚?此处存下一个疑问吧。
从链表中删除一个节点并对其重新初始化,将被删除节点置为空链状态:
static inline void list_del_init(struct list_head *entry)
{
__list_del_entry(entry);
INIT_LIST_HEAD(entry);
}
替换操作:以一个新节点替换一个旧节点。
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;
}
替换节点后并将old节点置为空链表
static inline void list_replace_init(struct list_head *old,
struct list_head *new)
{
list_replace(old, new);
INIT_LIST_HEAD(old);
}
搬移操作:将原本属于一个链表的节点移动到另一个链表的操作。
先将list节点从链表中删除,并将list插入到head后
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);
}
判断是否为尾节点:
此处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_empty(const struct list_head *head)
{
return head->next == head;
}
接下来的一个功能比较有意思,直接翻译自它的描述:“list_empty_careful的功能是判断某个链表是否为空链表,同时检查是否有cpu在修改头节点的prev或next指针”
static inline int list_empty_careful(const struct list_head *head)
{
struct list_head *next = head->next;
return (next == head) && (next == head->prev);
}
关于这个函数还有一小段注释,不过我并没有看懂。
内核数据结构的关注度好低啊。接下来一个函数的功能是将当前节点后的一个节点插入到当前节点之前。其实质就是交换两个节点的位置。
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);
}
}
判断当前节点是否为最后一个节点。
static inline int list_is_last(const struct list_head *list,
const struct list_head *head)
{
return list->next == head;
}
判断一个链表是否为空。
static inline int list_empty(const struct list_head *head)
{
return head->next == head;
}
以及很多函数,由于篇幅的关系,就把我认为几个重要的再给大家简单分析一下。
获取链表指针所在结构的类型:
#define list_entry(ptr, type, member) \
container_of(ptr, type, member) //定义位于/include/linux/kernel.h
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER) //定义位于/include/linux/stddef.h
一点一点的看,先来看container_of的三个参数,ptr是指向结构体中member的指针,type是结构体的类型,member是结构体中成员的名称。
了解了上述三个参数的含义,我们就可以逐步分析该宏定义的功能。
const typeof( ((type *)0)->member ) *__mptr = (ptr);
首先将0地址转化为type类型(此处要注意的一点是这种方法不能直接用于用户应用程序,因为用于程序无法访问0地址,该方法只能用于内核程序,因为内核可以访问最低的1G地址空间),通过typeof关键字获得变量类型,此处就是获得member的类型,进而声明一个member类型的临时变量指向ptr,此时mptr指向了struct中member所在的位置。
offsetof宏的功能与之类似,获得member在struct中的偏移量。
最后通过mptr与offsetof宏的结果相减,就可以得到struct结构的首地址,再通过(type *)强制转换为struct类型。
通过上述步骤就可以得到struct结构的地址。