14 Star 80 Fork 38

aosp-riscv / working-group

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
20221101-write-lkm.md 8.05 KB
一键复制 编辑 原始数据 按行查看 历史
unicornx 提交于 2022-11-01 11:48 . add article: write a lkm

文章标题:学习笔记:编写一个内核模块

简单的笔记总结,完全是为了备忘。

文章大纲

1. 参考:

2. 编写内核模块

一个模块至少要包含两个函数,一个是模块加载(初始化)函数,一个是模块卸载函数。

  • 模块加载函数:当通过 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 驱动中表明该驱动模块所支持的设备。

3. 模块的编译

标准使用方法:

make -C (Linux内核源代码目录) M=(模块源码目录,包含模块的源文件和 Makefile) modules

如果当前就处在模块所在的目录,则可以使用

make -C (Linux内核源代码目录) M=$(pwd) modules 

3.1. 一个 Makefile 的例子

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.cfile2.c),则应该以如下方式编写 Makefile 中的 obj-m

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

4. 内核模块操作常用命令:

4.1. lsmod

查看内核中已经加载的 ko 模块的信息。(注意不包括 builtin 的模块信息)

显示的信息:

  • 第一列:Module,模块名
  • 第二列:Size,模块大小
  • 第三列:Used by,表示当前有几个模块依赖于该模块

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 的模块。

4.2. modinfo

查看模块文件(.ko)的具体信息,具体参考 man 8 modinfo

1
https://gitee.com/aosp-riscv/working-group.git
git@gitee.com:aosp-riscv/working-group.git
aosp-riscv
working-group
working-group
master

搜索帮助