Linux 内核模块编程

Last edited
Last updated July 5, 2023
Pages
Tags

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_initmycdev_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
此时执行 make 就能够编译生成 demo.ko 驱动文件。如果遇到缺少 Module.symvers 依赖的问题,你需要现在内核目录下先执行一次 make modules参考链接
架构通用版本 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 pointer
    • bool :a bool, values 0/1, y/n, Y/N
    • invbool: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");
notion image

7.4 注意事项

一、给 char 类型变量进行命令行传参时需要传入这个数值的 ascii 十进制格式,如果直接传入字符,终端默认当作变量处理,例如:
sudo insmod demo.ko b=a # 错误 sudo insmod demo.ko b=97 # 正确
二、给字符串类型进行参数传递时,字符串中不能有空格,不然会被当作下一个变量的变量名