Bootstrap

Linux内核模块

一、Linux内核模块简介

1. 概述

由于Linux内核的整体架构庞大且组件多,若将所有需要的功能都打包到内核,会导致内核会很大且臃肿,为了解决这个问题,Linux提供了Module的机制。
Module机制就是在编译内核时本身并不需要包含所有功能,在需要某些功能时再将对应模块动态的加载到内核中,且模块一旦被加载就和内核中其他部分完全一样。

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

static int __init hello_init(void)
{
	printk(KERN_INFO "Hello World enter\n");
	return 0;
}
module_init(hello_init);

static void __exit hello_exit(void)
{
	printk(KERN_INFO "Hello World exit\n ");
}
module_exit(hello_exit);

MODULE_AUTHOR("studey");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("A simple Hello World Module");
MODULE_ALIAS("a simplest module");

以上是一个最简单的内核模块,编译会产生hello.ko目标文件。

  • insmode ./hello.ko命令可以加载模块,加载时会输出"Hello World enter",通过rmmode hello命令卸载时会输出"Hello World exit"。
  • modprobe命令比insmod命令强大,其在加载某模块时会同时加载该模块所依赖的其他模块,通过modprobe -r卸载时也将同时卸载其依赖的模块。各模块的依赖信息存放在“/lib/modules/$kernel-version/modules.dep”文件中。
  • lsmod 命令可以获取系统中已加载的所有模块及模块间的依赖关系,其实际上是读取并分析"/proc/modules"文件。已经加载模块的信息也存在于"/sys/module"目录下,加载hello.ko后将包含"sys/module/hello"目录
  • modinfo hello可以获取模块的信息,包括模块作者、模块说明等

printk()函数是内核空间的输出函数,与用户空间的printf()的差别在于可以定义输出的级别

2. 结构

一个LInux内核模块主要由以下几部分组成:

  1. 模块加载函数:通过insmod/modprobe命令加载内核模块时,模块的加载函数会自动被内核调用执行,完成本模块的相关初始化工作。
  2. 模块卸载函数:当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核调用执行,完成与模块加载函数相反的功能。
  3. 模块许可证声明:LICENSE声明描述内核模块的许可权限,大多数情况下内核都应该遵循GPL兼容许可权,如果不声明LICENSE会在模块加载时收到内核被污染的警告(Kernel Tainted)。
  4. 模块参数(可选): 模块加载时可以传递给它的值,对应模块内部的全局变量。
  5. 模块导出符号(可选):内核模块可以导出符号(对应模块内部的函数或变量)给其他模块使用
  6. 模块信息声明(可选 ):如模块作者等信息

二、具体介绍

1. 模块加载函数

Linux内核模块加载函数编写的标准:返回整型值,若初始化成功返回0,失败返回对应错误码(可以根据<linux/errno.h>中的错误符号定义)。 一般以__init标识声明,通过module_init()函数来指定。

static int _ _init initialization_function(void)
{
	/*  初始化代码 */
}
module_init(initialization_function);

所有的标识为__init的函数如果直接编译进内核而非生成.ko档的话,在链接的时候都会生成.init.text段,并在.intcall.init也保存了一份函数指针,初始化时内核会通过这些函数指针调用__init函数,初始化完成后释放init(包括.init.text、.initcall.init)区段的内存。

#define __init __attribute__ ((__section__ (".init.text")))

除了函数之外,对于知识初始化阶段需要的数据也可以被定义为__initdata,内核在初始化完后也会释放对应的内存。

static int hello_data __initdata = 1;
static int __init hello_init(void)
{
	printk(KERN_INFO "Hello, world %d\n", hello_data);
	return 0;
}
module_init(hello_init);
static void __exit hello_exit(void)
{
	printk(KERN_INFO "Goodbye, world\n");
}
module_exit(hello_exit);

代码中可以直接通过request_module(module_name)函数灵活地加载内核模块

2. 模块卸载函数

Linux内核模块卸载函数不返回任何值,要完成与模块加载函数相反的功能。一般以__exit标识声明,通过module_exit()函数来指定。

static void _ _exit cleanup_function(void)
{
	/*  释放代码 */
}
module_exit(cleanup_function);

如果对应的模块直接编译进内核而非生成.ko档的话,就不可能再卸载该模块了,所以标识为__exit的函数此时不会链接到内核。
除了函数之外,推出阶段需要的数据也可以用__exitdata来形容。

3. 模块参数

模块的参数通过module_param(参数名,参数类型,参数读/写权限)函数来指定,在装载内核模块时,用户可以向模块传递参数,形式为:insmode/modprobe 模块名 参数名1= 参数值, 参数名2=参数值,如果不传递则使用模块内定义的缺省值。
若是参数数组则通过module_param_array(数组名,数组类型,数组长,参数读/写权限)来指定。运行insmod/modprobe命令时,使用逗号分隔输入的数组元素。
如果模块被内置无法使用insmode,可以使用bootloader中通过bootargs设置“模块名 参数名1= 参数值, 参数名2=参数值”给内置的模块传递参数。

static char *book_name = "dissecting Linux Device Driver";
module_param(book_name, charp, S_IRUGO);
static int book_num = 4000;
module_param(book_num, int, S_IRUGO);

参数的类型可以是byte、short、ushort、int、uint、long、ulong、charp(字符指针)、bool或invbool(布尔的反),模块在编译时会将module_param()中声明的类型与变量定义的类型进行比较是否一致。
模块被加载后,在"/sys/module"目录下会出现该模块的目录,若模块参数指定的读/写权限不为0,则会在该模块目录下的“parameters”创建对应参数的节点,节点的读/写权限就是参数指定的权限,节点内容就是参数的值。

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

static char *book_name = "dissecting Linux Device Driver";
module_param(book_name, charp, S_IRUGO);

static int book_num = 4000;
module_param(book_num, int, S_IRUGO);

static int __init book_init(void)
{
	printk(KERN_INFO "book name:%s\n", book_name);
	printk(KERN_INFO "book num:%d\n", book_num);
	return 0;
}

...

对上述模块运行insmode book.ko命令装载时,相应参数都输出的是模块内的默认值,通过查看“/var/log/message”日志文件可以看出内核的输出:

# tail -n 2 /var/log/messages
Jul 2 01:03:10 localhost kernel: <6> book name:dissecting Linux Device Driver
Jul 2 01:03:10 localhost kernel: book num:4000

当用户运行insmod book.ko book_name='GoodBook'book_num=5000命令时,输出的是用户传递的参数:

# tail -n 2 /var/log/messages
Jul 2 01:06:21 localhost kernel: <6> book name:GoodBook
Jul 2 01:06:21 localhost kernel: book num:5000

另外在“/sys/module/book/paameters”路径下可以看到对应的参数, 并且可以通过cat book_name”和“cat book_num查看它们的值。

stydy@test:/sys/module/book/parameters$ tree
. ├── book_name └── book_num

4. 导出符号

导出的符号可以被其他模块使用,只需要像使用普通的函数一样,提前声明一下即可。
模块可以使用EXPORT_SYMBOL/EXPORT_SYMBOL_GPL(符号表)导出符号到内核符号表中。
EXPORT_SYMBOL_GPL只适用于包含GPL许可权的模块,若想要被非GPL模块引用,则可以将GPL去掉,或者通过写一个wrapper内核模块对GPL模块的函数以EXPORT_SYMBOL()形式封装后使用(可能会构成侵权)。

“/proc/kalllsyms”文件对应着内核符号表,记录了符号以及符号所在的内存地址。

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

int add_integar(int a, int b)
{
	return a + b;
}
EXPORT_SYMBOL_GPL(add_integar);

int sub_integar(int a, int b)
{
	return a - b;
}
EXPORT_SYMBOL_GPL(sub_integar);

MODULE_LICENSE("GPL v2");
# grep integar /proc/kallsyms
e679402c r __ksymtab_sub_integar [export_symb]
e679403c r __kstrtab_sub_integar [export_symb]
e6794038 r __kcrctab_sub_integar [export_symb]
e6794024 r __ksymtab_add_integar [export_symb]
e6794048 r __kstrtab_add_integar [export_symb]
e6794034 r __kcrctab_add_integar [export_symb]
e6793000 t add_integar [export_symb]
e6793010 t sub_integar [export_symb]

5. 模块声明与描述

模块MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION、MODULE_DEVICE_TABLE、MODULE_ALIAS分别声明模块的作者、描述、版本、设备表和别名

MODULE_AUTHOR(author);
MODULE_DESCRIPTION(description);
MODULE_VERSION(version_string);
MODULE_DEVICE_TABLE(table_info);
MODULE_ALIAS(alternate_name);

对于USB、PCI等设备驱动,通常会创建一个MODULE_DEVICE_TABLE表明该驱动模块所支持的设备。

/* table of devices that work with this driver */
static struct usb_device_id skel_table [] = {
{ USB_DEVICE(USB_SKEL_VENDOR_ID,
	USB_SKEL_PRODUCT_ID) },
	{ } /* terminating enttry */
};

MODULE_DEVICE_TABLE (usb, skel_table);

6. 模块的编译

# Kernel modules
obj-m += hello.o
hello-objs := file1.o file2.o
# Specify flags for the module compilation.
# 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

开启EXTRA_CFLAGS=-g -O0后编译出的ko当可以包含调试信息。

;