Linux驱动—内核模块参数,依赖(进一步讨论)

x33g5p2x  于2022-05-05 转载在 Linux  
字(9.9k)|赞(0)|评价(0)|浏览(379)

前面内容:1 Linux驱动—内核模块基本使用

内核模块的一般形式

module_

前面咱们做实验时候

不是介绍了这个是因为加载vser模块而导致内核被污染,并且因此禁止了锁的调试功能。

这是什么原因造成的呢?众所周知,Linux 是一个开源的项目,为了使Linux在发展的过程中不成为一个闭源的项目,这就要求任何使用Linux 内核源码的个人或组织在免费获得源码并可针对源码做任意的修改和再发布的同时,必须将修改后的源码发布。这就是所谓的GPL许可证协议

在此并不讨论该许可证协议的详细内容,而是讨论在代码中如何来反应我们接受该许可证协议。在代码中我们需要添加如下的代码来表示该代码接受相应的许可证协议。

所以要在代码中接收该协议:

MODULE_LICENSE("GPL");

MODULE_LICENSE是一个宏,里面的参数是一个字符串,代表接收的许可证协议。

可以是GPL BSD等

这些协议可以查看module.h头文件看

如果没有这个许可代码,那么后面使用API函数是不能被调用的
内核中的某些功能函数是不能够调用的,而我们在开发驱动时几乎不可避免地要去使用内核中的基础设施,即调用一些内核的API函数。

除了MODULE_这个后面加LICENSE许可证外,还有很多类似的描述模块信息的宏,比如MODULE_AUTHOR这就是作者信息

MODULE_DESCRIPTION 具体的描述。更详细的说明,也就是该模块的功能说明

ALIAS-别名,所以 MODULE_ALIAS 模块取一个别名

命名格式

模块的初始化函数init和清除函数clean是固定的,名字也不能变。
入口函数main

但是这样很刻板,喜欢搞事的想改下,
内核借助于GNU的函数命名规则
比如:

module_init(vser_init);
module_exit(vser_exit);

module_init 和module_exit 是二个宏,分别用于指定init_module 的函数别名是vser_init 以及 cleanup_module的函数别名是vser_exit

这样我们的函数初始化函数和清理函数可以用别名来命名了

但是这样又会出现重名的问题

为了避免重名可以 采用static修饰的方法。
经过static修饰后的函数的链接属性为内部。这是几乎所有的驱动程序的函数前面都要加上static的原因

为啥加_init

为了优化内存,只让函数被调用一次,所以该函数所占用的内存应该被释放掉,在函数名前加_init 可以达到这个目的。
_ init 是把标记的函数放到ELF文件的特定代码段,在模块加载这些段时将会单独分配内存,这些函数调用成功后,模块的加载程序会释放这部分内存空间。_ exit 用于修饰清除函数,和_ init的作用类似,但用于模块的卸载,如果模块不允许卸载,那么这段代码完全就不用加载。

完整形式+加载编译

加入上述内容后,一个模块程序的代码形式大致如下所示:

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

static int __init vser_init(void)
{
	printk("vser_init\n");
	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("Kevin Jiang <jiangxg@farsight.com.cn>");
MODULE_DESCRIPTION("A simple module");
MODULE_ALIAS("virtual-serial");

最后面四句是新加入的
许可证格式是GPL
作者信息是 Kevin Jiang 邮箱 jiangxg@farsight.com.cn
然后描述是一个简单的module<A simple module
别名是"virtual-serial"

Linux驱动—内核模块基本使用
编译安装(在PC平台上)
先make,然后安装make modules_install
就到内核目录下extra了
然后加载模块sudo insmod vser.ko
sudo insmod /lib/modules/4.15.0-46-generic/extra/vser.ko
那是你内核目录

最后dmesg查看结果: 这里是模块信息查询用modinfo

不会出现被污染的情况了

对于模块的加载可以用它的别名:
modprobe virtual-serial
Linux modprobe命令用于自动处理可载入模块。
modprobe可载入指定的个别模块,或是载入一组相依的模块。modprobe会根据depmod所产生的相依关系,决定要载入哪些模块。若在载入过程中发生错误,在modprobe会卸载整组的模块。
Linux modprobe命令

多个源文件编译成一个内核

对于复杂的驱动程序,将所有代码写在一个源文件不太现实,所以我们会用不同的源文件来实现相同的功能。

下面演示如何用多个源文件生成一个内核模块。

基本模块:

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");
	bar();
	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("Kevin Jiang <jiangxg@farsight.com.cn>");
MODULE_DESCRIPTION("A simple module");
MODULE_ALIAS("virtual-serial");

然后是其他模块
bar.c

#include <linux/kernel.h>

void bar(void)
{
	printk("bar\n");
}

最后Makefile

ifeq ($(KERNELRELEASE),)

ifeq ($(ARCH),arm)
KERNELDIR ?= /home/farsight/fs4412/linux-3.14.25-fs4412
ROOTFS ?= /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
else

obj-m := vser.o
vser-objs = foo.o bar.o

endif

这里跟以前的代码比就是增加了bar.c,在里面定义了一个bar函数
注意这里foo.c中调用了bar函数

最重要的是Makjefile第20行的vser-objs = foo.o bar.o
表面vser模块是由foo和bar二个目标文件共同生成的
编译安装加载测试与前面方法一样、

内核模块参数

就是类似函数的初始化参数
比如settime(2000) 2000这种

模块提供了模块参数来提供这种形式

模块参数允许用户在加载模块时候通过命令行指定参数值,在模块的加载过程中,加载程序会得到命令行参数,并转换为相应类型的值,然后赋值给对应的变量。这个过程发生在模块初始化init函数之前

内核支持的参数类型有: bool、 invbool (反转值bool类型)、charp (字符串指针)、short、 int、 long、 ushort、 uint、 ulong。
这些类型又可以复合成对应的数组类型。

为了说明模块参数的用法,下面分别以整型、整型数组和字符串类型为例进行说明:

#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("port: ");
	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("Kevin Jiang <jiangxg@farsight.com.cn>");
MODULE_DESCRIPTION("A simple module");
MODULE_ALIAS("virtual-serial");

看这个

static int baudrate = 9600; //波特率
static int port[4] = {0, 1, 2, 3}; 
static char *name = "vser";

这里定义了一个整型变量baudrate ,整型数组port,字符串指针。

而这里

module_param(baudrate, int, S_IRUGO);
module_param_array(port, int, NULL, S_IRUGO);
module_param(name, charp, S_IRUGO);

将这三种类型的变量声明为模块参数,分别用到了module_param
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 baurate=115200 port=1,2,3,4 name="virtual-serial"
dmesg

内核函数依赖

在介绍模块依赖之前,首先让我们学习一下导出符号
在之前的代码中,我们用printk函数来表示

但是显然这个函数是内核的一部分,我们的模块之所以能编译通过是因为对模块的编译仅仅是编译,并没有链接。

编译出来的.ko文件是一个普通的ELF文件,使用file命令和nm命令,可以查看更多

使用nm命令查看模块目标文件的符号信息时候,可以看到vser_exit 和vser_init的符号类型是t,表明他们是函数

而printk的符号类型是U,表明是一个未决符号。这表示在编译阶段不知道这个符号的地址。因为它被定义在其他文件中,没有放在模块代码中一起编译。

那么printk函数的地址问题咋解决,让我们看啊可能printk的代码(位于内核源码kernel/printk/printk.c)

就是实验板用的内核linux-3.14-fs4412这里

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);

通过一个EXPORT _SYMBOL的宏将printk导出,其目的是为动态加载的模块提供printk的地址信息。

大致原理是这样的:
通过EXPORT _SYMBOL的宏生成一个特定的结构并放在ELF文件的一个特定段中,在内核的启动过程中,会将符号的确切地址填充到这个结构的特定成员中。模块加载时,加载程序将去处理未决符号,在特殊段中搜索符号的名字,如果找到,则将获得的地址填充在被加载模块的相应段中,这样符号的地址就可以确定。使用这种方式处理未决符号,其实相当于把链接的过程推后,进行了动态链接,和普通的应用程序使用共享库函数的道理是类似的。可以发现,内核将会有大量的符号导出,为模块提供了丰富的基础设施。

通常情况下,一个模块只使用内核导出的符号,自己不导出符号。但是如果一个模块需要提供全局变量或函数给另外的模块使用,那么就需要将这些符号导出。

这在一个驱动程序代码调用另一个驱动程序代码时比较常见。

这样模块和模块之间就形成了依赖关系,使用导出符号的模块将会依赖于导出符号的模块,下面的代码说明了这一点

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("Kevin Jiang <jiangxg@farsight.com.cn>");
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\n");
}

EXPORT_SYMBOL_GPL(expfun);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kevin Jiang <jiangxg@farsight.com.cn>");

这上面代码中,dep.c定义了一个全局变量expval,定义了一个函数expfun, 并分别使用EXPORT_SYMBOL和EXPORT_SYMBOL_GPL导出。

在vser.c中,首先用extern声明了这个变量和函数,并打印了该变量的值和调用了该函数。

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;
}

Makefile:

ifeq ($(KERNELRELEASE),)

ifeq ($(ARCH),arm)
KERNELDIR ?= /home/farsight/fs4412/linux-3.14.25-fs4412
ROOTFS ?= /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
else

obj-m := vser.o
obj-m += dep.o

endif

在Makefile中,添加了一行这个
obj-m += dep.o
说明增加了对dep的编译。

编译安装后dmesg
自然会显示init 函数里面的内容

Vser_init 
expval:5
expfun

从上面的代码中,得到了init函数的输出

这里有几点需要注意:

  1. 如果我们使用insmod加载模块,则必须先加载dep模块,再加载vser模块。
    因为vser模块使用了dep模块导出的符号(expfun),如果在dep没有加载的情况下加载vser,那么加载则会因为处理未决符号而失败

这里看出modprobe命令优先于insmod在于可以自动加载被依赖的模块

这里是因为depmod 命令,关于depmod的命令,它会生成自动加载依赖的模块。而这又归功于depmod命令。保存在lib/modules/3.13.0-32-generic/modules.dep文件中,3.13.0-32-generic是Linux内核源码

这里是vser模块依赖的模块

  1. 两个模块存在依赖关系,如果分别编译两个模块,将会出现类似于下面的警告信息,并且即便加载顺序正确,加载也不会成功。

这是因为在编译vser模块时在内核的符号表中找不到expval和expfun的项,而vser模块又完全不知道dep模块的存在。

解决这个问题的方法是将两个模块放在一起编译,或者将dep模块放在内核源码中,先在内核源码下编译完所有的模块,再编译vser 模块。

  1. 卸载模块要先卸载vser模块,再卸载dep,因为会因为dep模块被vser使用而不能卸载。内核将会创建模块依赖关糸的链表,只有当依赖于这个模块的链表为空时,模块才能被卸载。

内核其他

Linux的内核是由全世界的志愿者来开发的,这个组织中的内核开发者会毫不顾虑地删除不适合的接口或者对接口进行修改,只要认为这是必要的。所以,往往在前一个版本这个接口函数以一种形式存在,而到了下一个版本函数的接口就发生了变化。
这对内核模块的开发具有重要的影响,就是所谓的内核模块版本控制。在一个版本上编译出来的内核模块.ko文件中详细记录了内核源码版本信息、体系结构信息、函数接口信息(通过CRC校验实现)等,在开启了版本控制选项的内核中加载一个模块时,内核将核对这些信息,如果不一致,则会拒绝加载。下面就是把 一个在3.13内核版本上编译的内核模块放在3.5内核版本的系统上加载的相关输出信息。

把3.13内核编译的模块放在3.5内核版本上

$ modinfo vser.ko

filename :vser.ko
alias:  virtual-serial
description: A simple module
author : Kevin Jiang <jiangxg@farsight. com. cn>
license: GPL
srcversion: BA8BD66A92BF5D4C7FA3110
depends:
vermagic: 3.13.0-32-generic SMP mod unload modversions 686

$ uname -r
3.5.0-23-generic

# insmod vser.ko
insmod: error inserting 'vser.ko': -1 Invalid module format
# dmesg
.......

[599.260504]vser:disagrees about version of symbol module_ layout

这里上面使用modinfo vser.ko查看模块信息:是3.13.0-32版本的

然后用uname -r 查出内核是3.5.0-23-generic

最后在3.5版本上编译加载模块(insmod vser.ko):
报错,不能使用
disagrees about version of symbol module_ layout

总结(内核模块和普通应用程序之间的差异)

最后再总结一下内核模块和普通应用程序之间的差异。

  1. 内核模块是操作系统内核的一部分,运行在内核空间;而应用程序运行在用户空间。
  2. 内核模块中的函数是被动地被调用的,比如初始化函数和清除函数分别是在内核模块被加载和被卸载的时候调用,模块通常注册一些服务性质的函数供其他功能单元在之后调用,而应用程序则是顺序执行,然后通常进入一个循环反复调用某些函数。
  3. 内核模块处于C函数库之下,自然就不能调用C库函数(内核源码中会实现类似的函数);而应用程序则可以随意调用C库函数。
  4. 内核模块要做一些清除性的工作,比如在一个操作失败后或者在内核的清除函数中;而应用程序有些工作通常不需要做,比如在程序退出前关闭所有已打开的文件。
  5. 内核模块如果产生了非法访问(比如对野指针的访问),将很有可能导致整个系统的崩溃:而应用程序通常只影响自己。.
  6. 内核模块中的并发更多,比如中断、多处理器;而应用程序一般只考虑多进程或多线程。
  7. 整个内核空间的调用链上只有4KB或8KB的栈,相对于应用程序来说非常的小。所以如果需要大的内存空间,通常应该动态分配。
  8. 虽然printk和printf的行为非常相似,但是通常printk不支持浮点数,例如要打印一个浮点变量,在编译时通常会出现如下警告,并且模块也不会加载成功。

相关文章