linux内核模块
一、模块组成
一个linux内核模块主要由如下几个部分组成:
1) 模块加载函数。当通过insmod或modprobe命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。"module_init(函数名)",返回整型值,若初始化成功,返回0,初始化失败,返回错误编码。linux内核中,可以使用request_module(const char *fmt, ...)函数加载内核模块。
request_module(module_name); // 灵活加载其他内核模块
2)模块卸载函数。通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行。
3) 模块许可声明。LiCENSE声明描述内核模块的许可权限,如果不声明LiCENSE,模块被加载时,将受到内核被污染(kernel Tainted)的警告。
GPL
GPL v2
GPL and additional rights
Dual BSD/GPL
Dual MPL/GPL
Proprietary
大多数情况下,内核模块应遵循GPL兼容许可权。Linux内核模块最常用的是以MODULE_LICENSE("GPL v2")语句声明模块采用GPL v2。
4)模块参数(可选)。模块参数是模块被加载时可以传递给它的值,它本身对应模块内部的全局变量。
5)模块导出符号(可选)。内核模块可以导出的符号(symbol,对应于函数或变量),若导出,其他模块可以使用本模块中的变量或函数。使用
EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名);
导出的symbol可以在/proc/kallsyms找到,它记录了符号以及符号所在的内存地址。
一个模块mod1中定义一个函数func1;在另外一个模块mod2中定义一个函数func2,func2调用func1。
在模块mod1中,EXPORT_SYMBOL(func1);
在模块mod2中,extern int func1();
就可以在mod2中调用func1了。
6)模块声明与描述(可选)。对于USB、PCI等设备驱动,通常会创建一个MODULE_DEVICE_TABLE,以表明该驱动模块所支持的设备。
二、模块参数
module_param(参数名, 参数类型, 参数读写权限);
module_param_array(数组名, 数组类型,数组长, 参数读写权限);
insmod module_name param=param_value
insmode不传递参数时,参数将使用模块内定义的缺省值。
如果模块被内置,就无法insmod了,但是bootloader可以通过在bootargs里设置“模块名.参数名=值”的形式给该内置的模块传递参数。
参数类型可以是byte、short、ushort、int、uint、long、ulong、charp(字符指针)、bool或invbool(布尔的反),在模块被编译时会将module_param中声明的类型与变量定义的类型进行比较,判断是否一致。
模块被加载后,在/sys/module/目录下将出现以此模块名命名的目录。当参数读写权限为0时,表示此参数不存在sysfs文件系统下对应的文件节点。如果此模块存在“参数读/写权限”不为0的命令行参数,在此模块的目录下还将出现parameters目录,其中包含一系列以参数名命名的文件节点,这些文件的权限值就是传入module_param()的参数读写权限,而文件的内容为参数的值。
运行insmod或modprobe命令时,应使用逗号分隔输入的数组元素。
三、模块的使用计数
Linux2.6以后的内核提供了模块计数管理接口try_module_get(&module)和module_put(&module),从而取代了Linux2.4内核中的模块使用计数管理宏。模块的使用计数一般不必由模块自身管理,而且模块计数管理还要考虑SMP和PREEMPT机制的影响。
try_module_get(&module)返回0,表示调用失败,希望使用的模块没有被加载或正在被卸载中。
Linux2.6以后的内核为不同类型的设备定义了struct module *owner域,用来指向管理此设备的模块。当开始使用某个设备时,内核使用try_module_get(dev->owner)去增加管理此设备的owner模块的使用计数;当不再使用此设备时,内核使用module_put(dev->owner)减少对管理此设备的管理模块的使用计数。这样,当设备在使用时,管理此设备的模块将不能被卸载。只有当设备不再被使用时,模块才允许被卸载。
对设备驱动而言,很少需要亲自调用try_module_get()与module_put(),因为此时开发人员所写的驱动通常为支持某具体设备的管理模块,对此设备owner模块的计数管理由内核里更底层的代码(如总线驱动或是此类设备共用的核心模块)来实现,从而简化设备驱动开发。
四、模块命令
lsmod/insmod/rmmod/modinfo/modprobe
$ lsmod | grep helloworld
helloworld 16384 0
$ sudo rmmod helloworld
$ sudo modprobe helloworld
$ sudo insmod helloworld
Mar 20 23:13:47 ubuntu-wang kernel: Goodbye, world!
Mar 20 23:13:47 ubuntu-wang sudo[8621]: pam_unix(sudo:session): session closed for user root
Mar 20 23:13:56 ubuntu-wang sudo[8624]: wang : TTY=pts/5 ; PWD=/home/wang/kernel/modules/hellox86 ; USER=root ; COMMAND=/sbin/insmod helloworld
Mar 20 23:13:56 ubuntu-wang sudo[8624]: pam_unix(sudo:session): session opened for user root by wang(uid=0)
Mar 20 23:13:56 ubuntu-wang sudo[8624]: pam_unix(sudo:session): session closed for user root
Mar 20 23:14:02 ubuntu-wang sudo[8631]: wang : TTY=pts/5 ; PWD=/home/wang/kernel/modules/hellox86 ; USER=root ; COMMAND=/sbin/insmod helloworld.ko
Mar 20 23:14:02 ubuntu-wang sudo[8631]: pam_unix(sudo:session): session opened for user root by wang(uid=0)
Mar 20 23:14:02 ubuntu-wang kernel: Hello, world!
$ modinfo helloworld.ko
filename: /home/wang/kernel/modules/hellox86/helloworld.ko
license: GPL v2
vermagic: 4.4.0-21-generic SMP mod_unload modversions
depends:
srcversion: 8803357E27B7FC988B73113
lsmod命令可以获取系统中已加载的所有模块以及模块间依赖关系。lsmod命令实际上是读取并分析"/proc/modules"文件。内核中已加载模块的信息也存在于/sys/module目录下,如加载hello.ko,则在/sys/module/hello目录包含模块相关内容。
modprobe在加载某模块时,会同时加载该模块所依赖的其它模块。使用modprobe命令加载的模块若以"modprobe -r filename"的方式卸载,将同时卸载其依赖的模块。模块之间的依赖关系存放在根文件系统的/lib/modules/
modprobe提示无法打开“modules.dep”这个文件,因此驱动挂载失败了。我们不用手动创建 modules.dep这个文件,直接输入depmod命令即可自动生成modules.dep,有些根文件系统可能没有depmod这个命令,如果没有这个命令就只能重新配置busybox,使能此命令,然后重新编译 busybox。输入“depmod”命令以后会自动生成modules.alias、modules.symbols 和 modules.dep 这三个文件。拷贝驱动文件到/lib/modules/4.1.15目录后可能需要重启板卡。
五、模块使用
ubuntu20.04中,x86系统构建的编译环境存放在/lib/modules/$(uname -r)/build下,实际衔接到了/usr/src/$(uname -r)
$ ls /lib/modules/5.13.0-35-generic/build -al
lrwxrwxrwx 1 root root 40 Mar 7 15:40 /lib/modules/5.13.0-35-generic/build -> /usr/src/linux-headers-5.13.0-35-generic
在引用内核头文件时,经常会用到#include <asm/xxx>,这个与平台有关系,内核在编译时会自动生成链接。
$ ls /lib/modules/5.13.0-35-generic/build/arch/x86/include/ -al
total 20
drwxr-xr-x 3 root root 4096 Mar 10 09:50 .
drwxr-xr-x 6 root root 4096 Mar 10 09:50 ..
lrwxrwxrwx 1 root root 65 Mar 7 15:40 asm -> ../../../../linux-hwe-5.13-headers-5.13.0-35/arch/x86/include/asm
drwxr-xr-x 4 root root 4096 Mar 10 09:50 generated
lrwxrwxrwx 1 root root 66 Mar 7 15:40 uapi -> ../../../../linux-hwe-5.13-headers-5.13.0-35/arch/x86/include/uapi
vscode配置的c_cpp_properties.json示例如下:
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/usr/src/linux-headers-5.13.0-35-generic/include",
"/usr/src/linux-headers-5.13.0-35-generic/arch/x86/include",
"/usr/src/linux-headers-5.13.0-35-generic/arch/x86/include/generated"
],
"defines": [
"__GNUC__",
"__KERNEL__",
"MODULE"
],
"compilerPath": "/usr/bin/gcc",
"cStandard": "gnu17",
"cppStandard": "gnu++14",
"intelliSenseMode": "linux-gcc-x64"
}
],
"version": 4
}
注:可在defines中定义内核.config的相关的CONFIG,vscode不会导入Linux内核的.config文件,如:CONFIG_REGMAP
5.1 单模块单文件
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("Dual BSD/GPL");
char *who = "world";
static int __init hello_init ( void )
{
printk(KERN_ALERT "Hello, %s!\n", who);
return 0;
}
static void __exit hello_exit ( void )
{
printk(KERN_ALERT "Goodbye, %s!\n", who);
}
module_init(hello_init);
module_exit(hello_exit);
X86 Makefile
ARCH ?= x86
#CROSS_COMPILE ?= arm-linux-gnueabihf-
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
# Kernel modules
obj-m += helloworld.o
# Specify flags for the module compilation.包含调试信息
EXTRA_CFLAGS=-g -O0
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
-rm –rf *.o *~ core.depend .*.cmd *.ko *.mod.c
make -C $(KERNELDIR) M=$(PWD) clean
$ sudo insmod helloworld.ko
$ journal -b
Mar 20 23:05:48 ubuntu-wang kernel: helloworld: module verification failed: signature and/or required key missing - tainting kernel
Mar 20 23:05:48 ubuntu-wang sudo[7983]: pam_unix(sudo:session): session closed for user root
Mar 20 23:05:48 ubuntu-wang kernel: Hello, world!
arm64 Makefile
#ARCH?=arm64
#CROSS_COMPILE ?= arm-linux-gnueabihf-
#CROSS_COMPILE?=/home/wang/openil/openil/output/host/usr/bin/aarch64-linux-gnu-
KERNELDIR ?= /home/wang/openil/openil/output/build/linux-OpenIL-linux-201810
obj-m += helloworld.o
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules ARCH=arm64 CROSS_COMPILE=/home/wang/openil/openil/output/host/usr/bin/aarch64-linux-gnu-
clean:
-rm –rf *.o *~ core.depend .*.cmd *.ko *.mod.c
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
目前测试开头的ARCH和CROSS_COMPILE不起作用,需要在命令中指定。
5.2 单模块多文件
obj-m := helloworld.o
helloworld-objs :=hello.o goo.o
hello.c可直接调用goo.c中的函数,只需在hello.c中声明即可。
5.3 多模块
obj-m := hello.o goo.o
注:其中,goo.c需要导出符号表,EXPORT_SYMBOL(goo),供hello.c调用。
$ cat Makefile
ARCH ?= x86
#CROSS_COMPILE ?= arm-linux-gnueabihf-
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
# Kernel modules
obj-m := helloworld.o goo.o
# Specify flags for the module compilation.
EXTRA_CFLAGS=-g -O0
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
-rm –rf *.o *~ core.depend .*.cmd *.ko *.mod.c
make -C $(KERNELDIR) M=$(PWD) clean
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("GPL v2");
char *who = "world";
extern int goo_add_int(int a, int b);
static int __init hello_init ( void )
{
int res;
res = goo_add_int(3, 4);
printk(KERN_ALERT "Hello, %s and result = %d!\n", who, res);
return 0;
}
static void __exit hello_exit ( void )
{
printk(KERN_ALERT "Goodbye, %s!\n", who);
}
module_init(hello_init);
module_exit(hello_exit);
// goo.c
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("GPL v2");
char *who = "goo world";
int goo_add_int(int a, int b)
{
return a+b;
}
static int __init hello_init ( void )
{
printk(KERN_ALERT "Hello, %s!\n", who);
return 0;
}
static void __exit hello_exit ( void )
{
printk(KERN_ALERT "Goodbye, %s!\n", who);
}
EXPORT_SYMBOL_GPL(goo_add_int);
module_init(hello_init);
module_exit(hello_exit);
六、创建设备文件
mknod /dev/chrdev c 200 0
七、上传下载文件
通过TFTP下载上传文件
tftp -gr test.txt 192.168.1.101 // 下载
tftp -pl test.txt 192.168.1.101 // 上传
#!/bin/bash
echo "param($#): $*"
if [ $# != 1 ]; then
echo "$0 filename"
exit -1
fi
tftp -gr $1 192.168.4.224
if [ $? == 0 ]; then
echo "down [$1] success"
else
echo "down [$1] failed"
fi
exit 0
八、示例demo
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
#include <linux/sched.h> //sched.h要放在uaccess.h之前引用
//Since Linux kernel 4.11, variable init_task is declared in linux/sched/task.h, not in linux/sched.h
#include <linux/sched/task.h>
//support for_each_process
#include <linux/list.h>
static int __init sched_fun_init(void)
{
struct task_struct *p, *ts = &init_task;
struct list_head *pos;
int count = 0;
list_for_each(pos, &ts->tasks) {
p = list_entry(pos, struct task_struct,tasks);
count++;
printk("<1> pid = %ld----%s\n", p->pid, p->comm);
}
printk("<1> count = %d\n", count);
return 0;
}
static void __exit sched_fun_exit(void)
{
printk("<1> sched_fun_exit\n");
}
module_init(sched_fun_init);
module_exit(sched_fun_exit);
MODULE_LICENSE("Dual BSD/GPL");
参考: