目录
内核模块的由来
第一个内核模块程序
内核模块工具
将多个源文件编译生成一个内核模块
内核模块参数
内核模块依赖
关于内核模块的进一步讨论
习题
内核模块的由来
最近一直在玩那些其它的技术,眼看快暑假了,我决定夯实一下我的驱动方面的技能,迎接我的实习,找了一本书,接下来就跟着这本书学了
先来看第二章,内核模块
Linux是宏内核(或单内核)的操作系统的典型代表,它和微内核(典型的代表是 Windows操作系统)的最大区别在于所有的内核功能都被整体编译在一起,形成一个单独的内核镜像文件。其显著的优点就是效率非常高,内核中各功能模块的交互是通过直接的函数调用来进行的。而微内核则只实现内核中相当关键和核心的一部分,其他功能模块被单独编译,功能模块之间的交互需要通过微内核提供的某种通信机制来建立。对于像 Linux 这类的宏内核而言,其缺点也是不言而喻的,如果要增加、删除、修改内核的某个功能,不得不重新编译整个内核,然后重新启动整个系统。这对驱动开发者来说基本上是不可接受的,因为驱动程序的特殊性,在驱动开发初期,需要经常修改驱动的代码,即便是经验丰富的驱动开发者也是如此。
为了弥补这一缺点,Linux引入了内核模块(后面在不引起混淆的情况下将其简称为“模块”)。简单地说,内核模块就是被单独编译的一段内核代码,它可以在需要的时候动态地加载到内核,从而动态地增加内核的功能。在不需要的时候,可以动态地卸载,从而减少内核的功能,并节约一部分内存(这要求内核配置了模块可卸载的选项才行)。而不论是加载还是卸载,都不需要重新启动整个系统。这种特性使它非常适合于驱动程序的开发(注意,内核模块不一定都是驱动程序,驱动程序也不一定都是模块的形式)。驱动开发者可以随时修改驱动的代码,然后仅编译驱动代码本身(而非整个内核),并将新编译的驱动加载到内核进行测试。只要新加入的驱动不会使内核崩溃,就可以不重新启动系统。
内核模块的这一特点也有助于减小内核镜像文件的体积,自然也就减少了内核所占用的内存空间(因为整个内核镜像将会被加载到内存中运行)。不必把所有的驱动都编译进内核,而是以模块的形式单独编译驱动程序,这是基于不是所有的驱动都会同时工作的原理。因为不是所有的硬件都要同时接入系统,比如一个USB 无线网卡。
讨论完内核模块的这些特性后,我们正式开始编写模块程序。
第一个内核模块程序
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
int init_module()
{
printk("init_module\n");
return 0;
}
void cleanup_module()
{
printk("cleanup_module\n");
}
使用模块init,清除模块cleanup
编译这个程序需要对应的makefile,不然连这些头文件都找不到,这些头文件是内核里的
ifeq就是逻辑与
ifneq就是逻辑或
ifeq ($(KERNELRELEASE),)
ifeq ($(ARCH),arm)
KERNELDIR ?= /home/book/Linux_4412/kernel/linux-3.14
ROOTFS ?= /home/book/nfs_rootfs
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
endif
PWD := $(shell pwd)modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) INSTALL_MOD_PATH=$(ROOTFS) modules_install
clean:
rm -rf *.o *.ko .*.cmd *.mod.* modules.order Module.symvers .tmp_versions
elseobj-m += vser.o
endif
细节很重要,往往简单的地方才是最容易忽视的
“=”是最普通的等号,然而在Makefile中确实最容易搞错的赋值等号,使用”=”进行赋值,变量的值是整个makefile中最后被指定的值。不太容易理解,举个例子如下:
VIR_A = A
VIR_B = $(VIR_A) B
VIR_A = AA
经过上面的赋值后,最后VIR_B的值是AA B,而不是A B。在make时,会把整个makefile展开,拉通决定变量的值相比于前面“最普通”的”=”,”:=”就容易理解多了。”:=”就表示直接赋值,赋予当前位置的值。同样举个例子说明
VIR_A := A
VIR_B := $(VIR_A) B
VIR_A := AA
最后变量VIR_B的值是A B,即根据当前位置进行赋值。因此相比于”=”,”:=”才是真正意义上的直接赋值。“?=”表示如果该变量没有被赋值,则赋予等号后的值。举例:
VIR ?= new_value
如果VIR在之前没有被赋值,那么VIR的值就为new_value.VIR := old_value
VIR ?= new_value
这种情况下,VIR的值就是old_value“+=”和平时写代码的理解是一样的,表示将等号后面的值添加到前面的变量上
$(MAKE)是make自定义的很多变量,在这里用来实现递归调用本身
-C $(KERNELDIR) 指明跳转到内核源码目录下读取那里的Makefile;让内核在编译内核外还编译M指明的目录也就是我们这个文件夹下的文件。
M=$(PWD) 表明然后返回到当前目录继续读入、执行当前的Makefile。
INSTALL_MOD_PATH=$(ROOTFS)
指示安装模块的路径,也就是生成的模块放到哪里
obj-y 编译到内核
obj-m 编译成模块
rm呢就是删除这些文件
ifeq ($(KERNELRELEASE),)
第一次编译时这个宏是空的KERNELRELEASE是一个变量,在Linux内核源代码的顶层Makefile中定义,用于指定当前正在构建的内核版本。
也就是在不断的make的第一次make我们可以执行当前目录else前面的那些配置和工作。
modules为makefile的默认目标被执行(因为是第一个),而 modules_install 和clean则是伪目标,执行make时,只执行modules 目标.
内核模块工具
sudo insmod ./vser.ko
dmesg
modprobe可以自动加载模块到内核,但是我用着不好使不知道为什么,在运行这个前要更新依赖,运行depmod命令。等我学一学在看这个问题。
modinfo可以智能查找模块
rmmod卸载模块
内核模块一般的形式
在前面的模块加载实验中,我们看到内核有以下打印信息的输出。
[ 83.884417] vser: module license 'unspecified' taints kernel.
[83.884423] Disabling lock debugging due to kernel taint
其大概意思是因为加载了 vser模块而导致内核被污染,并且因此禁止了锁的调试功能。这是什么原因造成的呢?众所周知,Linux 是一个开源的项目,为了使 Linux 在发展的过程中不成为一个闭源的项目,这就要求任何使用 Linux 内核源码的个人或组织在免费获得源码并可针对源码做任意的修改和再发布的同时,必须将修改后的源码发布。这就是所谓的GPL 许可证协议。在此并不讨论该许可证协议的详细内容,而是讨论在代码中如何来反应我们接受该许可证协议。在代码中我们需要添加如下的代码来表示该代码接受相应的许可证协议。
MODULE LICENSE("GPL");
MODULE_LICENSE是一个宏,里面的参数是一个字符串,代表相应的许可证协议。可以是:GPL、GPL v2、GPL and additional rights、Dual BSD/GPL、Dua MIT/GPL、Dual MPL/GPL 等,详细内容请参见include/linux/module.h头文件。这个宏将会生成一些模块信息,放在ELF文件中的一个特殊的段中,模块在加载时会将该信息复制到内存中检查该信息,可能读者会认为不加这行代码,即不接受许可证协议只是导致内核报案或关闭某些调试功能而已,对于可以不开源的这个结果,这个代价似乎是可以接受的但是正如本章的后面我们会讲到的一样,没有这行代码,内核中的某些功能再数是不够调用的,而我们在开发驱动时几乎不可避免地要去使用内核中的一些基础设施,
用一些内核的API函数。
除了MODULE_LICENSE之外,还有很多类似的描述模块信息的宏,比如MODULE AUTHOR,:MODULE DESCRIPTION用于模块的详细信息说明,通常是该模块的功能说明:MODULE_ALLA提供了给用户空间使用的一个更合适的别名,也就是使用MODULE_ALIAS可以取一别名。
模块的初始化函数和清除函数的名字是固定的,入口函数基本上都叫main。这对子追求个性化和更想表达函数真实意图的我们来说显得呆板了一些。幸亏内核借助于GNU的函数别名机制,使得我们可以更灵活地指定模块的初始化函数和清除函数的别名
module init(vser_init);module exit(vser exit);
module init 和module_exit是两个宏,分别用于指定initmodule的函数别名是 vserinit,以及cleanup_module的别名是vser_exit。这样我们的模块初始化函数和清除承数就可以用别名来定义了。
函数名可以任意指定又带来了一个新问题,那就是可能会和内核中已有的函数重名因为模块的代码最终也属于内核代码的一部分。C语言没有类似于C++的命名空间的概念,为了避免因为重名而带来的重复定义的问题,函数可以加static关键字修饰。经过static修饰后的函数的链接属性为内部,从而解决了该问题。这就是几乎所有的驱动程序的函数前都要加static关键字修饰的原因。
Linux是节约内存的操作系统的典范,任何可能节约下来的内存都不会被它放过。上面的模块代码看上去已经足够简单了,但仔细思考,还是会发现可以优化的地方。模块的初始化函数会且仅会被调用一次,在调用完成后,该函数不应该被再次调用。所以该函数所占用的内存应该被释放掉,在函数名前加_init可以达到此目的。_init是把标记的函数放到ELF文件的特定代码段,在模块加载这些段时将会单独分配内存,这些函数调用成功后,模块的加载程序会释放这部分内存空间。_exit用于修饰清除函数,和init的作用类似,但用于模块的卸载,如果模块不允许卸载,那么这段代码完全就不用加载。
(其实不一定被污染,我的就没有哈哈,书中的内核版本是3.14我的是5.4.0)
(具体原因不详)
然后我们的程序就变成这样了
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
static int __init vser_init(void)
{
printk("init_module\n");
return 0;
}
static void __exit vser_exit(void)
{
printk("cleanup_module\n");
}
module_init(vser_init);
module_exit(vser_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("姓名 <邮箱>");
MODULE_DESCRIPTION("A simple module");
MODULE_ALIAS("virtual-serial");
一定不要忘记函数的()里要写void
之前那个modprobe能用了,但是要把ko文件复制到/lib/modules/5.4.0-137-generic下
感觉应该是我的depmod有问题。
将多个源文件编译生成一个内核模块
对于一个比较复杂的驱动程序,将所有的代码写在一个源文件中通常是不太现实的。我们通常会把程序的功能进行拆分,由不同的源文件来实现对应的功能,应用程序是这样的,驱动程序也是如此。下面这个简单的例子演示了如何用多个源文件生成一个内核模块。
其实很简单就是修改一下makefile
#include <linux/kernel.h>
void bar(void)
{
printk("bar\n");
}
#include <linux/kernel.h>
void bar(void)
{
printk("bar\n");
}
book@100ask:~/makeru/driver/kernal$ cat foo.c
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
extern void bar(void);
static int __init vser_init(void)
{
printk("vser_init\n");
return 0;
}
static void __exit vser_exit(void)
{
printk("vser_exit\n");
}
内核模块参数
通过前面的了解,我们知道模块的初始化函数在模块被加载时调用。但是该函数不接受参数,如果我们想在模块加载时对模块的行为进行控制,就不是很方便了。比如编写了一个串口驱动,想要在串口驱动加载时波特率由命令行参数设定,就像运行普通的应用程序时,通过命令行参数来传递信息一样。为此模块提供了另外一种形式来支持这种行为,这就叫作模块参数。
模块参数允许用户在加载模块时通过命令行指定参数值,在模块的加载过程中,加载程序会得到命令行参数,并转换成相应类型的值,然后赋值给对应的变量,这个过程发生在调用模块初始化函数之前。内核支持的参数类型有:bool、invbool(反转值 bool类型)、charp(字符串指针)、short、int、long、ushort、uint、ulong。这些类型又可以复合成对应的数组类型。为了说明模块参数的用法,下面分别以整型、整型数组和字符串类型为例进行说明内核中没有字符类型(char),但有byte类型,使用时可以byte类型代替char类型,
但是在传递参数时不能直接传递字符,只能传递整形数,否则会报错
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
static int baudrate = 9600;
static int port[4] = {0,1,2,3};
static char *name = "vser";
module_param(baudrate, int, S_IRUGO);
module_param_array(port, int, NULL, S_IRUGO);
module_param(name, charp, S_IRUGO);
static int __init vser_init(void)
{
int i;
printk("vser_init\n");
printk("baudrate: %d\n", baudrate);
printk("prot:");
for(i = 0; i < ARRAY_SIZE(port); i++)
printk("%d", port[i]);
printk("\n");
printk("name: %s\n", name);
return 0;
}
static void __exit vser_exit(void)
{
printk("vser_exit\n");
}
module_init(vser_init);
module_exit(vser_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("name <E-mail>");
MODULE_DESCRIPTION("A simple module");
MODULE_ALIAS("virtual-serial");
代码第5行到第7行分别定义了一个整型变量、整型数组和字符串指针。代码第9行到第11行将这三个类型的变量声明为模块参数,分别用到了moduleparam和module param_array 两个宏,两者的参数说明如下。
module_param(name, type, perm)
module param array(name, type,nump; perm)name:变量的名字。type:变量或数组元素的类型。nump:数组元素个数的指针,可选。perm:在sysfs文件系统中对应文件的权限属性。权限的取值请参见<linux/stat.h>头文件,含义和普通文件的权限是一样的。但是如果perm为0,则在sysfs文件系统中将不会出现对应的文件。编译、安装模块后,在加载模块时,如果不指定模块参数的值,那么使用的命令和内核的打印信息如下。可见打印的值都是代码中的默认值。如果需要指定模块参数的值,可以使用下面的命令。
modprobe vser baudrate-115200 port-1,2,3,4 name-"virtual-serial”
dmesg
(我的modprobe不太好使我就用的insmod)
参看 sysfs文件系统下的内容,可以发现和模块参数对应的文件及相应的权限。
虽然在代码中增加模块参数的写权限可以使用户通过sysfs文件系统来修改模块参数的值,但并不推荐这样做。因为通过这种方式对模块参数进行的修改模块本身是一无所知的。
内核模块依赖
在介绍模块依赖之前,首先让我们学习一下导出符号。在之前的模块代码中,都用到了printk函数,很显然,这个函数不是我们来实现的,它是内核代码的一部分。我们的模块之所以能够编译通过,是因为对模块的编译仅仅是编译,并没有链接。编译出来的,ko文件是一个普通的ELF目标文件,使用file命令和nm命令,可以得到相关的细节信息。
使用nm命令查看模块目标文件的符号信息时,可以看到vser_exit和vser_init的符号类型是t,表示它们是函数;而printk的符号类型是U,表示它是一个未决符号。这表示在编译阶段不知道这个符号的地址,因为它被定义在其他文件中,没有放在模块代码中一起编译。那printk函数的地址问题怎么解决呢,让我们来看看printk的实现代码
(位于内核源码kernel/printk/printk.c)。/usr/src/linux-headers-5.4.0-137-generic/kernel
这是我的内核版本其实这里都是软连接
真正的源码在makefile指定的那个路径里
cat printk.c | head -n 1693 | tail -n +1674
asmlinkage int printk(const char *fmt, ...)
{
va_list args;
int r;
#ifdef CONFIG_KGDB_KDB
if (unlikely(kdb_trap_printk)) {
va_start(args, fmt);
r = vkdb_printf(fmt, args);
va_end(args);
return r;
}
#endif
va_start(args, fmt);
r = vprintk_emit(0, -1, NULL, 0, fmt, args);
va_end(args);
return r;
}
通过一个叫作EXPORT SYMBOL 的宏将printk导出,其目的是为动态加载的模块提供printk的地址信息。大致的工作原理是:利用EXPORT_SYMBOL 宏生成一个特定的结构并放在 ELF 文件的一个特定段中,在内核的启动过程中,公将符号的确切地场填充到这个结构的特定成员中。模块加载时,加载程字将去处理未决符号,在特殊段中2索符号的名字,如果找到,则将获得的地址填充在被加载模块的相应段中,这样符号的地址就可以确定。使用这种方式处理未决符号,其实相当于把链接的过程推后,进行了动态链接,和普通的应用程序使用共享库函数的道理是类似的。可以发现,内核将会有大量的符号导出。为模块提供了丰富的基础设施,
通常情况下,一个模块只使用内核导出的符号,自己不导出符号。但是如果一个初块需要提供全局变量或函数给另外的模块使用,那么就需要将这些符号导出。这在一个驱动程序代码调用另一个驱动程序代码时比较常见。这样模块和模块之间就形成了依赖关系,使用导出符号的模块将会依赖于导出符号的模块,下面的代码说明了这一点
vser.c
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
extern int expval;
extern void expfun(void);
static int __init vser_init(void)
{
printk("vser_init\n");
printk("expval: %d\n", expval);
expfun();
return 0;
}
static void __exit vser_exit(void)
{
printk("vser_exit\n");
}
module_init(vser_init);
module_exit(vser_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("name <E-mail>");
MODULE_DESCRIPTION("A simple module");
MODULE_ALIAS("virtual-serial");
dep.c
#include <linux/kernel.h>
#include <linux/module.h>
static int expval = 5;
EXPORT_SYMBOL(expval);
static void expfun(void)
{
printk("expfun");
}
EXPORT_SYMBOL_GPL(expfun);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("name <E-mail>");
喵喵的在开发板就好使,大概是我的ubuntu环境有问题。这几个驱动是之前做的开机自启脚本加载的。
在上面的代码中,dep.c里定义了一个全局变量expval,定义了一个函数expfun,并分别使用EXPORT SYMBOL 和EXPORT SYMBOL GPL 导出。在vser.c首先用extern声明了这个变量和函数,并打印了该变量的值和调用了该函数。在 Makefile中添加了第20行的代码,增加了对 dep模块的编译。编译、安装模块后,使用下面的命令加载并查看内核的打印信息。
$ modprobe vser$ dmesg
这里有几点需要特别说明。
(1)如果使用insmod命令加载模块,则必须先加载 dep 模块,再加载vser模块。因为vser模块使用了dep模块导出的符号,如果在dep模块没有加载的情况下加载vser模块,那么将会在加载的过程中因为处理未决符号而失败。从这里可以看出,modprobe令优于 insmod 命令的地方在于其可以自动加载被依赖的模块。而这又要归功于depmod命令,depmod 命令将会生成模块的依赖信息,保存在/1ib/modules/3.13.0-32-gene modules.dcp 文件中。其中,3.13.0-32-generic 是内核源码的版本,视版本的不同而不同查看该文件可以发现vser模块所依赖的模块。
$cat /lib/modules/3.13.0-32-generic/modules.depextra/vser.ko: extra/dep.ko
extra/dep.ko:
(我的ubuntu18.04和书中描述的不一致,我在我的开发板上也找不到depmod的文件,不过效果差不多,而且我的ubuntu里可能是ko文件太多了,还有大量的重名ko文件所以更新依赖失败了。也不是失败了,更新的不是我想要的。)
(2)两个模块存在依赖关系,如果分别编译两个模块,将会出现类似于下面的警告
信息,并且即便加载顺序正确,加载也不会成功。
NARNING:“expfun”【/home/farsight/fs4412/driver/module/ex5/vser.ko) undefined! WARNING:"expval”[/home/farsight/fs4412/driver/module/ex5/vser.kol)undefined!
$ sudo insmod dep.ko$ sudo insmod vser.ko
insmod: error inserting 'vser.ko': -1 Invalid parameters
这是因为在编译vser模块时在内核的符号表中找不到expval和expfun的项,而vser模块又完全不知道dep模块的存在。解决这个问题的方法是将两个模块放在一起编译或者将dep模块放在内核源码中,先在内核源码下编译完所有的模块,再编译 vser 模块
(3)卸载模块时要先卸载vser模块,再卸载dep模块,否则会因为dep 模块被 vsa模块使用而不能卸载。内核将会创建模块依赖关系的链表,只有当依赖于这个模块的表为空时,模块才能被卸载。
关于内核模块的进一步讨论
Linux的内核是由全世界的志愿者来开发的,这个组织中的内核开发者会毫不顾虑地删除不适合的接口或者对接口进行修改,只要认为这是必要的。所以,往往在前一个篇本这个接口函数以一种形式存在,而到了下一个版本函数的接口就发生了变化。这对内核模块的开发具有重要的影响,就是所谓的内核模块版本控制。在一个版本上编译出来的内核模块,ko文件中详细记录了内核源码版本信息、体系结构信息、函数接口信息(通过CRC校验实现)等,在开启了版本控制选项的内核中加载一个模块时,内核将核对这些信息,如果不一致,则会拒绝加载。下面就是把一个在3.13 内核版本上编译的内核模块放在 3.5 内核版本的系统上加载的相关输出信息。
modinfo vser,kofilename: vser,ko
alias: virtual-serial
deneriptioni: A simple module
authori : 手动打码
licese: GPL
sroveraloni BABBD66A92DF5D4C7VA3110
depends:
vermagic: 3.13.0-32-generie BME mod unload modversionn 606uname sr
3.5.0-23-generic
insmod veer.ko
lnsmod: error inserting 'veoriko’! -i Invalid module formatdmesg
vsert: disagreen about vernion of symbol module_layout
最后再总结一下内核模块和普通应用程序之间的差异。
(1)内核模块是操作系统内核的一部分,运行在内核空间:而应用程序运行在用户空间。
(2)内核模块中的函数是被动地被调用的,比如初始化的数和清除函数分别是在内核模块被加载和被卸载的时候调用,模块通常注册一些服务性质的函数供其他功能单元在之后调用,而应用程序则是顺序执行,然后通常进入一个循环反复调用某些函数。
(3)内核模块处于C函数库之下,自然就不能调用C库函数(内核源码中会实现类似的函数);而应用程序则可以随意调用C库函数。
(4)内核模块要做一些清除性的工作,比如在一个操作失败后或者在内核的清除函数中:而应用程序有些工作通常不需要做,比如在程序退出前关闭所有已打开的文件。
(5)内核模块如果产生了非法访问(比如对野指针的访问),将很有可能导致整个系统的崩溃;而应用程序通常只影响自己。
(6)内核模块中的并发更多,比如中断、多处理器:而应用程序一般只考虑多进程或多线程。
(7)整个内核空间的调用链上只有4KB或8KB的栈,相对于应用程序来说非常的小。所以如果需要大的内存空间,通常应该动态分配。
(8)虽然printk和printf的行为非常相似,但是通常printk不支持浮点数,例如要打印一个浮点变量,在编译时通常会出现如下警告,并且模块也不会加载成功。
WARNING:" extendsfdf2"[/home/faraight/fe4412/driver/module/ex5/vser.ko)undefined!
WARNING:" truncdfaf2"【/home/farsight/fs4412/driver/module/ex5/vser.ko)undetined! WARNING:“ divdf3”【/home/farsight/f84412/driver/module/ex5/vser.ko]undefined!
WARNING:"foatsidf”【/home/farsight/fs4412/driver/module/ex5/vser.ko] undefined!
习题
1,在默认情况下,模块初始化函数的名字是( A),模块清除函数的名字是( B)。
[A]init_module[B]cleanup_module[C] mod_init [D] mod_exit
2、加载模块可以用哪个命令( AD)。
[A]insmod [B] rmmod [C] depmod [D] modprobe
3、查看模块信息用哪个命令(C)。
[A] insmod [B] rmmod [C] modinfo [D] modprobe
4.内核模块参数的类型不包括(D)。
[A]布尔 [B]字符串指针 [C]数组 [D] 结构
b) type:数据类型内核支持模块参数类型有:bool、invbool(bool的发转,true变为false,false变为true)、charp(char类型指针值)、int、long、short、uint、ulong、ushort
5.内核模块导出符号用哪个宏(C )。
[A]MODULE EXPORT [B]MODULE_PARAM
[C]ENPORT SYMBOL [D] MODULE_LICENSE
pararm是传参数的
MODULE EXPORT这个我都没找到好像没有这个宏
6.内核模块能否调用C库的函数接口(B )。
[A]能 [B] 不能
7.在内核模块代码中,我们能否定义任意大小的局部变量( B)。
[A] 能 [B]不能
这题有歧义,只能是一定范围内的自由