分享

Linux内核中链表和散列表的实现原理揭秘

 千年长叹 2012-02-23

Linux内核中链表和散列表的实现原理揭秘

分类: Linux C/C++编程 源代码分析 编程原理 [推荐热贴] [参考手册] 开源框架 OOP和设计模式 1284人阅读 评论(7) 收藏 举报

                                                                                                               

                                                          By沈东良(良少)http://blog.csdn.net/shendl



        Linux内核的实现,大量使用了数据结构,包括了数组、链表和散列表。其中用的最多的是双向循环链表。Linux内核使用的是自己定义的链表和散列表,简单而高效,使用方法也非常的别具一格。

        研究Linux内核的链表和散列表对于看懂Linux内核源代码有重要的意义。本文基于kernel2.6.39版本进行分析。



Linux的链表和散列表定义在include/linux/types.h文件中

structlist_head {
223 struct list_head *next, *prev;
224};
225

226structhlist_head {
227 struct hlist_node *first;
228};
229

230structhlist_node {
231 struct hlist_node *next, **pprev;
232};
233

      list_head就是使用最为广泛的双向循环链表。这个数据结构可以说是LinuxKernel的基石,大量内核源代码使用了这个数据结构。

     hlist_headhlist_node常常用于散列表中。



Linux的链表和散列表的操作函数的定义在include/linux/list.h文件中

            初始化双向循环链表,只有一个元素的双向循环链表,nextprev指向自身。

staticinline voidINIT_LIST_HEAD(structlist_head *list)
25{
26list->next = list;
27list->prev = list;
28}
29



        初始化散列表的链表。

       空的散列表链表的first==NULL。每一个散列表链表的元素初始化时nextpprev指针都是NULL,而不是指向自身。

     我们可以看到,散列表链表hlist_node虽然和双向循环链表list_head一样,都有两个指针,但有本质的区别。

      散列表链表hlist_node不是循环链表。它有头和尾,是单向的链表。

       散列表链表hlist_node之所以有两个指针,是为了提高插入和删除链表的效率。hlist_node的插入,只需要一个相邻的hlist_head或者hlist_node节点即可。它的删除,只需要它本身即可定位其相邻的前后两个元素。

570
571#defineHLIST_HEAD_INIT { .first =NULL }
572#defineHLIST_HEAD(name) struct hlist_headname = { .first =NULL }
573#defineINIT_HLIST_HEAD(ptr) ((ptr)->first = NULL)
574staticinline voidINIT_HLIST_NODE(structhlist_node *h)
575{
576h->next = NULL;
577h->pprev = NULL;
578}
579

脱离链表的元素的状态

staticinline void__list_add(structlist_head *new,
38 struct list_head *prev,
39 struct list_head *next)
40{
41next->prev = new;
42new->next = next;
43new->prev = prev;
44prev->next = new;
45}
46



/*
80* Delete a list entry by making the prev/next entries
81* point to each other.
82*
83* This is only for internal list manipulation where we know
84* the prev/next entries already!
85*/
86staticinline void__list_del(structlist_head *prev, structlist_head *next)
87{
88next->prev = prev;
89prev->next = next;
90}
91





92/**
93* list_del - deletes entry from list.
94* @entry: the element to delete from the list.
95* Note: list_empty() on entry does not return true after this, the entry is
96* in an undefined state.
97*/
98#ifndefCONFIG_DEBUG_LIST
99staticinline void__list_del_entry(structlist_head *entry)
100{
101__list_del(entry->prev,entry->next);
102}
103
104staticinline voidlist_del(structlist_head *entry)
105{
106__list_del(entry->prev,entry->next);
107entry->next = LIST_POISON1;
108entry->prev = LIST_POISON2;
109}
110#else




散列表链表的脱离链表代码



90staticinline void__hlist_del(structhlist_node *n)
591{
592 struct hlist_node *next = n->next;
593 struct hlist_node **pprev = n->pprev;
594 *pprev =next;
595 if (next)
596next->pprev = pprev;
597}
598
599staticinline voidhlist_del(structhlist_node *n)
600{
601__hlist_del(n);
602n->next = LIST_POISON1;
603n->pprev = LIST_POISON2;
604}
605

        看看LIST_POISON1LIST_POISON2是何方神圣。

16
17/*
18* These are non-NULL pointers that will result in page faults
19* under normal circumstances, used to verify that nobody uses
20* non-initialized list entries.
21*/
22#defineLIST_POISON1 ((void *) 0x00100100 + POISON_POINTER_DELTA)
23#defineLIST_POISON2 ((void *) 0x00200200 + POISON_POINTER_DELTA)
24


        表示链表元素是未初始化的,既不在链表中,也没有经过初始化,不应该使用。



遍历Linuxkernel的链表时删除元素的方法

     在list_head双向循环链表的迭代中删除元素的方法,见我之前写的《遍历Linuxkernel的链表时删除元素的方法》一文,地址:http://blog.csdn.net/shendl/article/details/6397898



      散列表链表也有同样的问题:

665
pos是当前元素
666#definehlist_for_each(pos,head) \
667 for (pos = (head)->first;pos && ({prefetch(pos->next); 1; }); \
668pos =pos->next)
669多了一个n元素,保存当前元素的下一个元素,这样就可以避免删除当前元素后,无法继续迭代的问题了。
670#definehlist_for_each_safe(pos,n,head) \
671 for (pos = (head)->first;pos && ({n =pos->next; 1; }); \
672pos =n)
673


链表元素的使用方式和container_of

      LinuxKernel的链表使用方式和其他一般的链表的使用方式大相径庭!

       一般的链表,总是有一个链表结构体,它的内部有指针指向实际的对象。一般是这样子的:

struct node{

struct node * next;

struct node * prev;

void * data;

};

       然后有一些操作函数,负责使用这样的链表元素实现链表。这应该算是非常标准的链表形式了。大量已有的各类语言的链表都是类似这样实现的。

      再回头看看LinuxKernel的链表定义:

structlist_head {
223 struct list_head *next, *prev;
224};
225
226structhlist_head {
227 struct hlist_node *first;
228};
229
230structhlist_node {
231 struct hlist_node *next, **pprev;
232};

        咦!实际的数据元素呢?这种链表有什么用?一堆指针链接在一起,却没有数据,有个毛用!


       :-)确实挺奇怪的,放眼全球各类语言,也没有哪一个数据结构库的链表是这样子的!



        我们看看kernel是怎样使用这些链表的:

structinode {
736/* RCU path lookup touches following: */
737umode_ti_mode;
738uid_ti_uid;
739gid_ti_gid;
740 const struct inode_operations *i_op;
741 struct super_block *i_sb;
742
743spinlock_ti_lock;/* i_blocks, i_bytes, maybe i_size */
744 unsigned int i_flags;
745 struct mutexi_mutex;
746
747 unsigned long i_state;
748 unsigned long dirtied_when;/* jiffies of first dirtying */
749
750structhlist_nodei_hash;
751 struct list_headi_wb_list;/* backing dev IO list */
752 struct list_headi_lru;/* inode LRU list */
753 struct list_headi_sb_list;
754 union {
755 struct list_headi_dentry;
756 struct rcu_headi_rcu;
757 };
758 unsigned long i_ino;
759atomic_ti_count;
760 unsigned int i_nlink;
761

...

        这些链表的实例成了数据结构的属性!

        是的,kernel的链表的使用方式和一般的链表是相反的。不是数据在链表内,而是链表在数据内!

       inodei_hash散列表链表,把inode实例链接到一个全局的inode散列表中,fs/inode.c文件定义了一些操作散列表的方法:

/**
432* __insert_inode_hash - hash an inode
433* @inode: unhashed inode
434* @hashval: unsigned long value used to locate this object in the
435* inode_hashtable.
436*
437* Add an inode to the inode hash for this superblock.
438*/
439void__insert_inode_hash(structinode *inode, unsigned longhashval)
440{
441 struct hlist_head *b = inode_hashtable +hash(inode->i_sb,hashval);
442
443spin_lock(&inode_hash_lock);
444spin_lock(&inode->i_lock);
445hlist_add_head(&inode->i_hash,b);
446spin_unlock(&inode->i_lock);
447spin_unlock(&inode_hash_lock);
448}
449EXPORT_SYMBOL(__insert_inode_hash);
450
451/**
452* remove_inode_hash - remove an inode from the hash
453* @inode: inode to unhash
454*
455* Remove an inode from the superblock.
456*/
457voidremove_inode_hash(structinode *inode)
458{
459spin_lock(&inode_hash_lock);
460spin_lock(&inode->i_lock);
461hlist_del_init(&inode->i_hash);
462spin_unlock(&inode->i_lock);
463spin_unlock(&inode_hash_lock);
464}
465EXPORT_SYMBOL(remove_inode_hash);
466

        我们看到,这些函数调用了我们熟悉的hlist_node的操作函数,对inodehlist_node类型属性进行了处理。



         这样就把具有相同hash值的inodei_hash链接到了一个链表上。



         回到我们的老问题,我们怎么样才能从链表元素找到包括该链表元素的真实对象呢?毕竟真实对象才是我们的链表真正需要链接的东西。



       再看一个函数:

1082/*
1083* search the inode cache for a matching inode number.
1084* If we find one, then the inode number we are trying to
1085* allocate is not unique and so we should not use it.
1086*
1087* Returns 1 if the inode number is unique, 0 if it is not.
1088*/
1089static int test_inode_iunique(structsuper_block *sb, unsigned longino)
1090{
1091 struct hlist_head *b = inode_hashtable +hash(sb,ino);
1092 struct hlist_node *node;
1093 struct inode *inode;
1094
1095spin_lock(&inode_hash_lock);
1096hlist_for_each_entry(inode,node,b,i_hash) {
1097 if (inode->i_ino == ino &&inode->i_sb == sb) {
1098spin_unlock(&inode_hash_lock);
1099 return 0;
1100 }
1101 }
1102spin_unlock(&inode_hash_lock);
1103
1104 return 1;
1105}
1106


hlist_for_each_entry是怎么把inode找到的?

/**

*hlist_for_each_entry - iterate over list of given type

*@tpos: thetype * to use as a loop cursor. 类型的指针类型的循环变量,也就是真正的值。

*@pos: the&structhlist_node to use as a loop cursor.循环链表的变量

*@head: the head for your list.表头

*@member: the name of the hlist_node within thestruct.类型内的成员

*/

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


      上面的typeof(*tpos)typeofGCC的扩展,现在好像是C99的特性,用来获得对象的类型。

      这里,使用*得到指针指向的对象,然后取得类型,就得到了对象的真正类型!



          传递的是:包含list_head/hlist_node的结构体类型和对应的属性名i_hash,这些信息有什么用?

344/**
345* list_entry - get the struct for this entry
346* @ptr: the &struct list_head pointer.
347* @type: the type of the struct this is embedded in.
348* @member: the name of the list_struct within the struct.
349*/
350#definelist_entry(ptr,type,member) \
351container_of(ptr,type,member)
352

664#definehlist_entry(ptr,type,member)container_of(ptr,type,member)




谜底在就在container_of宏:

606/**
607* container_of - cast a member of a structure out to the containing structure
608* @ptr: the pointer to the member.
609* @type: the type of the container struct this is embedded in.
610* @member: the name of the member within the struct.
611*
612*/
613#definecontainer_of(ptr,type,member) ({ \
614 const typeof( ((type *)0)->member ) *__mptr = (ptr); \
615 (type *)( (char *)__mptr - offsetof(type,member) );})
616



这里使用了C语言隐秘的技巧:

   
 


606 const typeof( ((type *)0)->member ) *__mptr = (ptr);   

(type*)0把地址0强制类型转换为包含hlist_node链表元素的结构体的指针类型。

     我们知道地址0是不允许使用的,会造成崩溃但是这里并没有给地址0开头的数据赋值,所以没有关系。



        然后求其->member成员的类型,得到了一个hlist_node*类型的临时变量,给它赋值,就是该链表元素的地址。

14#ifndefoffsetof

15#defineoffsetof(TYPE,MEMBER) ((size_t) &((TYPE *)0)->MEMBER)
16#endif
17


      这里,故伎重演,利用地址0进行强制类型转换,转换成结构体类型的指针,然后求i_hash成员的地址。成员地址-0就得到了该成员相对于结构体实例的距离。

       实际的链表元素的地址-链表元素在结构体内的相对偏移,就得到了该结构体实例的地址。然后(type*)强制类型转换。

606 tpos= hlist_entry(pos,typeof(*tpos),member); 1;});

这个赋值语句,被宏替换,变成:

606 tpos=container_of(pos,typeof(*tpos),member); 1;});

又被宏替换,成了:

 


606 tpos=({ consttypeof( ((typeof(*tpos)*)0)->member ) *__mptr = (pos);(typeof(*tpos) *)( (char *)__mptr -offsetof(typeof(*tpos),member) );})



	这就是最后得到的C语句。

	这里左式是待赋值的变量,右式是一个语句块!  怎么可以呢?
	确实可以!
	这里()把语句块包围起来,使整个语句块能够参与表达式。右式的返回值是语句块内最后一个表达式的值。
	
	至此,我们能够从结构体内的链表元素属性,反向得到结构体实例!Linux Kernel双向循环链表和散列表链表的谜底揭开了!

LinuxKernel如何实现散列表

         前文中,我们说了,hlist_headhlist_node这两个数据结构是用来实现散列表的。我把它们称为散列表链表,因为它们实现的是散列表的一部分:链表。

         散列表,逻辑上是<hashvalue>这样一个表格形式的数据对的集合。


         让我们先考虑一个散列表的伪实现:

         如,考虑一个散列表查询函数的原型示例:void* lookup(void * obj,int hash);

         这个函数首先通过hash在散列表中搜索,然后会得到[0,1...n]value

        因为hash可能会出现冲突,多个不同对象的hash值相同。

         然后可以根据查询对象的其他特征进行匹配,找出真正需要的对象。如objnameuuid,或者就是obj的地址。

         如果找到,返回hashtable内的对象的指针。如果找不到返回NULL


由此,我们想到如何构造一个散列表:

1,一个数组,元素是hlist_head对象。hlist_head指向一个散列表链表。

2hash就是数组的索引。

       hash值相同的对象,存入相同的hlist_head链表。


     LinuxKernel的散列表就是这样实现的。


实例Inode-cache

      LinuxKernel文件系统中,inode存放在一个全局的散列表Inode-cache中,代码在fs/inode.c中:

static structhlist_head *inode_hashtable__read_mostly;
1628inode_hashtable =
1629alloc_large_system_hash("Inode-cache",
1630 sizeof(struct hlist_head),
1631ihash_entries,
1632 14,
1633HASH_EARLY,
1634 &i_hash_shift,
1635 &i_hash_mask,
1636 0);
1637



          它使用内核启动时保留的内存分配了一个数据。


/*

*search theinode cache for a matchinginode number.

*If we find one, then theinode number we are trying to

*allocate is not unique and so we should not use it.

*如果是唯一的,返回1。也就是说inode_hash表里没有数据。

否则不是唯一的,返回0。也就是说表里有这个inode

*Returns 1 if theinode number is unique, 0 if it is not.

*/

staticinttest_inode_iunique(structsuper_block *sb, unsignedlongino)

{

      根据sb的地址+inode的唯一号码(在一个超级块内是唯一的)得到散列值。

      structhlist_head *b = inode_hashtable + hash(sb, ino);

      structhlist_node *node;

      structinode *inode;

     锁住整个散列表

     spin_lock(&inode_hash_lock);

     hlist_for_each_entry(inode,node, b, i_hash) {

     如果i_sb超级块的地址相同,并且inodei_ino序号也相同,那么解锁整个表,返回0

if(inode->i_ino== ino && inode->i_sb== sb) {

         spin_unlock(&inode_hash_lock);

return0;

}

}

spin_unlock(&inode_hash_lock);

否则返回1

return1;

}

         上例中,如果hash相同,那么就根据inode所属的i_sb超级块对象的地址是否相同,以及inodei_ino是否相同决定散列表中的inode对象是否就是我们正在寻找的对象。

        inode可以通过i_sbi_ino在逻辑上唯一定位。


实例 Dentrycache

        LinuxKerneldentry也保存在一个散列表Dentrycache中,代码在fs/dcache.c

102static struct hlist_bl_head *dentry_hashtable__read_mostly;
103
3003dentry_hashtable =
3004alloc_large_system_hash("Dentry cache",
3005 sizeof(struct hlist_bl_head),
3006dhash_entries,
3007 13,
3008HASH_EARLY,
3009 &d_hash_shift,
3010 &d_hash_mask,
3011 0);
3012·
3013 for (loop = 0;loop < (1 <<d_hash_shift);loop++)
3014INIT_HLIST_BL_HEAD(dentry_hashtable + loop);
3015



        实现方法都一样!


PS: Blog格式有点难看,上传了PDF版本, 想要的朋友请移步: http://d.download.csdn.net/down/3441424/shendl   

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多