第二课 Linux内核模块

1、内核模块机制

Linux内核的功能有两种方式加载到内核中:

  • 第一种:编译链接阶段就将所需功能代码编译进内核中。
    • 优点:内核启动后就可以直接使用该功能。
    • 缺点:
      • 内核会很臃肿,包含许多不需要的功能;
      • 编译内核非常耗时,修改、新增或删除组件,就得重新编译一次内核。
  • 第二种:使用模块机制,内核和模块单独编译,模块动态加载进内核。
    • 优点:
      • 内核简洁,只需包含必要的功能;
      • 修改模块代码后,只需要重新编译模块,编译速度快。

模块的特点:

  • 模块本身不会被编译进内核映像,从而控制了内核的大小;
  • 模块一旦被加载,就和内核中其他部分完全一样。

2、模块的程序结构

模块主要由以下几部分组成:

  1. 模块加载函数(必须)
  2. 模块卸载函数(必须)
  3. 模块许可证声明(必须)
  4. 模块参数(可选)
  5. 模块导出符号(可选)
  6. 模块作者等信息声明(可选)

2.1、模块加载函数

主要完成模块的一些初始化工作,如申请内存资源、申请设备号、注册设备等,其形式如下:

static int __init xxxx_init(void)
{
	...
	return 0;
}
module_init(xxxx_init);

完成模块加载函数的代码编写后,需要使用宏module_init(xxx_init);注册该模块加载函数。
调用时机:
当使用insmodmodprobe命令加载内核模块时,内核会调用该模块加载函数。
在代码中加载其他模块的方式:

request_module(const char *fmt, ...);

2.2、模块卸载函数

主要完成模块的一些去初始化工作,如释放申请的内存资源、设备号等,其形式如下:

static void __exit xxxx_exit(void)
{
	....
}
module_exit(xxxx_exit);

完成模块卸载函数的代码编写后,需要使用宏module_exit(xxxx_exit);注册该模块卸载函数。
调用时机:
当使用rmmodmodprobe命令卸载内核模块时,内核会调用该模块卸载函数。

2.3、模块许可证声明

许可证声明描述内核模块的许可权限,属必须添加的内容。如果不声明许可证,内核加载模块时会提示被污染警告(Kernel Tainted)。
可接受的许可证有:“GPL”、“GPL v2”、“GPL and additional rights”、“Dual BSD/GPL”、“Dual MPL/GPL”、“Proprietary”。
模块许可证声明使用如下宏:

MODULE_LICENSE("GPL");

2.4、模块参数

加载模块时,可通过模块参数方式向模块中的变量传递参数,该变量本身对应模块内部的全局变量。
可使用如下方式为模块定义一个参数:

//可参考`linux/moduleparam.h`
module_param(参数名, 参数类型, 参数读/写权限);
module_param_array(数组名, 数组类型, 数组长, 参数读/写权限);

参数名:模块内部的全局变量
参数类型:

  • byte
  • hexint
  • short
  • ushort
  • int
  • uint
  • long
  • ulong
  • charp:字符指针
  • bool:布尔值,0/1、y/n、Y/N
  • invbool:反布尔值

参数读/写权限:(参考linux/stat.h

  • S_IRUGO
  • S_IWUGO
  • S_IXUGO

例如:

static char *name = "hello, world!!!";
module_param(name, charp, S_IRUGO);
int array_num[10];
int array_len;
module_param_array(array, int, &array_len, S_IRUGO);

在加载内核模块时,可通过如下方式向模块传递参数:

insmod/modprobe 模块名 参数名=参数值
insmod/modprobe 模块名 数组名=元素1,元素2,元素3  //元素之间以逗号分隔,不能再加空格

在加载内核模块时,若不向模块传递参数,则参数将使用默认值。
如果模块被编译进内核,也就无法通过insmod/modprobe方式加载模块和传递参数。但可以修改bootloader,在bootargs里设置“模块名.参数名=值”的形式向该内置模块传递参数。
当模块被加载后,会在/sys/module目录下出现以此模块命名的目录。
当“参数读写权限”为0时,则在sysfs文件系统下不存在对应的文件节点。
当“参数读写权限”不为0时,则在sysfs文件系统下的/sys/module/parameters目录会出现一系列以参数名命令的文件节点。这些文件权限就是参数的读写权限,而文件内容就是参数的值。

2.5、模块导出符号

/proc/kallsyms文件对应内核符号表,记录了符号以及符号所在的内存地址。
模块可使用如下宏将其符号(全局变量或函数)导出到内核符号表中,供其他模块使用。其他模块使用这些符号前,只需先声明一下即可。

EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名); //只适用于包含GPL许可权的模块

2.6、模块声明与描述

MODULE_AUTHOR(); //声明作者信息
MODULE_DESCRIPTION(); //声明描述信息,如功能
MODULE_VERSION(); //声明版本信息
MODULE_DEVICE_TABLE(); //声明设备表,如USB、PCI等设备驱动,通常会创建这个表说明支持的设备
MODULE_ALIAS(); //给模块取别名

2.7、模块使用计数

Linux2.6以后的内核提供了模块计数管理接口,来配合设备模型。
内核为不同类型的设备定义了struct module *owner域,用来指向管理此设备的模块。
当开始使用某个设备时,内核调用try_module_get增加管理此设备的模块的使用计数。
当不再使用此设备时,内核使用module_put减少对管理此设备的管理模块的使用计数。
以此达到效果:当设备正在使用时,管理此设备的模块不能被卸载;当设备没有在使用时,模块才允许被卸载。

int try_module_get(struct module *module); //增加模块使用计数
void module_put(struct module *module); //减少模块使用计数

Linux2.6以后的内核,计数管理由内核更底层的代码实现,开发人员所写驱动很少需要亲自调用计数管理接口。

3、模块编译方式

编写一个简单的Makefile完成模块的编译。

# 获取内核版本信息
KVERS = $(shell uname -r)
# 内核模块,模块名对应源文件名
obj-m += modulename.o
# 若一个模块包含多个源文件,需添加如下变量
modulename-objs := file1.o file2.o

# 模块编译的特殊标志,可选
#EXTRA_CFLAGS = -g -O0

build: kernel_modules
kernel_modules:
	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
clean:
	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean

Makefile文件需跟源代码位于同一级目录,运行make命令编译生成模块,运行make clean命令清除编译产物。

4、模块相关命令及文件

insmod module_name.ko:加载模块,但不会自动解析该模块所依赖的其他模块,也不会加载依赖的其他模块。
modprobe module_name.ko:加载模块,自动解析和加载该模块所依赖的其他模块。通过解析modules.dep文件获取依赖关系。
rmmod module_name:卸载模块。
modprobe -r module_name:卸载模块及其依赖的模块。
lsmod:打印系统中已加载的所有模块以及模块间的依赖关系。通过读取和解析/proc/modules实现。
modinfo module_name.ko:打印模块的信息,如模块作者、模块说明、模块许可证等。

/proc/modules:存放内核已加载模块的信息,如模块名、设备号等。
/sys/module/module_name:存放内核已加载模块的信息,一个目录对应一个模块。
/sys/module/module_name/parameters:存放模块中以参数名命名的文件节点。
/lib/modules//modules.dep:存放模块之间的依赖关系,由整体编译内核时由depmod工具生成。
/proc/kallsyms:内核符号表,记录所有的内核符号及符号所在的内存地址。

5、实例

模块源代码:

#include 
#include 
#include 

static char *hello_name = "hello world";
module_param(hello_name, charp, S_IRUGO);

int hello_year = 1997;
module_param(hello_year, int, S_IRUGO);

unsigned int hello_array[10] = {12, 23, 34, 45, 56, 67, 78, 89, 90, 100};
unsigned int hello_array_len;
module_param_array(hello_array, uint, &hello_array_len, S_IRUGO);


static int hello_init(void)
{
    int i = 0;
    printk(KERN_INFO "call hello_init\n");
    printk(KERN_INFO "hello_name:%s\n", hello_name);
    printk(KERN_INFO "hello_year:%d\n", hello_year);
    for (i = 0; i < sizeof(hello_array) / sizeof(hello_array[0]); i++)
    {
        printk(KERN_INFO "hello_array[%d]:%u\n", i, hello_array[i]);
    }
    return 0;
}
module_init(hello_init);

static void hello_exit(void)
{
    printk(KERN_INFO "call hello_exit\n");
}
module_exit(hello_exit);

MODULE_AUTHOR("Handsome <[email protected]>");
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("a simple test module");
MODULE_ALIAS("simple_test");

Makefile文件:

KVERS = $(shell uname -r)

obj-m := hello.o

build: kernel_modules

kernel_modules:
	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
clean:
	make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean

操作步骤:

  1. make编译生成hello.ko
  2. sudo bash进入root
  3. insmod hello.ko加载模块,查看打印
  4. rmmod hello卸载模块,查看打印
  5. insmod hello.ko hello_name="welcome to heaven!!!" hello_year=2060 hello_array=11,22,33,44,55加载模块并传递参数,查看打印
  6. lsmod查看已加载模块中是否有本模块
  7. cat /proc/modules查看已加载模块中是否有本模块
  8. cat /proc/kallsyms查看内核符号表中是否有本模块的符号,如函数名、变量名。
  9. modinfo hello.ko查看本模块的信息
  10. 查看/sys/modules/hello/parameters中是否有hello_namehello_yearhello_array 这三个参数对应的文件并查看文件内容。
  11. rmmod hello再次卸载模块

你可能感兴趣的:(linux,驱动开发,c语言)