一、一个最简单的Linux内核模块的插入和删除
(一)内核模块的插入
执行指令vi test_hello.c创建test_hello.c文件,代码如下:
#include <linux/init.h> //init.h包含了宏__init和__exit
#include <linux/module.h> //module.h头文件包含了对模块的版本控制;
#include <linux/kernel.h> //kernel.h包含了常用的内核函数;
//模块许可声明
MODULE_LICENSE("Dual BSD/GPL");
//模块加载函数
static int hello_init(void)
{
//KERN_ALERT为消息打印级别,内核中定义:#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
printk(KERN_ALERT "hello,I am here\n");
return 0;
}
//模块卸载函数
static void hello_exit(void)
{
printk(KERN_ALERT "goodbye,kernel\n");
}
//模块注册
module_init(hello_init); //指明入口点
module_exit(hello_exit); //指明出口点
//可选
MODULE_AUTHOR("edsionte Wu");
MODULE_DESCRIPTION("This is a simple example!\n");
MODULE_ALIAS("A simplest example");
执行指令vi Makefile创建Makefile文件,代码如下:
#obj-m表示把文件test_hello.o作为"模块"进行编译,不会编译到内核,但是会生成一个独立的 "test_hello.ko" 文件
obj-m += test_hello.o
CURRENT_PATH:=$(shell pwd) #模块所在的当前所在路径
LINUX_KERNEL:=$(shell uname -r) #linux内核代码的当前版本
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL) #linux内核的当前版本源码路径
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules #编译模块
# 内核的路径 当前目录编译完放哪 表明编译的是内核模块
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean #清理模块
执行指令make进行编译,编译后,使用ls命令查看当前目录,其中test_hello.ko就是我们生成的模块
然后依次执行如下指令:
sudo insmod test_hello.ko //加载内核模块
dmesg //查看内核打印信息
指令lsmod是显示所有内核模块的信息,这里我们加上参数查看刚插入的内核模块test_hello,执行指令lsmod |grep ‘test_hello’
(二)内核模块的删除
执行指令sudo rmmod test_hello进行模块的删除,经lsmod |grep ‘test_hello’指令查找也确实没有了这个模块
执行指令make clean清除上次的make命令所产生的object文件(后缀为“.o”的文件)及可执行文件,执行ls可以看到只剩下了test_hello.c和Makefile两个文件
二、内核模块中,采用内核中的求最大数和最小数的代码求最大数和最小数,并输出。
(一)求最大值
max.c代码:
#include<linux/kernel.h>
#include<linux/module.h>
#include<linux/init.h>
/**
* 模块的初始化函数,模块的入口函数,加载模块,需超级用户权限
*/
static int __init lk_maxnum(void)
{
int x = 1, y = 2;
printk("max=%d\n", max(x++, y++));
printk("x = %d, y = %d\n", x, y);
return 0;
}
/**
* 出口函数,卸载模块,需超级用户权限
*/
static void __exit lk_exit(void)
{
printk("The maxnum moudle has exited!\n");
}
module_init(lk_maxnum); //内核入口点,调用初始化函数,包含在module.h中
module_exit(lk_exit); //出口点
MODULE_LICENSE("GPL"); //许可证
Makefile文件除了第一行.o文件名不同外其他的都是相同的,下面不再做说明
运行结果:
(二)求最小值
min.c代码:
#include<linux/kernel.h>
#include<linux/module.h>
#include<linux/init.h>
/**
* 模块的初始化函数,模块的入口函数,加载模块,需超级用户权限
*/
static int __init lk_minnum(void)
{
int x = 1, y = 2;
printk("min=%d\n", min(x++, y++));
printk("x = %d, y = %d\n", x, y);
return 0;
}
/**
* 出口函数,卸载模块,需超级用户权限
*/
static void __exit lk_exit(void)
{
printk("The minnum moudle has exited!\n");
}
module_init(lk_minnum); //内核入口点,调用初始化函数,包含在module.h中
module_exit(lk_exit); //出口点
MODULE_LICENSE("GPL"); //许可证
运行结果:
三、内核模块中,利用内核提供的API,在双链表中插入100个节点,并删除其中的10个节点,把未删除的节点输出。
do_list.c
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/list.h>
MODULE_LICENSE("GPL");
#define N 100 //链表节点数
struct numlist {
int num; //数据
struct list_head list; //指向双链表前后节点的指针
};
struct numlist numhead; //头节点
static int __init doublelist_init(void)
{
//初始化头节点
struct numlist *listnode; //每次申请链表节点时所用的指针
struct list_head *pos;
struct numlist *p;
int i;
printk("doublelist is starting...\n");
INIT_LIST_HEAD(&numhead.list);
//建立 100 个节点,依次加入到链表当中
for (i = 0; i < N; i++) {
listnode = (struct numlist *)kmalloc(sizeof(struct numlist), GFP_KERNEL);
listnode->num = i+1;
list_add_tail(&listnode->list, &numhead.list);
printk("Node %d has added to the doublelist...\n", i+1);
}
//依次删除其中的10个节点
struct list_head *n;
i = 1;
list_for_each_safe(pos, n, &numhead.list) { //为了安全删除节点而进行的遍历
if(i<=10){
list_del(pos); //从双链表中删除当前节点
p= list_entry(pos, struct numlist, list); //得到当前数据节点的首地址,即指针
kfree(p); //释放该数据节点所占空间
printk("Node %d has removed from the doublelist...\n", i++);
}
if(i==11) {break;}
}
//遍历链表,把未删除的节点输出。
i = 1;
list_for_each(pos, &numhead.list){
p = list_entry(pos, struct numlist, list);
printk("Node %d's data:%d\n", i, p->num);
i++;
}
return 0;
}
static void __exit doublelist_exit(void)
{
printk("doublelist is exiting !\n");
}
module_init(doublelist_init);
module_exit(doublelist_exit);
涉及相关源代码:
WRITE_ONCE(list->next, list)的作用就是安全地将list->next指向list!
GFP_KERNEL是linux内存分配器的标志,标识着内存分配器将要采取的行为。
运行结果:
……
……
……
……
四、内核模块传递参数,参考《Linux内核实验手册》
知识点补充:
module_param(name, type, perm)
功能:内核模块传参
参数:
@name 变量名/传参名
@type 参数的数据类型,short ,ushort(无符号短整型),int ,uint ,charp(字符指针)
@perm 权限,一般情况下,我们不需要在模块执行以后,进行参数传递,所以perm权限一般设置成0
对求最大值代码进行下修改,代码如下:
#include<linux/kernel.h>
#include<linux/module.h>
#include<linux/init.h>
/**
* 模块的初始化函数,模块的入口函数,加载模块,需超级用户权限
*/
int x=1,y=2;
module_param(x,int,0);
module_param(y,int,0);
static int __init lk_maxnum(void)
{
printk("max=%d\n", max(x++, y++));
printk("x = %d, y = %d\n", x, y);
return 0;
}
/**
* 出口函数,卸载模块,需超级用户权限
*/
static void __exit lk_exit(void)
{
printk("The maxnum moudle has exited!\n");
}
MODULE_LICENSE("GPL"); //许可证
module_init(lk_maxnum); //内核入口点,调用初始化函数,包含在module.h中
module_exit(lk_exit); //出口点
依次执行如下指令:
make
//加载的时候,如果传递参数,则变量值就是传递过来的值,否则就是默认的初始化值
sudo insmod max.ko x=3 y=4
dmesg
运行结果:
五、Makefile文件中,至少有2个以上的文件
这里修改一下求最小值的代码 ,把min.c文件写成两个文件main.c和minnum.c
main.c代码如下:
#include<linux/kernel.h>
#include<linux/module.h>
#include<linux/init.h>
extern int minnum(int x,int y); //extern表明变量或者函数是定义在其他其他文件中的
/**
* 模块的初始化函数,模块的入口函数,加载模块,需超级用户权限
*/
static int __init lk_minnum(void)
{
int x = 1, y = 2;
printk("x = %d, y = %d\n", x, y);
printk("minnum=%d\n", minnum(x, y));
return 0;
}
/**
* 出口函数,卸载模块,需超级用户权限
*/
static void __exit lk_exit(void)
{
printk("The minnum moudle has exited!\n");
}
module_init(lk_minnum); //内核入口点,调用初始化函数,包含在module.h中
module_exit(lk_exit); //出口点
MODULE_LICENSE("GPL"); //许可证
minnum.c代码如下:
int minnum(int x,int y)
{
if(x>y) return y;
return x;
}
Makefile代码如下:
#产生目标文件
obj-m:=min.o
min-objs :=main.o minnum.o
#定义当前路径
CURRENT_PATH:=$(shell pwd)
#定义内核版本号
LINUX_KERNEL:=$(shell uname -r)
#定义内核源码绝对路径
LINUX_KERNEL_PATH:=/usr/src/linux-headers-$(LINUX_KERNEL)
#编译模块
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules
#清理模块
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean
然后依次执行如下指令:
make
sudo insmod min.ko
dmesg
运行结果:
六、内核模块中采用红黑树算法,并输出树中的每个节点(参考内核代码)
红黑树是一颗二叉搜索树,它在每个节点上增加了一个存储位表示节点的颜色,可以是RED或BLACK。通过对任何一条从根到叶子的简单路径上各个节点的颜色进行约束,红黑树确保没有路径会比其他路径长出2倍,因而是近乎平衡的。
红黑树性质:
- 每个节点或是红色,或是黑色
- 根节点是黑色
- 每个叶子节点是黑色的(叶子是空节点)
- 如果一个节点是红色的,则它的两个子节点都是黑色的
- 对每个节点,从该结点到其所有后代结点的简单路径上,均包含相同的黑色结点
Linux内核红黑树的算法都定义在/include/linux/rbtree.h和/lib/rbtree.c两个文件中。
红黑树节点定义:
struct rb_node {
unsigned long __rb_parent_color;
struct rb_node *rb_right;
struct rb_node *rb_left;
} __attribute__((aligned(sizeof(long))));
这里的巧妙之处是使用成员rb_parent_color同时存储两种数据,一是其双亲结点的地址,另一是此结点的着色。attribute((aligned(sizeof(long))))属性保证了红黑树中的每个结点的首地址都是32位对齐的(在32位机上),也就是说每个结点首地址的bit[1]和bit[0]都是0,因此就可以使用bit[0]来存储结点的颜色属性而不干扰到其双亲结点首地址的存储。
指向红黑树根结点的指针:
struct rb_root
{
struct rb_node *rb_node;
};
初始化新结点:
static inline void rb_link_node(struct rb_node * node, struct rb_node * parent,
struct rb_node ** rb_link)
{
node->rb_parent_color = (unsigned long )parent; //设置其双亲结点的首地址(根结点的双亲结点为NULL),且颜色属性设为黑色
node->rb_left = node->rb_right = NULL; //初始化新结点的左右子树
*rb_link = node; //指向新结点
}
插入函数:
extern void rb_insert_color(struct rb_node *, struct rb_root *);
删除函数:
extern void rb_erase(struct rb_node *, struct rb_root *);
遍历:rb_first和rb_next函数可组成中序遍历,即以升序遍历红黑树中的所有结点。
struct rb_node *rb_first(const struct rb_root *root)
{
······
}
struct rb_node *rb_next(const struct rb_node *node)
{
······
}
rb_entry()函数:通过结构体成员的指针来返回结构体的指针。
#define rb_entry(ptr, type, member) container_of(ptr, type, member)
rbtree.c代码如下:
#include<linux/kernel.h>
#include<linux/module.h>
#include<linux/init.h>
#include <linux/rbtree.h>
#include <linux/slab.h>
MODULE_LICENSE("GPL");
struct mytype {
struct rb_node mynode;
int num;
};
static int __init rbtree_init(void)
{
struct rb_root root = RB_ROOT;
struct mytype *newnode;
struct rb_node *pos;
int i;
//插入10个节点
for(i=0;i<10;i++)
{
newnode=(struct mytype *)kmalloc(sizeof(struct mytype), GFP_KERNEL);
newnode->num=i+1;
rb_insert_color(&newnode->mynode,&root);
printk("Node %d has added to the rb_tree...\n", i+1);
}
//遍历红黑树
for (pos = rb_first(&root); pos; pos = rb_next(pos))
printk("Node's data:%d\n", rb_entry(pos, struct mytype, mynode)->num);
return 0;
}
static void __exit rbtree_exit(void)
{
printk("rbtree is exiting !\n");
}
module_init(rbtree_init);
module_exit(rbtree_exit);
运行结果:
运行指令期间未见报错,但仅实现了插入10个节点,未能遍历红黑树,对此表示不解。
经学习张瑞婷的作业,发现在插入节点处代码编写的有问题,经修改成功运行:
rbtree.c代码如下:
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/init.h>
#include <linux/rbtree.h>
#include <linux/slab.h>
struct mytype {
struct rb_node node;
int key;
};
static void print_rbtree(struct rb_root *tree)
{
struct rb_node *tmp;
for(tmp = rb_first(tree); tmp; tmp = rb_next(tmp))
printk("%d ", rb_entry(tmp, struct mytype, node)->key);
}
static int my_insert(struct rb_root *root, struct mytype *data)
{
struct rb_node **new = &(root->rb_node), *parent = NULL;
while(*new) {
struct mytype *this = container_of(*new, struct mytype,node);
int result = data->key-this->key;
parent = *new;
if(result < 0) {
new = &((*new)->rb_left);
} else if(result > 0) {
new = &((*new)->rb_right);
} else {
return 0;
}
}
rb_link_node(&data->node, parent, new);
rb_insert_color(&data->node, root);
return 1;
}
static int __init lkm_init(void)
{
struct rb_root mytree = RB_ROOT;
struct mytype *tmp;
int i;
printk("order of data inputing:\n");
for(i=1;i<=9;i++)
{
tmp=kmalloc(sizeof(struct mytype),GFP_KERNEL);
if(i%2==1)
tmp->key=12+i;
else
tmp->key=12-i;
my_insert(&mytree,tmp);
printk("%d ",tmp->key);
}
printk("\nred-balck tree is:\n");
print_rbtree(&mytree);
printk("\n");
return 0;
}
static void __exit lkm_exit(void)
{
printk("Goodbye\n");
}
module_init(lkm_init);
module_exit(lkm_exit);
MODULE_LICENSE("GPL");
运行结果: