Linux设备驱动基础之Linux内核模块

337 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情 


一、Linux 和模块

32 位系统上,Linux 内核将 4G 空间分为 0~3G 的用户空间和 3~4G 的内核空间。用户程序运行在用户空间,可通过中断或者系统调用进入内核空间;Linux 内核已经内核模块则只能在内核空间运行。 Linux 内核具有很强的可裁剪性,很多功能或者外设驱动都可以编译成模块,在系统运行中动态插入或者卸载,在此过程中无需重启系统。模块化设计使得 Linux 系统很灵活,可以将一些很少用到或者暂时不用的功能编译为模块,在需要的时候再动态加载进内核,可以减小内核的体积,加快启动速度,这对嵌入式应用极为重要。

二、编写内核模块

#include <linux/module.h>
#include <linux/init.h>

static int __init hello_init(void)
{
	printk("Hello, I'm ready!\n");
	return 0;
}
static void __exit hello_exit(void)
{
	printk("I'll be leaving, bye!\n");
}
module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");

2.1、头文件

内核模块需要包含内核相关头文件,不同模块根据功能的差异,所需要的头文件也不相同,但是<linux/module.h><linux/init.h>是必不可少的。

#include <linux/module.h>
#include <linux/init.h>

2.2、模块初始化

模块的初始化负责注册模块本身。如果一个内核模块没有被注册,则其内部的各种方法无法被应用程序使用,只有已注册模块的各种方法才能够被应用程序使用并发挥各方法的实际功能。模块并不是内核内部的代码,而是独立于内核之外,通过初始化,能够让内核之外的代码来替内核完成本应该由内核完成的功能,模块初始化的功能相当于模块与内核之间衔接的桥梁,告知内核“我进来了,我已经做好准备为您服务了”。

模块的初始化定义通常如程序:

static int __init module_init_func(void)
{
	初始化代码
}
module_init(module_init_func);

几点说明: (1) 模块初始化函数一般都需声明为 static,因为初始化函数对于其它文件没有任何意义; (2) __init 表示初始化函数仅仅在初始化期间使用,一旦初始化完毕,将释放初始化函数所占用的内存,类似的还有__initdata; (3) module_init 是必须的,没有这个定义,内核将无法执行初始化代码。module_init宏定义会在模块的目标代码中增加一个特殊的代码段,用于说明该初始化函数所在的位置。 当使用 insmod 将模块加载进内核的时候,初始化函数的代码将会被执行。模块初始化代码只与内核模块管理子系统打交道,并不与应用程序交互。

2.3、模块退出

当系统不再需要某个模块,可以卸载这个模块以释放该模块所占用的资源。模块的退出相当于告知内核“我要离开了,将不再为您服务了”。 实现模块退出的函数常称为模块的退出函数或者清除函数

static void __exit module_exit_func(void)
{
模块退出代码
}
module_exit(module_exit_func);

几点说明: (1) 模块退出函数没有返回值; (2)__exit标记这段代码仅用于模块卸载; (3) module_exit 不是必须的。但是,没有 module_exit 定义的模块无法被卸载,如果需要支持模块卸载则必须有 module_exit。 当使用 rmmod 卸载模块时,退出函数的代码将被执行。模块退出代码只与内核模块管理子系统打交道,并不直接与应用程序交互。

2.4、许可证

Linux 内核是开源的,遵守 GPL 协议,所以要求加载进内核的模块也最好遵循相关协议。为模块指定遵守的协议用 MODULE_LINCENSE来声明,如:

MODULE_LICENSE("GPL");

内核能够识别的协议有“GPL”、“GPL v2”、“GPL and additional rightsGPL 及附加权利)”、“Dual BSD/GPLBSD/GPL 双重许可)”、“Dual MPL/GPLMPL/GPL 双重许可)”以及“Proprietary(私有)”。 如果一个模块没有指定任何许可协议,则会被认为是私有协议。采用私有协议的模块,在加载过程中会出现警告,并且不能被静态编译进内核。

2.5、符号导出

所有的内核符号默认都是不导出的。如果希望一个模块的符号能被其它模块使用,则必须显式的用 EXPORT_SYMBOL 将符号导出。如:

2.6、模块描述

模块编写者还可以为所编写的模块增加一些其它描述信息,如模块作者、模块本身的描述或者模块版本等,例如:

MODULE_AUTHOR("Linux <Linux.com>");
MODULE_DESCRIPTION("kenrnel");
MODULE_VERSION("V1.00");

模块描述以及许可证声明一般放在文件末尾。

2.7、编译

模块代码编写完毕,需要进行编译,得到模块文件才能使用。编译模块需要内核代码,并配置和编译内核代码,就算有源码,但是没经过编译,也是不能用于编译模块的。编译模块的内核配置必须与所运行内核的编译配置一样,否则将有可能无法加载或者运行。 编译内核很简单。假定一个模块文件 hello.c,欲编译得到 hello.ko 文件,则只需在 Makefile 文件中编写一行:

obj-m := hello.o

如果一个模块由 file1.cfile2.c 等多个文件组成,要编译得到 module.ko 文件,则makefile 内容如下:

obj-m := module.o
module-objs := file1.o file2.o

2.8、加载和卸载

加载模块使用 insmod 命令,卸载模块使用 rmmod 命令。例如加载和卸载 hello.ko 模块:

#insmod hello.ko
#rmmod hello.ko

加载和卸载模块必须具有 root 权限。 对于可接受参数的模块,在加载模块的时候为变量赋值即可,卸载模块无需参数。假如hello.ko 模块有变量 num,在插入的时候设置 num 的值为 8,则加载和卸载命令为:

#insmod hello.ko num=8
#rmmod hello.ko