昨天看了些Linux方面的书,书中有讲到Linux内核中List的实现,自己也想看看技术牛人们的list的代码时如何写的。所以今天一大早起来跑步完后,载了份Linux源码来看,里面实现的方式与自己以前的实现有很大的差别。
先看看Linux的List结构体:
struct list_head {
struct list_head *next, *prev;
};
这是一个双向链表,我们应该很熟系了,我们自己会这样写:
struct List_Node {
T value;
struct List_Node *pre;
struct List_Node *next;
};
Linux中这样实现:
struct List_Node { T value; struct list_head head; };
仔细想了想,因为Linux中会大量的使用链表这个数据结构,而每种里面所存储的数据是不一样,所以采取将数据与链表头分开的写法,对于每种不同的数据,只要操作链表头就能将它们串在一起,但是如果只操作list_head的话,怎样才能取得结点中的数据呢?Linux中定义了两个宏,这两个宏也是源码中令我们初学者想不到的方式:
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
//ptr为每个list_head的地址(指向member的指针) type为每个节点的结构体类型(如上面的List_Node) member就是上面的list_head类型
container_of定义在<kernel.h>中,说明这种方式在内核中时经常使用的
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
在看一下offsetof宏的定义,它在<stddef.h>中
#define offsetof(TYPE, MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
通过上面可以看到,将一个ptr即list_head的地址传给container_of时,将其传给_mptr指针(( ((type *)0)->member ) *__mptr = (ptr)),__mptr中保存的值就是type中member对象的地址,然后通过&((TYPE *)0)->MEMBER),现将0转化为TYPE*,然后再取出MEMBER的地址,其就为MEMBER相对与TYPE型节点的偏移地址,用__mptr减去这个偏移地址就是结点真正的地址,然后通过这个指针取出数据。
源码中还提供这样两个宏:
#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)//向前遍历链表
所以我们可以这样子用:
1 struct group
2 {
3 int nNum;
4 struct list_head head;
5 };
6
7 struct group *groups,*p;
..........
8 groups = &testhead;//假设链表中已有数据,且testhead是头
9 struct list_head *pos;
10
11 list_for_each(pos,&groups->head)
12 {
13 p = list_entry(pos,struct group,head);
14
15 printf("%d ",p->nNum);
16 }
17
当然源码中也定义了许多其他操作:
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)//初始化链表节点使其指向本身
static inline void INIT_LIST_HEAD(struct list_head *list)//初始化时指向自己本身
{
list->next = list;
list->prev = list;
}
list_add(struct list_head *new, struct list_head *head);//插入操作
{
__list_add(new, head, head->next);//在head后面插入,源码中还定义了一种在前面插入的办法
}
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;
}
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
prev->next = next;
}
static inline void __list_del_entry(struct list_head *entry)
{
__list_del(entry->prev, entry->next);//del entry结点,不过在源码中没有找到回收entry的函数,有可能回收的工作由包含list_head节点自己实现
}
static inline void list_replace(struct list_head *old,
struct list_head *new);//用new替换old节点
static inline int list_empty(const struct list_head *head)
{
return head->next == head;
}//是否为空
static inline void __list_splice(const struct list_head *list,
struct list_head *prev,
struct list_head *next)
{
struct list_head *first = list->next;//list的第一个节点
struct list_head *last = list->prev;//list的最后一个节点
first->prev = prev;//将第一个节点插到原来节点后面
prev->next = first;
last->next = next;//将原来节点后面节点的prev指向最后一个节点
next->prev = last;
}
static inline void list_splice(const struct list_head *list,
struct list_head *head)//将list链表插入到head处
{
if (!list_empty(list))
__list_splice(list, head, head->next);
}
上面这个拼接工作也是整个链表中,我感觉比较复杂的工作。自己第一次实现链表拼接的时候,在这个实现上调试了很长的时间.......
看完后觉得整个list.h源码与平时自己的实现想法最大的区别,就是对节点的定义上,平常中如果要实现通用链表的时候,我会使用将节点中数据定义为一个void*的指针,指向我们需要用到的数据的办法,实现通用型链表,至于这两种方法的利弊,现在感受不是很深...