编写内核模块
内核模块是Linux内核向外部提供的一个插口,其全称为动态可加载内核模块(Loadable Kernel Module,LKM),我们简称为模块。Linux内核之所以提供模块机制,是因为它本身是一个单内核(monolithic kernel)。单内核的最大优点是效率高,因为所有的内容都集成在一起,但其缺点是可扩展性和可维护性相对较差,模块机制就是为了弥补这一缺陷。很多驱动程序都以模块的形式存在,用户可以有选择的加载需要的驱动程序。
一、 什么是模块
模块是具有独立功能的程序,它可以被单独编译,但不能独立运行。它在运行时被链接到内核作为内核的一部分在内核空间运行,这与运行在用户空间的进程是不同的。模块通常由一组函数和数据结构组成,用来实现一种文件系统、一个驱动程序或其他内核上层的功能。
二、 编写一个简单的模块
模块和内核都在内核空间运行,模块编程在一定意义上说就是内核编程。因为内核版本的每次变化,其中的某些函数名也会相应地发生变化,因此模块编程与内核版本密切相关。以下例子针对2.6内核:
tasklet.c文件
1 #include <linux/module.h> 2 #include <linux/init.h> 3 #include <linux/kernel.h> 4 #include <linux/interrupt.h> 5 6 static struct tasklet_struct my_tasklet; 7 8 static void tasklet_handler (unsigned long data) 9 { 10 printk(KERN_ALERT "tasklet_handler is running.\n"); 11 } 12 13 static int __init test_init(void) 14 { 15 tasklet_init(&my_tasklet, tasklet_handler, 0); 16 tasklet_schedule(&my_tasklet); 17 return 0; 18 } 19 20 static void __exit test_exit(void) 21 { 22 tasklet_kill(&my_tasklet); 23 printk(KERN_ALERT "test_exit running.\n"); 24 } 25 MODULE_LICENSE("GPL"); 26 27 module_init(test_init); 28 module_exit(test_exit);
1.头文件说明:所有模块都要使用头文件module.h,此文件必须包含进来;头文件kernel.h包含了常用的内核函数;头文件init.h包含了宏_init和_exit,它们允许释放内核占用的内存。interrupt.h文件时编写tasklet(关于tasklet的说明可以参考linux中断与异常)必须要用的头文件。
2.test_init是模块的初始化函数,它必需包含诸如要编译的代码、初始化数据结构等内容,其中的tasklet_init初始化我们自己创建的tasklet,tasklet_schedule函数则是将我们初始化后的tasklet挂到tasklet链表中,并激活软中断来执行我们的tasklet。
3.test_exit是模块的退出和清理函数。此处可以做所有终止该内核模块时相关的清理工作。
4.函数module_init()和module_exit()是模块编程中最基本也是必须的两个函数。module_init()向内核注册模块,并调用注册做模块的初始化工作的函数,而module_exit()注销模块并注册模块注销时执行的清理函数。
5.第10行使用了printk()函数,该函数是由内核定义的,功能与C库中的printf()类似,它把要打印的信息输出到终端或系统日志。字符串前面的KERN_ALERT是输出的级别,表示立即在终端输出,可以调用dmesg命令来查看输出。
有了上面的各个组成,一个模块就算编写好了。
三、 编译模块
编译模块不想编译普通的c文件,直接敲几个命令就可以了,编译模块必须用Makefile文件,而且Makefile文件必须按一定的规则编写,这个和平时编写的Makefile文件不一样。编写内核模块的Makefile文件内容几乎相同的。下面是模版:
obj-m += tasklet.o # 产生tasklet 模块的目标文件 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 #清理
除了第一行要把tasklet换成编写的目标模块的.c文件名外,其它都是一样的。
一切都准备好之后,直接make一下就ok了。
四、 运行模块
运行模块也不能像运行普通程序一样,直接./tasklet来运行,必须通过专门的命令来运行。
使用$insmod tasklet.ko命令直接将我们的模块插入到内核中,模块被插入内核时就会调用我们注册的初始化函数test_init,用dmesg命令查看输出,可以看到tasklet_handler is running.消息被打印了。
使用$lsmod命令来查看系统中的模块:
使用$rmmod tasklet来卸载模块,卸载模块的同时会执行注册函数test_exit,用dmesg可以查看打印消息。