linux内核系列(2)——linux内核链表

因为高超的设计理念,linux内核中的链表被很多人津津乐道。实际上,链表本身只是内核提供的一组结构体、宏定义和函数的集合,与linux内核本身没有直接关系。内核链表的设计思路已经在另一篇博客中写了,有兴趣的同学可以去看一下,这里是链接

今天,我们来看一下真实内核链表的基本操作过程。我们的目标是做一个内核模块儿,在模块儿内使用内核提供的链表,完成构建节点,插入节点,排序节点,输出节点等一系列操作,最后在退出时释放链表节点。

在介绍业务之前,先来看一下内核链表的组成

struct list_head 
{
	struct list_head *next;
	struct list_head *prev;
};
可见,内核提供的是一个双向链表,每个节点里面只有两个指向自身类型的指针。

为了模拟场景,我们假设有5个名为cat的结构体是我们的链表节点,其基本定义如下:

struct cat
{
	int wight;
	char colour[32];
	char name[32];
};
为了形成链表,我们将内核链表节点插入到业务结构体中

struct cat
{
	int wight;
	enum colour;
	char name[32];
	struct list_head list;
};
我们还是来先看一下代码,然后再解释

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/list.h>

MODULE_LICENSE("GPL");

//业务结构体
struct cat
{
	int wight;
	char colour[32];
	char name[32];
	struct list_head list;
};
struct cat cats[] = 
{
	{12, "Blue", "Tom"},
	{8, "Gray", "Jim"},
	{10, "Yellow", "Jack"},
	{9, "Black", "Garfield"},
	{10, "None", "Kitty"}
};
static LIST_HEAD(cat_list); //定义一个头结点

static int insert_five_cats(void)
{
	int i = 0;
	for(i=0; i<5; i++)
		list_add(&(cats[i].list), &cat_list); //插入节点
	return 0;
}
static int sort_five_cats(void)
{
	struct list_head *rig;
	strcut list_head *lef;
	for(rig=cat_list.prev; rig!=cat_list.next; rig=rig->prev)
	{
		for(lef=cat_list.next; lef!=rig; lef=lef->next)
		{
			if(list_entry(lef, struct cat, list)->weight > \
				list_entry(lef->next, struct cat, list)->weight)
			{
				list_move(lef, lef->next);	
			}
		}
	}
	return 0;
}
static int print_five_cats(void)
{
	struct list_head *p;
	list_for_each(p, &cat_list)
	{
		printk("%d %s %s", \
			list_entry(p, struct cat, list)->weight,\
 			list_entry(p, struct cat, list)->colour,\
			list_entry(p, struct cat, list)->name);
	}
	return 0;
}
static int lkp_init(void)
{
	insert_five_cats();
	printd("printing before sort...\n");
	print_five_cats();
	sort_five_cats();
	printd("printing after sort...\n");
	print_five_cats();
	return 0;
}

static void lkp_exit(void)
{
	printk("cat program has finished!\n");
}

module_init(lkp_init);
module_exit(lkp_exit);
首先,为了要使用内核链表,我们必须包含头文件<linux/list.h>。

#include <linux/list.h>

LIST_HEAD宏是在list.h中定义的,用于定义内核链表的头节点,加一个static表示该定义只在当前文件有效

static LIST_HEAD(cat_list);
这个宏的具体定义如下

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

#define LIST_HEAD(name) \
	struct list_head name = LIST_HEAD_INIT(name)
把它展开一下,就可以知道是什么意思了。

然后,在插入函数中我们遇到了第一个内核链表函数

list_add(&(cats[i].list), &cat_list); //插入节点
这个函数的作用是将第一个参数(新的业务节点中的链表节点),插入到第二个参数(已经在链表中的节点)后面。
在讲排序函数之前,我们有必要来看一下内核链表的内存分布,这样有利于理解这个函数的运行。直接上图


图中,橘色的节点为链表头结点,可见,链表是一个带头结点的双向循环链表,在这里我们用冒泡排序法来完成排序动作,已经排好的节点放在最右边,这个指针有rig指针指示,而遍历用的指针用lef指示。

在操作过程中,用到了另一个宏

list_entry(lef, struct cat, list)
这个宏是内核链表经典的宏之一,它的作用是将一个链表节点地址转换为业务节点地址,具体实现是通过另一个宏container_of实现的,这个比较经典,我们展开讲一下

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

#define list_entary(ptr, type, member) \
	container_of(ptr, type, member)
这个宏比较复杂,首先,来看一下它的参数:第一个是一个指针,在我们的函数中,它就是内嵌的节点指针;第二个是一个类型名,在这里,它是我们的业务节点类型;第三个是链表节点在业务节点中的名字。然后,这个宏它由两句组成第一句是定义了一个名为__mptr的指针并将其赋值为宏参数ptr,第二句是将上面定义的指针减去了一个参数然后返回为业务类型。先说第一句,分析后可知道定义的类型是链表节点类型,也就是ptr的类型,即__mptr指向了业务节点中的链表节点。后面一句,是将这个地址减去了链表节点中指针节点相对于业务节点首地址的偏移量,因为是减去,所以实际上是向前移了,则减去后的值正好是业务节点的首地址。至于为什么要用__mptr作为中间变量转换一下,目前还没有研究明白,自己揣测可能是因为习惯性写法,先用临时变量接收一下,防止改变prt的值。有明白的大神还请多多赐教。后面转char*比较好理解,就是因为offset返回的是偏移的字节数,所以要按照字节数来减。

最后两句是注册模块儿用的,和前面一节的用法完全相同。

最最后,用上一节的方法,讲这个模块儿插入内核就可以查看程序的运行结果了。


一、linux内核链表 1、普通链表的数据区域的局限性 之前定义数据区域时直接int data,我们认为我们的链表中需要存储的是一个int类型的数。但是实际上现实编程中链接中的节点不可能这么简单,而是多种多样的。 一般实际项目中的链表,节点中存储的数据其实是一个结构体,这个结构体中包含若干的成员,这些成员加起来构成了我们的节点数据区域。 2、一般性解决思路:即把数据区封装为一个结构体 (1)因为链表实际解决的问题是多种多样的,所以内部数据区域的结构体构成也是多种多样的。 这样也导致了不同程序当中的链表总体构成是多种多样的。 我们无法通过一套泛性的、普遍适用的操作函数来访问所有的链表,意味着我们设计一个链表就得写一套链表的操作函数(节点创建、插入、删除、遍历……)。 (2)实际上深层次分析会发现 不同的链表虽然这些方法不能通用需要单独写,但是实际上内部的思路和方法是相同的,只是函数的局部地区有不同。 实际上链表操作是相同的,而涉及到数据区域的操作就有不同 (3)问题 能不能有一种办法把所有链表中操作方法里共同的部分提取出来用一套标准方法实现,然后把不同的部分留着让具体链表的实现者自己去处理。 3、内核链表的设计思路 (1)内核链表中实现一个纯链表的封装,以及纯链表的各种操作函数 纯链表就是没有数据区域,只有前后向指针; 各种操作函数是节点创建、插入、删除、遍历。 这个纯链表本身自己没有任何用处,它的用法是给我们具体链表作为核心来调用。 4、list.h文件简介 (1)内核中核心纯链表的实现在include/linux/list.h文件中 (2)list.h中就是一个纯链表的完整封装,包含节点定义和各种链表操作方法。 二、内核链表的基本算法和使用简介 1、内核链表的节点创建、删除、遍历等 2、内核链表的使用实践 (1)问题:内核链表只有纯链表,没有数据区域,怎么使用? 使用方法是将内核链表作为将来整个数据结构的结构体的一个成员内嵌进去。类似于公司收购,实现被收购公司的功能。 这里面要借助container_of宏。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值