linux可加载内核模块机制(LKM)

1. linux可加载内核模块机制(LKM)介绍

  • linux模块机制 在不需要重新编译重启内核的情况下可以动态的加载或者卸载一个模块。
  • 内核模块(*.ko)本质是一个 ​未链接的ELF可重定位文件,其代码中的内核符号引用在编译时未绑定地址。 加载时由内核模块加载器执行 动态重定位。
  • 内核模块运行在内核态。运行效率和静态编译基本一致,同时又支持动态加载和卸载的特性。

2. 内核模块代码的组成 - 一个最简单的内核模块

  • 一个简单的内核模块示例

    c 复制代码
    #include <linux/init.h>
    #include <linux/module.h>
    static int __init hello_init(void)
    {
    	printk(KERN_EMERG  "hello world!\n");
    	// dump_stack();
     	return 0;
    }
    static void __exit hello_exit(void)
    {
    	printk(KERN_EMERG  "goodbye world!\n");
    	// dump_stack();
    }
    module_init(hello_init);    /* 模块入口,加载模块时会盗用 */
    module_exit(hello_exit);    /* 模块出口,卸载模块时会调用 */
    MODULE_LICENSE("GPL");                    /* 模块许可证书,必须声明否则会产生kernel tainted警告 */
    MODULE_AUTHOR("xxxxxxxxxx");                        /* 作者信息 */
    MODULE_DESCRIPTION("a kernel module demo");         /* 模块描述信息 */
    MODULE_VERSION("V1.0");                             /* 模块版本号 */
    MODULE_ALIAS("module alias name: hello_demo");      /* 模块别名 */
  • Makefile

    Makefile 复制代码
    .PHONY: all clean
    #ifneq ($(KERNELRELEASE),)
    obj-m := hello.o
    #else
    EXTRA_CFLAGS += -DDEBUG 
    KDIR := ../../linux-orangepi
    all:
    		make -C $(KDIR) M=$(PWD) modules
    clean:
    		make -C $(KDIR) M=$(PWD) modules clean
    #endif
  • 上述Makefile是一个支持在内核外编译编译模块的Makefile,只需要指定内核路径即可(当前硬件运行的内核,必须与指定内核源码的路径一致,否则会造成内核污染),编译生成*.ko文件。可以通过如下命令动态的加载和卸载模块,如下是一些常用的路径。

    • insmod hello.ko:安装一个模块
    • rmmod hello:卸载一个模块
    • lsmod:查看当前已安装模块
    • modinfo hello.ko:获得当前模块的信息

3. 向模块传递参数

我们在编写一个内核模块,如编写一个驱动程序时,通常需要向模块中传递一些参数,比如uart驱动的波特率等,又或者模块在调试阶段,可以通过参数对模块进行调试。

  • linux内核提供了如下向模块传递参数的宏

    c 复制代码
    /*
     * 向内核传递一个参数
     * name:变量的名称
     * type:变量的类型
     * 	- bool:布尔型 
     * 	- inbool:布尔反值
     *  - charp:字符指针 
     *  - short:短整型 
     *  - ushort:无符号短整型 
     *  - int:整型
     *  - uint:无符号整型 
     *  - long:长整型
     *  - ulong:无符号长整型。
     * perm:访问权限
     * 	#define S_IRWXU 00700	/* 文件所有者 可读/可写/可执行 */
     * 	#define S_IRUSR 00400	/* 文件所有者 可读 */
     * 	#define S_IWUSR 00200	/* 文件所有者 可写 */
     * 	#define S_IXUSR 00100	/* 文件所有者 可执行 */
     * 
     * 	#define S_IRWXG 00070	/* 与文件所有者同组的用户 可读/可写/可执行 */
     * 	#define S_IRGRP 00040	/* 与文件所有者同组的用户 可读 */
     * 	#define S_IWGRP 00020	/* 与文件所有者同组的用户 可写 */
     * 	#define S_IXGRP 00010	/* 与文件所有者同组的用户 可执行 */
     * 
     * 	#define S_IRWXO 00007	/* 其他用户 可读/可写/可执行 */
     * 	#define S_IROTH 00004	/* 其他用户 可读 */
     * 	#define S_IWOTH 00002	/* 其他用户 可写 */
     * 	#define S_IXOTH 00001	/* 其他用户 可执行 */
     */
    #define module_param(name, type, perm)
    /* 
     * 向内核传递一个数组
     * nump:数组长度
     */
    #define module_param_array(name, type, nump, perm)
    /* 
     * 向内核传递一个字符串
     * len:字符串长度
     */
    #define module_param_string(name, string, len, perm)
  • 用法示例

    c 复制代码
    #include <linux/init.h>
    #include <linux/module.h>
    static int debug_level = 1;          // 整型参数,默认值1
    static char *device_name = "default"; // 字符串参数,默认值"default"
    static int ports[5];                 // 存储整型数组
    static int port_count;                // 记录实际传入的元素数量
    static char target_ip[16]; // 存储IP地址的缓冲区
    module_param(debug_level, int, 0644); // 权限:用户可读写,其他用户只读
    MODULE_PARM_DESC(debug_level, "Debug level (0=disable, 1=enable)");
    module_param(device_name, charp, 0444); // charp表示字符指针,权限:所有用户只读
    MODULE_PARM_DESC(device_name, "Target device name");
    module_param_array(ports, int, &port_count, 0644); 
    MODULE_PARM_DESC(ports, "Array of port numbers");
    module_param_string(ip, target_ip, sizeof(target_ip), 0644);
    MODULE_PARM_DESC(ip, "Destination IP address");
    static int __init hello_init(void)
    {
    	printk(KERN_EMERG  "hello world!\n");
    
    	printk(KERN_EMERG "Debug: %d, Device: %s\n", debug_level, device_name);
    
    	for (int i = 0; i < port_count; i++) {
    		printk(KERN_EMERG "Port[%d]: %d\n", i, ports[i]);
    	}
    	printk(KERN_EMERG "Target IP: %s\n", target_ip);
    	return 0;
    }
    static void __exit hello_exit(void)
    {
    	printk(KERN_EMERG  "goodbye world!\n");
    	// dump_stack();
    }
    module_init(hello_init);    /* 模块入口,加载模块时会盗用 */
    module_exit(hello_exit);    /* 模块出口,卸载模块时会调用 */
    MODULE_LICENSE("GPL");                    /* 模块许可证书,必须声明否则会产生kernel tainted警告 */
    MODULE_AUTHOR("xxxxxxxxxx");                        /* 作者信息 */
    MODULE_DESCRIPTION("a kernel module demo");         /* 模块描述信息 */
    MODULE_VERSION("V1.0");                             /* 模块版本号 */
    MODULE_ALIAS("module alias name: hello_demo");      /* 模块别名 */
  • 加载命令

    bash 复制代码
    insmod my_module.ko debug_level=3 device_name="eth0" ports=80,443,8080 ip="192.168.1.1"

4. 内核模块符号的导出

  • c语言中的符号默认是全局符号,所以静态编译时全局符号默认会加入全局符号表。

  • 模块是分开编译的,模块内的全局符号作用域止于模块,其他模块是不能使用。

  • linux内核提供了一种机制可以显示的导出全局符号,可供内核或者其他模块使用,使用是只需要将要导出的符号使用 如下宏声明即可:

    c 复制代码
    /linux-orangepi/include/linux/export.h
    
    /* 将符号 sym(函数或全局变量)导出到公共内核符号表 */
    #define EXPORT_SYMBOL(sym)		_EXPORT_SYMBOL(sym, "")
    /* 导出符号 sym,但仅限GPL兼容模块使用。非GPL模块加载时会因符号检查失败而拒绝链接 */
    #define EXPORT_SYMBOL_GPL(sym)		_EXPORT_SYMBOL(sym, "_gpl")
    /* 将符号 sym 导出到特定命名空间 ns,避免全局符号污染,增强模块化安全性。 */
    #define EXPORT_SYMBOL_NS(sym, ns)	__EXPORT_SYMBOL(sym, "", __stringify(ns))
    /* 同时限制符号的命名空间和GPL许可证,提供最高级别的访问控制 */
    #define EXPORT_SYMBOL_NS_GPL(sym, ns)	__EXPORT_SYMBOL(sym, "_gpl", __stringify(ns))
  • 模块导出的全局符号存储在/lib/modules/6.1.43/build/Module.symvers

5. 将模块编译进内核

在编写一个内核模块时,通常会在内核外独立编译加载运行,当功能稳定后一般会考虑将其固话到内核,本节介绍将模块编译进内核的流程

  • 修改Makefile

    • 将编写的模块放到这个目录下./linux-orangepi/drivers/char (假设这个模块是一个字符驱动),
    • 在当前目录的Makefile下增加如下内容 obj-$(CONFIG_HELLO) += hello.o
      • 在linux内核的Makefile中,变量obj-y对应的值会静态编译,obj-n对应的值不会编译, obj-m对应的值会编译为一个模块
      • CONFIG_HELLO 可在Kconfig 中增加一个配置项,用来决定当前模块 静态编译/不编译/编译为一个模块
  • 修改Kconfig增加一个配置项

    Makefile 复制代码
    config HELLO
    tristate "A simplist kernel module: hello"
    default n
    help
      a kernel module demo: hello world


    config HELLO通过menuconfig配置生成.config,最终生成CONFIG_HELLO被Makefile使用

  • 编译并加载内核

    • 通过menuconfig配置

    • 编译内核

      bash 复制代码
      make -j10
    • 安装内核及模块

      c 复制代码
      sudo make modules_install	/* 安装内核模块 */
      sudo make install			/* 安装内核 */
      sudo make dtbs_install INSTALL_DTBS_PATH=/boot/dtb/	/* 安装设备树文件到boot/btb中 */
    • 重启 Linux 系统就会加载新编译的内核了