Linux 内核 hlist 详解

摘要:
//指向每个哈希桶的第一个节点的指针}//指向下一个节点structhlist_node**pprev的指针;//指向上一个节点的下一个指针的地址};hlist_节点巧妙地将pprev指向上一个节点的下一个指针的地址_头的第一个域指向的节点类型和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指针的地址
};

结构如下图所示:

 Linux 内核 hlist 详解第1张

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桶对应链表中的第一个普通结点

Linux 内核 hlist 详解第2张

对应的程序代码如下:


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
}

代码精简后

static inline void __hlist_del(struct hlist_node *n) 

{

  n->pprev=&(n->next);

  if (next)

    n->next->pprev =n->pprev;

}

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

Linux 内核 hlist 详解第3张
对应的程序代码如下:


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的结构
}

代码精简后

static inline void __hlist_del(struct hlist_node *n) 

{

  n->pprev=&(n->next);

  if (next)

    n->next->pprev =n->pprev;

}

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

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

Linux 内核 hlist 详解第4张

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


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

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

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

 Linux 内核 hlist 详解第5张

在遍历上,如果使用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;
}

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

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

/* 用新的hlist_head 更换旧的hlist_head */ 
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)

免责声明:文章转载自《Linux 内核 hlist 详解》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇redis集群如何解决重启不了的问题el表达式具体解释下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

qt动画入门

Qt-4.6新增了Animation Framework(动画框架),让我们可以方便的写一些生动的程序。不必像曾经的版本号一样,全部的控件都枯燥的呆在伟大光荣的QLayout里,或许它们可以唱个歌,跳个舞。     所谓动画就是在一个时间段内的不同一时候间点有不同的状态。仅仅要定义好这样状态。实现动画就是水到渠成的事情。当然做这件事情,最好用的就是状态...

java集合类笔试选择题整理含答案

1、ArrayList list=new ArrayList(20);中的list扩充几次()A. 0B. 1C. 2D. 3答案:A分析:已经指定了长度, 所以不扩容2.List、Set、Map哪个继承自Collection接口,一下说法正确的是()A. List MapB. Set MapC. List SetD. List Map Set答案:C分析...

终于理解二级指针的作用了

之前学习swap函数时,知道传递指针可以实现对要交换变量本尊的修改,而直接传递值做不到这一点.究其原因,是因为函数传递参数时是以拷贝的形式,因此函数内部对其拷贝进行操作,不会影响到本尊. 如果想要通过函数实现对一级指针的值进行修改该如何去做呢?如果直接把它传进去,其实修改的是它的拷贝,而对它并没有影响.这个时候就是二级指针出场的时候了. #include...

Yii2的深入学习--行为Behavior

我们先来看下行为在 Yii2 中的使用,如下内容摘自 Yii2中文文档 行为是 [[yiiaseBehavior]] 或其子类的实例。行为,也称为 mixins,可以无须改变类继承关系即可增强一个已有的 [[yiiaseComponent|组件]] 类功能。当行为附加到组件后,它将“注入”它的方法和属性到组件,然后可以像访问组件内定义的方法和属性一样访问它...

前端vue使用stomp.js、sock.js完成websocket

前端vue使用stomp.js、sock.js完成websocket Sock.js Sock.js 是一个JavaScript库,为了应对很多浏览器不支持websocket协议问题。SockJ会自动对应websocket,如果websocket不可用,就会自动降为轮训的方式。 Stomp.js STOMP-Simple Text Oriented Mes...

Quartz.NET文档 入门教程

概述 Quartz.NET是一个开源的作业调度框架,非常适合在平时的工作中,定时轮询数据库同步,定时邮件通知,定时处理数据等。 Quartz.NET允许开发人员根据时间间隔(或天)来调度作业。它实现了作业和触发器的多对多关系,还能把多个作业与不同的触发器关联。整合了 Quartz.NET的应用程序可以重用来自不同事件的作业,还可以为一个事件组合多个作业...