Linux 内核模块

1 Linux 内核模块简介

Linux 内核是一个十分庞大的系统,如何能够为其瘦身,订制适合自己应用场景的 linux 系统,这就需要了解模块技术。

linux 内核与各模块的关系分为两种:

  • 把所有需要的功能都直接编译进入内核,但这会带来两个问题,一是生成的内核很大,从而导致内核占用内存也很大;二是如果内核需要新增或者删除功能的话,必须要重新编译内核。
  • 编出的内核本身不需要包含所有功能,而这些功能在需要使用时动态加载到内核,如此动态加载的功能单元就叫做模块。

Linux 提供的这种机制被称为模块,模块具有这样的特点:

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

2 模块样例

以下是一个样例模块 hello.ko 样例:

/*
 *a simple kernel module: hello
 *
 *Licensed under GPLv2 or later
 */

#include 
#include 

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("penghuster ");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("A simple Hello World Module.");
MODULE_ALIAS("a simplest module.");

编译此模块的方法可以分为两种方式:

2.1 与内核编译同步捆绑进行

在内核的相关功能单元(如 drivers)下创建 hello 文件夹,然后根据内核Kconfig 和 Makefile的编写规则,编写 hello 模块的 Kconfig 和 Makefile 文件。
drivers/hello/Kconfig:

#
# Hello driver configuration 
#

#menu "HELLO driver"
comment "hello driver"

config CONFIG_HELLO
    bool "support HELLO"

#endmenu

drivers/hello/Makefile:

#drivers/test/test/Makefile
#
#Makefile for the TEST CPU
#

obj-$(CONFIG_HELLO) += hello.o

另外在drivers/Kconfig文件中添加行 source "drivers/hello/Kconfig"
在drivers/Makefile文件中添加 obj-y += hello/

注意:hello/Makefile也可直接写为 obj-m += hello.oobj-y += hello.o 从而可以省略 Kconfig的相关工作。Kconfig是为了 make menuconfig 配置而添加。

2.2 独立编译模块

需要进行独立模块编译前,需要确认 /lib/modules/目录下是否已经安装你需要进行模块编译的内核版本。如果没有,请安装对应版本的 linux-image 和 Linux-image-extra 包,具体命令如下:

sudo apt-get install --reinstall linux-image-3.13.0-34-generic
sudo apt-get install --reinstall linux-image-extra-3.13.0-34-generic`

然后编写如下Makefile:

##此处表示编写在本机上使用的,交叉编译器进行交叉编译,此处也需要改变。
KVERS = $(shell uname -r) 

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=$(PWD) modules
    
clean:
    make -C /lib/modules/$(KVERS)/build M=$(PWD) clean

2.3 内核模块的相关操作命令

insmod ./hello.ko ---加载模块
rmmod hello ---卸载模块
lsmod / cat /proc/modules ---查看所有模块
/sys/module/hello 下执行 tree -a ---查看模块的详细组成文件及其信息
/lib/modules//modules.dep ---文件查看模块依赖
modinfo hello.ko ---查看模块信息
modprobe ./hello.ko ---加载该模块的同时,加载该模块的所有依赖模块
modprobe -r hello ---卸载该模块,同时卸载其依赖模块

3 Linux 内核模块的程序结构

3.1 模块加载函数

初始化函数原型

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

内核中也可以主动调用以下函数来灵活加载模块
request_module(module_name);
在 Linux ,所有标识为 __init 的函数如果直接编译进入内核,成为内核镜像的一部分,在连接的时候都会放到 .init.text 段区。

注意:很明显模块通过 insmod 加载,并没有编入到内核镜像中,两者是通过MODULE宏变量来实现是否编入内核。
#ifdef MODULE
#define module_init(initfn) static inline initcall_t __inittest(void)
{ return initfn; }
#else
#define module_init(x) __initcall(x);
#endif

#define __init __attribute__((__section__(".init.text")))
所有的 __init 函数在区段 .initcall.init 中保存了一份函数指针,在初始化时内核会通过这些函数指针调用这些 __init 函数,并在初始化完成后,释放 init区段(包括 .init.text / .initcall.init)的内存.
除函数外数据也可以定义为 __initdata. 只有初始化阶段需要的函数.

3.2 模块卸载函数

函数原型

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

同样也存在 __exitdata 只在退出阶段可用的数据。

3.3 模块参数

我们可以使用 "module_param(param_name, param_type, r/w)" 为模块定义一个参数。例如:

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(char *), bool 或 invbool(布尔的反)。

模块参数的传递,用户可以在加载模块时使用形式为 insmod(/modprbe) module_name param_name = praram_value,如果模块被内置,则使用 bootloader 通过在 bootargs 里设置 “模块名.参数名 = 值” 值得形式传递给内核。

另外也可使用 “module_param_array(array_name, array_type, array_length, r/w)” 来传递数组参数。运行 insmod 或 modprobe 时,应该使用逗号分隔开数组元素。

模块加载成功后,在 /sys/module/ 目录下会出现以此模块名命名的目录。当参数的读写属性为 0 时,标示在 sysfs 文件系统下,不存在对应的文件节点;当模块存在 “参数读写属性” 不为 0 的命令行参数时, 此模块目录下将出现 parameters 目录,此目录下将出现与参数同名的文件,其文件的读写属性与参数一致。

3.4 导出符号

Linux 的 “/proc/kallsyms” 文件对应着内核符号表,它记录了符号以及符号所在的内存地址。
模块可以使用如下宏导出符号到内核符号中:

EXPORT_SYMBOL(symbol_name);
EXPORT_SYMBOL_GPL(symbol_name);

导出的符号可以被其它模块使用,只需要使用前申明下即可。EXPORT_SYMBOL_GPL 只适用于包含 GPL 许可权的模块。

3.5 模块声明与描述

Linux 内核模块的一般信息声明

MODULE_AUTHOR("penghuster ");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("A simple Hello World Module.");
MODULE_ALIAS("a simplest module.");
MODULE_DEVICE_TABLE(table_info);

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

3.6 模块的使用计数

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

Linux 2.6 以后内核提供了模块计数管理接口 try_module_get(&module)则增加使用计数函数 和 module_put(&module) 减少使用计数函数,从而取代 linux 2.4 的方式。只有当模块使用计数为 0 时,方能够卸载模块。

你可能感兴趣的:(Linux 内核模块)