Linux内核中的数据结构与算法(三)哈希链表

四,哈希链表

谈到链表就不得不谈Linux内核中另外一个重要的结构,哈希链表。讨论这个结构前,你需要对哈希的最基本的概念要清楚哦,由于我们已经讲过Linux内核中的普通链表的结构,这里我们对比他们的区别来了解哈希链表会直观一些。

Linux链表认为双指针表头双循环链表对于HASH表来说过于浪费,因而设计了一套用于HASH表的hlist的数据结构,单指针表头双循环链表。hlish表头仅有一个指向首节点的指针,而没有指向尾节点的指针,这样在海量的HASH表中存储的表头就能减少一半的空间消耗。

  1. 定义和初始化

我们先来看看哈希链表头的定义以及哈希链表节点的定义。

此定义在include/linux/types.h中可以看到:

struct hlist_head {
	struct hlist_node *first;  //指向每一个hash桶的第一个结点的指针
};

struct hlist_node {
 struct hlist_node *next, **pprev; //pprev是指向上一个结点的next指针的地址
};

所以从定义上我们就可以知道哈希链表和内核普通双向链表的节点唯一的区别就在于,pprev是个两级指针,为什么是二级指针我们稍后会仔细讨论,了解哈希算法的同学就知道哈希链表并不需要双向循环的技能,它一般适用于单向散列的场景。再加上前面提到的节省存储空间的考虑,在嵌入式设备领域是非常明智的选择。
此外在include/linux/list.h中包含链表的相关初始化操作,本章节后续除非特意说明代码皆出自本文件。

#define HLIST_HEAD_INIT { .first = NULL }
#define HLIST_HEAD(name) struct hlist_head name = {  .first = NULL }
#define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL)

初始化哈希桶,INIT_HLIST_HEAD,哈希链表头节点初始化为NULL

INIT_HLIST_HEAD

初始化哈系桶中的每一个节点

static inline void INIT_HLIST_NODE(struct hlist_node *h)
{
 h->next = NULL;
 h->pprev = NULL;
}

2.链表节点插入

哈希链表的插入操作提供了几种不同方式的插入,有的小伙伴对于WRITEONCE有疑问的我这里多说一句,其实看WRITE_ONCE与READ_ONCE()的实现就能看出来,起到关键作用的关键字volatile,它告诉编译器声明这个变量很重要,不要把它当成一个普通的变量,做出错误的优化。保证 CPU 每次都从内存重新读取变量的值,而不是用寄存器中暂存的值。因为在 多线程/多核 环境中,不会被当前线程修改的变量,可能会被其他的线程修改,从内存读才可靠。还有一部分原因是,这两个宏可以作为标记,提醒编程人员这里面是一个多核/多线程共享的变量,必要的时候应该加互斥锁来保护。

2.1 链表头插入

那现在我们再开始讨论哈希链表的插入操作,首先来看添加节点到链表头的接口:

static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h)
{
 struct hlist_node *first = h->first;
 WRITE_ONCE(n->next, first);
 if (first)
     WRITE_ONCE(first->pprev, &n->next);
 WRITE_ONCE(h->first, n);
 WRITE_ONCE(n->pprev, &h->first);
}

在这里我们插入第一个节点,first链表原来为空。

hlist_add_head链表为空时

当first链表不为空时,我们插入一个节点

1.n->next = first; first原来指向node old.
2.first指向node old, 不为空。first->pprev = &n->next; 相当于node old.pprev = &n->next
;即node old 的pprev保存的是new节点的next元素的地址。
3.h->first = n;
4.n->pprev = &h->hirst;pprev指向的是first自身的地址;

2.2把节点插入到指定节点前面

static inline void hlist_add_before(struct hlist_node *n,
 struct hlist_node *next)
{
 WRITE_ONCE(n->pprev, next->pprev);
 WRITE_ONCE(n->next, next);
 WRITE_ONCE(next->pprev, &n->next);
 WRITE_ONCE(*(n->pprev), n);
}

1.n->pprev = next->pprev,即插入节点之前前一个节点的next的地址;
2.n->next=next;
3.next->pprev = &n->next;
4.(n->pprev) = n; n节点到pprev指向的是node old 的next的地址。*(n->pprev)保存的是n节点的地址,也就是node  old.next = n;

2.2把节点插入到指定节点后边

static inline void hlist_add_behind(struct hlist_node *n,
 struct hlist_node *prev)
{
 WRITE_ONCE(n->next, prev->next);
 WRITE_ONCE(prev->next, n);
 WRITE_ONCE(n->pprev, &prev->next);

 if (n->next)
     WRITE_ONCE(n->next->pprev, &n->next);
}

1.n->next = next->next; node_next是最后一个节点,其next指向NULL;
2.prev->next = n; node old next 的next元素指向n节点;
3.n->pprev=&next->next;n节点的pprev指向next节点的next元素的所在地址;
此外如果n->next不为空带代表原prev->next不是最后一个节点,我们需要把原prev的下一个节点的pprev写成n->next的地址,即n->next->pprev=&n->next;

3.链表节点删除

static inline void __hlist_del(struct hlist_node *n)
{
 struct hlist_node *next = n->next;
 struct hlist_node **pprev = n->pprev;

 WRITE_ONCE(*pprev, next);
 if (next)
     WRITE_ONCE(next->pprev, pprev);
}
static inline void hlist_del(struct hlist_node *n)
{
 __hlist_del(n);
 n->next = LIST_POISON1;
 n->pprev = LIST_POISON2;
}

对于删除节点来说,n作为要删除的节点,需要注意的是当n是最后一个节点时不需要修改下一个节点的pprev,否则需要修改下一个节点的pprev为n前一个节点的next的地址;

Linux 内核还提供了删除节点并且初始化该节点的结构,值得注意的是这里面有一个hlist_unhashed(n)判断n节点是否在哈希桶的一个函数。

static inline void hlist_del_init(struct hlist_node *n)
{
 if (!hlist_unhashed(n)) {
     __hlist_del(n);
     INIT_HLIST_NODE(n);
 }
}

4.遍历

#define hlist_entry(ptr, type, member) container_of(ptr,type,member)

#define hlist_for_each(pos, head) \
 for (pos = (head)->first; pos ; pos = pos->next)

#define hlist_for_each_safe(pos, n, head) \
 for (pos = (head)->first; pos && ({ n = pos->next; 1; }); \
         pos = n)

#define hlist_entry_safe(ptr, type, member) \
    ({ typeof(ptr) ____ptr = (ptr); \
       ____ptr ? hlist_entry(____ptr, type, member) : NULL; \
    })

与普通链表类似,哈希链表的遍历也提供了一个根据struct hlist_node的ptr得到ptr所指地址的结构体的首地址的接口hlist_entry(ptr, type, member)。

hlist_entry_safe(ptr, type, member)多了一步判断ptr是否为空的操作,如果为空则返回NULL。

所以hlist_for_each(pos, head)同理也就是一个for循环,从头遍历到链表尾。

而hlist_for_each_safe(pos, n, head)的作用也是一个for循环,与上一个不同的地方在于多了一个n,防止在便利过程中锻炼的发生。删除时用这个接口。

那么hlist_entry与hlist_for_each的结合就是hlist_for_each_entry(pos, head, member) 

#define hlist_for_each_entry(pos, head, member)             \
 for (pos = hlist_entry_safe((head)->first, typeof(*(pos)), member);\
         pos;                           \
         pos = hlist_entry_safe((pos)->member.next, typeof(*(pos)), member))

同样Linux也提供了安全遍历接口hlist_for_each_entry_safe(pos, n, head, member)

#define hlist_for_each_entry_safe(pos, n, head, member)         \
 for (pos = hlist_entry_safe((head)->first, typeof(*pos), member);\
         pos && ({ n = pos->member.next; 1; });         \
         pos = hlist_entry_safe(n, typeof(*pos), member))

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值