关于libevent与FreeBSD内核中TAILQ队列的理解
在看libevent源码中TAILQ的时候发现了一些让我迷惑的地方,就是里面的双端队列以及链表中节点的next与prev指针,它的设计与我们一般的链表以及linux内核的list完全不一样,因为里面的prev根本不是指向前一个节点,而是指向前一个节点的next元素的地址:
#define TAILQ_HEAD(name, type) \ struct name { \ struct type *tqh_first; /* first element */ \ struct type **tqh_last; /* addr of last next element */ \ } #define TAILQ_HEAD_INITIALIZER(head) \ { NULL, &(head).tqh_first } #define TAILQ_ENTRY(type) \ struct { \ struct type *tqe_next; /* next element */ \ struct type **tqe_prev; /* address of previous next element */ \ }
还有一个令人迷惑的地方是下面的两个macros:
#define TAILQ_LAST(head, headname) \ (*(((struct headname *)((head)->tqh_last))->tqh_last)) /* XXX */ #define TAILQ_PREV(elm, headname, field) \ (*(((struct headname *)((elm)->field.tqe_prev))->tqh_last))
首先我们定义两个用于分析的实例
struct user{
int i;
TAILQ_ENTRY(user) entry
};
TAILQ_HEAD(head,user) tq
head * ptq = & tq
为什么要强制转换问headname*呢,先将TAILQ_LASR进行分解来说明,TAILQ_PREV类似:
@1 (head)->tqh_last
@2 ( (struct headname *) (@1) )
@3 ( *((@2)->tqh_last) )
我们要获得自己定义的tq的最后一个节点的地址应这样使用
TAILQ_LAST(ptq, head)
由@1 我们得到的是ptq->tqh_last,即最后一个节点TAILQ_ENTRY(type)的struct type *tqe_next;元素的地址,这时候我们只要获得struct type **tqh_last的值就能得到该节点的地址值,就是last节点(注意不是前一个),现在如何获得struct type **tqh_last是关键所在。仔细观察我们发现TAILQ_ENTRY与TAILQ_HEAD的内存布局实际上是一样的,都是对应两个元素共八个字节,现在有两种方法获得struct type **tqh_last。
1.struct type *tqe_next的地址值直接加4
2.将struct type *tqe_next强制转换成head,也就是源代码中的方法
现在主要分析第二中解决方案,@2是一个head* 指针,通过@3之后我们实际得到的是前一个节点的struct type *tqe_next地址的解引用,就是last节点的地址,这里有点迷惑((@2)->tqh_last)实际对应的是TAILQ_ENTRY节点的struct type **tqe_prev元素,它指向的是前一个节点的struct type *tqe_next地址,这样一来我们便能得到最好一个节点的地址了,TAILQ_PREV使用的方法也是一样的。至于为什么使用2方法不用1方法,我是这样理解的,使用1方法需要进行加法运算,会消耗CPU周期,这对于网络应用与内核是很关键的,而且通过使用方法2可以易于移植,因为当将内核移植到64位机器的时候方法1肯能就会产生错误。
也许上面的讲解一时无法理解,但是只要记住一点的就是TAILQ_ENTRY与TAILQ_HEAD的内存布局是一样的,为什么TAILQ_LAST和TAILQ_PREV中不将(head)->tqh_last)转换为TAILQ_HEAD而不是转换为TAILQ_ENTRY是因为TAILQ_ENTRY这个结构体是直接定义在你用户定义的结构体中,无法获取他的类型来进行转换