学习Linux优化技巧:掌握hlist使用方法 (linux hlist 使用)

Linux作为一个自由开源的操作系统,无论在服务器、嵌入式设备还是普通PC上都有着广泛的应用,但由于其内核庞大复杂,未经优化的系统往往会导致系统性能低下和资源占用高等问题。而hlist是Linux内核中优化链表的一项工具,能够有效地提高Linux内核的搜索效率,本文将对hlist使用方法进行介绍,帮助大家更好地进行Linux优化。

一、hlist的基本概念

hlist是Linux中的一种双向链表,它在数据结构处理上有许多优势,其中主要包括以下两个方面:

1.快速定位:相对于普通双向链表,hlist具有更快的定位能力,通过将节点的HASH地址存储在节点类别中,可以实现快速定位节点。这一利用HASH地址的优点是可以防止重复节点,极大地提高了搜索效率。

2.高效插入和删除:hlist是基于内核链表实现的高效插入和删除操作,内核链表的优点在于可以将特殊的event和time事件以双向链表的形式按照优先级进行排序,在同步读写时也具有良好的互操作性。

二、hlist的使用方法

在Linux内核中,hlist有多种使用方式,可以通过定义结构体使用hlist来维护节点,同时也可以通过一些函数实现链表的一些基础操作。

2.1 hlist结构体的定义

在Linux中,可以通过定义一个hlist_node类型的结构体作为节点,并通过以下方式将hlist_node类型维护起来。

“`

struct hlist_node {

struct hlist_node *next, **pprev;

};

“`

其中,next是指向下一个节点的指针,pprev是一个指向指针的指针,指向上一个节点的结构体的next指针。

而通过以下方式定义一个hlist_head类型的结构体作为链表头:

“`

struct hlist_head {

struct hlist_node *first;

};

“`

其中,first是一个指向节点的指针,它指向了哈希桶的之一个节点。因此可以通过这样的方式对链表进行便捷的遍历、添加和删除等操作。

2.2.hlist中的基本操作

2.2.1、节点的插入

下面是一个简单示例,说明如何使用hlist将一个节点插入到链表中:

“`

#include

“`c

struct node{

int key;

struct hlist_node node;

};

int mn()

{

struct node p = { 0 }, *pos;

struct hlist_head head = { 0 };

//初始化p节点

p.key = 1;

//将结构体中的节点添加到链表中

//hlist_add_head是hlist提供的一个添加头节点的函数

hlist_add_head(&p.node, &head);

//遍历链表,打印出元素的值

hlist_for_each_entry(pos, &head, node)

printf(“%d\n”, pos->key);

return 0;

}

“`

可以看到,在示例中,首先定义了一个node结构体,它包含了一个int类型的key变量和一个hlist_node结构体。然后通过hlist_add_head()函数将其添加到链表中。而每一个被hlist维护的结构体,都应该包含一个hlist_node类型的成员变量。

2.2.2、节点的删除

下面是一个简单示例,说明如何使用hlist将一个节点从链表中删除:

“`

#include

“`c

struct node{

int key;

struct hlist_node node;

};

int mn()

{

struct node p[3] = { 0 }, *pos;

struct hlist_head head = { 0 };

int i;

//初始化3个节点并添加到链表中

for(i=0;i

p[i].key = i + 1;

hlist_add_head(&p[i].node, &head);

}

//将第2个节点从链表中删除

hlist_del(&p[1].node);

//遍历链表,打印出元素的值

hlist_for_each_entry(pos, &head, node)

printf(“%d\n”, pos->key);

return 0;

}

“`

可以看到,在示例中,通过两次调用hlist_add_head()将3个节点添加到链表中,然后使用hlist_del()函数将第2个节点从链表中删除。

三、hlist的实例应用

本篇文章介绍了hlist的基本概念和使用方法,hlist的应用范围非常广泛,可以应用于很多优化场景,比如系统内核的优化和高效的事件处理,这里只介绍一个基于hlist实现的LINUX内核模块,希望能为读者提供一些参考。

3.1 内核模块的实现

以下为一个基于hlist实现的Linux内核模块示例

“`c

#include

#include

#include

#include

MODULE_LICENSE(“Dual BSD/GPL”);

/* 定义一个结构体

* 包括:一个字符数组和一个linux内核hlist的头结构体h

*/

struct id_t{

char name[20];

struct hlist_head h;

};

/* hash桶数量*/

#define HASH_NUM 5

/* 声明id数组 */

static struct id_t ids[HASH_NUM];

/* 初始化哈希列表 */

static void init_id_list(void)

{

int i;

struct id_t *ide;

/* 分配内存空间并初始化 */

for(i=0;i HASH_NUM;i++){

ide = &ids[i];

memset(ide,0,sizeof(struct id_t));

INIT_HLIST_HEAD(&ide->h); //初始化hlist头部

}

}

/* 删除id列表 */

static void delete_id_list(void)

{

int i;

struct id_t *ide, *tmp;

struct hlist_node *next;

/* 循环遍历哈希桶并删除 */

for(i=0;i HASH_NUM;i++){

ide = &ids[i];

hlist_for_each_entry_safe(ide, next, &ide->h, h){

hlist_del(&ide->h);

kfree(ide);

}

}

}

/* 向哈希列表中添加一个节点 */

static void add_id_list(struct id_t *p)

{

struct hlist_head *head; /*哈希桶*/

head = &(ids[p->name[0] % HASH_NUM].h); //使用名字的之一个字母进行哈希值的匹配

hlist_add_head(&p->h, head); //添加节点到hlist

}

/* 在哈希列表中查找指定的节点 */

static struct id_t *search_id_list(const char *name)

{

struct id_t *p = NULL;

struct hlist_head *head; /*哈希桶*/

struct hlist_node *npos; /*链表节点*/

head = &(ids[name[0] % HASH_NUM].h); //通过哈希值匹配到哈希桶中的位置

hlist_for_each(npos,head){ //遍历hlist列表

p = hlist_entry(npos, struct id_t, h); //将链表节点nm转化为数据data

if (!strcmp(p->name,name)) /*比较字符串是否相等*/

return p;

}

return NULL;

}

/* Linux内核框架中的初始化函数*/

static int __init set_obj_init(void)

{

struct id_t *p;

init_id_list();

p = kmalloc(sizeof(*p), GFP_KERNEL);

if (!p)

return -ENOMEM;

memset(p, 0, sizeof(*p));

strncpy(p->name,”hello”,20); //设置名字

printk(KERN_INFO “set_obj_init : add one item!\n”); //告诉内核初始化完成

add_id_list(p); //添加节点

return 0;

}

/*删除内核框架,释放申请的内存空间 */

static void __exit set_obj_exit(void)

{

printk(KERN_INFO “set_obj_exit : goodby!\n”); //告诉内核模块被卸载

delete_id_list(); //删除节点

}

module_init(set_obj_init);

module_exit(set_obj_exit);

“`

在本示例中,我们创建了具有两个成员变量的结构体:一个字符数组和一个hlist头结构。我们在init_id_list()初始化函数中分配空间并初始化hlist_head h,接下来我们使用add_id_list()函数将节点添加到哈希表中,使用search_id_list()函数来查找已添加的节点。在set_obj_init()函数中设置节点名称,并在set_obj_exit()中删除节点释放所有空间。

四、

本文着重介绍了Linux内核中的hlist,它是linux中非常重要的一种数据结构,与其他链表相比,它具有快速的定位能力和高效的操作,能够有效地提高系统性能。在实际应用中,我们可以利用hlist实现各种链表优化工具,如事件处理、内核调度和内存管理等,同时也希望本次介绍的内核层次的Linux模块示例能为读者提供一定的参考。

相关问题拓展阅读:

ovs 创建的网桥是一个tun设备么

OVS 内核模块最核心的模块就是 openvswitch.ko,它真正实现了 OVS 的交换机。交换机,在 Linux 里面称作 bridge,而 OVS 的交换机,称作 datapath。

OVS 内核模块代码,位于“datapath”目录下,而“datapath.c”则实现了 datapath。datapath 就是一个 bridge(交换机),它跟普通的 bridge(比如 linux 原生支持的 bridge)的区别如下:

(1)叫法不同,一个叫 datapath,一个叫 bridge。笔者一再强调 datapath 的叫法问题,就是为了想说明 datapath 没有什么特别的,它只是取了一个有点“特别”的名字而已。

(2)交换协议不同。普通 bridge,支持的银携陆二层 Ethernet 协议,而 datapath 支持的是 OpenFlow 协议(OpenFlow 协议,笔者会在后面讲述)。当然,datapath 也支持传统的二层 Ethernet 协议,不过这一点,笔者可以暂时不必在意。

我们再看一看 datapath 的数据结构,就能更好理解 datapath 的概念:(位于 datapath.h 文件中)

struct datapath {

struct rcu_head rcu;

struct list_head list_node;

/* Flow table. */

struct flow_table table;

/* Switch ports. */

struct hlist_head *ports;

/* Stats. */

struct dp_stats_percpu __percpu *stats_percpu;

/* Network namespace ref. */

possible_net_t net;

u32 user_features;

};

之一次看到这个代码的童鞋,可能有点锋顷晕,不过没关系,我们先不要管那么多,忽略掉所有其他内容,只关注重点字段:

struct flow_table table:

OpenFlow 流转发表。datapath 归根结底就是依据这转发表进行流转发,以实现 bridge(交换机)的功能。

struct hlist_head *ports:

普通 Linux bridge 上具有(绑定) port,datapath 上也能够(同时也是需要)绑定 port。这个 port 类型是 vport。至于数据结构 hlist_head,这个请忽略,只需要理隐尺解成这是一批 port 即可

请搜索微信公众号“标哥说天下”,我应该会稍晚的时候,将相关内容发表到这个公众号里。(现在还在写,还没写完)

补充一下,tun 设备,应该可以按照“端口”这个概念来理解。(请搜索微信公众号“标哥说天下”,我已经有文章对 tun 做了说明)

编写自定义函数:建立双向链表,该链表有20个结点,20个结点所需的数值由随机函数产生。 编写自定义函数:

关于# include 问题你看看这个

我将你代码含告改了点就没的错误了,代码如下:

# include

#include 橘老薯

struct Worker

{

int num;

float pay;

struct Worker *next;

};

int n;

int main()

{

struct Worker *head,*create(void);

void list(struct Worker*);

head=create();

list(head);

return 0;

}

struct Worker *create(void)

{

struct Worker *head, *p1, *p2;

printf(“create a linked list:\n”);

head=0;

n=0;

p1=p2=(struct Worker*)malloc(sizeof(struct Worker));

scanf(“%d%f”,&p1->num,&p1->pay);

while(p1->num!=0)

{

n++;

if(n==1)head=p1;

else p2->next=p1 ;

p2=p1;

p1=(struct Worker*)malloc(sizeof(struct Worker));

scanf(“%d%f”,&p1->num,&p1->pay);

}

p2->next=0;

free(p1);

return head;

}

void list(struct Worker *p)

{

printf(“圆者The linked list:\n”);

while( p!=0 )

{

printf(“\n%\t%f”,p->num,p->pay);

p=p->next;

}

}

链表是一种常用的组织有序数据的数据结构,它通过指针将一系列数据节点连接成一条数据链,是线性表的一种重要实现方式。相对于数组,链表具有更好的动态性,建立链表时无需预先知道数据总量,可以随机分配空间,可以高效地在链表中的任意位置实时插入或删除数据。链表的开销主要是访问的顺序性和组织链的空间损失。

通常链表数据结构至少应包含两个域:数据域和指针域,数据域用于铅团存储数据,指针域用于建立与下一个节点的联系。按照指针域的组织以及各个节点之间的联系形式,链表又可以分为单链表、双链表、循环链表等多种类型,下面分别给出这几类常见链表类型的示意图:

1. 单链表

图1 单链表

单链表是最简单的一类链表,它的特点是仅有一个指针域指向后继节点(next),因此,对单链表的遍历只能从头至尾(通常是NULL空指针)顺序进行。

2. 双链表

图2 双链表

通过设计前驱和后继两个指针域,双链表可以从两个方向遍历,这是它区别于单链表的地方。如果打乱前驱、后继的依赖关系,就可以构成”二叉树”;如果再让首节点的前驱指向链表尾节点、尾节点的后继指向首节点(如图2中虚线部分),就构成了循环链表;如果设计更多的指针域,就可以构成各种复杂的树状数据结构。

3. 循环链表

循环链表的特点是尾节点的后继指向首节点。前面已经给出了双循环链表的示意图,它的特点是从任意一个节点出发,沿两个方向的槐罩橘任何一个,都能找到链表中的任意一个数据。如果去掉前驱指针,就是单循环链表。

在Linux内核中使用了大量的链表结构来组织数据,包括设备列表以及各种功能模块中的数据组织。这些链表大多采用在实现的一个相当精彩的链表数据结构。本文的后继部分就将通过示例详细介绍这一数据结构的组织和使用。

回页首

二、 Linux 2.6内核链表数据结构的实现

尽管这里使用2.6内核作为讲解的基础,但实际上2.4内核中的链表结构和2.6并没有什么区别。不同之处在于2.6扩充了两种链表数据结构:链表的读拷贝更新(rcu)和HASH链表(hlist)。这两种扩展都是基于最基本的list结构,因此,本文主要介绍基本链表结构,然后再简要介绍一下rcu和hlist。

链表数据结构的定义很简单(节选自,以闷码下所有代码,除非加以说明,其余均取自该文件):

struct list_head {

struct list_head *next, *prev;

};

list_head结构包含两个指向list_head结构的指针prev和next,由此可见,内核的链表具备双链表功能,实际上,通常它都组织成双循环链表。

和之一节介绍的双链表结构模型不同,这里的list_head没有数据域。在Linux内核链表中,不是在链表结构中包含数据,而是在数据结构中包含链表节点。

在数据结构课本中,链表的经典定义方式通常是这样的(以单链表为例):

struct list_node {

struct list_node *next;

ElemTypedata;

};

因为ElemType的缘故,对每一种数据项类型都需要定义各自的链表结构。有经验的C++程序员应该知道,标准模板库中的采用的是C++ Template,利用模板抽象出和数据项类型无关的链表操作接口。

在Linux内核链表中,需要用链表组织起来的数据通常会包含一个struct list_head成员,例如在中定义了一个nf_sockopt_ops结构来描述Netfilter为某一协议族准备的getsockopt/setsockopt接口,其中就有一个(struct list_head list)成员,各个协议族的nf_sockopt_ops结构都通过这个list成员组织在一个链表中,表头是定义在中的nf_sockopts(struct list_head)。从下图中我们可以看到,这种通用的链表结构避免了为每个数据项类型定义自己的链表的麻烦。Linux的简捷实用、不求完美和标准的风格,在这里体现得相当充分。

图3 nf_sockopts链表示意图

回页首

三、 链表操作接口

1. 声明和初始化

实际上Linux只定义了链表节点,并没有专门定义链表头,那么一个链表结构是如何建立起来的呢?让我们来看看LIST_HEAD()这个宏:

#define LIST_HEAD_INIT(name) { &(name), &(name) }

#define LIST_HEAD(name) struct list_head name = LIST_HEAD_INIT(name)

当我们用LIST_HEAD(nf_sockopts)声明一个名为nf_sockopts的链表头时,它的next、prev指针都初始化为指向自己,这样,我们就有了一个空链表,因为Linux用头指针的next是否指向自己来判断链表是否为空:

static inline int list_empty(const struct list_head *head)

{

return head->next == head;

}

除了用LIST_HEAD()宏在声明的时候初始化一个链表以外,Linux还提供了一个INIT_LIST_HEAD宏用于运行时初始化链表:

#define INIT_LIST_HEAD(ptr) do { \

(ptr)->next = (ptr); (ptr)->prev = (ptr); \

} while (0)

我们用INIT_LIST_HEAD(&nf_sockopts)来使用它。

2. 插入/删除/合并

a) 插入

对链表的插入操作有两种:在表头插入和在表尾插入。Linux为此提供了两个接口:

static inline void list_add(struct list_head *new, struct list_head *head);

static inline void list_add_tail(struct list_head *new, struct list_head *head);

因为Linux链表是循环表,且表头的next、prev分别指向链表中的之一个和最末一个节点,所以,list_add和list_add_tail的区别并不大,实际上,Linux分别用

__list_add(new, head, head->next);

__list_add(new, head->prev, head);

来实现两个接口,可见,在表头插入是插入在head之后,而在表尾插入是插入在head->prev之后。

假设有一个新nf_sockopt_ops结构变量new_sockopt需要添加到nf_sockopts链表头,我们应当这样操作:

list_add(&new_sockopt.list, &nf_sockopts);

从这里我们看出,nf_sockopts链表中记录的并不是new_sockopt的地址,而是其中的list元素的地址。如何通过链表访问到new_sockopt呢?下面会有详细介绍。

b) 删除

static inline void list_del(struct list_head *entry);

当我们需要删除nf_sockopts链表中添加的new_sockopt项时,我们这么操作:

list_del(&new_sockopt.list);

被剔除下来的new_sockopt.list,prev、next指针分别被设为LIST_POSITION2和LIST_POSITION1两个特殊值,这样设置是为了保证不在链表中的节点项不可访问–对LIST_POSITION1和LIST_POSITION2的访问都将引起页故障。与之相对应,list_del_init()函数将节点从链表中解下来之后,调用LIST_INIT_HEAD()将节点置为空链状态。

c) 搬移

Linux提供了将原本属于一个链表的节点移动到另一个链表的操作,并根据插入到新链表的位置分为两类:

static inline void list_move(struct list_head *list, struct list_head *head);

static inline void list_move_tail(struct list_head *list, struct list_head *head);

例如list_move(&new_sockopt.list,&nf_sockopts)会把new_sockopt从它所在的链表上删除,并将其再链入nf_sockopts的表头。

d) 合并

除了针对节点的插入、删除操作,Linux链表还提供了整个链表的插入功能:

static inline void list_splice(struct list_head *list, struct list_head *head);

假设当前有两个链表,表头分别是list1和list2(都是struct list_head变量),当调用list_splice(&list1,&list2)时,只要list1非空,list1链表的内容将被挂接在list2链表上,位于list2和list2.next(原list2表的之一个节点)之间。新list2链表将以原list1表的之一个节点为首节点,而尾节点不变。如图(虚箭头为next指针):

图4 链表合并list_splice(&list1,&list2)

当list1被挂接到list2之后,作为原表头指针的list1的next、prev仍然指向原来的节点,为了避免引起混乱,Linux提供了一个list_splice_init()函数:

static inline void list_splice_init(struct list_head *list, struct list_head *head);

该函数在将list合并到head链表的基础上,调用INIT_LIST_HEAD(list)将list设置为空链。

3. 遍历

遍历是链表最经常的操作之一,为了方便核心应用遍历链表,Linux链表将遍历操作抽象成几个宏。在介绍遍历宏之前,我们先看看如何从链表中访问到我们真正需要的数据项。

a) 由链表节点到数据项变量

我们知道,Linux链表中仅保存了数据项结构中list_head成员变量的地址,那么我们如何通过这个list_head成员访问到作为它的所有者的节点数据呢?Linux为此提供了一个list_entry(ptr,type,member)宏,其中ptr是指向该数据中list_head成员的指针,也就是存储在链表中的地址值,type是数据项的类型,member则是数据项类型定义中list_head成员的变量名,例如,我们要访问nf_sockopts链表中首个nf_sockopt_ops变量,则如此调用:

list_entry(nf_sockopts->next, struct nf_sockopt_ops, list);

这里”list”正是nf_sockopt_ops结构中定义的用于链表操作的节点成员变量名。

list_entry的使用相当简单,相比之下,它的实现则有一些难懂:

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

container_of宏定义在中:

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

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

(type *)( (char *)__mptr – offsetof(type,member) );})

offsetof宏定义在中:

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

size_t最终定义为unsigned int(i386)。

这里使用的是一个利用编译器技术的小技巧,即先求得结构成员在与结构中的偏移量,然后根据成员变量的地址反过来得出属主结构变量的地址。

container_of()和offsetof()并不仅用于链表操作,这里最有趣的地方是((type *)0)->member,它将0地址强制”转换”为type结构的指针,再访问到type结构中的member成员。在container_of宏中,它用来给typeof()提供参数(typeof()是gcc的扩展,和sizeof()类似),以获得member成员的数据类型;在offsetof()中,这个member成员的地址实际上就是type数据结构中member成员相对于结构变量的偏移量。

如果这么说还不好理解的话,不妨看看下面这张图:

图5 offsetof()宏的原理

对于给定一个结构,offsetof(type,member)是一个常量,list_entry()正是利用这个不变的偏移量来求得链表数据项的变量地址。

b) 遍历宏

在的nf_register_sockopt()函数中有这么一段话:

……

struct list_head *i;

……

list_for_each(i, &nf_sockopts) {

struct nf_sockopt_ops *ops = (struct nf_sockopt_ops *)i;

……

}

……

函数首先定义一个(struct list_head *)指针变量i,然后调用list_for_each(i,&nf_sockopts)进行遍历。在中,list_for_each()宏是这么定义的:

#define list_for_each(pos, head) \

for (pos = (head)->next, prefetch(pos->next); pos != (head); \

pos = pos->next, prefetch(pos->next))

它实际上是一个for循环,利用传入的pos作为循环变量,从表头head开始,逐项向后(next方向)移动pos,直至又回到head(prefetch()可以不考虑,用于预取以提高遍历速度)。

那么在nf_register_sockopt()中实际上就是遍历nf_sockopts链表。为什么能直接将获得的list_head成员变量地址当成struct nf_sockopt_ops数据项变量的地址呢?我们注意到在struct nf_sockopt_ops结构中,list是其中的之一项成员,因此,它的地址也就是结构变量的地址。更规范的获得数据变量地址的用法应该是:

struct nf_sockopt_ops *ops = list_entry(i, struct nf_sockopt_ops, list);

大多数情况下,遍历链表的时候都需要获得链表节点数据项,也就是说list_for_each()和list_entry()总是同时使用。对此Linux给出了一个list_for_each_entry()宏:

#define list_for_each_entry(pos, head, member)……

与list_for_each()不同,这里的pos是数据项结构指针类型,而不是(struct list_head *)。nf_register_sockopt()函数可以利用这个宏而设计得更简单:

……

struct nf_sockopt_ops *ops;

list_for_each_entry(ops,&nf_sockopts,list){

……

}

……

某些应用需要反向遍历链表,Linux提供了list_for_each_prev()和list_for_each_entry_reverse()来完成这一操作,使用方法和上面介绍的list_for_each()、list_for_each_entry()完全相同。

如果遍历不是从链表头开始,而是从已知的某个节点pos开始,则可以使用list_for_each_entry_continue(pos,head,member)。有时还会出现这种需求,即经过一系列计算后,如果pos有值,则从pos开始遍历,如果没有,则从链表头开始,为此,Linux专门提供了一个list_prepare_entry(pos,head,member)宏,将它的返回值作为list_for_each_entry_continue()的pos参数,就可以满足这一要求。

4. 安全性考虑

在并发执行的环境下,链表操作通常都应该考虑同步安全性问题,为了方便,Linux将这一操作留给应用自己处理。Linux链表自己考虑的安全性主要有两个方面:

a) list_empty()判断

基本的list_empty()仅以头指针的next是否指向自己来判断链表是否为空,Linux链表另行提供了一个list_empty_careful()宏,它同时判断头指针的next和prev,仅当两者都指向自己时才返回真。这主要是为了应付另一个cpu正在处理同一个链表而造成next、prev不一致的情况。但代码注释也承认,这一安全保障能力有限:除非其他cpu的链表操作只有list_del_init(),否则仍然不能保证安全,也就是说,还是需要加锁保护。

b) 遍历时节点删除

前面介绍了用于链表遍历的几个宏,它们都是通过移动pos指针来达到遍历的目的。但如果遍历的操作中包含删除pos指针所指向的节点,pos指针的移动就会被中断,因为list_del(pos)将把pos的next、prev置成LIST_POSITION2和LIST_POSITION1的特殊值。

当然,调用者完全可以自己缓存next指针使遍历操作能够连贯起来,但为了编程的一致性,Linux链表仍然提供了两个对应于基本遍历操作的”_safe”接口:list_for_each_safe(pos, n, head)、list_for_each_entry_safe(pos, n, head, member),它们要求调用者另外提供一个与pos同类型的指针n,在for循环中暂存pos下一个节点的地址,避免因pos节点被释放而造成的断链。

回页首

四、 扩展

1. hlist

图6 list和hlist

精益求精的Linux链表设计者(因为list.h没有署名,所以很可能就是Linus Torvalds)认为双头(next、prev)的双链表对于HASH表来说”过于浪费”,因而另行设计了一套用于HASH表应用的hlist数据结构–单指针表头双循环链表,从上图可以看出,hlist的表头仅有一个指向首节点的指针,而没有指向尾节点的指针,这样在可能是海量的HASH表中存储的表头就能减少一半的空间消耗。

因为表头和节点的数据结构不同,插入操作如果发生在表头和首节点之间,以往的方法就行不通了:表头的first指针必须修改指向新插入的节点,却不能使用类似list_add()这样统一的描述。为此,hlist节点的prev不再是指向前一个节点的指针,而是指向前一个节点(可能是表头)中的next(对于表头则是first)指针(struct list_head **pprev),从而在表头插入的操作可以通过一致的”*(node->pprev)”访问和修改前驱节点的next(或first)指针。

2. read-copy update

在Linux链表功能接口中还有一系列以”_rcu”结尾的宏,与以上介绍的很多函数一一对应。RCU(Read-Copy Update)是2.5/2.6内核中引入的新技术,它通过延迟写操作来提高同步性能。

我们知道,系统中数据读取操作远多于写操作,而rwlock机制在p环境下随着处理机增多性能会迅速下降(见参考资料4)。针对这一应用背景,IBM Linux技术中心的Paul E. McKenney提出了”读拷贝更新”的技术,并将其应用于Linux内核中。RCU技术的核心是写操作分为写-更新两步,允许读操作在任何时候无阻访问,当系统有写操作时,更新动作一直延迟到对该数据的所有读操作完成为止。Linux链表中的RCU功能只是Linux RCU的很小一部分,对于RCU的实现分析已超出了本文所及,有兴趣的读者可以自行参阅本文的参考资料;而对RCU链表的使用和基本链表的使用方法基本相同。

#include “stdio.h”

#include “stdlib.h”

#include “time.h”

/* 双向链表结点结构 */

typedef struct node

{

int data;/* 数据域 */

struct node *previor; /* 前驱指针 */

struct node *next;/* 后继指针 */

} DLNode;

/* 函数声明区 */

void Init(DLNode *head);

void Create(DLNode *head, int size);

int Remove(DLNode *head, int data);

void Travel1(DLNode *head);

void Travel2(DLNode *head);

void main()

{

int size = 20;/* 链表大小 */

int num;

DLNode *head = (DLNode *)malloc(sizeof(DLNode)); /* 初始化头结点 */

head->data = 0;

head->previor = NULL;

head->next = NULL;

Create(head, size); /* 创建双向链表 */

printf(“正向输出双向链表 : \n”);

Travel1(head); /* 遍历双向链表 */

printf(“逆向输出双向链表滚茄并 : \n”);

Travel2(head); /* 遍历双向链纳迅表 */

printf(“remove : “);

scanf(“%d”, &num);

if(Remove(head, num) == 1) /* 删除双向链表中的结点 */

{

printf(“正向输出删除结点后的双向链表:\n”);

Travel1(head);

printf(“逆向输出删除结点后的双向链表:\n”);

Travel2(head);

}

else

{

printf(“remove node fail.\n”);

}

}

/* 初始化双向链表 */

void Init(DLNode *head)

{

head = (DLNode *)malloc(sizeof(DLNode));

head->data = 0;

head->previor = NULL;

head->next = NULL;

}

/* 创建双向链表 */

void Create(DLNode *head, int size)

{

int i;

DLNode *p, *q;

int num;

if(head == NULL)

{

Init(head);

}

q = head;

srand(time(NULL));

for(i=0; idata = num;

p->previor = q;

p->next = NULL;

q->next = p;

q = p;

}

}

/* 删除非递减有序双向链表中的结点,删除成功返回1,否则返回0 */

int Remove(DLNode *head, int key)

{

DLNode *p, *q;

if(head==NULL || head->next==NULL)

{

return 0;

}

for(q=head,p=head->next; p!=NULL && p->data!=key; q=p,p=p->next);

if(p==NULL)

return 0;

q->next = p->大迹next;

if(p->next != NULL)

{

p->next->previor = q;

}

free(p);

return 1;

}

/* 正向遍历双向链表 */

void Travel1(DLNode *head)

{

DLNode *p;

for(p=head->next; p!=NULL; p=p->next)

{

printf(“%d “, p->data);

}

printf(“\n”);

}

/* 逆向遍历双向链表 */

void Travel2(DLNode *head)

{

DLNode *p;

for(p=head->next; p->next!=NULL; p=p->next);

for(; p!=head; p=p->previor)

{

printf(“%d “, p->data);

}

printf(“\n”);

}

#include

#include

#include

typedef struct _list {

int n;

struct _list *next;

struct _list *prior;

} Link, *PLink;

void dlstore(PLink i,PLink *start,PLink *last)

{

if(!*last)*last=*start=i;

else(*last)->next=i;

i->next=NULL;

i->prior=*last;

if(i==*start)i->prior=NULL;

*last=i;

}

void dldelete(PLink i,PLink *start,PLink *last)

{

if(i->prior)i->prior->next=i->next;

else{

*start=i->next;

if(*start)(*start)->prior=NULL;

}

if(i->next)i->next->prior=i->prior;

else *last=i->prior;

}

int main()

{

int i;

PLink l,start=0,last=0,*t;

srand(time(0));

for(i=0;in=rand()%100;

printf(“%d “,l->n);

dlstore(l,&start,&last);

}

printf(“\n”);

printf(“橡枣方向(Y):”信如改);

i=getchar();

if(i==’Y’||i==’y’)t=&start;

else t=&last;

for(i=0;in);

dldelete((*t),&start,&last);

}

printf(“\n”);

linux hlist 使用的介绍就聊到这里吧,感谢你花时间阅读本站内容,更多关于linux hlist 使用,学习Linux优化技巧:掌握hlist使用方法,ovs 创建的网桥是一个tun设备么,编写自定义函数:建立双向链表,该链表有20个结点,20个结点所需的数值由随机函数产生。 编写自定义函数:的信息别忘了在本站进行查找喔。


数据运维技术 » 学习Linux优化技巧:掌握hlist使用方法 (linux hlist 使用)