因为高超的设计理念,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返回的是偏移的字节数,所以要按照字节数来减。
最后两句是注册模块儿用的,和前面一节的用法完全相同。
最最后,用上一节的方法,讲这个模块儿插入内核就可以查看程序的运行结果了。