每天看三页《深入Linux内核架构》——Linux内核中双链表与散列表的源码分析与使用说明

最初读到链表部分,因为Linux中内置的链表函数与普通c语言链表构造不同而心生疑惑,最近完全看明白了,来做个总结。本贴内容来源于:

C语言基本功 (三)-迷墙人-ChinaUnix博客

《深入Linux内核架构》P16-P17 P974~P978

以及自己的一些理解,如有侵权请作者联系我删除本帖。

学习双链表时也同时看了下面两个帖子,非常详尽明了,推荐作为本贴的补充。

(51条消息) linux 内核链表的实现_Louis@L.M.的博客-CSDN博客_linux内核链表实现

双向链表及创建(C语言)详解 (biancheng.net)

其中双链表与散列表的结构与内置函数大致相同,首先介绍一下双链表:

A.双链表

一. Linux中双链表与普通双链表的区别

一个普通的双链表节点结构用c语言实现为:

struct line {

struct line * prev ; //指向前一元素

int data1 ;

...

struct line * next ; //指向后一元素

} * line1;

普通的链表在数据结构中嵌入链表指针(例:struct line)。而Linux在内核中定义了链表元素(或称链表结点) list_head。对于需要使用链表的数据结构,将链表元素嵌入数据结构(例:task_struct)中,并将该数据结构(task_struct)称为宿主结构。

链表元素:

<include/linux/list.h>中:

struct list_head{
struct list_head *next, *prev ; //指针next指向后一个链表元素,指针prev指向前一个链表元素
} ;

对于需要使用链表的数据结构,该链表元素可以如下放置到数据结构中:

struct task_struct{
int data1;//存储的数据1号
...
struct list_head  run_list;//链表元素
}  * task1 ;

由此带来的,普通的双链表结构与Linux双链表结构在使用上的区别是:

  1. 普通链表通常除了对其数据结构进行了定义,还定义了若干对该结构的操作。但对于大量使用链表的linux内核来说,如果定义了一个结构就要定义其相关的操作的话,显然代码量不小。 为了提高效率,内核采用了一套通用的、一般的,可以用到各种不同数据结构的队列操作,即struct list_head。(采用链表元素的原因)

  1. Linux内核经常需要一个实例放置到多个链表上,此时只需在数据结构中放置多个链表元素,在调用内置双链表函数时,通过链表元素名称区分不同链表指针。(便捷性)

  1. 对于数据结构struct line,line1可以直接通过line1->next->data1访问下一节点中的数据。但对于数据结构task_struct,task1->run_list仅能访问到下一节点中的链表元素,而无法访问到下一节点中的数据信息。(需要解决的问题)

二. Linux内核中,通过链表元素访问宿主:container_of ( ptr, type, member) (******重要******)

下面介绍一下上述问题的解决方式。Linux内核中定义了list_entry函数用于返回指定链表元素对应的宿主地址。list_entry定义如下:

<include/linux/list.h>中:

#define list_entry (ptr , type , member) \
             container_of (ptr , type , member)

其中,ptr为指向链表元素的指针,type用于指定宿主对象的类型(例struct task_struct),member用于指定宿主对象中链表元素的成员名(例 run_list )。

container_of 定义如下:

<include/linux/kernel.h>中:

#define offsetof(TYPE, MEMBER)  ((size_t) &((TYPE *)0)->MEMBER)

解释:

offsetof(TYPE, MEMBER) 用于计算MEMBER成员相对于TYPE起始位置的偏移量。计算过程为,

(size_t) &((TYPE *)0)->MEMBER 分为4步:

  1. (TYPE *)0 :将地址0强制转换为TYPE 结构的指针

  1. ((TYPE *)0)->MEMBER :访问该结构的MEMBER成员

  1. &((TYPE *)0)->MEMBER:取出该结构的MEMBER成员的地址

  1. (size_t) &((TYPE *)0)->MEMBER:将地址转换为size_t 类型( size_t 类型是C标准库中定义的一个与机器相关的unsigned类型,其大小足以保证存储内存中对象的大小。)

总结:该方法的精妙之处在于,将0强制类型转换为TYPE *。在结构TYPE起始地址为0的前提下,MEMBER成员的地址就是MEMBER成员的偏移地址。

<include/linux/kernel.h>中:

/**
 * container_of 通过某数据结构的某成员(如链表元素)来获得,包含该成员的数据结构(该链表元素的宿主)  的    实例
 * @ptr:指向成员数据(如链表元素)的指针
 * @type:数据结构(宿主)的类型
 * @member:成员在数据结构(宿主)中的成员名
 */
#define container_of ( ptr,  type,  member)  ({
const typeof ( ( ( type * ) 0 )->member) *_mptr = (ptr) ;                \
( type * ) ( ( char * )_mptr -offsetof ( type,member ) ); } )

解释:container_of ( ptr, type, member) 用于在已知某数据结构某成员地址,数据结构类型,成员名的基础上,返回指向该数据结构实例的指针。分为2步:

  1. const typeof ( ( ( type * ) 0 )->member) *_mptr = (ptr) ; 将0强制转换为type *类型,并访问成员member,把_mptr定义成member类型的const指针,并把_mptr初始化为ptr的值。

  1. ( type * ) ( ( char * )_mptr -offsetof ( type,member ) ); 分为三步:

2.1. ( char * )_mptr :为确保指针运算加减的步长是一个字节,将指针_mptr强制转换为 char * 类 型。

2.2. ( char * )_mptr -offsetof ( type,member ) :使用偏移量offsetof ( type,member ) 来移动 _mptr,使之不再指向链表元素,而是指向数据结构(宿主)实例。

2.3. ( type * ) ( ( char * )_mptr -offsetof ( type,member ) ); 最后将指针强制转换为type * 类型。

三. Linux内核中的其他双链表操作

1.定义并初始化头结点

struct list_head{
struct list_head *next, *prev ;//指针next指向后一个链表元素,指针prev指向前一个链表元素 
} ;
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}

LIST_HEAD(name):定义并初始化头结点。

static inline void INIT_LIST_HEAD(struct list_head *list) :通过内联函数的方式初始化头结点。

2.对链头head链入一个新队列new

所有对链表的操作(增加、删除、移动、判空)都是对链表元素list_head进行的。增加链表结点可以使用将新结点添加到链表结点head之的list_add函数,以及添加到链表结点head之的list_add_tail函数。因为head为链表头的话,head前一项为链表的末尾(tail),所以取tail之意。

两个函数的定义如下:

<include/linux/list.h>中:

/*
 * 在两个连续的链表项之间添加一个新项。
 * 该函数只用于链表内部处理,适用于已经知道前后两个链表项prev/next的情况
 */
static inline void __list_add(struct list_head *new, 
                              struct list_head *prev, 
                              struct list_head *next)
{
        next->prev = new;
        new->next = next;
        new->prev = prev;
        prev->next = new;
}
/**
 * list_add:添加一个新的链表项
 * @new:需要添加的新链表项
 * @head: 添加在head链表项之后
 * 
 * 在指定的链表元素head之后插入一个新的链表项new。这对实现栈是有用的。
 */
static inline void list_add ( struct list_head *new , static list_head *head)
{
        __list_add ( new , head , head->next ) ;
}
/**
 * list_add_tail:用于在head元素之前,紧接着插入new元素。如果指定head为表头,由于链表是循环的,那么new
 * 元素就插入到链表的末尾(该函数因此而得名)
 * @new:需要添加的新链表项
 * @head: 添加在head链表项之前
 */
static inline void list_add_tail(struct list_head *new,
                                 struct list_head *head)
{
    __list_add(new, head->prev, head);
}

其中__list_add()为底层函数,“__”通常表示该函数是底层函数,供其他模块调用,此处实现了较好的代码复用。

3.删除某一链表结点

如果要删除链表某一结点,可以使用函数list_del或list_del_init。需要注意的是上述函数仅仅能够把链表元素从链表中去掉。而用户需要自己负责释放链表对应的数据结构所占用的内存空间。该空间也是最初由用户分配的。

<include/linux/list.h>中:

#define LIST_POISON1 ((void *) 0x00100100)
#define LIST_POISON2 ((void *) 0x00200200)

/*
*  __list_del :通过使链表项的prev/next 指向彼此,来删除一个链表项。
*              这只用于内部的链表处理,这种情况下,链表项的prev/next项都是已知的!
* @ prev:待删除的结点的前面一个结点
* @ next:待删除的结点的后面一个结点
*/
static inline void __list_del (struct list__head * prev , struct list_head * next)
{
        next->prev = prev;
        prev->next = next;
}

/**
 * list_del:从链表删除一个链表结点。同时将entry所指向的结点指针域封死。
 * @entry:需要从链表删除的链表结点。
 * 请注意:此后,对该链表项调用list_empty并不返回true,此时链表项处于未定义状态。
 */
static inline void list_del (struct list_head *entry)
{
       __list_del (entry->prev, entry->next) ;
       entry->next = LIST_POSITION1 ;
       entry->prev = LIST_POSITION2 ;
}

对LIST_POISON1,LIST_POISON2的解释说明: Linux 内核中解释:These are non-NULL pointers that will result in page faults under normal circumstances, used to verify that nobody uses non-initialized list entries.书中解释:删除项的next和prev 指针指向的两个LIST_POISON 值用于调试,以便在内存中检测已经删除的链表元素。

/**
 * 删除entry所指向的结点,同时调用LIST_INIT_HEAD()把被删除节点为作为链表头构建一个新的空双循环链表。
 */
static inline void list_del_init(struct list_head *entry)
{
      __list_del(entry->prev, entry->next);
      INIT_LIST_HEAD(entry);
}

4.移动节点

移动节点的作用是:第一步,将某一链表结点从原链表中删除,并使原链表中的前向后向结点相连。第二步,将刚删除的单一链表结点,接入新链表中。

Linux提供了两个移动操作函数:list_move和list_move_tail。

/*
 * list_move:将list结点从原链表中删除,使原链表中的前后向结点相连。并将list结点添加在head结点之后。
*/
static inline void list_move(struct list_head *list, struct list_head *head)
{
      __list_del(list->prev, list->next);
      list_add(list, head);
}
/*
 * list_move_tail:将list结点从原链表中删除,使原链表中的前后向结点相连。并将list结点添加在head结点之前。
*/
static inline void list_move_tail(struct list_head *list, struct list_head *head)
{
      __list_del(list->prev, list->next);
      list_add_tail(list, head);
}

5.链表判空

Linux中由list-head构成的双向循环链表中,通常有一个头节点,其不含有有效信息, 初始化时prev和next都指向自身。判空操作是判断除了头节点外是否有其他节点。使用函数list_empty或list_empty_careful(更严谨的判断函数)测试链表是否为空。

如果是只有一个结点,判空函数会返回1,表示空;但这个空不是没有任何结点,而是只有一个头结点, 因为头节点只是纯粹的list节点,没有有效信息,故认为为空。

static inline int list_empty(const struct list_head *head)
{
      return head->next == head;
}

判空函数的更严格版本:

static inline int list_empty_careful(const struct list_head *head)
{
      struct list_head *next = head->next;
      return (next == head) && (next == head->prev);
}

注意:list_empty_careful里的empty list是指只有一个空的头结点,而不是毫无任何结点。并且该头结点必须其head->next==head->prev==head。

6.链表拼接

Linux还支持两个链表的拼接,提供给用户的具体函数是list_splice和list_splice_init:

底层函数:

/*
 * __list_splice:将非空链表list插入到另一个现存链表head之后。list结点被视为头结点被删除。
*/
static inline void __list_splice(struct list_head *list,
                                 struct list_head *head)
{
    struct list_head *first = list->next;
    struct list_head *last = list->prev;
    struct list_head *at = head->next;
    
    first->prev = head;
    head->next = first;
    
    last->next = at;
    at->prev = last;
}

用户接口函数,增加了判断list非空:

static inline void list_splice(struct list_head *list,
                               struct list_head *head)
{
    if (!list_empty(list))
        __list_splice(list, head);
}

list_splice_init在list_splice基础上增加了,将list结点的*prev和*next都指向自身:

static inline void list_splice_init(struct list_head *list,
                                    struct list_head *head)
{
    if (!list_empty(list)){
        __list_splice(list, head);
        INIT_LIST_HEAD(list);//结点初始化,使list的*prev和*next都指向自身
    }
}

7.遍历链表(******重要******)

遍历是双循环链表的基本操作,为此Linux定义了一些宏。 list_for_each遍历链表中的所有list_head节点,不涉及到对宿主结构的处理。list_for_each实际是一个 for 循环,利用传入的指向list_head结构的指针pos作为循环变量,从链表头开始(并跳过链表头),逐项向后移动指针,直至又回到链表头。

/*
 * list_for_each:遍历链表的所有元素
 * @pos: 链表中的当前位置
 * @head: 指定了表头
*/
#define list_for_each(pos, head) /
    for (pos = (head)->next; prefetch(pos->next), pos != (head); /
        pos = pos->next)

head为头节点,遍历过程中首先从(head)->next开始,当pos==head时退出,故head节点并没有访问,这和list 结构设计有关,通常头节点就是纯粹的list结构,不含有其他有效信息,或者头节点含有其他信息,因此头节点只是作为双向链表遍历一遍的检测标志。

prefetch语句向编译器提供信息,说明哪些数据应该优先从内存传输到处理器的高速缓存中。

-----asm-x86_64/processor.h---prefetch()---------

static inline void prefetch(void *x)
{
    asm volatile("prefetcht0 %0" :: "m" (*(unsigned long *)x));
}

将x指针作强制类型转换为unsigned long *型,然后取出该内存操作数,送入高速缓存。

上述list_for_each函数的使用示例:

struct task_struct{
...
    struct list_head run_list;
...
}

struct list_head *p;
list_for_each(p,&list) //list为一个链表头
    if (condition)
    return list_entry(p, struct task_struct, run_list);//返回该链表结点对应的实例的指针
return NULL;

list_for_each()有prefetch()用于复杂的表的遍历,而__list_for_each()无prefetch()用于简单的表的遍历,此时表项比较少,无需缓存。

#define __list_for_each(pos, head) /
    for (pos = (head)->next; pos != (head); pos = pos->next)

反向遍历结点:

#define list_for_each_prev(pos, head) /
    for (pos = (head)->prev; prefetch(pos->prev), pos != (head); /         pos = pos->prev)

如果在遍历过程中,包含有删除或移动当前链表节点的操作,由于这些操作会修改遍历指针,这样会导致遍历的中断。这种情况下,必须使用list_for_each_safe宏,在操作之前将遍历指针缓存下来:

/*
 * list_for_each_safe - iterate over a list safe against removal of list entry
 */
#define list_for_each_safe(pos, n, head) /
    for (pos = (head)->next, n = pos->next; pos != (head); /
        pos = n, n = pos->next)

在for循环中n暂存pos下一个节点的地址,避免因pos节点被释放而造成的断链。也就是说你可以遍历完当前节点后将其删除,同时可以接着访问下一个节点,遍历完毕后就只剩下一个头节点。这就safe。十分精彩。典型用途是多个进程等待在同一个等待队列上,若事件发生时唤醒所有进程,则可以唤醒后将其依次从等待队列中删除。

8. 遍历宿主对象(******重要******)

如果只提供对list_head结构的遍历操作是远远不够的,我们希望实现的是对宿主结构的遍历,即在遍历时直接获得当前链表节点所在的宿主结构项,而不是每次要同时调用list_for_each和list_entry。对此,Linux提供了list_for_each_entry()宏,第一个参数为传入的遍历指针,指向宿主数据结构,第二个参数为链表头,为list_head结构,第三个参数为list_head结构在宿主结构中的成员名。

<include/linux/list.h>中:

/*
* list_for_each_entry: 遍历给定类型的链表
* @pos: 是一个宿主数据类型的变量,用作宿主的循环游标
* @head:list_head类型,作为链表的表头
* @member:宿主内部list_head实例的名称
*/
#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 ) )

结合一个使用例程,来介绍上述宏。例程用来遍历与一个超级块(struct super_block)相关联的所有文件(通过 struct file表示)。这些struct file实例包含在一个双链表中,表头位于struct super_block实例中,如下所示:

<fs.h>中,关于示例所用数据结构的定义:

//数据所在数据结构
struct file {
struct list_head  f_list
...
};

//表头所在数据结构
struct super_block {
...
struct  list_head  s_files;
...
};

遍历宿主对象的示例程序:

struct super_block *sb = get_some_sb();
struct file *f;
list_for_each_entry(f, &sb->s_files, f_list){
      /*  用于处理f中成员的代码  */
}

为方便理解,重新写一下list_for_each_entry(pos, head, member) 宏:

#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 ) )

上下对比可知,f 为一个struct file类型的空指针,struct file中存储着有效数据。f相当于 pos 。 链表头存储在struct super_block类型的指针sb中,&sb->s_files 对应于链表头head。f_list为存储有效数据的宿主struct file内嵌的链表元素的成员名,对应于member,即用于指出f_list为struct file中内嵌的结点。

list_for_each_entry(pos, head, member)宏的解释:最开始的时候head指向链表头,但是链表头是不包含有效信息的。因此(head )->next使链表指针跳过链表头,直接指向下一个链表结点,该结点也是第一个存储了有效信息的宿主内嵌的链表结点。用 (head)->next, pos->member.next ( 即f->f_list.next )来在内嵌于结构体中的链表结点中遍历。 每遍历一个结点,就用pos = list_entry ( (head )->next, typeof ( * pos ) , member ); 函数返回 (head )->next 结点所在的宿主的指针,并赋值给pos。在用户补充的循环体中可以通过pos访问链表对应的宿主数据结构。prefetch ( pos->member.next)用于指定哪些数据应该优先从内存传输到处理器的高速缓存中。&pos->member != (head) :将宿主数据结构pos的链表结点与头结点比较作为结束循环的条件,当循环回链表头时停止循环。

当遍历到头节点时,此时并没有pos这样一个typeof(*pos)类型的数据指针,而是以member域强制扩展了一个typeof(*pos)类型的pos指针,此时其member域的地址就是head指针所指向的头节点,遍历结束,头节点的信息没有被访问。

注意:宏的所有代码都位于for循环的循环入口(循环条件)中。在宏具体使用之前,没有添加循环体。宏的用途在于,逐项将当前链表元素所在的宿主的指针保存到pos中,使得其在循环体中可用。

倒着遍历的版本,分析与前类似。

#define list_for_each_entry_reverse(pos, head, member) /
    for (pos = list_entry((head)->prev, typeof(*pos), m+ember); /
        prefetch(pos->member.prev), &pos->member != (head); /
        pos = list_entry(pos->member.prev, typeof(*pos), member))

内核中的list_prepare_entry()宏,用来防止pos为空,搭配函数list_for_each_entry_continue使用:

#define list_prepare_entry(pos, head, member) /
    ((pos) ? : list_entry(head, typeof(*pos), member))

分析:注意,冒号 :前面是个空值,表示不进行操作。即:若pos不为空,则pos为其自身。等效于: (pos)? (pos): list_entry(head,typeof(*pos),member) 注意内核格式::前后都加了空格。

#define list_for_each_entry_continue(pos, head, member) /
    for (pos = list_entry(pos->member.next, typeof(*pos), member); /
        prefetch(pos->member.next), &pos->member != (head); /
        pos = list_entry(pos->member.next, typeof(*pos), member))

此时不是从头节点开始遍历的,但仍然是以头节点为结束点的,即没有遍历完 整个链表。 要注意并不是从pos开始的,而是从其下一个节点开始的,因为第一个有效pos是从pos->member.next扩展得的。

下面是两个安全版本的遍历宏。务必注意,如果在循环中有删除项(或把项从一个链表移动到另一个链表)的操作,由于这些操作会修改遍历指针,这样会导致遍历的中断。这种情况下,必须使用安全版本的宏,在操作之前将遍历指针缓存下来。

安全版本的从链表头遍历宏:

/**
 * list_for_each_entry_safe - iterate over list of given type safe against removal of list entry
 * @pos: the type * to use as a loop counter.
 * @n: another type * to use as temporary storage
 * @head: the head for your list.
 * @member: the name of the list_struct within the struct.
 */
#define list_for_each_entry_safe(pos, n, head, member) /
    for (pos = list_entry((head)->next, typeof(*pos), member), /
        n = list_entry(pos->member.next, typeof(*pos), member); /
        &pos->member != (head); /
        pos = n, n = list_entry(n->member.next, typeof(*n), member))

它们要求调用者另外提供一个与pos同类型的指针n,在for循环中暂存pos下一个节点的地址,避免因pos节点被释放而造成的断链。

安全版本的从指定的pos->member.next开始遍历宏:

#define list_for_each_entry_safe_continue(pos, n, head, member) /
    for (pos = list_entry(pos->member.next, typeof(*pos), member), /
        n = list_entry(pos->member.next, typeof(*pos), member); /
        &pos->member != (head); /
        pos = n, n = list_entry(n->member.next, typeof(*n), member))

9.双链表使用示例

#include "list.h"
struct kool_list{
    int to;
    struct list_head list;
    int from;
};
int main(int argc, char **argv){
    struct kool_list *tmp;
    struct list_head *pos, *q;
    unsigned int i;
    struct kool_list mylist;
    INIT_LIST_HEAD(&mylist.list);
    /* 您也可以使用宏LIST_HEAD(mylist)来声明并初始化这个链表 */
    /*向链表中添加元素*/
    for(i=5; i!=0; --i){
        tmp= (struct kool_list *)malloc(sizeof(struct kool_list));
        /*INIT_LIST_HEAD(&tmp->list); 调用这个函数将初始化一个动态分配的list_head。也可以不调用它,因为在后面调用的add_list()中将设置next和prev域。*/
        printf("enter to and from:");
        scanf("%d %d", &tmp->to, &tmp->from);
        /*将tmp添加到mylist链表中*/
        list_add(&(tmp->list), &(mylist.list));
        /*也可以使用list_add_tail()将新元素添加到链表的尾部。*/
    }
    printf("/n");
    /*现在我们得到了数据结构struct kool_list的一个循环链表,我们将遍历这个链表,并打印其中的元素。*/
    /*list_for_each()定义了一个for循环宏,第一个参数用作for循环的计数器,换句话说,在整个循环过程中它指向了当前项的list_head。第二个参数是指向链表头的指针,在宏中保持不变。*/
    printf("traversing the list using list_for_each()/n");
    list_for_each(pos, &mylist.list){
        /*此刻:pos->next指向了下一项的list变量,而pos->prev指向上一项的list变量。而每项都是 struct kool_list类型。但是,我们需要访问的是这些项,而不是项中的list变量。因此需要调用list_entry()宏。*/
        tmp= list_entry(pos, struct kool_list, list);
        /*给定pos:指向struct list_head的指针,struct kool_list:它所属的宿主数据结构的类型,以及list:它在宿主数据结构中的名称,list_entry返回指向宿主数据结构的指针。例如,在上面一行, list_entry()返回指向pos所属struct kool_list项的指针。*/
        printf("to= %d from= %d/n", tmp->to, tmp->from);
    }
    printf("/n");
    /* 因为这是一个循环链表,我们也可以向前遍历。只需要将list_for_each替换为list_for_each_prev。我们也可以使用list_for_each_entry()遍历链表,在给定类型的项间进行循环。例如:*/
    printf("traversing the list using list_for_each_entry()/n");
    list_for_each_entry(tmp, &mylist.list, list)
    printf("to= %d from= %d/n", tmp->to, tmp->from);
    printf("/n");
    /*下面将释放这些项。因为我们调用list_del()从链表中删除各项,因此需要使用list_for_each()宏的"安全"版本,即 list_for_each_safe()。务必注意,如果在循环中有删除项(或把项从一个链表移动到另一个链表)的操作,必须使用这个宏。*/
    printf("deleting the list using list_for_each_safe()/n");
    list_for_each_safe(pos, q, &mylist.list){
        tmp= list_entry(pos, struct kool_list, list);
        printf("freeing item to= %d from= %d/n", tmp->to, tmp->from);
        list_del(pos);
        free(tmp);
    }
    return 0;
}

注意:上述代码在使用gcc编译时需要加上__KERNEL__定义。

B.散列表(hlist哈希链表)

散列表是双链表的修改版本。在list.h中分别有双链表list与散列表(哈希链表)hlist的定义,可以对比一下:

<list.h>

struct list_head {
    struct list_head *prev, *next;
} ;

struct hlist_head {
    struct hlist_node *first ;
} ;

struct hlist_node {
    struct hlist_node *next ,  **pprev ;
} ;

像list_head这样具有两个元素(prev、next)的表头对于hash表来说过于浪费。因此Linux又创建了hash表专用的hlist结构——单表头双循环的链表结构。与list相比,hlist的表头(hlist_head)仅有一个指向第一个元素的指针,而没有指向最后一个元素的指针,对于可能被海量使用的hash表来说hlist_head表头节省了一半的资源。由于hlist的表头只有一个指向第一个元素的指针,链表的末端无法再用常数时间访问,但对散列表来说这通常是不需要的。

如A节中讲到的,list链表在链表元素(list_head)中定义了prev成员,作为指向前一项链表元素的指针。但hlist却无法延用上述list的方法来实现前向元素的访问,因为在hlist中链表头与链表元素的类型不同,前一项的元素既可能是链表头(struct hlist_head类型)也可能是链表元素(struct hlist_node类型),无法定义一个指针能同时指向它们。hlist_node不得不改用pprev成员。由于first和next指针的类型是相同的,因此 hlist_node中定义了成员struct hlist_node **pprev用于指向前向链表元素中的next(也可能是first)指针。从而在表头插入的操作中,可以通过一致的“ *(node->pprev) ”访问和修改前节点的next(或first)指针。

操作散列表时,可以使用与双链表相似的API,唯一的差别在于需要将“list” 替换为 “hlist”。例如,list_add_head将变成hlist_add_head , list_del将变成hlist_del。

可以使用RCU机制来防止针对散列表的并发访问。如果需要这样做,那么散列表操作需要加上后缀_rcu。例如:hlist_del_rcu用于删除一个散列表元素。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值