在libhv库中,有一个双向链表的实现,刚看的时候感觉有点疑惑,因为这个链表没有数据,只有前指针和后指针,跟过去学的不太一样,于是仔细分析了一下。
首先是结构体:
struct list_head {
struct list_head *next, *prev;
};
#define list_node list_head
该结构体不包含数据信息。
下面的初始化:
static inline void list_init(struct list_head *list)
{
list->next = list;
list->prev = list;
}
初始化就是将prev和next都指向自己,假如我要初始化list_head类型的p1,list_init(&p1),初始化后是这样的:
哈哈,随便画了画,凑合着看吧,这里假设p1的指针是100,那么prev==next==100,初始化后p1就成了链表的head。
添加成员
static inline void __list_add(struct list_head *n,
struct list_head *prev,
struct list_head *next)
{
next->prev = n;
n->next = next;
n->prev = prev;
prev->next = n;
}
static inline void list_add(struct list_head *n, struct list_head *head)
{
__list_add(n, head, head->next);
}
static inline void list_add_tail(struct list_head *n, struct list_head *head)
{
__list_add(n, head->prev, head);
}
其中__list_add是内部接口,list_add是前插入,list_add_tail是尾插入。先说list_add,假设我要在上面的p1的基础上添加一个p2,调用list_add(&p2, &p1),会间接调用__list_add,这里head和head->next都是指向的p1。在经过__list_add的4步指针操作后,就成了这个样子:
注意这里的p1是链首,p2才是真正意义上的第一个节点,所以这里p2在p1的后面与前插入并不矛盾。如果再插入一个p3,就能看出来了。再添加一个p3,这时候调用__list_add的第二个参数指向p1,第三个参数指向p2,经过4步指针操作,得到的结果为:
next方向, 表示为p1->p3->p2->p1
prev方向,表示为p1->p2->p3->p1,
p3添加到了p1和p2的中间,也就是说,如果以p1为首地址,那么以后再添加都是添加到p1的后面,当然也可以添加到其他位置,只要改一改首地址就可以了,毕竟是双向的,无所谓头,无所谓尾。就像上面这个图一样,p1、p2和p3都可以作为首地址。
如果知道了上面的添加操作,理解尾添加就很简单了,以上面的p1、p2和p3为例,如果p1是首元素,那么在p2的后面添加一个元素不就相当于在p2和p1之间添加一个元素,跟以p2为首地址,执行前插入一样。
删除操作
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
next->prev = prev;
prev->next = next;
}
static inline void list_del(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
}
还是以p1、p2和p3的图为例,如果要删除p3,不过就是将p2的前指针改为指向p1,p1的后指针改为指向p2,这样p1和p2就连接起来了,就又退回到了p1和p2的那张图,删除操作非常简单。
替换操作
static inline void list_replace(struct list_head *old,
struct list_head *n)
{
n->next = old->next;
n->next->prev = n;
n->prev = old->prev;
n->prev->next = n;
}
使用n替换old,替换操作也很简答, 假设我要用p4替换p3,那么要让p4的next指向p2,p2的prev指向p4,p4的prev指向p1,修改p1的next指向p4,就把p3剔除了。
判断某个元素是不是最后一个:
static inline int list_is_last(const struct list_head *list,
const struct list_head *head)
{
return list->next == head;
}
根据上面的p1、p2、p3那张图,p2是最后一个,p2的next指向的就是p1,实际上也可以判断p1的prev是不是p2来判断,一样的。
判断链表为空:
static inline int list_empty(const struct list_head *head)
{
return head->next == head;
}
根据p1那个图,自己指向自己,就是空,这里的空表示只有首元素。
但是下面有个函数挺奇怪的,
static inline int list_empty_careful(const struct list_head *head)
{
struct list_head *next = head->next;
return (next == head) && (next == head->prev);
}
根据名称,应该是更小心的判断链表是否为空,方法是比前面的判断多判断了 next==prev,看注释是说这种情况是为了应付另一个cpu也在处理同一个链表的情况,而且只有其他cpu只使用list_del_init()函数才能保证不需要同步机制就能达到安全,注释如下
using list_empty_careful() without synchronization
* can only be safe if the only activity that can happen
* to the list entry is list_del_init(). Eg. it cannot be used
* if another CPU could re-list_add() it.
而list_del_init是这样的:
#define INIT_LIST_HEAD list_init
static inline void list_del_init(struct list_head *entry)
{
__list_del_entry(entry);
INIT_LIST_HEAD(entry);
}
感觉这个所谓的安全指针对是被删除的那个元素,因为list_del_init最后会对该元素初始化,所以如果调用list_del,删除的元素的prev和next是不确定的,所以无法通过prev==next判断是否为空。而之所以说不能使用list_add,可以看上面的p1、p2的那张图,p1的next和prev都指向p2的,所以next==prev仍然是成立的,虽然不是空。暂时先这样理解了,对不对其实我也不确定。。。。
将首元素移动到末尾
static inline void __list_del_entry(struct list_head *entry)
{
__list_del(entry->prev, entry->next);
}
static inline void list_move_tail(struct list_head *list,
struct list_head *head)
{
__list_del_entry(list);
list_add_tail(list, head);
}
static inline void list_rotate_left(struct list_head *head)
{
struct list_head *first;
if (!list_empty(head)) {
first = head->next;
list_move_tail(first, head);
}
}
上面的list_rotate_left函数是将第一个节点移动到末尾。以上面p1、p3、p2的图举例,就是将第一个元素p3移动到p2的后面,不要忘记p1是head,并不是第一个节点,p3才是第一个节点。方法就是先将第一个节点p3从链表中删除,然后再将该节点尾插入末尾。
判断是否只有一个元素
static inline int list_is_singular(const struct list_head *head)
{
return !list_empty(head) && (head->next == head->prev);
}
list_is_singular判断是否链表只有一个节点,很简单,前面的p1、p2的那张图,除了作为head的p1,只有一个节点p2,这时候p1的prev和next都是指向的p2,所以不为空,并且prev==next就表示只有一个节点。
基本的功能差不多就这些,其他的功能感觉不怎么用的到,先不看了。看一些在libhv中怎么使用的吧,还记得最开始说的吗,这个链表没有数据,所以在使用的时候,跟平时在书上讲的那种有数据的链表是不一样的,在nlog.c文件中, 有一个相关用法。结构体定义如下:
typedef struct network_logger_s {
hloop_t* loop;
hio_t* listenio;
struct list_head clients;
} network_logger_t;
typedef struct nlog_client {
hio_t* io;
struct list_node node;
} nlog_client;
static network_logger_t s_logger = {0};
network_logger_t就是链表的头,nlog_client就是链表的节点。也就是说,如果想使用链表,只要将自己需要的数据和链表定义成一个结构体就可以了。
初始化很简单:
list_init(&s_logger.clients);
添加链表元素:
nlog_client* client;
HV_ALLOC_SIZEOF(client);
client->io = io;
hmutex_lock(&s_mutex);
list_add(&client->node, &s_logger.clients);
就是要注意不管是初始化还是添加数据,都是使用结构体中的链表成员,不要直接使用自己定义的结构体。那么有个问题,就是链表中记录的是结构体中链表成员的位置,如何获取除了链表之外,自己定义的数据呢?
void network_logger(int loglevel, const char* buf, int len) {
struct list_node* node;
nlog_client* client;
hmutex_lock(&s_mutex);
list_for_each (node, &s_logger.clients) {
client = list_entry(node, nlog_client, node);
hio_write(client->io, buf, len);
}
hmutex_unlock(&s_mutex);
}
#define list_for_each(pos, head) \
for (pos = (head)->next; prefetch(pos->next), pos != (head); \
pos = pos->next)
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
#define container_of(ptr, type, member) \
((type*)((char*)(ptr) - offsetof(type, member)))
最上面是nlog.c中执行的遍历,下面是涉及相关宏定义。可以忽略list_for_each中的prefetch,list_for_each就是简单的链表遍历,重点是list_entry,该宏就是通过链表获取刚才定义的结构体。将宏定义展开就是:((type*)((char*)(node) - offsetof(nlog_client, node))),注意list_entry(node, nlog_client, node)前后两个node表示不同的意思,第一个node表示list_node类型的指针,第二个node表示nlog_client结构体中定义的node成员。offsetof表示获取node成员在结构体nlog_client中的偏移量。也就是说我想从链表中获取自己定义的nlog_client,方法就是使用链表节点的指针,减去该节点所在结构体的偏移量,就获得了该结构体的首地址。
最后是删除操作,比较简单,就是别忘了释放内存。
nlog_client* client = (nlog_client*)hevent_userdata(io);
if (client) {
hevent_set_userdata(io, NULL);
hmutex_lock(&s_mutex);
list_del(&client->node);
hmutex_unlock(&s_mutex);
HV_FREE(client);
}