1. 概述
我们编写 Linux 内核模块的软件代码,通过某种方式将其加载到内核空间进行生效,相当于是给 Linux 内核安装了一个实现特定功能的插件。
2. 内核模块编程的三要素
- 入口:内核模块程序执行的入口,安装模块时执行,主要负责一些资源的申请工作
- 出口:内核模块的出口,卸载内核模块时执行,主要用于释放入口中申请的资源
- 许可证:声明当前模块遵守 GPL 协议
3. Linux内核模块编程实例
// filename: demo.c #include <linux/init.h> #include <linux/module.h> static int __init mycdev_init(void) { return 0; } static void __exit mycdev_exit(void) { } module_init(mycdev_init); module_exit(mycdev_exit); MODULE_LICENSE("GPL");
static
声明当前函数只能在当前文件中使用
int
声明返回类型,且必须返回,否则编译报错
__init
是一个宏,它的作用是指定入口函数放在.init.text
段中
mycdev_init
和mycdev_exit
函数名可以更换
- 如果无参数,一定要写一个
void
module_init
是一个宏函数,用于向内核声明当前内核模块的入口函数地址
module_exit
同上,用于向内核声明当前内核模块的出口地址
MODULE_LICENSE
声明当前模块遵循 GPL 协议
4. Linux内核模块的编译
内核模块的编译依赖内核源码,因为需要使用内核提供的接口。编译的方式有两种:1. 内部编译;2. 外部编译
4.1 内部编译(静态编译)
一类内核源码树进行编译,也就是将自己的内核模块文件添加到内核源码树,通过内核模块编译命令进行编译(Linux 内核移植 的第 7 节)
- 将内核模块放在内核模块指定路径下
- 修改路径下
Kconfig
,将我们自己的模块源码信息添加在Kconfig
中
- 回到内核顶层目录,执行
make menuconfig
,找到我们自己内核模块的选配项,选择<M>
- 执行玩图形化界面选配后,
.config
被修改
- 内核顶层目录执行
make modules
或者make uImage
4.2 外部编译(动态编译)
不依赖内核源码树进行自己的内核模块编译,但需要手写一个 Makefile
Makefile 示例:
#指定内核顶层目录的路径 KERNELDIR:=/home/ubuntu/linux-5.10.61/ #编译为ARM架构的内核路径 #KERNELDIR:=/lib/modules/5.4.0-125-generic/build #编译生成x86架构文件的内核路径 #指定当前源码所在的路径 PWD:=$(shell pwd) #将shell命令pwd的执行结果赋值给变量PWD all: #make modules表示进行模块化编译 #make -C $(KERNELDIR)先切换路径到KERNELDIR下,按照这个路径下Makefile的规则进行make #M=$(PWD)指定模块化编译的路径 make -C $(KERNELDIR) M=$(PWD) modules clean: #编译清除 make -C $(KERNELDIR) M=$(PWD) clean #将指定的.o文件独立链接为模块文件 obj-m:=demo.o
架构通用版本 Makefile:
arch?=arm #存储编译架构的变量 modname?=demo #存储模块文件名的变量 #指定内核顶层目录的路径 ifeq ($(arch),arm) KERNELDIR:=/home/ubuntu/linux-5.10.61/ #编译为ARM架构的内核路径 else KERNELDIR:=/lib/modules/$(shell uname -r)/build #编译生成x86架构文件的内核路径 endif #指定当前源码所在的路径 PWD:=$(shell pwd) #将shell命令pwd的执行结果赋值给变量PWD all: #make modules表示进行模块化编译 #make -C $(KERNELDIR)先切换路径到KERNELDIR下,按照这个路径下Makefile的规则进行make #M=$(PWD)指定模块化编译的路径 make -C $(KERNELDIR) M=$(PWD) modules clean: #编译清除 make -C $(KERNELDIR) M=$(PWD) clean #将指定的.o文件独立链接为模块文件 obj-m:=$(modname).o
编译的指令:
make arch=x86 modname=demo
5. 模块的安装、卸载、查看
- 安装:
insmod ***.ko
- 卸载:
rmmod ***
- 查看已经安装的模块:
lsmod
- 查看模块的信息:
modinfo ***.ko
6. 内核模块中的消息打印函数printk
6.1 函数使用格式
printk("格式控制符", 输出列表); printk(消息打印级别 "格式控制符", 输出列表);
6.2 printk函数的消息过滤方式
printk
输出的内容属于内核消息,对于内核的消息有些很重要,需要输出到终端,有些消息不太重要,没必要在终端显示,所以内核设计者针对 printk
函数输出的消息设置了打印级别,只有当消息的打印级别高于终端支持的消息输出最小级别,此时消息才在终端显示。消息打印级别分为 0-7 级,共 8 级。其中数字越小级别越高,常用的消息级别是 3-7 级。无论是否过滤,都可以在
dmesg
指令输出的日志中查看(使用 dmesg -c
可以清除历史日志) 。// 打印级别的定义位于 include/linux/kern_levels.h #define KERN_EMERG KERN_SOH "0" /* system is unusable */ #define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */ #define KERN_CRIT KERN_SOH "2" /* critical conditions */ #define KERN_ERR KERN_SOH "3" /* error conditions */ #define KERN_WARNING KERN_SOH "4" /* warning conditions */ #define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */ #define KERN_INFO KERN_SOH "6" /* informational */ #define KERN_DEBUG KERN_SOH "7" /* debug-level messages */
6.3 实例代码
#include <linux/init.h> #include <linux/module.h> // 内核模块入口函数 static int __init mycdev_init(void) { printk(KERN_ERR "模块安装,执行入口函数\n"); return 0; } // 内核模块出口函数 static void __exit mycdev_exit(void) { printk(KERN_ERR "模块卸载,执行出口函数\n"); } module_init(mycdev_init); module_exit(mycdev_exit); MODULE_LICENSE("GPL");
6.4 查看默认级别
终端输入
cat /proc/sys/kernel/printk
,结果输出四个数字 4 4 1 7
分别表示:- 终端的级别
printk
消息默认的级别(大于或者等于终端级别时,就能输出)
- 终端支持的消息的最高的级别(最高就是 1,因为 0 时候系统未使能)
- 终端支持的消息的最低的级别
6.5 修改默认级别
修改 Ubuntu 的默认消息打印级别:
# 重启后又会重置,需要找到初始化脚本修改才能固定 sudo su echo 4 3 1 7 > /proc/sys/kernel/printk
修改开发板默认消息打印级别:
vim rootfs/etc/init.d/rcS
- 追加或者修改
echo 4 3 1 7 >/proc/sys/kernel/printk
6.6 虚拟终端
Ubuntu 桌面 GUI 的终端中是无法直接打印
printk
消息的,可以使用虚拟终端(tty):- 打开虚拟终端端:
ctrl+alt+f2-f6
- 关闭虚拟终端:
ctrl+alt+f1(fn)
7. 内核模块传参
7.1 概述
内核模块编写后可以加载到不同的 Linux 系统内核中,每个内核参数可能略有不同,所以为了配置不同 Linux 内核,可以在安装模块时给模块内的某个变量传递参数数值来适配。
传参的格式:
insmod **.ko key1=val1 key2=val2
7.2 相关API
module_param(name, type, perm)
参数:
name
:要传参的变量名
type
:变量类型byte
,hexint
,short
,ushort
,int
,uint
,long
,ulong
charp
:a character pointerbool
:a bool, values 0/1, y/n, Y/Ninvbool
:the above, only sense-reversed (N = true)
perm
:文件权限。当我们使用 module_param 函数声明某个变量时,在sys/module
当前内核模块名/parameters/
目录下生成一个以这个变量名为名的文件,文件的内容就是要传参的变量的数值,文件的权限就是由@perm
参数传递(0664
)
MODULE_PARM_DESC(_parm, desc)
用于添加对要传参的变量的描述,可以通过
modinfo ***.ko
命令查看模块中传参的变量描述参数:
_parm
: 要传参的变量名
desc
: 添加的描述,是一个字符串
7.3 实例代码
#include <linux/init.h> #include <linux/module.h> int a = 10; // 会作为默认值 module_param(a, int, 0664); // 声明当前变量可以进行命令行传参 MODULE_PARM_DESC(a, "value is a int"); // 内核模块入口函数 static int __init mycdev_init(void) { printk(KERN_ERR "模块安装,执行入口函数\n"); printk("a=%d\n",a); return 0; } // 内核模块出口函数 static void __exit mycdev_exit(void) { printk(KERN_ERR "模块卸载,执行出口函数\n"); } module_init(mycdev_init); module_exit(mycdev_exit); MODULE_LICENSE("GPL");
7.4 注意事项
一、给
char
类型变量进行命令行传参时需要传入这个数值的 ascii 十进制格式,如果直接传入字符,终端默认当作变量处理,例如:sudo insmod demo.ko b=a # 错误 sudo insmod demo.ko b=97 # 正确
二、给字符串类型进行参数传递时,字符串中不能有空格,不然会被当作下一个变量的变量名