当前位置:首页>Linux>Linux内核模块开发:结构、编译、加载、卸载与高级特性实践

Linux内核模块开发:结构、编译、加载、卸载与高级特性实践

  • 2026-09-02 20:24:13
Linux内核模块开发:结构、编译、加载、卸载与高级特性实践

Linux内核模块

不编译进内核映像,控制内核大小。可动态加载到Linux内核中执行的程序模块,不加载不执行,加载执行便成为内核一部分。

模块文件后缀名为.ko,如hello.ko就是一个模块目标文件。

加载模块:insmod  ./hello.ko     or   modprobe ./hello.ko

卸载模块:rmmod   hello   or   modprobe -r <模块名>

lsmod命令     查看已加载的所有模块和模块间的依赖关系。

lsmod等价于 cat  /proc/modules
/proc/modules目录下存放的是模块的信息。

modinfo    <模块名>   查看模块作者、模块说明、支持的参数等东西

驱动开发人员可以调用request_module(const char *fmt,...)函数加载内核模块:

request_module(module_name);

Linux内核模块的程序组成

1、模块加载函数

insmod或者modprobe加载模块后,模块加载函数就加载到内核中执行初始化模块。 Linux模块加载函数一般以__init标识声明。

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

Linux内核中,使用request_module(module_name)函数加载内核模块。

以__init标记的函数如果直接编译进内核,称为内核镜像的一部分,在连接时会被放在.init.text段中

这个__init是一个宏,在内核中如下定义:

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

在.init.text的函数都在区段.initcall.init保存了一份函数指针,内核初始化时会通过函数指针调用进行初始化,初始化完成后释放 .init.text 段的所有内存。

2、模块卸载函数

与模块加载函数为相反的过程,卸载模块执行卸载函数。 卸载函数一般以__exit标识声明。

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

__exit修饰卸载函数,一旦内核将该模块编译进内核,则会忽略卸载函数,不编译进内核镜像。因为编译进内核后就属于内置的模块,不可卸载。

3、模块许可证声明

许可证(LICENSE)声明描述内核模块的许可权,不声明LICENSE,模块被加载时,将收到内核被污染(Kernel Tainted)的警告。Linux内核模块最常见的是以MODULE_LICENSE(“GPLv2”)语句声明模块采用GPLv2。不写模块许可证明编译会出现警告_

4、模块参数(可选)

模块参数是模块被加载的时候可以传递给它的值,它本身对应模块内部的全局变量,也可以在某个目录的文件之类的。

模块参数可以在不同场合下给内核模块传递不同参数,提高内核模块的灵活性。

module_param(参数,参数类型,参数读/写权限)  为模块声明一个参数 如:

static char * person_name = "Ares";
module_param(person_name,charp,S_IRUGO);
static int  person_age    =  20;
module_param(person_age,int,S_IRUGO);

参数类型可以是byte、short、ushort、int、uint、long、ulong、charp(字符指针)、bool或invbool(布尔的反)

加载模块时传递模块参数值:

insmod xxx.ko person_name="guangjie" person_age=25

insmode/modprobe  模块名  参数名=参数值      加载内核模块时向模块传递参数 模块被加载后,在/sys/module/目录下将出现以此模块名命名的目录

示例:

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

staticint int_param = 250;
module_param(int_param, int, S_IRUGO);

staticchar *char_param = "module param test";
module_param(char_param, charp, S_IRUGO);

staticint __init param_init(void)
{
    printk(KERN_INFO "int param : %d\r\n", int_param);
    printk(KERN_INFO "char pointer param : %s\r\n", char_param);
return0;
}

staticvoid __exit param_exit(void)
{
    printk(KERN_INFO "module param exit\r\n");
}

module_init(param_init);
module_exit(param_exit);

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("module param test");

加载内核模块以及制定内核模块参数:

insmod module_param.ko int_param=520 char_param="ILoveYou"

在/sys/module/module_param/parameters 下可以查看模块的参数:

module_parame为模块名

root@imx6ull14x14evk:/sys/module/module_param/parameters# ls
char_param  int_param

查看模块参数的值:

cat char_param
cat int_param

执行下面命令,查看内核日志文件的打印信息:

tail -n 2 /var/log/messages

5、模块导出符号(可选)

内核模块可以导出的符号(symbol,对应于函数或变量)。导出之后其他模块则可以使用本模块中的变量或函数。 Linux的/proc/kallsyms文件对应着内核符号表,记录了符号以及符号所在的内存地址。 模块用一下宏导出符号到内核符号表

EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名);
#include
#include<1inux/module.h>

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

int sub(int a, int b)
{
     return a - b;
}

EXPORT_SYMBOL_GPL(sub);
MODULE_LICENSE("GPLv2");

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

MODULE_AUTHOR(author);  声明模块作者MODULE_DESCRIPTION(description);  模块的描述即模块是干嘛的吧

MODULE_VERSION(version_string);     模块的版本

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

MODULE_ALIAS(alternate_name);         给模块取别名

7、导出符号共享

Linux的 /proc/kallsyms 文件对应内核符号表,记录符号及其内存地址。作用:比如导出一个函数到符号表中,那外部程序就可以调用这个函数。

导出符号到内核符号表:
  • EXPORT_SYMBOL(符号名)
  • EXPORT_SYMBOL_GPL(符号名)//只适合包含GPL许可权的模块

符号共享有两种:

  • 变量共享
  • 函数共享

查看内核符号表:

cat /proc/kallsyms | grep xxx   //xxx指符号名

Mafile的修改:

  • 有依赖内核模块(即使用到它导出的符号),要放在一起进行编译
  • clean伪目标增加和清除共享文件夹指令

模块手动加载:

  • 加载时,必须先加载相关依赖模块
  • 卸载时,顺序相反

模块自动加载:

  • 所有内核模块同一放到/lib/modules/内核版本目录下

    cp *ko /lib/modules/内核版本
  • 建立模块依赖关系

    depmod -a
  • 查看模块依赖

    cat /lib/modules/内核版本/module.dep
  • 加载模块及起依赖模块

    modprobe xxx   //xxx指模块名
  • 卸载模块及其依赖模块

    modprobe -r xxx
//module_param.c
#include<linux/module.h>
#include<linux/kernel.h>
#include<linux/init.h>

staticint int_param = 250;
module_param(int_param, int, S_IRUGO);

staticchar charparam = 20;
module_param(charparam, byte, S_IRUGO);

staticchar *char_param = "module param test";
module_param(char_param, charp, S_IRUGO);

EXPORT_SYMBOL(int_param);     //导出符号
EXPORT_SYMBOL(charparam);     //导出符号

intmy_add(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(my_add);

intmy_sub(int a, int b)
{
return a - b;
}
EXPORT_SYMBOL(my_sub);

staticint __init param_init(void)
{
    printk(KERN_INFO "int param : %d\r\n", int_param);
    printk(KERN_INFO "char pointer param : %s\r\n", char_param);
return0;
}

staticvoid __exit param_exit(void)
{
    printk(KERN_INFO "module param exit\r\n");
}

module_init(param_init);
module_exit(param_exit);

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("module param test");
//export_symbol.h
#ifndef _EXPORT_SYMBOL_H_
#define _EXPORT_SYSBOL_H_


externchar charparam;    //声明module_param导出的变量符号
externint int_param;

intmy_add(int a, int b);  //声明module_param导出的函数符号
intmy_sub(int a, int b);

#endif
//export_symbol.c
staticint __init export_symbol_init(void)
{
    printk("export symbol init\r\n");
    printk(KERN_INFO"my_add : %d\r\n", my_add(int_param, charparam));
    printk(KERN_INFO"my_sub : %d\r\n", my_sub(int_param, charparam));
return0;
}

staticvoid __exit export_symbol_exit(void)
{
    printk("export symbol exit\r\n");
}

module_init(export_symbol_init)
module_exit(export_symbol_exit);

MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("symbol test");

8、模块使用计数

通过对模块使用计数来判断,内核模块是否还被使用,只有当模块不再被使用时,模块才允许被卸载。

inttry_module_get(struct module *module);   //获取模块使用计数 返回0则表示调用失败,希望的模块
voidmodule_put(struct module *module);   //减少模块使用计数

对设备的使用计数由更底层的内核代码实现,而不需要内核开发人员去管理计数,简化了开发。

最新文章

随机文章