您的位置:首页 > 运维架构 > Linux

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

2011-07-14 11:37 246 查看
By沈东良(良少)http://blog.csdn.net/shendlLinux内核的实现,大量使用了数据结构,包括了数组、链表和散列表。其中用的最多的是双向循环链表。Linux内核使用的是自己定义的链表和散列表,简单而高效,使用方法也非常的别具一格。研究Linux内核的链表和散列表对于看懂Linux内核源代码有重要的意义。本文基于kernel2.6.39版本进行分析。

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

structlist_head{
223structlist_head*next,*prev;
224};
225
226structhlist_head{
227structhlist_node*first;
228};
229
230structhlist_node{
231structhlist_node*next,**pprev;
232};
233
list_head就是使用最为广泛的双向循环链表。这个数据结构可以说是LinuxKernel的基石,大量内核源代码使用了这个数据结构。hlist_head和hlist_node常常用于散列表中。

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

初始化双向循环链表,只有一个元素的双向循环链表,next和prev指向自身。staticinlinevoidINIT_LIST_HEAD(structlist_head*list)25{26list->next=list;27list->prev=list;28}29初始化散列表的链表。空的散列表链表的first==NULL。每一个散列表链表的元素初始化时next和pprev指针都是NULL,而不是指向自身。我们可以看到,散列表链表hlist_node虽然和双向循环链表list_head一样,都有两个指针,但有本质的区别。散列表链表hlist_node不是循环链表。它有头和尾,是单向的链表。散列表链表hlist_node之所以有两个指针,是为了提高插入和删除链表的效率。hlist_node的插入,只需要一个相邻的hlist_head或者hlist_node节点即可。它的删除,只需要它本身即可定位其相邻的前后两个元素。570571#defineHLIST_HEAD_INIT{.first=NULL}572#defineHLIST_HEAD(name)structhlist_headname={.first=NULL}573#defineINIT_HLIST_HEAD(ptr)((ptr)->first=NULL)574staticinlinevoidINIT_HLIST_NODE(structhlist_node*h)575{576h->next=NULL;577h->pprev=NULL;578}579

脱离链表的元素的状态

staticinlinevoid__list_add(structlist_head*new,38structlist_head*prev,39structlist_head*next)40{41next->prev=new;42new->next=next;43new->prev=prev;44prev->next=new;45}46/*80*Deletealistentrybymakingtheprev/nextentries81*pointtoeachother.82*83*Thisisonlyforinternallistmanipulationwhereweknow84*theprev/nextentriesalready!85*/86staticinlinevoid__list_del(structlist_head*prev,structlist_head*next)87{88next->prev=prev;89prev->next=next;90}9192/**93*list_del-deletesentryfromlist.94*@entry:theelementtodeletefromthelist.95*Note:list_empty()onentrydoesnotreturntrueafterthis,theentryis96*inanundefinedstate.97*/98#ifndefCONFIG_DEBUG_LIST99staticinlinevoid__list_del_entry(structlist_head*entry)100{101__list_del(entry->prev,entry->next);102}103104staticinlinevoidlist_del(structlist_head*entry)105{106__list_del(entry->prev,entry->next);107entry->next=LIST_POISON1;108entry->prev=LIST_POISON2;109}110#else散列表链表的脱离链表代码90staticinlinevoid__hlist_del(structhlist_node*n)591{592structhlist_node*next=n->next;593structhlist_node**pprev=n->pprev;594*pprev=next;595if(next)596next->pprev=pprev;597}598599staticinlinevoidhlist_del(structhlist_node*n)600{601__hlist_del(n);602n->next=LIST_POISON1;603n->pprev=LIST_POISON2;604}605看看LIST_POISON1和LIST_POISON2是何方神圣。1617/*18*Thesearenon-NULLpointersthatwillresultinpagefaults19*undernormalcircumstances,usedtoverifythatnobodyuses20*non-initializedlistentries.21*/22#defineLIST_POISON1((void*)0x00100100+POISON_POINTER_DELTA)23#defineLIST_POISON2((void*)0x00200200+POISON_POINTER_DELTA)24表示链表元素是未初始化的,既不在链表中,也没有经过初始化,不应该使用。

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

在list_head双向循环链表的迭代中删除元素的方法,见我之前写的《遍历Linuxkernel的链表时删除元素的方法》一文,地址:/article/1672140.html散列表链表也有同样的问题:665pos是当前元素666#definehlist_for_each(pos,head)\667for(pos=(head)->first;pos&&({prefetch(pos->next);1;});\668pos=pos->next)669多了一个n元素,保存当前元素的下一个元素,这样就可以避免删除当前元素后,无法继续迭代的问题了。670#definehlist_for_each_safe(pos,n,head)\671for(pos=(head)->first;pos&&({n=pos->next;1;});\672pos=n)673

链表元素的使用方式和container_of宏

LinuxKernel的链表使用方式和其他一般的链表的使用方式大相径庭!一般的链表,总是有一个链表结构体,它的内部有指针指向实际的对象。一般是这样子的:structnode{structnode*next;structnode*prev;void*data;};然后有一些操作函数,负责使用这样的链表元素实现链表。这应该算是非常标准的链表形式了。大量已有的各类语言的链表都是类似这样实现的。再回头看看LinuxKernel的链表定义:structlist_head{223structlist_head*next,*prev;224};225226structhlist_head{227structhlist_node*first;228};229230structhlist_node{231structhlist_node*next,**pprev;232};咦!实际的数据元素呢?这种链表有什么用?一堆指针链接在一起,却没有数据,有个毛用!:-)确实挺奇怪的,放眼全球各类语言,也没有哪一个数据结构库的链表是这样子的!我们看看kernel是怎样使用这些链表的:structinode{736/*RCUpathlookuptouchesfollowing:*/737umode_ti_mode;738uid_ti_uid;739gid_ti_gid;740conststructinode_operations*i_op;741structsuper_block*i_sb;742743spinlock_ti_lock;/*i_blocks,i_bytes,maybei_size*/744unsignedinti_flags;745structmutexi_mutex;746747unsignedlongi_state;748unsignedlongdirtied_when;/*jiffiesoffirstdirtying*/749750structhlist_nodei_hash;751structlist_headi_wb_list;/*backingdevIOlist*/752structlist_headi_lru;/*inodeLRUlist*/753structlist_headi_sb_list;754union{755structlist_headi_dentry;756structrcu_headi_rcu;757};758unsignedlongi_ino;759atomic_ti_count;760unsignedinti_nlink;761…...这些链表的实例成了数据结构的属性!是的,kernel的链表的使用方式和一般的链表是相反的。不是数据在链表内,而是链表在数据内!inode的i_hash散列表链表,把inode实例链接到一个全局的inode散列表中,fs/inode.c文件定义了一些操作散列表的方法:/**432*__insert_inode_hash-hashaninode433*@inode:unhashedinode434*@hashval:unsignedlongvalueusedtolocatethisobjectinthe435*inode_hashtable.436*437*Addaninodetotheinodehashforthissuperblock.438*/439void__insert_inode_hash(structinode*inode,unsignedlonghashval)440{441structhlist_head*b=inode_hashtable+hash(inode->i_sb,hashval);442443spin_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);450451/**452*remove_inode_hash-removeaninodefromthehash453*@inode:inodetounhash454*455*Removeaninodefromthesuperblock.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的操作函数,对inode的hlist_node类型属性进行了处理。这样就把具有相同hash值的inode的i_hash链接到了一个链表上。回到我们的老问题,我们怎么样才能从链表元素找到包括该链表元素的真实对象呢?毕竟真实对象才是我们的链表真正需要链接的东西。再看一个函数:1082/*1083*searchtheinodecacheforamatchinginodenumber.1084*Ifwefindone,thentheinodenumberwearetryingto1085*allocateisnotuniqueandsoweshouldnotuseit.1086*1087*Returns1iftheinodenumberisunique,0ifitisnot.1088*/1089staticinttest_inode_iunique(structsuper_block*sb,unsignedlongino)1090{1091structhlist_head*b=inode_hashtable+hash(sb,ino);1092structhlist_node*node;1093structinode*inode;10941095spin_lock(&inode_hash_lock);1096hlist_for_each_entry(inode,node,b,i_hash){1097if(inode->i_ino==ino&&inode->i_sb==sb){1098spin_unlock(&inode_hash_lock);1099return0;1100}1101}1102spin_unlock(&inode_hash_lock);11031104return1;1105}1106hlist_for_each_entry是怎么把inode找到的?/***hlist_for_each_entry-iterateoverlistofgiventype*@tpos:thetype*touseasaloopcursor.类型的指针类型的循环变量,也就是真正的值。*@pos:the&structhlist_nodetouseasaloopcursor.循环链表的变量*@head:theheadforyourlist.表头*@member:thenameofthehlist_nodewithinthestruct.类型内的成员*/#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-getthestructforthisentry346*@ptr:the&structlist_headpointer.347*@type:thetypeofthestructthisisembeddedin.348*@member:thenameofthelist_structwithinthestruct.349*/350#definelist_entry(ptr,type,member)\351container_of(ptr,type,member)352664#definehlist_entry(ptr,type,member)container_of(ptr,type,member)

谜底在就在container_of宏:

606/**607*container_of-castamemberofastructureouttothecontainingstructure608*@ptr:thepointertothemember.609*@type:thetypeofthecontainerstructthisisembeddedin.610*@member:thenameofthememberwithinthestruct.611*612*/613#definecontainer_of(ptr,type,member)({\614consttypeof(((type*)0)->member)*__mptr=(ptr);\615(type*)((char*)__mptr-offsetof(type,member));})616这里使用了C语言隐秘的技巧:
606consttypeof(((type*)0)->member)*__mptr=(ptr);
(type*)0把地址0强制类型转换为包含hlist_node链表元素的结构体的指针类型。我们知道地址0是不允许使用的,会造成崩溃。但是这里并没有给地址0开头的数据赋值,所以没有关系。然后求其->member成员的类型,得到了一个hlist_node*类型的临时变量,给它赋值,就是该链表元素的地址。14#ifndefoffsetof15#defineoffsetof(TYPE,MEMBER)((size_t)&((TYPE*)0)->MEMBER)16#endif17这里,故伎重演,利用地址0进行强制类型转换,转换成结构体类型的指针,然后求i_hash成员的地址。成员地址-0就得到了该成员相对于结构体实例的距离。实际的链表元素的地址-链表元素在结构体内的相对偏移,就得到了该结构体实例的地址。然后(type*)强制类型转换。606tpos=hlist_entry(pos,typeof(*tpos),member);1;});这个赋值语句,被宏替换,变成:606tpos=container_of(pos,typeof(*tpos),member);1;});又被宏替换,成了:606tpos=({consttypeof(((typeof(*tpos)*)0)->member)*__mptr=(pos);(typeof(*tpos)*)((char*)__mptr-offsetof(typeof(*tpos),member));})
	这就是最后得到的C语句。	这里左式是待赋值的变量,右式是一个语句块!怎么可以呢?	确实可以!	这里()把语句块包围起来,使整个语句块能够参与表达式。右式的返回值是语句块内最后一个表达式的值。		至此,我们能够从结构体内的链表元素属性,反向得到结构体实例!LinuxKernel双向循环链表和散列表链表的谜底揭开了!

LinuxKernel如何实现散列表

前文中,我们说了,hlist_head和hlist_node这两个数据结构是用来实现散列表的。我把它们称为散列表链表,因为它们实现的是散列表的一部分:链表。散列表,逻辑上是<hash,value>这样一个表格形式的数据对的集合。让我们先考虑一个散列表的伪实现:如,考虑一个散列表查询函数的原型示例:void*lookup(void*obj,inthash);这个函数首先通过hash在散列表中搜索,然后会得到[0,1...n]个value。因为hash可能会出现冲突,多个不同对象的hash值相同。然后可以根据查询对象的其他特征进行匹配,找出真正需要的对象。如obj的name,uuid,或者就是obj的地址。如果找到,返回hashtable内的对象的指针。如果找不到返回NULL。由此,我们想到如何构造一个散列表:1,一个数组,元素是hlist_head对象。hlist_head指向一个散列表链表。2,hash就是数组的索引。hash值相同的对象,存入相同的hlist_head链表。LinuxKernel的散列表就是这样实现的。

实例Inode-cache

LinuxKernel文件系统中,inode存放在一个全局的散列表Inode-cache中,代码在fs/inode.c中:staticstructhlist_head*inode_hashtable__read_mostly;1628inode_hashtable=1629alloc_large_system_hash("Inode-cache",1630sizeof(structhlist_head),1631ihash_entries,163214,1633HASH_EARLY,1634&i_hash_shift,1635&i_hash_mask,16360);1637它使用内核启动时保留的内存分配了一个数据。/**searchtheinodecacheforamatchinginodenumber.*Ifwefindone,thentheinodenumberwearetryingto*allocateisnotuniqueandsoweshouldnotuseit.*如果是唯一的,返回1。也就是说inode_hash表里没有数据。否则不是唯一的,返回0。也就是说表里有这个inode。*Returns1iftheinodenumberisunique,0ifitisnot.*/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超级块的地址相同,并且inode的i_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超级块对象的地址是否相同,以及inode的i_ino是否相同决定散列表中的inode对象是否就是我们正在寻找的对象。inode可以通过i_sb和i_ino在逻辑上唯一定位。

实例Dentrycache

LinuxKernel的dentry也保存在一个散列表Dentrycache中,代码在fs/dcache.c:102staticstructhlist_bl_head*dentry_hashtable__read_mostly;1033003dentry_hashtable=3004alloc_large_system_hash("Dentrycache",3005sizeof(structhlist_bl_head),3006dhash_entries,300713,3008HASH_EARLY,3009&d_hash_shift,3010&d_hash_mask,30110);3012·3013for(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

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: