内核模块
序言
linux驱动开发的特点
1.linux驱动是内核级别的开发,驱动程序的任何问题都会引起整个系统崩溃。(若驱动对一个非法指针进行解引用,通常会引起整个内核的崩溃)
2.驱动程序通常要进行中断处理。在中断上下文(或类似的原子上下文)中的编程有严格的限制,处理不好也会内核崩溃。
3.驱动程序有更多的并发环境需要考虑。比如中断、多处理器下的系统编程。
4.驱动程序是被动接受上层调用的代码,为上层提供服务的代码,所以在驱动中有很多注册和注销函数。
5.驱动的关键就是构造核心的数据结构然后注册到内核。主要学习这些核心的数据结构和与之向相关的一套API
6.内核源码中有同一类设备驱动的很好的实现范例,面对不熟悉的驱动框架,可以参考内核源码,从而掌握该类驱动的开发。
7.驱动能独立于内核之外,并能动态加载卸载。
8.有一类设备访问按字节流方式进行,访问单位可小到字节,对这类设备叫字符设备驱动。
在类UNIX系统中,设备也被当做文件来对待,或者说将设备抽象成了文件。这样就可以统一应用层代码对普通文件和设备文件的访问接口。
1.1 内核模块程序
内核模块就是被单独编译的一段内核代码,它可以在需要的时候动态加载到内核,从而动态地增加内核的功能。只要新加载的驱动不会使内核崩溃,就不需要重启系统。
/*vser.c*/
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
int init_module(void)
{
printk(“module init\n”);
return 0;
}
void cleanup_module(void)
{
printk("cleanup module\n");
}
一个模块程序都要直接或间接包含上述三个头文件。
模块初始化函数和模块清除函数
模块初始化函数:进行内存分配、驱动注册等
模块清除函数:完成内存释放、主动注销
ifeq($(KERNELRELEASE), )
ifeq($(ARCH),arm)
KERNELDIR ?=/home/
ROOTFS ?=/nfs/roofts
else
KERNELDIR ?=/lib/modules/$(shell uname -r )/build
endif
PWD:=$(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M= $(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) INSTALL_MOD_PATH=$(ROOTFS) modules_install
clean:
rm -rf *.o *.ko .*.cmd *.mod .*modules.order Module .symvers .tmp_versions
else
obj-m=vser.o
endif
make
make module_install
make ARCH=arm
make ARCH=arm_modules_install
若要编译运行在ARM目标机上的驱动,则用上述命令。
1.2 内核相关工具
1.模块加载
insmod vser.ko
dmesg
depmod
modprobe vser
使用modprobe 不指定路径和后缀。
2.模块信息
在安装模块并运行了depmod命令之后,可以不指定路径和后缀,也可查看某一指定.ko文件:
modinfo vser
3.模块卸载:
rmmod vser
内核模块的一般形式
需要添加
MODULE_LICENSE("GPL");
代表相应的许可证协议。
module_init和module_exit是两个宏,分别用于指定init_module的函数别名是vser_init,以及cleanup_module的别名vser_exit.
为避免因为重名而带来重复定义的问题,函数前可加static关键字修饰,经过static修饰后的函数链接属性为内部,这就是几乎所有的驱动程序函数前要加static的原因。
__init 和__exit每次仅会执行一次
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
static int __init vser_init(void)
{
printk("vser_init\n");
return 0;
}
static void __exit vser_exit(void)
{
printk("vser_exit\n");
}
module_init(vser_init);
module_exit(vser_exit);
MODULE_LICENSE("GPL");
2.4将多个源文件编译成一个内核模块
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
extern void bar(void);
static int __init vser_init(void)
{
printk("vser_init\n");
bar();
return 0;
}
static void __exit vser_exit(void)
{
printk("vser_exit\n");
}
module_init(vser_init);
module_exit(vser_exit);
MODULE_LICENSE("GPL");
#include <linux/kernel.h>
void bar(void)
{
printk("bar\n");
}
在Makelfie中添加
obj-m=vser.o
vser-objs=foo.o bar.o
2.5 内核模块参数
模块的初始化函数在模块被加载时调用,但是该函数不接受参数,要想在模块加载时对模块的行为进行控制,就不方便。模块参数允许用户通过命令行指定参数值,在模块加载过程中,加载程序会得到命令行参数,并转换成相应类型的值,然后赋值给对应变量。
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
static int baudrate=9600;
static int port[4]={0,1,2,3};
static char *name="vser";
module_param(baudrate,int,S_IRUGO);
module_param_array(port,int,NULL,S_IRUGO);
module_param(name,charp,S_IRUGO);
static int __init vser_init(void)
{
int i;
printk("vser_init\n");
printk("baudrate:%d\n",baudrate);
printk("port: ");
for(i=0;i<ARRAY_SIZE(port);i++){
printk("%d",port[i]);
}
printk("name:%s \n",name);
return 0;
}
static void __exit vser_exit(void)
{
printk("vser_exit\n");
}
module_init(vser_init);
MODULE_LICENSE("GPL");
module_param(name,type,perm);
module_param_array(name.type,nump,perm);
name :变量名
type:变量或数组元素的类型
nump:变量或数组个数的指针,可选。
perm:对应文件权限(和普通文件的权限一样)
modprobe vser baudrate=115200 port=1,2,3,4 name=“virtural-serial”
参看sysfs文件系统下的内容,可发现和模块参数对应的文件及相应权限
ls -l /sys/module/vser/parameters
cat /sys/module/vser/parameters/port
2.6 内核模块依赖
使用nm命令查看模块目标文件的符号信息时,可以看到vser_exit和vser_init的符号类型是t表示是函数;printk的符号类型为U,表示它是一个未决符号
EXPORT_SYMBOL(printk);
通过一个叫做EXPORT_SYMBOL的宏将printk导出,目的是为动态加载的模块提供printk的地址信息。可以发现,内核将会有大量的符号导出,为模块提供了丰富的基础设施。
若一个模块需要提供全局变量或函数给另外的模块使用,那么就需要将这些符号导出。这在一个驱动程序代码调用另一个驱动程序代码时比较常见。故模块间形成依赖关系,使用导出符号的模块将会依赖于导出符号的模块。
vser.c
extern int expval;
extern void expfun(void);
dep.c
#include <linux/kernel.h>
#include <linux/module.h>
static int expval=5;
EXPORT_SYMBOL(expval);
static void expfun(void)
{
printk("expfun");
}
EXPORT_SYMBOL_GPL(expfun);
MODULE_LICENSE("GPL");
Makefile
obj-m+=dep.o
dep.c里定义了一个全局变量expval,定义了一个函数expfun,并分别使用
EXPORT_SYMBOL和·EXPORT_SYMBOL_GPL导出。在vser.c中首先用extern 声明这个函数和变量。在Makefile中添加对dep模块的编译。
几点说明:
1.若使用insmod命令加载模块,必须先加载dep模块,再加载vser模块。
2.modprobe 可以自动加载被依赖的模块。归功于depmod命令。
3.depmod命令将生成的模块依赖信息保存在/lib/modules/3.13.0-32-generic/modules.dep
cat /lib/modules/3.13.0-32-generic/modules.dep
......
extra/vser.ko:extra /dep.ko
extra/dep.ko
2.两个模块存在依赖关系,若分别编译两个模块,再加载,将报“invaild parameters”的错误。解决这个问题的方法是将两个模块放在一起编译,或将dep模块放在内核源码中,先在源码下编译,再编译vser。
3.卸载时要先卸载vser模块,再卸载dep模块。
2.7 关于内核模块的进一步讨论
在一个版本内核下编译的模块.ko文件无法在另一个版本内核加载。
1)内核模块运行在内核空间,而应用程序运行在用户空间
2)模块通常注册一些服务性质的函数供其他功能单元在之后调用,而应用程序则是顺序执行,通常进入一个循环往复调用某些函数。
3)内核C函数库之下,就不能调用C库函数。
4)内核模块要做一些清除性工作,比如在一个操作失败或在内核的清除函数中。
5)内核模块若产生了非法访问,将可能导致系统崩溃。
6)内核模块并发更多,比如中断、多处理器。
7)整个内核空间的调用链只有4KB或8KB的栈,若需要大的内存空间,通常应动态分配。
8)printk不支持浮点打印。
测试实例分析
Makefile 文件中需要注意:
在make的时候出现了“empty variable name”,
最后是如下原因:在Makefile中有如下一句话,出现这个错误的原因是“=”左边多了一个空格
$(MAKE) -C $(KERNELDIR) M= $(PWD) modules
如果在"="右边多一个空格,则会出现另外的错误:
*** Error during update of the kernel configuration.
make[3]: *** [silentoldconfig] Error 1
make[2]: *** [silentoldconfig] Error 2
make[1]: Nothing to be done for `/home/chen/hello_module’.
make[1]: *** No rule to make target include/config/auto.conf', needed by
include/config/kernel.release’. Stop.
make[1]: Leaving directory `/usr/src/linux-headers-2.6.35-22-generic’
make: *** [all] Error 2
sudo insmod vser.ko baudrate=1080
用modprobe vser 不能传递参数,建议以后仍用insmod