链表之TAILQ

TAILQ 介绍

请添加图片描述

TAILQ 队列是 FreeBSD 内核中的一种队列数据结构,在一些著名的开源库中(如 DPDKlibevent)有广泛的应用。

TAILQ 和Linuxlist的组织方式不一样,后者是单纯地将 struct list_head 作为链表的挂接点,并没有用户的信息,它们的差别如下图。

请添加图片描述

Linux中的list只将struct list_head作为用户元素的挂接点,因此在正向遍历链表时,需要使用container_of这类接口才能获取用户的数据,而TAILQ由于tqe_next指针直接指向用户元素,所以理论上,正向遍历TAILQlist更快.

头文件

头文件来自我最近看的项目:apache-mynewt-core-1.9.0\sys\sys\include\sys\queue.h

仅截取部分。

// "bsd_queue.h"  为了测试,我重新起了名字

/*
 *	@(#)queue.h	8.5 (Berkeley) 8/20/94
 * $FreeBSD: src/sys/sys/queue.h,v 1.32.2.7 2002/04/17 14:21:02 des Exp $
 */

/*
 * Tail queue declarations.
 */
#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 */	\
}

/*
 * Tail queue functions.
 */
#define	TAILQ_EMPTY(head)	((head)->tqh_first == NULL)

#define	TAILQ_FIRST(head)	((head)->tqh_first)

#define	TAILQ_FOREACH(var, head, field)					\
	for ((var) = TAILQ_FIRST((head));				\
	    (var);							\
	    (var) = TAILQ_NEXT((var), field))

#define	TAILQ_FOREACH_REVERSE(var, head, headname, field)		\
	for ((var) = TAILQ_LAST((head), headname);			\
	    (var);							\
	    (var) = TAILQ_PREV((var), headname, field))

#define	TAILQ_INIT(head) do {						\
	TAILQ_FIRST((head)) = NULL;					\
	(head)->tqh_last = &TAILQ_FIRST((head));			\
} while (0)

#define	TAILQ_INSERT_AFTER(head, listelm, elm, field) do {		\
	if ((TAILQ_NEXT((elm), field) = TAILQ_NEXT((listelm), field)) != NULL)\
		TAILQ_NEXT((elm), field)->field.tqe_prev = 		\
		    &TAILQ_NEXT((elm), field);				\
	else								\
		(head)->tqh_last = &TAILQ_NEXT((elm), field);		\
	TAILQ_NEXT((listelm), field) = (elm);				\
	(elm)->field.tqe_prev = &TAILQ_NEXT((listelm), field);		\
} while (0)

#define	TAILQ_INSERT_BEFORE(listelm, elm, field) do {			\
	(elm)->field.tqe_prev = (listelm)->field.tqe_prev;		\
	TAILQ_NEXT((elm), field) = (listelm);				\
	*(listelm)->field.tqe_prev = (elm);				\
	(listelm)->field.tqe_prev = &TAILQ_NEXT((elm), field);		\
} while (0)

#define	TAILQ_INSERT_HEAD(head, elm, field) do {			\
	if ((TAILQ_NEXT((elm), field) = TAILQ_FIRST((head))) != NULL)	\
		TAILQ_FIRST((head))->field.tqe_prev =			\
		    &TAILQ_NEXT((elm), field);				\
	else								\
		(head)->tqh_last = &TAILQ_NEXT((elm), field);		\
	TAILQ_FIRST((head)) = (elm);					\
	(elm)->field.tqe_prev = &TAILQ_FIRST((head));			\
} while (0)

#define	TAILQ_INSERT_TAIL(head, elm, field) do {			\
	TAILQ_NEXT((elm), field) = NULL;				\
	(elm)->field.tqe_prev = (head)->tqh_last;			\
	*(head)->tqh_last = (elm);					\
	(head)->tqh_last = &TAILQ_NEXT((elm), field);			\
} while (0)

#define	TAILQ_LAST(head, headname)					\
	(*(((struct headname *)((head)->tqh_last))->tqh_last))

#define	TAILQ_NEXT(elm, field) ((elm)->field.tqe_next)

#define	TAILQ_PREV(elm, headname, field)				\
	(*(((struct headname *)((elm)->field.tqe_prev))->tqh_last))

#define	TAILQ_REMOVE(head, elm, field) do {				\
	if ((TAILQ_NEXT((elm), field)) != NULL)				\
		TAILQ_NEXT((elm), field)->field.tqe_prev = 		\
		    (elm)->field.tqe_prev;				\
	else								\
		(head)->tqh_last = (elm)->field.tqe_prev;		\
	*(elm)->field.tqe_prev = TAILQ_NEXT((elm), field);		\
} while (0)

举例

来自 https://manpages.courier-mta.org/htmlman3/tailq.3.html

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include "bsd_queue.h" //没有用 Linux 的,换成了我自己的

struct entry {
    int data;
    TAILQ_ENTRY(entry) entries;             /* Tail queue */
};

TAILQ_HEAD(tailhead, entry);

int
main(void)
{
    struct entry *n1, *n2, *n3, *np;
    struct tailhead head;                   /* Tail queue head */
    int i;

    TAILQ_INIT(&head);                      /* Initialize the queue */

    n1 = malloc(sizeof(struct entry));      /* Insert at the head */
    TAILQ_INSERT_HEAD(&head, n1, entries);

    n1 = malloc(sizeof(struct entry));      /* Insert at the tail */
    TAILQ_INSERT_TAIL(&head, n1, entries);

    n2 = malloc(sizeof(struct entry));      /* Insert after */
    TAILQ_INSERT_AFTER(&head, n1, n2, entries);

    n3 = malloc(sizeof(struct entry));      /* Insert before */
    TAILQ_INSERT_BEFORE(n2, n3, entries);

    TAILQ_REMOVE(&head, n2, entries);       /* Deletion */
    free(n2);
                                            /* Forward traversal */
    i = 0;
    TAILQ_FOREACH(np, &head, entries)
        np->data = i++;
                                            /* Reverse traversal */
    TAILQ_FOREACH_REVERSE(np, &head, tailhead, entries)
        printf("%i\n", np->data);
                                            /* TailQ deletion */
    n1 = TAILQ_FIRST(&head);
    while (n1 != NULL) {
        n2 = TAILQ_NEXT(n1, entries);
        free(n1);
        n1 = n2;
    }
    TAILQ_INIT(&head);

    exit(EXIT_SUCCESS);
}

代码分析

TAILQ_ENTRY 和 TAILQ_HEAD

struct entry {
    int data;
    TAILQ_ENTRY(entry) entries;             /* Tail queue */
};

TAILQ_HEAD(tailhead, entry);

TAILQ_ENTRY(entry) entries 中的 entry 由用户指定,是外层大结构体的标签,宏展开后就是下面的第 1,4,5 行中的 entry;entries 也由用户指定,是无标签结构体的成员名,宏展开后是下面第 6 行中的 entries

struct entry {
    int data;
    struct { 
        struct entry *tqe_next; 
        struct entry **tqe_prev; 
    } entries;
};

struct tailhead { 
    struct entry *tqh_first; 
    struct entry **tqh_last; 
};

TAILQ_HEAD(tailhead, entry) 中的 tailhead 由用户指定,是表头结构体的标签,如上面第 9 行的 tailhead,entry 要和 TAILQ_ENTRY 中的第一个参数保持一致。

struct entry **tqh_laststruct entry **tqe_prev 看起来有点诡异,为什么是二级指针?

改成一级的行不行?

如果把第 5 行改成 struct entry *tqe_prev,肯定不行,如果它的前面是头节点怎么办,头节点的定义里面没有 struct entry 啊!

再仔细看看,不管是头节点还是普通节点,都有 struct entry * 这个类型,那就定义一个类型为 struct entry ** 的变量好了。这样前插的时候就可以统一处理了,例子见 TAILQ_INSERT_BEFORE

总结一下,定义 TAILQ 的时候,有 3 个基本要素:

  1. 结构体 struct entry:TAILQ_ENTRY() 的参数和 TAILQ_HEAD() 的第二个参数

  2. 结构体 struct tailhead:TAILQ_HEAD() 的第一个参数

  3. 变量 entries:紧跟在 TAILQ_ENTRY() 后面

    TAILQ_INIT

    struct entry *n1, *n2, *n3, *np;
    struct tailhead head;                   /* Tail queue head */
    int i;

    TAILQ_INIT(&head);                      /* Initialize the queue */

第 5 行,宏展开是

(((&head))->tqh_first) = ((void *)0); 
(&head)->tqh_last = &(((&head))->tqh_first); 

意思是表头的 tqh_first 为 NULL,tqh_last 指向自己的 tqh_first

TAILQ_INSERT_HEAD

    n1 = malloc(sizeof(struct entry));      /* Insert at the head */
    TAILQ_INSERT_HEAD(&head, n1, entries);

2:宏展开

if (((((n1))->entries.tqe_next) = (((&head))->tqh_first)) != ((void *)0)) 
    (((&head))->tqh_first)->entries.tqe_prev = &(((n1))->entries.tqe_next); 
else 
    (&head)->tqh_last = &(((n1))->entries.tqe_next); 

(((&head))->tqh_first) = (n1); 
(n1)->entries.tqe_prev = &(((&head))->tqh_first); 

要把 n1 插入到队列的第一个位置,需要修改哪些指针呢?

  1. n1 的 entries.tqe_next
  2. n1 的 entries.tqe_prev
  3. (&head))->tqh_first
  4. 之前第一个节点的 entries.tqe_prev
  5. 如果 n1 是唯一的节点,还需要修改 (&head)->tqh_last

好,我们看代码是如何解决的。

第 1 行解决了 1

第 2 行解决了 4,这个要解释一下,(((&head))->tqh_first)->entries.tqe_prev 表示插入 n1 之前的第一个节点的 tqe_prev,插入后,它应该指向 n1 的 tqe_next

第 4 行解决了 5

第 6 行解决了 3

第 7 行解决了 2

TAILQ_INSERT_TAIL

    n1 = malloc(sizeof(struct entry));      /* Insert at the tail */
    TAILQ_INSERT_TAIL(&head, n1, entries);

把 n1 插入到队列的末尾,需要修改哪些指针呢?

  1. (&head)->tqh_last
  2. n1 的 entries.tqe_next 应该为 NULL
  3. n1 的 entries.tqe_prev
  4. n1 插入之前最后一个节点(叫 n0 吧)的 entries.tqe_next

第 2 行,展开宏

(((n1))->entries.tqe_next) = ((void *)0); 
(n1)->entries.tqe_prev = (&head)->tqh_last; 
*(&head)->tqh_last = (n1); 
(&head)->tqh_last = &(((n1))->entries.tqe_next);

第 1 行解决了 2

第 2 行解决了 3

第 3 行解决了 4,因为插入之前 (&head)->tqh_last 指向 n0 的 entries.tqe_next,对 (&head)->tqh_last 解引用,就得到变量 n0 的 entries.tqe_next,它应该等于 n1

第 4 行解决了 1

TAILQ_INSERT_AFTER

    n2 = malloc(sizeof(struct entry));      /* Insert after */
    TAILQ_INSERT_AFTER(&head, n1, n2, entries);

把 n2 插入到 n1 的后面,需要修改哪些指针呢?

  1. n1 的 entries.tqe_next
  2. n2 的 entries.tqe_prev
  3. n2 的 entries.tqe_next
  4. 插入 n2 之前,如果 n1 后面有个 n3,则需要修改 n3 的 entries.tqe_prev;
  5. 插入 n2 之前,如果 n1 是最后一个节点,则要修改 (&head)->tqh_last

第 2 行宏展开:

if (((((n2))->entries.tqe_next) = (((n1))->entries.tqe_next)) != ((void *)0)) 
    (((n2))->entries.tqe_next)->entries.tqe_prev = &(((n2))->entries.tqe_next); 
else 
    (&head)->tqh_last = &(((n2))->entries.tqe_next); 

(((n1))->entries.tqe_next) = (n2); 
(n2)->entries.tqe_prev = &(((n1))->entries.tqe_next); 

第 1 行解决了 3

第 2 行解决了 4

第 4 行解决了 5

第 6 行解决了1

第 7 行解决了 2

TAILQ_INSERT_BEFORE

    n3 = malloc(sizeof(struct entry));      /* Insert before */
    TAILQ_INSERT_BEFORE(n2, n3, entries);

把 n3 插入到 n2 的前面,需要修改哪些指针呢?

  1. n3 的 entries.tqe_next

  2. n2 的 entries.tqe_prev

  3. n3 的 entries.tqe_prev

  4. 插入 n3 之前,如果 n2 的前面有个 n1,则需要修改 n1 的 entries.tqe_next,

    (n1)->entries.tqe_next = n3;

  5. 插入 n3 之前,如果 n2 是第一个节点,则要修改 (&head)->tqh_first,

    (&head)->tqh_first = n3;

第 2 行宏展开

 (n3)->entries.tqe_prev = (n2)->entries.tqe_prev; 
 (((n3))->entries.tqe_next) = (n2); 
 *(n2)->entries.tqe_prev = (n3); 
 (n2)->entries.tqe_prev = &(((n3))->entries.tqe_next); 

第 1 行解决了 3

第 2 行解决了 1

第 3 行解决了 4 和 5(马上会解释)

第 4 行解决了 2

解释:

插入 n3 之前,如果 n2 的前面有个 n1,

那么 *(n2)->entries.tqe_prev 代表变量 (n1)->entries.tqe_next

插入 n3 之前,如果 n2 是第一个节点,

那么 *(n2)->entries.tqe_prev 代表变量 (&head)->tqh_first

所以这两种情况可以合并,写成

*(n2)->entries.tqe_prev = (n3);

TAILQ_REMOVE

    TAILQ_REMOVE(&head, n2, entries);       /* Deletion */
    free(n2);

要把 n2 从链表移除,需要修改哪些指针呢?

  1. n2 前面那个节点(假设是 n1)的 entries.tqe_next
  2. n2 后面那个节点(假设是 n3)的 entries.tqe_prev
  3. 如果 n2 是最后一个节点,那么要修改 (&head)->tqh_last

第 1 行宏展开

if (((((n2))->entries.tqe_next)) != ((void *)0)) 
    (((n2))->entries.tqe_next)->entries.tqe_prev = (n2)->entries.tqe_prev; 
else 
    (&head)->tqh_last = (n2)->entries.tqe_prev; 
*(n2)->entries.tqe_prev = (((n2))->entries.tqe_next); 

第 1 行判断 n2 是否为最后一个节点

第 2 行解决了 2

第 4 行解决了 3

第 5 行解决了 1,这个解释一下。(n2)->entries.tqe_prev 指向的是 n1 的 entries.tqe_next,对 (n2)->entries.tqe_prev 解引用,得到变量 (n1)->entries.tqe_next,需要给它赋值为 n3 的地址,即 (n2)->entries.tqe_next;如果 (n2)->entries.tqe_prev 指向表头的 tqh_first,第 5 行也是成立的

TAILQ_FOREACH

    i = 0;/* Forward traversal */
    TAILQ_FOREACH(np, &head, entries)
        np->data = i++;

第 2 行就是

for ((np) = (((&head))->tqh_first); (np); (np) = (((np))->entries.tqe_next))

比较简单,不需要解释。

TAILQ_FOREACH_REVERSE

    /* Reverse traversal */
    TAILQ_FOREACH_REVERSE(np, &head, tailhead, entries)
        printf("%i\n", np->data);

这次我们换个思路,不看展开后是什么,而是看看宏的定义

#define	TAILQ_FOREACH_REVERSE(var, head, headname, field)		\
	for ((var) = TAILQ_LAST((head), headname);			\
	    (var);							\
	    (var) = TAILQ_PREV((var), headname, field))

#define	TAILQ_LAST(head, headname)					\
	(*(((struct headname *)((head)->tqh_last))->tqh_last))

#define	TAILQ_PREV(elm, headname, field)				\
	(*(((struct headname *)((elm)->field.tqe_prev))->tqh_last))

第 7 行,可以得到最后一个节点的地址。解释一下:

(head)->tqh_last 是最后一个节点的 entries.tqe_next 的地址,可是这个地址不是用户想要的,用户要的是节点的地址,就是大结构体( struct entry )的地址,所以只好曲线救国,如下图,从 1 到 2 再到 3
请添加图片描述

从 entries.tqe_next 的地址怎么得到 entries.tqe_prev 的值呢?

其实也不难,虽然 struct entry 里面 entries 这个结构体变量没有标签,但是它的元素构成和 struct headname 完全一样,都是一个 struct entry * 和 一个 struct entry **,所以可以把 entries.tqe_next 的地址强制转换为 (struct headname *) 类型: (struct headname *)((head)->tqh_last)

然后访问它的 tqh_last 成员(也就是图中的 prev):

((struct headname *)((head)->tqh_last))->tqh_last

这样就得到了前一个节点的 entries.tqe_next 的地址,再对它解引用:

*(((struct headname *)((head)->tqh_last))->tqh_last)

就得到了 entries.tqe_next 变量,这恰好是大结构体( struct entry )的地址

我们再看 TAILQ_PREV 这个宏,道理类似。

请添加图片描述

如图所示,从 1 到 2 再到 3

设当前节点的地址是 elm(图中用最后这个节点来示意),(elm)->field.tqe_prev ,表示顺着 1,得到它前面那个节点的 next 的地址,对这个地址强制转换为 struct headname *,即

(struct headname *)((elm)->field.tqe_prev)

再访问 prev,也就是顺着 2,得到了 elm 前面的前面的节点的 next 地址

((struct headname *)((elm)->field.tqe_prev))->tqh_last

对它解引用,就得到 elm 前面的前面的节点的 next,即

*(((struct headname *)((elm)->field.tqe_prev))->tqh_last)

它刚好是 3 这个指针表示的地址。

其他的宏比较简单,就不介绍了

参考资料

https://manpages.courier-mta.org/htmlman3/tailq.3.html

TAILQ 队列之一二事 - SegmentFault 思否

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值