文章标题:学习笔记:编写一个内核模块
简单的笔记总结,完全是为了备忘。
文章大纲
一个模块至少要包含两个函数,一个是模块加载(初始化)函数,一个是模块卸载函数。
模块加载函数:当通过 insmod 或 modprobe 命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。
Linux 内核模块加载函数一般用 static
关键字声明为内部链接,并以 __init
标识。之所以标识为 __init
,用途是如果编译内核时模块是以静态方式包含在 vmlinux 中,则在链接的时候标识为 __init
的函数会被放在 .init.text
这个 section,同时还会在 .initcall.init
section 中保存一份函数指针,在内核初始化阶段会通过这些函数指针调用这些初始化函数,在初始化阶段完成后,这些 init section 所在的 segment 会被释放以节省内存。
模块加载函数返回整型值,若初始化成功,返回 0。初始化失败时,应该返回错误编码。内核的错误码是一个负数,在 <linux/errno.h>
中定义,形如 -ENODEV
等。
模块加载函数必须以 module_init(函数名)
的形式被指定。
示例代码如下:
static int __init foo_init(void)
{
//...
}
module_init(foo_init);
模块卸载函数: 当通过 rmmod 命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块加载函数相反的功能。
Linux内核模块卸载函数一般用 static
关键字声明为内部链接,并以 __exit
标识。和 __init
类似,__exit
使对应函数在运行完成后自动回收内存。具体可以查看内核代码中 __init
和 __exit
这两个宏的定义。
模块卸载函数不返回任何值,而且必须以 module_exit(函数名)
的形式指定。
示例代码如下:
static void __exit foo_exit(void)
{
//...
}
module_exit(foo_exit);
编写一个模块时,还会涉及以下内容的编写:
模块许可证声明(必须)
模块许可证(LICENSE)声明描述内核模块的许可权限,如果不声明 LICENSE,模块被加载时,将收到内核被污染(kernel tainted)的警告。在内核中,可接受的 LICENSE 包括 “GPL”, “GPL v2”, “GPL and additonal rights”,“Dual BSD/GPL”,“Dual MPL/GPL” 和 “Proprietary”。
大多数情况下,内核模块应遵循 GPL 兼容许可权。Linux 内核模块最常见的是声明模块采用 BSD/GPL 双LICENSE,如下:
MODULE_LICENSE("Dual BSD/GPL");
模块参数(可选)
模块参数是模块被加载的时候可以被传递给它的值,它本身对应模块内部的全局变量。
在装载内核模块时,用户可以向模块传递参数,形式为 insmode(或 modprobe) 模块名 参数名=参数值
,如果不传递,参数将使用模块内定义的默认值。
模块内部可以用 module_param(参数名,参数类型,参数读/写权限)
定义一个参数,例如:
static char *str = "hello,world";
static int num = 4000;
module_param(num, int, S_IRUGO);
module_param(str, charp, S_IRUGO);
模块导出符号(可选)
内核模块可以导出符号(symbol,对应于函数或变量),这样其它模块可以使用本模块中的变量或函数。
Linux 的 /proc/kallsyms
文件对应着内核符号表,它记录了符号以及符号所在的内存地址。
模块可以使用如下宏导出符号到内核符号表:
EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名);
导出的符号可以被其他模块使用,使用前声明一下即可。EXPORT_SYMBOL_GPL()
适用于包含 GPL 许可证的模块。例如:
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
init add(int a,int b)
{
return a + b;
}
EXPORT_SYMBOL_GPL(add);
模块作者等信息声明(可选)
我们可以使用 MODULE_AUTHOR
, MODULE_DESCRIPTION
, MODULE_VERSION
, MODULE_DEVICE_TABLE
, MODULE_ALLAS
分别声明模块的作者,描述,版本,设备表和别名。
其中注意 MODULE_DEVICE_TABLE
常用于 PCI 或者 USB 驱动中表明该驱动模块所支持的设备。
标准使用方法:
make -C (Linux内核源代码目录) M=(模块源码目录,包含模块的源文件和 Makefile) modules
如果当前就处在模块所在的目录,则可以使用
make -C (Linux内核源代码目录) M=$(pwd) modules
Makefile 的编写参考 例子
make 执行过程中会导致该 Makefile 文件被加载执行两遍。
第一遍执行时,由于 KERNELRELEASE
没有被定义,所以先执行 else
下的逻辑。-C $(KERNEL_DIR)
会导致 make 跳转到我们指定的对应内核源码目录下读取那里的 Makefile;M=$(CURDIR)
告诉内核的 Makefile 构建的模块源码在当前 Makefile 所在的目录。
内核的 build system 会在构建 modules 时返回到当前目录继续读入当前的 Makefile 并执行之。进入第二遍执行。此时 KERNELRELEASE
已被被定义,make 将读取 else 之前的内容,即 obj-m := lkm_example.o
,按照 linux 内核的 build-system 规则解释并构建。
在设置 KERNEL_DIR
时,例子是直接设置内核源码路径,这常常用于嵌入式开发的交叉编译环境构建,如果是针对本机环境构建内核模块,有下面两种设置的方式(在 Ubuntu 环境下验证过):
/usr/src/$(KERNEL_VER)
: 针对下载了本机内核对应的源代码的情况/lib/modules/$(KERNEL_VER)/build
:无需下载内核对应源代码,利用升级内核时缺省下载的 build 目录如果一个模块包括多个 .c
文件(如 file1.c
、file2.c
),则应该以如下方式编写 Makefile 中的 obj-m
:
obj-m := modulename.o
modulename-objs := file1.o file2.o
查看内核中已经加载的 ko 模块的信息。(注意不包括 builtin 的模块信息)
显示的信息:
lsmod 的实际内容来自 /proc/modules
,有些 lsmod 的实现实际就是简单地显示 /proc/modules
的内容(譬如 busybox)。我们也可以自己查看这个文件了解类似信息。这个文件中的内容每一行对应一个加载到内核中的 ko 模块的信息。每一行包含了多个字段,解释如下,例子:
假设内容如下:
# cat /proc/modules
usb_storage 39646 0 - Live 0x00000000
bluetooth 158447 10 rfcomm,bnep, Live 0x00000000
列 | 解释 |
---|---|
usb | 模块名 |
3964 | 每个模块占用的内存数量 |
0 | 加载引用计数 |
- | 其他字符串,用于注明引用该模块的其他模块的名字,用 , 分隔,如果没有则显示为 -
|
Live | 这个模块当前是否活动 |
0x00000000 | 起始地址 |
我们也可以通过 sysfs 了解内核中加载的模块信息,通过查看 /sys/module
目录,每个加载的 module 对应其下的一个子目录。注意 /sys/module
中列出的模块既包括 ko 也包括 builtin 的模块。
查看模块文件(.ko)的具体信息,具体参考 man 8 modinfo
。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。