Linux 内核 hlist 详解

在Linux内核中,hlist(哈希链表)使用非常广泛。本文将对其数据结构和核心函数进行分析。

和hlist相关的数据结构有两个:hlist_head 和 hlist_node

//hash桶的头结点
struct hlist_head {
	struct hlist_node *first;//指向每一个hash桶的第一个结点的指针
};
//hash桶的普通结点
struct hlist_node {
	struct hlist_node *next;//指向下一个结点的指针
	struct hlist_node **pprev;//指向上一个结点的next指针的地址
};

结构如下图所示:


hlist_head结构体只有一个域,即first。 first指针指向该hlist链表的第一个节点。
hlist_node结构体有两个域,next 和pprev。 next指针很容易理解,它指向下个hlist_node结点,倘若该节点是链表的最后一个节点,next指向NULL。
pprev是一个二级指针, 它指向前一个节点的next指针的地址
。为什么我们需要这样一个指针呢?它的好处是什么?
在回答这个问题之前,我们先研究另一个问题:为什么哈希表的实现需要两个不同的数据结构?
哈希表的目的是为了方便快速的查找,所以哈希表中hash桶的数量通常比较大,否则“冲突”的概率会非常大,这样也就失去了哈希表的意义。如何做到既能维护一张大表,又能不使用过多的内存呢?就只能从数据结构上下功夫了。所以对于哈希表的每个hash桶,它的结构体中只存放一个指针,解决了占用空间的问题。现在又出现了另一个问题:数据结构不一致。显然,如果hlist_node采用传统的next,prev指针,对于第一个节点和后面其他节点的处理会不一致。这样并不优雅,而且效率上也有损失。
hlist_node巧妙地将pprev指向上一个节点的next指针的地址,由于hlist_head的first域指向的结点类型和hlist_node指向的下一个结点的结点类型相同,这样就解决了通用性!

如果要删除hash桶对应链表中的第一个普通结点

对应的程序代码如下:

static inline void __hlist_del(struct hlist_node *n) 
{ 
 struct hlist_node *next = n->next;//获取指向第二个普通结点的指针 
 struct hlist_node **pprev = n->pprev;//保留待删除的第一个结点的pprev域(即头结点first域的地址),此时 pprev = &first 
 *pprev = next; 
 /*
 因为pprev = &first,所以*pprev = next,相当于 first = next
 即将hash桶的头结点指针指向原来的第二个结点,如上图中的黑线1
 */
 if (next) //如果第二个结点不为空
  next->pprev = pprev;//将第二个结点的pprev域设置为头结点first域的地址,如上图中的黑线2 
}

如果要删除hash桶对应链表中的非第一个结点


对应的程序代码如下:

static inline void __hlist_del(struct hlist_node *n) 
{ 
 struct hlist_node *next = n->next;//获取指向待删除结点的下一个普通结点的指针 
 struct hlist_node **pprev = n->pprev;//获取待删除结点的pprev域 
 *pprev = next; //修改待删除结点的pprev域,逻辑上使待删除结点的前驱结点指向待删除结点的后继结点,如上图中的黑线1

 if (next) //如果待删除结点的下一个普通结点不为空
      next->pprev = pprev;//设置下一个结点的pprev域,如上图中的黑线2,保持hlist的结构 
}

可以看到删除第一个普通结点和删除非第一个普通结点的代码是一样的。

下面再来看看如果hlist_node中包含两个分别指向前驱结点和后继结点的指针

很明显删除hash桶对应链表中的非第一个普通结点,只需要如下两行代码:

n->next->prev = n->prev;
n->prev->next = n->next;

可是,如果是删除的hash桶对应链表中的第一个普通结点:
此时 n->prev->next = n->next 就会出问题,因为hash桶的表头结点没有next域
所以,明显在这种情况下删除hash桶对应链表的第一个普通结点和非第一个普通结点的代码是不一样的。
同样的情况也存在于插入操作。

附一张在hash桶头结点之后,插入第一个普通结点的图:


在遍历上,如果使用hlist_hode, list_node指针进行遍历,两者过程大致相似。

#define list_for_each(pos, head) \
        for (pos = (head)->next; prefetch(pos->next), pos != (head); \
                pos = pos->next)
 
#define hlist_for_each(pos, head) \
        for (pos = (head)->first; pos && ({ prefetch(pos->next); 1; }); \
             pos = pos->next)

如果使用其寄生结构的指针进行遍历,则 hlist list 也略有不同, hlist 在遍历时需要一个指向 hlist_node 的临时指针,该指针的引入,一是为了遍历,而 list 的遍历在 list_entry 的参数中实现了,更主要的目的在于判断结束,因为 hlist 最后一个节点的 next NULL ,只有 hlist_node 指向 NULL 时才算结束,而这个 NULL 不包含在任何寄生结构内,不能通过 tpos->member 的方式访问到,故临时变量 pos 的引入时必须的。

#define list_for_each_entry(pos, head, member) \
        for (pos = list_entry((head)->next, typeof(*pos), member); \
             prefetch(pos->member.next), &pos->member != (head); \
             pos = list_entry(pos->member.next, typeof(*pos), member))
 
#define hlist_for_each_entry(tpos, pos, head, member) \
        for (pos = (head)->first; \
             pos && ({ prefetch(pos->next); 1;}) && \
                ({ tpos = hlist_entry(pos, typeof(*tpos), member); 1;}); \
             pos = pos->next)

另外, list hlist 的遍历都实现了 safe 版本,因在遍历时,没有任何特别的东西来阻止对链表执行删除操作(通常在使用链表时使用锁来保护并发访问)。安全版本的遍历函数使用临时存放的方法使得检索链表时能不被删除操作所影响。

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

附上linux内核中与hlist相关的完整代码:

//hash桶的头结点 
struct hlist_head { 
     struct hlist_node *first;//指向每一个hash桶的第一个结点的指针 
}; 
//hash桶的普通结点 
struct hlist_node { 
 struct hlist_node *next;//指向下一个结点的指针 
 struct hlist_node **pprev;//指向上一个结点的next指针的地址 
}; 
//以下三种方法都是初始化hash桶的头结点 
#define HLIST_HEAD_INIT { .first = NULL } 
#define HLIST_HEAD(name) struct hlist_head name = { .first = NULL } 
#define INIT_HLIST_HEAD(ptr) ((ptr)->first = NULL) 

//初始化hash桶的普通结点 
static inline void INIT_HLIST_NODE(struct hlist_node *h) 
{ 
 h->next = NULL; 
 h->pprev = NULL; 
} 

//判断一个结点是否已经存在于hash桶中 
static inline int hlist_unhashed(const struct hlist_node *h) 
{ 
     return !h->pprev; 
} 

//判断一个hash桶是否为空 
static inline int hlist_empty(const struct hlist_head *h) 
{ 
     return !h->first; 
} 

static inline void __hlist_del(struct hlist_node *n) 
{ 
 struct hlist_node *next = n->next;//获取指向待删除结点的下一个结点的指针 
 struct hlist_node **pprev = n->pprev;//保留待删除结点的pprev域 
 *pprev = next;//修改待删除结点的pprev域,逻辑上使待删除结点的前驱结点指向待删除结点的后继结点 
 if (next) 
  next->pprev = pprev;//设置待删除结点的下一个结点的pprev域,保持hlist的结构 
} 

static inline void hlist_del(struct hlist_node *n) 
{ 
 __hlist_del(n);//删除结点之后,需要将其next域和pprev域设置为无用值 
 n->next = LIST_POISON1; 
 n->pprev = LIST_POISON2; 
} 

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

//将普通结点n插入到头结点h对应的hash桶的第一个结点的位置 
static inline void hlist_add_head(struct hlist_node *n, struct hlist_head *h) 
{ 
 struct hlist_node *first = h->first; 
 n->next = first; 
 if (first) 
  first->pprev = &n->next; 
 h->first = n; 
 n->pprev = &h->first; 
} 

/* next must be != NULL */ 
//在next结点之前插入结点n,即使next结点是hash桶中的第一个结点也可以 
static inline void hlist_add_before(struct hlist_node *n, struct hlist_node *next) 
{ 
 n->pprev = next->pprev; 
 n->next = next; 
 next->pprev = &n->next; 
 *(n->pprev) = n; 
} 

//在结点n之后插入结点next 
static inline void hlist_add_after(struct hlist_node *n, struct hlist_node *next) 
{ 
 next->next = n->next; 
 n->next = next; 
 next->pprev = &n->next; 

 if(next->next) 
  next->next->pprev = &next->next; 
} 

/* 
 * Move a list from one list head to another. Fixup the pprev 
 * reference of the first entry if it exists. 
 */ 
static inline void hlist_move_list(struct hlist_head *old, struct hlist_head *new) 
{ 
 new->first = old->first; 
 if (new->first) 
  new->first->pprev = &new->first; 
 old->first = NULL; 
} 

//通过一个结构体内部一个成员的地址获取结构体的首地址 
#define hlist_entry(ptr, type, member) container_of(ptr,type,member) 

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

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

/** 
 * hlist_for_each_entry	- iterate over list of given type 
 * @tpos:	the type * to use as a loop cursor. 
 * @pos:	the &struct hlist_node to use as a loop cursor. 
 * @head:	the head for your list. 
 * @member:	the name of the hlist_node within the struct. 
 */ 
#define hlist_for_each_entry(tpos, pos, head, member)	 \ 
 for (pos = (head)->first;	 \ 
      pos && ({ prefetch(pos->next); 1;}) &&	 \ 
  ({ tpos = hlist_entry(pos, typeof(*tpos), member); 1;}); \ 
      pos = pos->next) 

/** 
 * hlist_for_each_entry_continue - iterate over a hlist continuing after current point 
 * @tpos:	the type * to use as a loop cursor. 
 * @pos:	the &struct hlist_node to use as a loop cursor. 
 * @member:	the name of the hlist_node within the struct. 
 */ 
#define hlist_for_each_entry_continue(tpos, pos, member)	 \ 
 for (pos = (pos)->next;	 \ 
      pos && ({ prefetch(pos->next); 1;}) &&	 \ 
  ({ tpos = hlist_entry(pos, typeof(*tpos), member); 1;}); \ 
      pos = pos->next) 

/** 
 * hlist_for_each_entry_from - iterate over a hlist continuing from current point 
 * @tpos:	the type * to use as a loop cursor. 
 * @pos:	the &struct hlist_node to use as a loop cursor. 
 * @member:	the name of the hlist_node within the struct. 
 */ 
#define hlist_for_each_entry_from(tpos, pos, member)	 \ 
 for (; pos && ({ prefetch(pos->next); 1;}) &&	 \ 
  ({ tpos = hlist_entry(pos, typeof(*tpos), member); 1;}); \ 
      pos = pos->next) 

/** 
 * hlist_for_each_entry_safe - iterate over list of given type safe against removal of list entry 
 * @tpos:	the type * to use as a loop cursor. 
 * @pos:	the &struct hlist_node to use as a loop cursor. 
 * @n:	 another &struct hlist_node to use as temporary storage 
 * @head:	the head for your list. 
 * @member:	the name of the hlist_node within the struct. 
 */ 
#define hlist_for_each_entry_safe(tpos, pos, n, head, member) \ 
 for (pos = (head)->first;	 \ 
      pos && ({ n = pos->next; 1; }) && \ 
  ({ tpos = hlist_entry(pos, typeof(*tpos), member); 1;}); \ 
      pos = n)


  • 30
    点赞
  • 128
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
哈希表(Hash Table)是一种用于实现快速查找的数据结构,它通过将关键字映射到一个固定大小的数组中,以达到快速查找的目的。在 Linux 内核中,哈希表被广泛应用于实现各种数据结构,例如文件系统、网络协议栈等。 Linux 内核中的哈希表是由 struct hlist_head 和 struct hlist_node 两个结构体组成的链表结构。其中,struct hlist_head 表示哈希表的头部,而 struct hlist_node 则表示哈希表中的每一个元素。哈希表的大小一般是 2 的幂次方,这样可以通过位运算来计算元素在哈希表中的位置,提高哈希表的查找效率。 在 Linux 内核中,哈希表的实现主要有两种方式:分离式和联合式。 分离式哈希表将哈希表分成多个独立的桶,每个桶都有自己的哈希函数和链表。这种方式可以减小哈希冲突的概率,提高查找效率。 联合式哈希表则将所有元素都存放在同一个链表中,通过在链表中查找元素来实现哈希表的查找功能。这种方式虽然比较简单,但是由于所有元素都存放在同一个链表中,容易出现哈希冲突,查找效率较低。 在 Linux 内核中,哈希表还提供了一些常见的操作函数,例如: - hash_init:初始化哈希表; - hash_add:向哈希表中添加元素; - hash_del:从哈希表中删除元素; - hash_find:在哈希表中查找元素。 总的来说,哈希表是 Linux 内核中非常重要的数据结构之一,它的应用广泛,可以提高系统的性能和稳定性。同时,哈希表的实现也非常灵活,可以根据不同的应用场景选择合适的实现方式。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值