Linux内核模块(.ko文件)

Linux内核模块简介

模块的基本概念

Linux内核非常庞大,包含很多组件。我们如何把需要的部分包含在内核中?
两种办法:
1)把所有需要的功能都编译进内核中。
会导致2个问题:生成的内核会非常大;为现有内核添加、删除功能,将不得不重新编译。

2)不包含所有功能,需要的时候,动态地加载代码到内核。
这种机制称为模块(Module)

模块特点:

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

Linux中,一个.ko文件就是一个模块文件,可以通过insmod/rmmod/lsmod命令,动态地加载/卸载/查看模块。

一个最简单的Linux内核模块

hello.c

#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); /* 告诉内核hello_init是模块驱动程序的入口函数 */

static void __exit hello_exit(void) /* 内核模块卸载函数 */
{
    printk(KERN_INFO "Hello World exit\n");
}
module_exit(hello_exit); /* 告诉内核hello_init是模块驱动程序的出口函数 */

MODULE_AUTHOR("Barray Song <21cnbao@gmail.com>");
MODULE_LICENSE("GPL v2"); /* 必须, 声明遵循GPL协议 */
MODULE_DESCRITION("A simple Hello World Module");
MODULE_ALIAS("a simplest module");

包含内核模块加载函数、卸载函数,对GPL v2许可权限的声明,一些描述信息。

利用内核编译该文件,生成hello.ko模块文件夹。可通过"insmod ./hello.ko"加载驱动,"rmmod hello"卸载驱动。加载时,输出"Hello World enter",卸载时输出"Hello World exit"。

printk与printf

printk是内核模块输出函数,运行在内核空间;printf是glibc库函数,运行在用户空间。
printk可定义输出级别,printf不支持输出级别定义。

lsmod命令

lsmod命令列出已加载模块信息,实际是读取、分析"/proc/modules",可以用cat命令输出已加载模块信息

# lsmod
# cat /proc/modules

另外,内核中已经加载模块的信息也存在于/sys/module目录下。加载hello.ko后,内核中将包含/sys/module/hello目录,该目录下又有一个refcnt文件和一个sections目录。在/sys/module/hello下运行"tree -a"命令,可得到目录树形结构(前提是运行安装安装了tree软件)

modprobe命令

modprobe命令比insmod命令更强大,加载模块时,会同时加载该模块所依赖的其他模块。用modprobe命令加载的模块,如果以"modprobe -r filename"方式卸载,同时卸载依赖的模块。

模块的依赖关系存放在根文件系统的/lib/modules//modules.dep文件中,是在整体编译内核的时候,由depmod工具生成的。其格式很简单:

kernel/lib/cpu-notifier-error-inject.ko: kernel/lib/notifier-error-inject.ko
kernel/lib/pm-notifier-error-inject.ko: kernel/lib/notifier-error-inject.ko
kernel/lib/lru_cache.ko:
kernel/lib/cordic.ko:
kernel/lib/rbtree_test.ko:
kernel/lib/interval_tree_test.ko:
updates/dkms/vboxvideo.ko: kernel/drivers/gpu/drm/drm.ko

modinfo <模块名> 命令可获得模块信息,包括模块作者、说明、所支持的参数及vermagic:

# modinfo 100ask_led.ko
filename:       /mnt/100ask_led.ko
license:        GPL
depends:
vermagic:       4.9.88 SMP preempt mod_unload modversions ARMv7 p2v8

Linux内核模块程序结构

一个Linux内核模块主要组成:
1)模块加载函数
如前面通过宏定义module_init声明的hello_init,就是模块加载函数。当通过insmod或modprobe命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。

2)模块卸载函数
如前面通过module_exit声明的hello_exit。当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块加载函数相反的功能。

3)模块许可证声明
通过MODULE_LICENSE许可证声明描述内核的许可权限,如不声明LICENSE,模块被加载时,将收到内核被污染(Kernel Tainted)的警告。

可接受LICENSE:"GPL", "GPL v2", "GPL and additional rights", "Dual BSD/GPL", "Dual MPL/GPL", "Proprietary"。
推荐使用"GPL"或"GPL v2"。

4)模块参数(可选)
模块参数是模块被加载时,可以传递给它的值。本身对应模块内部的全局变量。

5)模块导出符号(可选)
内核模块可以导出的符号(symbol,对应于函数或变量),若导出,其他模块则可以使用本模块的变量或函数。

6)模块作者等信息声明(可选)


模块加载函数

Linux内核模块加载函数通常以__init标识声明,典型模块加载函数形式:

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

若初始化成功,则应返回0;失败时,返回错误编码,是一个负值,定于<linux/errno.h>。

Linux内核中,驱动开发人员可以通过request_module(const char* fmt, ...)加载内核模块。调用示例:

request_module(module_name);

标识__init有什么用?
Linux中,所有标识为__init的函数如果直接编译进内核,成为内核镜像的一部分,在连接的时候都会放在.init.text区段内。

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

所有__init函数中区段.initcall.init中还保存了一分函数指针,在初始化时,内核会通过这些函数指针调用这些__init函数,并且在初始化完成后,释放init区段(包括.init.text,.initcall.init等)的内存。

数据可以被定义为__initdata。对于只是在初始化阶段需要的数据,内核中初始化完成后,可以释放其内存。如下面hello_data定义为__initdata:

static int hello_data __initdata = 1;

static int __init hello_init(void)
{
    ...
}
module_init(hello_init);

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

模块卸载函数

Linux内核模块卸载函数以__exit标识声明。典型形式:

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

模块卸载函数在模块卸载的时候执行,不返回任何值,且必须以"module_exit(函数名)"形式声明。功能与模块加载函数相反。

__exit修饰模块卸载函数,告诉内核如果相关模块被直接编译进内核,则cleanup_function函数会被省略,不编译进内核镜像。因为既然模块被内置,就不可能卸载了,卸载函数也没必要存在。
类似地,退出阶段用到的数据,可以用__exitdata来修饰。


模块参数

用 "module_param(参数名, 参数类型, 参数读/写权限)" 为模块定义一个参数。如下面代码定义1个整型参数和1个字符指针参数:

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

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

装载内核模块时,用户可以向模块传递参数,形式:"insmod(或modprobe) 模块名 参数名=参数值";如果不传递,参数将使用模块内定义的缺省值。
如果模块被内置,就无法insmod,但bootloader可通过在bootargs里设置"模块名, 参数名=值"的形式给该内置的模块传递参数。

支持的类型参数:byte,short,ushort,int,uint,long,ulong,charp(字符指针),bool或invbool(布尔的反)。模块编译时,会将module_param中声明的类型与变量定义的类型进行比较,判断是否一致。

另外,模块也支持拥有参数数组,形式:"module_param_array(数组名, 数组类型, 数组长, 参数读/写权限)"


导出符号

Linux "/proc/kallsyms" 文件对应内核符号表,记录了符号及符号所在的内存地址。由于代码编译出来的模块是.ko文件,要想让本模块的符号被别的模块使用,就需要导出符号。

宏导出符号到内核符号表:

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

这样导出的符号就能被其他模块使用。

例子:导出整数加、减运算函数符号的内核模块

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

int add_integer(int a, int b)
{
    return a + b;
}
EXPORT_SYMBOL_GPL(add_integer); /* 导出符号 */

int sub_integer(int a, int b)
{
    return a * b;
}
EXPORT_SYMBOL_GPL(sub_integer);

MODULE_LICENSE("GPL v2");      /* 导出符号 */

模块声明与描述

Linux内核模块中,可以用以下以MODULE开头几个宏做模块声明:

MODULE_AUTHOR(author);               /* 模块的作者 */
MODULE_DESCRIPTION(description);     /* 模块的描述 */
MODULE_VERSION(version_string);      /* 模块的版本 */
MODULE_DEVICE_TABLE(table_info);     /* 模块的设备表 */
MODULE_ALIAS(alternate_name);        /* 模块的别名 */

模块的使用计数

Linux 2.4内核,模块自身通过MOD_INC_USE_COUNT,MOD_DEC_USE_COUNT宏来管理自己被使用的计数。

Linux 2.6以后内核,提供模块计数管理接口try_module_get(&module),module_put(&module),取代2.4的计数管理宏。

#include <linux/module.h>

/* 增加模块使用计数
 * 若返回0, 表示调用失败; 返回0, 表示希望使用的模块没有被加载或正在被卸载中
 */
int try_module_get(struct module *module);

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

Linux 2.6以后,内核为不同类型的设备定义了struct module *owner域,用来指向管理此设备的模块。当开始使用某个设备时,内核使用try_module_get(dev->owner)增加管理此设备的owner模块的使用计数;当不再使用时,内核使用module_put(dev->owner)减少使用计数。这样,当设备在使用时,管理此设备的模块将不能被卸载;只有当设备不再被使用(计数为0)时,模块才允许被卸载。

Linux 2.6以后,对于设备驱动而言,很少需要亲自调用try_module_get()与module_put(),因为会由内核里更底层的代码来实现,从而简化设备驱动开发。


模块的编译

为最开始的简单模块hello.c编写一个简单的Makefile:

KVERS = $(shell uname -r)

# Kernel modules
obj-m += hello.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

这个Makefile应该与源码hello.c位于同一目录,开启选项EXTRA_CFLAGS=-g -O0,可以得到包含调试信息的hello.ko模块。运行make命令即可得到hello.ko文件。

上面是一个模块只包含一个.c文件,如果包含多个.c文件,如file1.c,file2.c,可以用如下方式编写Makefile:

obj-m := modulename.o
modulename-objs := file1.o file2.o

参考

[1]宋宝华. Linux设备驱动开发详解[M]. 人民邮电出版社, 2010.

posted @ 2022-06-20 08:25  明明1109  阅读(7348)  评论(0编辑  收藏  举报