写C++程序的都知道,标准库STL给我们提供了大量容器供我们使用,如list,set, list能提供高效的插入删除操作,而set能提供高效的查询操作,但是当我们需要一个既能保证搞笑查询又能高效插入删除的时候该如何选择呢,显然,单纯的使用list或set是无法满足我们的需求的。基于近期一个项目的业务特点,选用了list.h这个数据结构,这里对list.h做一点介绍,也是自己的一个总结。
基于linux内核版本2.6.9的list.h。(网上看到有说2.6和2.4内核存在一些差异, 我自己还看到2.6.9的list.h跟2.6.18的list.h的实现也有一些差异,2.6.18里边提供了hlist以及read copy update功能)
list_head结构定义在 里,它是一个double linked list的结构。 底下是它的结构定义:
struct list_head { struct list_head *next, *prev; };
相信CS专业的同学都知道,数据结构课上老师交给我们定义双链表应该是这样的:
struct ListNode { ListNode *prev; ListNode *next; int data; };
这里我们就有疑问了,上边list_head这样的结构可以存放数据吗? 当然是不行的,因为这个结构根本就不是拿来让人当资料存的,而是给了我们用来组织数据的。而且这样的方式也提供了更好的通用性。 首先, 我们先来看看两个宏定义:
#define LIST_HEAD(name) \ struct list_head name = { &name, &name } #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0)
这两个宏是用来将list_head做初始化的,它的初始化就是将next和prev这两个元素设为跟struct的地址相同。 所以, 如果我们在程序里看到这样的程序, 它的意思就是定义一个list_head结构的变量head,并将prev和next都设成hello的地址。
LIST_HEAD(head)
因此, 如果要检查这个list是否是空的, 只要检查head.next是否等于&hello就可以了。事实上, Linux也提供了一个叫list_empty()的函数来检查list是否为空的。
static __inline__ int list_empty(struct list_head *head) { return head->next == head; }
现在我们来介绍如何加入或删除list_head到上面的head链表里。 Linux提供二个函数来做这些事, 分别是list_add()和lis_del()。 这两个函式的定义都放在list.h里, 而且其代码也都很简单,只是单纯double linked list的串接和删除。 有关于这个结构, 其实最重要的应该是它提供的这个宏。
#define list_entry(ptr, type, member) \ ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
我们现在来做个实验, 相信各位会更容易了解这个macro的。 请看一下下面这段程序码。
struct HelloWorld { int x, y; struct list_head list; } hello;
假设int是4个byte。 那么以下这一行会得到8
(unsigned long) (&((struct HelloWorld *)0)->list)
有的人会对这一行程序感到奇怪, (struct HelloWorld*)0不就是一个NULL的指标吗? 怎么可以用0->list去使用list这个元素呢? 难道不怕造成segmentation fault吗? 请注意一下, 我们在0->list的前面还加上了一个&。 如果没有&, 那上面这一行就会segmentation fault了。 如果你加上了&, 那就没问题了。 Segmentation fault通常是去使用不合法的指针所指向的地址内容所造成的, 如果我们加上了&就表示我们没有要去使用这个不合法地址的内容,我们只是要那个元素的地址而已, 因此, 不会造成segmentation fault。 其实, 结构体的分配在内存里是连续的。 所以, 如果我们去读取某个元素时,像&hello->list。 会先取得hello变数的地址, 再然后再计算HelloWorld结构里list元素所在的offset, 再将hello的地址加上list元素的offset,求得list元素真正的地址。 然后再去读list元素的内容。 这是compiler帮我们做的。 那我们现在就来看看上面那一行究竟是什么意思。 首先, 我们先把上面那一行想象成下面这个样子。
ptr = 0; (unsigned long) (&((struct HelloWorld *)ptr)->list)
这样是不是容易懂了吗, 就是要取得&ptr->list的地址而已。所以, 如果ptr是100的话, 那会得到100+8=108。 因为前面有二个int, 每一个int是4个byte。 经过转型, 就得到了(unsigned long)型态的108。 如果ptr是0的话, 那同理, 我们会得到0+8=8。 也就是这个元素在HelloWorld结构里的offset。
现在, 如果我们已经知道了list在HelloWorld结构中的offset,而且我们现在也知道hello这个变数里list的地址的话, 那有没有办法得到hello本身的地址呢? 可以的, 就像图6一样, 如果我们知道list的地址, 只要将list的地址减8就可以知道了hello的地址了。
struct list_head *plist = &hello.list; printf( "&hello = %x\n", (char*)plist - (unsigned long) 8 ));
而这种方式就是list_head的用法, 它是专门用来当作别的结构的元素,只要我们得到这个元素的位置和包含这个元素的结构是哪一种, 我们可以很轻易的算出包含此元素的结构地址,linux给我们提供了这样一个宏:
#define list_entry(ptr, type, member) \ ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member)))
ptr:结构中包含的list_head元素的指针
type:结果的名字
member:结构中list_head元素的名字
参考:http://www.cnblogs.com/riky/archive/2006/12/28/606573.html