Linux:模块加载与参数传递
Abstract
Linux内核是模块化的,由一个尽可能小的基本内核,和一堆实现进阶功能的内核模块组成。
支持模块的好处有三个,一是让基本内核非常精简,二是允许在运行时添加功能,三是支持设备的热插拔,因为设备驱动也是以内核模块的形式实现的。大多数情况下,可以认为模块的功能就是注册和删除设备驱动。
模块和应用程序的功能是不同的,它并不负责完成工作,而是将完成工作所需要的函数,结构体,变量等注册到内核,并在设备不再需要的时候清除内核。
编写内核通常由四步组成
- 编写设备功能
- 编写初始化/退出函数
- 编译模块
- 装载/卸载模块
设备功能
Linux设备通常分为三类
- 字符设备(PCI设备,USB设备等)
- 块设备(磁盘等)
- 网络设备
对设备的划分纯粹是功能性的,换句话说,字符设备共享一套编程接口(尽管它们的功能不尽相同),块设备共享一套,网络设备则是另一套。
拿字符设备来说,具体的编程接口由三个结构体定义:
- file_operations,定义对设备的操作
- file,设备的运行时标识
- inode,设备在系统中的标识
file_operation中,定义了很多对设备的操作函数,open,release,read,write等,它们的参数和返回值是由内核统一规定的,这使得内核能够以统一的接口处理不同的设备。
在定义自己的设备时,需要提供一个file_operations结构体,其中自己定义的函数以下图的方式映射,未定义的函数则为NULL。
关于file_operation的具体定义,以及它是如何使用file和inode的,这是另一个有趣的故事,目前,就让我们使用一个toy example,file_operations是一个空结构体,该设备的每个函数都为空1!
/*Demo设备向系统注册操作功能,声明设备操作的功能接口函数*/
struct file_operations Demo_ops={
};
编写初始化/退出函数
假设已经编写好了关于设备功能的函数,下一步就是编写该模块被注册时调用的函数。
初始化函数一般被声明为static,这使得它只在内部可见,被其他模块调用时需要借助于称为导出的技术,__init标记是另外一个可选标记,它使得该函数在初始化之后从内核中清除出去。
/*系统初始化*/
static int __init Demo_Init(void)
{
int ret = register_chrdev(DEMO_MAJOR, DEVICE_NAME, &Demo_ops);
if(ret){
printk("Init Failed");
}
else{
printk("Init Successed!");
}
return ret;
}
在初始化函数内部,使用register_chrdev(Major,Name,file_operations)注册字符设备,Major是主设备号,在0~255之间,name是设备名,在/proc/devices中使用,Demo_ops就是前面建立的file_operations结构体。
与初始化函数相对的,模块必须编写退出函数,在模块被卸载时调用以释放资源。
static void __exit Demo_Exit(void)
{
unregister_chrdev(DEMO_MAJOR, DEVICE_NAME);
printk("Bye~\n");
}
注意register_chrdev和unregister_chrdev都是在2.6版本之前使用的函数,在2.6版本之后,一般使用cdev注册。
在编写完这两个函数之后,需要将其与模块初始化与卸载绑定。
/*内核模块入口,相当于main()函数,完成模块初始化*/
module_init(Demo_Init);
/*卸载时调用的函数入口,完成模块卸载*/
module_exit(Demo_Exit);
其中module_init和module_exit都是预先定义好的宏,不可改变。
最后为模块设定许可证,表示其开源协议即可,如果没有显式设定许可证,则会被认为是“Proprietary(专有)”。
MODULE_LICENSE("GPL");
编译模块
和任何一个C语言程序一样,模块在实际使用之前需要经过编译,编译在linux内核中一般通过make命令和makefile完成。
例如:
$:make a
linux会在当前目录下查找Makefile,并在其中寻找a:something项,并将其执行。
例如:
a:cat Makefile
此时:make a 将Makefile打印到终端
$:make a
在编译模块时,我们使用的Makefile一般如下:
obj-m := demo_drv.o
KERNELDIR := /lib/modules/3.2.14/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
rm -f *.ko *.mod.c *.o ~core.depend
当在终端执行:
$ make
会找到Makefile里的default项,执行
make -C $(KERNELDIR) M=$(PWD) modules
其中,-C表明对module的编译规则需要到KERNELDIR目录下的Makefile中寻找,M=PWD表示最终要编译的参数在当前目录,而在Makefile开头声明的obj-m:=demo_drv.o,则指示了make的目标:demo_drv.c会先被编译为demo_drv.o,然后得到demo_drv.ko2。
同理,make modules_install和make clean也会执行对应的功能。
装载/卸载 模块
模块编译之后,得到.ko文件,这就是我们要装载的模块,语法是简单的:
sudo insmod mymodule.ko
sudo rmmod mymodule
代码和结果
#include<linux/init.h>
#include<linux/kernel.h>
#include<linux/module.h>
#include<linux/fs.h>
#include<linux/cdev.h>
#define DEMO_MAJOR 98
#define DEMO_MINOR 0
#define DEVICE_CNT 1
#define DEVICE_NAME "demo_drv"
/*Demo设备向系统注册操作功能,声明设备操作的功能接口函数*/
struct file_operations Demo_ops={
};
/*系统初始化*/
static int __init Demo_Init(void)
{
int ret = -1;
ret = register_chrdev(DEMO_MAJOR, DEVICE_NAME, &Demo_ops);
if(ret){
printk("Demo_module failed with %d\n[--kernel--]",ret);
return
}
else{
printk("Demo_driver register success!!![--kernel--]\n");
}
return 0;
}
/*系统卸载*/
static void __exit Demo_Exit(void)
{
unregister_chrdev(DEMO_MAJOR, DEVICE_NAME);
printk("Demo Driver unregister success!!![--kernel--]\n");
}
/*驱动程序版本及GPL协议证书信息*/
MODULE_DESCRIPTION("Simple driver module");
MODULE_LICENSE("GPL");
/*内核模块入口,相当于main()函数,完成模块初始化*/
module_init(Demo_Init);
/*卸载时调用的函数入口,完成模块卸载*/
module_exit(Demo_Exit);
obj-m := demo_drv.o
KERNELDIR := /lib/modules/3.2.14/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
rm -f *.ko *.mod.c *.o ~core.depend
将这两个文件放在同一个目录下
make
sudo insmod demo_drv.ko
dmesg
sudo rmmod demo_drc
dmesg