linux内核数据结构之链表

1.概述

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

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

1.1单链表

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

1.2双链表

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

1.3循环链表

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

1.4链表的内核实现

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

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

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

和前面介绍的双链表结构模型不同,这里的list_head没有数据域。在Linux内核链表中,不是在链表结构中包含数据,而是在数据结构中包含链表节点。设计哲学是:既然链表不能包含万事万物,那么就让万事万物来包含链表。

2.链表使用

2.1 链表头定义
  • 方法1
#define LIST_HEAD_INIT(name) { &(name), &(name) }

static struct list_head bsr_devs = LIST_HEAD_INIT(bsr_devs);
  • 方法2
#define LIST_HEAD(name) \
	struct list_head name = LIST_HEAD_INIT(name)
	
static LIST_HEAD(apm_user_list);
  • 方法3
struct audit_tree {
	atomic_t count;
	int goner;
	struct audit_chunk *root;
	struct list_head chunks;
	struct list_head rules;
	struct list_head list;
	struct list_head same_root;
	struct rcu_head head;
	char pathname[];
};

static inline void INIT_LIST_HEAD(struct list_head *list)
{
	list->next = list;
	list->prev = list;
}

static struct audit_tree *alloc_tree(const char *s)
{
	struct audit_tree *tree;

	tree = kmalloc(sizeof(struct audit_tree) + strlen(s) + 1, GFP_KERNEL);
	if (tree) {
		atomic_set(&tree->count, 1);
		tree->goner = 0;
		INIT_LIST_HEAD(&tree->chunks);
		INIT_LIST_HEAD(&tree->rules);
		INIT_LIST_HEAD(&tree->list);
		INIT_LIST_HEAD(&tree->same_root);
		tree->root = NULL;
		strcpy(tree->pathname, s);
	}
	return tree;
}

上面三个方法中,最常使用的还是INIT_LIST_HEAD,通常情况下头指针会定义一些关于链表的统计数据,从上面可以看出,前两种方法不适合在头指针中有数据的情况

2.2 链表操作

为了方便对链表的相关操作进行解析,下面定义链表头的结构体以及链表节点结构体

typedef struct test_list_head {
    
	unsigned int count;
	
    struct list_head list;
	
}TEST_LIST_HEAD;

typedef struct test_list{
	
	unsigned int data;
	struct list_head list;
	
} TEST_LIST;

TEST_LIST_HEAD head;

INIT_LIST_HEAD(&head.list)                                                                                                                                
2.2.1链表插入
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;
}

static inline void list_add(struct list_head *new, struct list_head *head)
{
	__list_add(new, head, head->next);
}
static inline void list_add_tail(struct list_head *new, struct list_head *head)
{
	__list_add(new, head->prev, head);
}

list_add采用的是头部插入,list_add_tail是尾部插入,list_add(&new1.list, &head.list)插入new1,结果如下

在这里插入图片描述
new2分别采用尾插法和头插法结果如下图:

头插法加入new2
在这里插入图片描述
尾插法加入new2
在这里插入图片描述
头插法和尾插法用的都是__list_add,只是传入的参数next,prev不同而已,从前面插入情况可以看出链表头指针在关于链表的操作中跟链表节点是一致的,体现了内核设计“通用性”

2.2.2链表删除
static inline void list_del(struct list_head *entry)
{
	__list_del(entry->prev, entry->next);
	entry->next = LIST_POISON1;
	entry->prev = LIST_POISON2;
}

static inline void __list_del(struct list_head * prev, struct list_head * next)
{
	next->prev = prev;
	prev->next = next;
}

采用尾插法加入new2后,执行list_del(&new2.list)的结果如下:
在这里插入图片描述

2.2.3链表判断
  • list_empty:判断链表是否为空

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

    当头指针的next域指向自身时,表示当前链表还没有链表节点

  • list_is_last:判断当前链表节点是否为最后一个节点

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

    如果当前的链表节点的next域指向head,则表明当前节点为链表的最后节点

2.2.4链表遍历

链表的遍历无疑是链表中最重要的操作,再讨论链表遍历之前,让我们先来看下如何取得链表节点的数据域。在内核链表中,利用list_entry宏,通过list_head成员的地址以及链表节点结构体倒推出链表节点的地址,从而取得节点数据,list_entry宏的定义如下:

/**
 * list_entry - get the struct for this entry
 * @ptr:	the &struct list_head pointer.
 * @type:	the type of the struct this is embedded in.
 * @member:	the name of the list_struct within the struct.
 */
#define list_entry(ptr, type, member) \
	container_of(ptr, type, member)

ptr是指针,指向链表节点中的struct list_head成员,type是链表节点的struct定义,member是链表节点中list_head成员的name。下面我们来重点看下contain_of:

#define container_of(ptr, type, member) ({			\
	const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
	(type *)( (char *)__mptr - offsetof(type,member) );})

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

typeof是gcc的扩展用法,关于具体的使用可以参看笔者的另一篇文章,typeof关键字,简单来说就是取得对应的数据类型,然后定义一个__mptr指针并用ptr进行初始化,很多同学可能觉得定义_ _mpr纯属多余,直接使用ptr,其实不然,宏定义中是不对参数类型进行检查的,这里重新定义了一个临时指针-mptr,如果ptr和-mptr的类型不匹配,编译则会报错,可见此处可强制对ptr指针进行类型检查。

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

笔者认为((type *)0)->member,这里将0地址强制转换为type 类型的指针,其中的0可以是任何地址,因为这里仅仅是为了利用typeof获取member成员的数据类型,跟地址是无关的,不过我们考虑下面的用法

&((TYPE *)0)->MEMBER

我们取得MEMBER成员以后,利用&获得它的地址,因为这里是强制将0地址转换为TYPE类型指针的,所以MEMBER的地址直接就变成了偏移量,这也就是offset_of宏的原理,如下所示:

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

利用offset_of宏,我们能够知道成员相对于结构体开始地址的偏移地址,然后利用成员地址减去偏移地址就得到结构体的地址:

#define container_of(ptr, type, member) ({			\
	const typeof( ((type *)0)->member ) *__mptr = (ptr);	\
	(type *)( (char *)__mptr - offsetof(type,member) );})

在了解到如何通过内嵌的struct 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))

list_for_each_entry从头开始遍历链表,首先利用head->next和list_entry获取第一个链表节点指针pos,下一个链表节点通过pos->member.next获取,遍历结束的条件是:

&pos->member != (head)

利用list_entry取出链表节点指针pos后,得到struct list_head member的地址后,判断是否与头指针的struct list_head 成员的地址相等,相等则停止遍历。

当我们在遍历链表的时候,如果对链表节点的操作会改变链表的连接关系时,例如利用list_del_init删除节点,那么我们就要小心了,因此内核为我们提供了list_for_each_entry_safe

/**
 * 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 cursor.
 * @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))

相比上一个宏多了safe,之所以safe,是因为利用额外传入的链表节点指针n,缓存了下一个链表节点,下面是内核中的一个例子:

	struct audit_watch *owatch, *nwatch, *nextw;
	struct audit_krule *r, *nextr;
	struct audit_entry *oentry, *nentry;
	struct audit_buffer *ab;

	mutex_lock(&audit_filter_mutex);
	list_for_each_entry_safe(owatch, nextw, &parent->watches, wlist) {
		if (audit_compare_dname_path(dname, owatch->path, NULL))
			continue;

3.例子

LIST_HEAD(test_head);

typedef struct list_info{
	
	uint32 data;
	
	struct list_head list;
	
} LIST_INFO;


static uint32 i = 1;

static ssize_t list_debug_add_one(struct device *dev,
					 struct device_attribute *attr,
					 char *buf)
{
	LIST_INFO *p = NULL;
	
	p = (LIST_INFO *)kmalloc(sizeof(struct list_info), GFP_KERNEL);
	
	if( NULL != p){
		
		p->data = i*10;
		
		printk("[%u]data:%u\r\n",i,p->data);
		
		i++;
		list_add(&p->list,&test_head);	
	}
	
	return 0;
					 
}

static ssize_t list_debug_read_all(struct device *dev,
					 struct device_attribute *attr,
					 char *buf)
{
	LIST_INFO *entry;
	
	list_for_each_entry(entry,&(test_head), list)
	{
		printk("data:%d\r\n",entry->data);
	}
	
	return 0;
					 
}

static	DEVICE_ATTR(add_one, S_IRUSR, list_debug_add_one,NULL);
static	DEVICE_ATTR(read_all, S_IRUSR, list_debug_read_all,NULL);

struct attribute *list_attrs[] = {
	
	&dev_attr_add_one.attr,
	&dev_attr_read_all.attr,

	NULL,
};

struct attribute_group list_group = {
	.name  	= "list",
	.attrs  = list_attrs,
};

执行结果如下:

# cat add_one 
[1]data:10
# 
# cat add_one 
[2]data:20
# 
# cat add_one 
[3]data:30
# 
# cat read_all 
data:30
data:20
data:10
# 
# echo 10 > delete_one 
del success,data:10
# 
# cat read_all 
data:30
data:20
# 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值