Linux内核的链表
来源:互联网 发布:mac safari httm5 编辑:程序博客网 时间:2024/06/06 15:47
自2.1内核开发版以来,linux就引入了正式的链表实现,采用的是双向循环链表。因此,在开发过程中应该使用这些已有的接口。
1. 定义
链表结构体定义在文件<linux/list.h>中。
struct list_head {
struct list_head *next;
struct list_head *prev;
};
使用时通常需要把它嵌入到你自己的结构体中。
例:
struct my_struct {
struct list_head list;
unsigned long dog;
void * cat;
};
下面给出一些接口:
struct my_struct *p;
初始化:
/*动态初始化*/
p->dog = 0;
p->cat = NULL;
INIT_LIST_HEAD(&p->list);
/*静态初始化*/
struct my_struct mine = {
.list = LIST_HDAD_INIT(mine.list);
.dog = 0;
.cat = NULL;
};
/*直接声明并初始化*/
static LIST_HEAD(fox); /*声明并初始化一个名为fox的静态链表*/
2. 基本操作
给链表增加一个节点:
list_add(struct list_head *new, struct list_head *head);
/*该函数向指定链表的head节点后插入new节点。*/
list_add_tail(struct list_head *new, struct list_head *head);
/*该函数向指定链表的head节点前插入new节点。*/
从链表中删除一个节点:
list_del(struct list_head *entry);
/*该函数仅仅是将entry元素从链表中移走。*/
list_del_init(struct list_head *entry);
/*该函数将entry元素从链表中移走并再次初始化entry。*/
把节点从一个链表移到另一个链表:
list_move(struct list_head *list, struct list_head *head);
/*该函数从一个链表中摘除list项,然后将其加入另一个链表的head节点后。*/
list_move_tail(struct list_head *list, struct list_head *head);
/*同上,只是将list项插入到head项前。*/
检查一个链表是否为空:
list_empty(struct list_head *head);
/*如果指定的链表为空,该函数返回非0值,否则返回0。*/
合并两个链表:
list_splice(struct list_head *list, struct list_head *head);
/*将list指向的链表插入到指定链表中head元表后。*/
list_splice_init(struct list_head *list, struct list_head *head);
/*同上,不同的是由list指向的链表要被重新初始化。*/
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成员相对于结构变量的偏移量。
对于给定一个结构,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节点被释放而造成的断链。
5. hlist
精益求精的Linux链表设计者(因为list.h没有署名,所以很可能就是Linus Torvalds)认为双头(next、prev)的双链表对于HASH表来说"过于浪费",因而另行设计了一套用于HASH表应用的hlist数据结构--单指针表头双循环链表,从上图可以看出,hlist的表头仅有一个指向首节点的指针,而没有指向尾节点的指针,这样在可能是海量的HASH表中存储的表头就能减少一半的空间消耗。
因为表头和节点的数据结构不同,插入操作如果发生在表头和首节点之间,以往的方法就行不通了:表头的first指针必须修改指向新插入的节点,却不能使用类似list_add()这样统一的描述。为此,hlist节点的prev不再是指向前一个节点的指针,而是指向前一个节点(可能是表头)中的next(对于表头则是first)指针(struct list_head **pprev),从而在表头插入的操作可以通过一致的"*(node->pprev)"访问和修改前驱节点的next(或first)指针。
6. rcu
在Linux链表功能接口中还有一系列以"_rcu"结尾的宏,与以上介绍的很多函数一一对应。RCU(Read-Copy Update)是2.5/2.6内核中引入的新技术,它通过延迟写操作来提高同步性能。
我们知道,系统中数据读取操作远多于写操作,而rwlock机制在smp环境下随着处理机增多性能会迅速下降(见参考资料4)。针对这一应用背景,IBM Linux技术中心的Paul E. McKenney提出了"读拷贝更新"的技术,并将其应用于Linux内核中。RCU技术的核心是写操作分为写-更新两步,允许读操作在任何时候无阻访问,当系统有写操作时,更新动作一直延迟到对该数据的所有读操作完成为止。Linux链表中的RCU功能只是Linux RCU的很小一部分,对于RCU的实现分析已超出了本文所及,有兴趣的读者可以自行参阅本文的参考资料;而对RCU链表的使用和基本链表的使用方法基本相同。
- Linux内核的链表
- Linux内核---60.linux内核链表list的使用
- linux内核链表的实现
- linux 内核的链表操作
- linux内核链表的实现
- Linux内核链表的精彩实现
- linux 内核的链表操作
- linux内核的链表机制
- linux 内核的链表操作方法
- linux内核源码里的链表
- linux 内核链表的应用
- Linux内核双向链表的实现
- Linux内核链表的一点随笔
- LINUX内核链表的详细解析
- 16.linux内核链表的实现
- linux内核链表
- linux内核链表
- Linux内核链表
- 网络DNS域名转换成IP地址
- 消息队列(Message Queue)简介及其使用
- 解决[c3p0] A PooledConnection that has already signalled a Connection error is still in use
- “番茄”让时间变成我们的朋友
- 七年IT经验的七个总结
- Linux内核的链表
- test2
- 网站设计
- 关于inet_aton和inet_ntoa(perl)
- 234
- 基于Cookie的攻击和防范学习总结
- Visual C++中的C运行时库浅析
- Oracle EM 中主机身份证明的问题
- Oracle数据库datetime格式处理摘记