Skip to main content
 首页 » 操作系统

Linux驱动模块(1)——杂项汇总

2022年07月19日23落叶无声

kconfig kbuild makefile 模块,头文件导出请见 Documentation/kbuild/

1.modutils中提供了相关的insmod,rmmod,modinfo工具
2.modprobe在识别出目标模块所依赖模块后也是调用insmod.
3.从外部看模块只是普通可重定位的目标文件。可重定位文件的函数都不会引用绝对地址,而只是指向代码中的相对地址,因此可以在内存
中的任意偏移地址加载。
4.加载模块只需要一个系统调用init_module,即可在内核中完成所有的操作。
5.nm工具可用于产生模块(或任意目标文件)中所有的外部函数列表。
# nm atmel_mxt_ts.ko
# nm a.out
U代表未解决的引用;T表示位于代码段;D表示位于数据段

6.配置 CONFIG_KALLSYMS 选项为y即可在内核中启用 kallsyms 功能:
有的符号是大写的,有的是小写。大写的符号是全局的。
b 符号在BSS段中
c 普通符号,是未初始化区域
d 符号在数据段中
g 符号针对小object,在初始化数据区
i 非直接引用其他符号的符号
n 调试符号
r 符号在只读存储区
s 符号针对小object,在未初始化数据区
t 符号在代码段
u 符号引用还未解决

7.modutils标准工具中的depmod工具可用于计算系统各个模块之间的依赖关系,每次启动或安装模块时就会运行该程序,默认会将
依赖关系保存到 /lib/modules/<uname -r>/module.dep 中,表示格式B:A表示模块B依赖于模块A

8./proc/kallsyms中列出了内核导出的所有符号(早些版本应该是/lib/modules/version/System.map),模块中未解决引用的符号会在这里面找。

9.内核中模块的信息存放在.modinfo段中。模块中的信息可以使用modinfo来看。

10.模块的自动加载与热插拔
1)在用户空间完成模块加载比在内核空间完成模块加载更方便,内核将该工作委托给kmod进程。注意kmod并不是一个永久守护进程,内核会
按需启用它。

2)eg:mount -t vfat /dev/fd0 /mnt/floppy 若之前内核中没有vfat.ko,这时会先插入这个驱动模块,在mount返回后,所需的模块已经载
入内核了。执行过程:内核发现其数据结构中没有vfat的信息 --> 内核请求 --> modprobe --> 模块查找 --> request_module --> 加载模块
request_module
├── 为modeprob准备环境
├── 同时调用request_module的次数过多(50个)?==> return
└── call_usermodehelper
modprobe_path通常是/sbin/modprobe,但是可以通过/proc/sys/kernel/modprobe或sysctl改变。

3).有时候内核需要加载模块的时候可能会出现无法唯一确定哪个模块能提供所需的功能,因此产生模块别名(module alias)
eg:U盘插入到系统中,被主机控制器识别为新设备,我们知道需要装载的模块时usb-storage,但是内核是如何知道的呢:
附加在每个模块上都有一个“小小的数据库”,里面描述了该模块所支持的设备。对于USB设备,数据库的信息包括所支持的接口类型列表,
厂商ID或能够标识设备的任意类型信息。
数据库信息通过模块别名(module alias)提供,它是模块的通用标识。modules.h中的MODULE_INFO、MODULE_ALIAS宏,eg raid5.c
udevd实现模块的热插拔也是使用了这个信息。

eg:

root:/lib/modules/4.14.35# cat modules.alias | grep usb_storage
alias usb:v03EBp2002d0100dc*dsc*dp*ic*isc*ip*in* usb_storage
可以看出usb_storage.ko支持的哪些设备:Vender-ID: 03EB Product-ID: 2002,后面的*是任意匹配

内核启动过程中,总线枚举时把读取的设备ID等信息发送到udevd,udevd根据modules.alias文件找到匹配的驱动模块,使用工具modprobe加载之,
同时modprobe根据 modules.dep文件发现其依赖的ko文件,如果依赖的ko没有加载那么就先加载依赖的ko,然后再加载本驱动。

4)比直接别名更重要的是设备数据库,内核使用宏MODULE_DEVICE_TABLE来实现这样的数据库。

MODULE_DEVICE_TABLE(platform, xxx_ids);

一般用在热插拔的设备驱动中,上述xxx_ids结构是此驱动所支持的设备列表。
作用:将xxx_ids结构输出到用户空间,这样udevd在加载模块时,就知道了此设备应该加载哪个驱动模块。
用法:MODULE_DEVICE_TABLE(设备类型,设备表),其中,设备类型,包括USB,PCI,platform等,也可以自己起名字; 设备表也是自己定义的,它的最后一项必须是空,用来标识结束。
module.alias:root@xx:/lib/modules/4.14.35#
# cat modules.alias | grep ohci-platform
alias platform:ohci-platform ohci_platform
# cat modules.alias | grep ehci-platform
alias platform:ehci-platform ehci_platform

5)add_uevent_var() 位于kobject_uevent.c中,可以用于向uevent中的插拔消息提供一个新的键值对!
通过比较MODALIAS值和各个模块提供的别名,udevd可以找到需要插入的模块。
①模块代码中使用MODULE_ALIAS指定别名;
②设备插入时的udev事件中使用下面方法上报设备的别名
add_uevent_var(env, "MODALIAS=virtio:d%08Xv%08X", dev->id.device, dev->id.vendor);
这个函数这个使用方法在内核中还存在,应该没有被遗弃!!

11.模块的插入和删除
1)两个系统调用,init_module() 和 delete_module(), 可以使用man 2 来看
init_module
├── load_module
├── 将模块插入到内核链表
├── mod->init
└── 释放初始化数据/代码占用的区域

sys_delete_module
├── find_module
├── 确认模块未被使用
├── mod->exit
└── free_module


2)模块状态:
enum module_state {
MODULE_STATE_LIVE, /*模块正常运行时*/
MODULE_STATE_COMING, /*模块装载期间*/
MODULE_STATE_GOING, /*模块正在移除*/
MODULE_STATE_UNFORMED, /* Still setting it up. */
};

12.license_is_gpl_compatible()用来判断给定许可证是否GPL兼容
13.在模块自身和所依赖的所有其它模块都已经编译完成之前,模块中的有些段是无法生成的
14.模块的初始化和清理函数保存在.gnu.linkonce.module段中的module实例中,该实例位于每个模块自动生成的附加文件中。名为module.mod.c中
15.使用EXPORT_SYMBOL()导出符号,它会在__ksymtab段中产生一个结构。此宏包含的__CRC_SYMBOL()来实现版本控制,主语模块的版本控制是per-symbol
的。
16. 模块信息常用宏
MODULE_INFO 一般模块信息
MODULE_LICENSE 模块许可证
MODULE_AUTHOR 模块作者
MODULE_DESCRIPTION 模块描述
MODULE_ALIAS 指定模块别名(udevd使用它执行热插拔加载模块)

17.版本控制
1)基本版本控制
module.mod.c中MODULE_INFO(vermagic, VERMAGIC_STRING)
#define VERMAGIC_STRING \
UTS_RELEASE " " \ /*字符串形式的内核版本 generated中:#define UTS_RELEASE "4.14.35"  uapi/linux中: #define LINUX_VERSION_CODE 265763*/

MODULE_VERMAGIC_SMP MODULE_VERMAGIC_PREEMPT \
MODULE_VERMAGIC_MODULE_UNLOAD MODULE_VERMAGIC_MODVERSIONS \
MODULE_ARCH_VERMAGIC \
MODULE_RANDSTRUCT_PLUGIN
展开就是:"4.14.0 SMP preempt mod_unload aarch64"

modinfo会打印一个模块的这个值:

root:~# modinfo usbcore
vermagic: 4.14.35 SMP preempt mod_unload aarch64

内核和模块中都会存储VERMAGIC_STRING的一份副本。只有这两份副本匹配时,模块才能加载。这意味着模块和内核的下列方面必须一致才能加载:
①SMP配置(是否启用)
②抢占配置(是否启用)
③使用的编译器版本
④特定于体系结构的常数

注意:基本版本控制中内核的版本虽然会存储,但是在进行比较时会忽略。因此内核版本不同的模块,只要剩余版本字符串匹配,这个检查就不影
响模块的装载。

2)per-symbol的版本控制
在EXPORT_SYMBOL()中有个__CRC_SYMBOL()来产生此symbol的crc,加载的时候会比较
版本控制函数check_version()

18.内核不仅在设备插入与移除时向用户空间提供消息,实际上内核在很多一般事件发生时都会发送消息。模块的插入与移除也会.
设备模型的每一个部分都可以向用户层发送注册和撤销注册事件。

19.将module.info.c文件编译成目标文件,并使用ld将其与模块现存的.o目标文件链接起来,结构命名为module.ko,这就是最终的模块!

20.不仅设备驱动程序可以编译成模块,内核中除了最基础部分外都可以编译成模块。

21.modules.builtin列出的是内嵌在内核中的驱动模块。sc00是没有编译成模块的,而且也不存在kernel/drivers/media/目录,使用它可以知道内核中有没有编译想要的模块
# cat modules.builtin | grep sc00
kernel/drivers/media/i2c/sc00.ko
# cat modules.builtin | grep usbhid 编译成模块的这个文件中没有列出来

22.modules.order列出了编译成模块的模块

23.modules.symbols 驱动模块中使用EXPORT_SYMBOL_GPL()导出来的符号,格式alias symbol:<函数名> <驱动模块名>,表示:此驱动模块依赖或者导出了此符号
root@xx:/lib/modules/4.14.35# cat modules.symbols | grep ohci_hcd
alias symbol:ohci_init_driver ohci_hcd
alias symbol:ohci_suspend ohci_hcd
alias symbol:ohci_restart ohci_hcd
alias symbol:ohci_resume ohci_hcd
alias symbol:ohci_setup ohci_hcd
alias symbol:ohci_hub_control ohci_hcd
alias symbol:ohci_hub_status_data ohci_hcd

内核中导出的所有的符号通过/proc/kallsyms查看

24./proc/modules文件查看模块驻留在内核的什么地方
# cat /proc/modules
vspm 98304 2 vsp2,vspm_if, Live 0xffff000000730000 (O)


25.TODO:查这个文件:modules.devname

26.modules.softdep 模块间添加依赖

atmel_mxt_ts.c:
MODULE_SOFTDEP("pre: cyttsp6_i2c");  // cyttsp6_i2c.ko先加载后它才会加载!

root@g8s:/lib/modules/4.14.35# cat modules.softdep
# Soft dependencies extracted from modules themselves.
softdep atmel_mxt_ts pre: cyttsp6_i2c   # 对应会生成这个。

26.模块热插拔相关service, 若kill掉它,模块的热插拔机制将被破坏,导致没有使用脚本加载的驱动无法被加载。

systemd中实现热插拔的机制: 
/lib/systemd/system/systemd-udev-trigger.service 
root@mm:~#  
root@mm:~# cat /lib/systemd/system/systemd-udev-trigger.service 
#  This file is part of systemd. 
# 
#  systemd is free software; you can redistribute it and/or modify it 
#  under the terms of the GNU Lesser General Public License as published by 
#  the Free Software Foundation; either version 2.1 of the License, or 
#  (at your option) any later version. 
 
[Unit] 
Description=udev Coldplug all Devices 
Documentation=man:udev(7) man:systemd-udevd.service(8) 
DefaultDependencies=no 
Wants=systemd-udevd.service 
After=systemd-udevd-kernel.socket systemd-udevd-control.socket systemd-hwdb-update.service 
Before=sysinit.target 
ConditionPathIsReadWrite=/sys 
 
[Service] 
Type=oneshot 
RemainAfterExit=yes 
ExecStart=/bin/udevadm trigger --type=subsystems --action=add ; /bin/udevadm trigger --type=devices --action=add

27.内核模块参数解析与配置

二进制文件对每一个内核参数都包含一个kernel_param实例,这一点对静态加载的和动态加载的内核模块都是成立的。
start_kernel()中的parse_early_param()负责解析命令行参数(其实也就是各个模块的模块参数),驱动加载do_initcalls中去匹配和设置。

调用流程:

start_kernel 
    parse_early_param
     /*先处理使用__setup_param()填充在__setup_start和__setup_end之的函数指针*/ parse_early_options
//只对console和earlycon这两个内核参数感兴趣 rest_init kernel_init kernel_init_freeable do_basic_setup
/*再处理使用module_param()填充在__start___param和__stop___param之的函数指针*/ do_initcalls
//加载驱动 do_initcall_level parse_args parse_one

在vmlinux.lds.h中查看__start___param和__stop___param对应__param段,__setup_start和__setup_end对应.init.setup段。

do_initcall_level()和start_kernel()中调用的配置命令行参数的parse_args原型如下:

parse_args(initcall_level_names[level], initcall_command_line, __start___param,  
    __stop___param - __start___param, level, level, NULL, &repair_env_string);

__param段的起始和结束地址分别为:__start___param 和 __stop___param,parse_one()会遍历整个__param段构成的列表,将命令行传入的
名称/值与列表中存在的名称进行匹配,匹配上后就调用其对应的kernel_param.ops.set()来设置传入的这个值。

注意:是在__param段中,而非_init段中,内核启动完后__param所占用的内存不会被释放。

struct kernel_param { 
    const char *name; /*__param_str_##name*/ 
    struct module *mod; /*THIS_MODULE*/ 
    const struct kernel_param_ops *ops; /*&param_ops_##type*/ 
    const u16 perm; /*eg:0664, 它有比较严格的规则,要求: USER_READABLE >= GROUP_READABLE >= OTHER_READABLE, USER_WRITABLE >= GROUP_WRITABLE,不允许OTHER_WRITABLE*/ 
    s8 level; /*恒为-1 编译时宏替换是-1,但是从do_initcall_level中看应该会被改为和xxx_initcall(fn)相同的数值*/ 
    u8 flags; /*恒为0*/ 
    union { /*这里面存module_param_named的arg2的地址*/ 
        void *arg; 
        const struct kparam_string *str; 
        const struct kparam_array *arr; 
    }; 
}; 
 
//注释:perm不一定是module_param指定的0664,VERIFY_OCTAL_PERMISSIONS(perm)处理后赋值给kernel_param.perm的

举个例子:

module_param_named(autoconf, ipv6_defaults.autoconf, int, 0444); 
/*-------------展开如下:--------------------*/ 
    param_check_int(autoconf, &ipv6_defaults.autoconf); 
    static const char __param_str_autoconf[] = "autoconf"; 
    static struct kernel_param __moduleparam_const __param_autoconf __used 
    __attribute__ ((unused,__section__ ("__param"),aligned(sizeof(void *)))) 
    = { 
        __param_str_autoconf, 
        THIS_MODULE, 
        &param_ops_int, 
        VERIFY_OCTAL_PERMISSIONS(0444), 
        -1, 
        0, 
        { &ipv6_defaults.autoconf } 
    } 
    static const char __UNIQUE_ID_autoconftype__LINE__[] 
    __used __attribute__((section(".modinfo"), unused, aligned(1))) = "parmtype=autoconf:int"
__setup_param("earlycon", imx_keep_uart_earlycon, imx_keep_uart_clocks_param, 0); 
/*------------------------等效如下---------------------------------*/ 
static const char __setup_str_imx_keep_uart_earlycon[] __initconst __aligned(1) = "earlycon"; 
static struct obs_kernel_param __setup_imx_keep_uart_earlycon 
    __used __section(.init.setup)     __attribute__((aligned((sizeof(long))))) 
    = { 
        __setup_str_imx_keep_uart_earlycon, 
        imx_keep_uart_clocks_param, 
        0 
    } 
 
/*此__setup_param对应的obs_kernel_param结构如下:*/ 
struct obs_kernel_param { 
    const char *str;    /*"earlycon"*/ 
    int (*setup_func)(char *); /*imx_keep_uart_clocks_param*/ 
    int early; /*0*/ 
};

__setup_param指定的函数指针放在.init.setup段中了,vmlinux.lds.h中INIT_SETUP(initsetup_align)中描述如下:
VMLINUX_SYMBOL(__setup_start) = .;
KEEP(*(.init.setup))
VMLINUX_SYMBOL(__setup_end) = .;

也就是说commandline中指定的启动参数可能由__setup_param()对应的函数来处理,也可能由module_param()对应的函数来处理
这些函数分别在编译期就指定在__setup_start和__setup_end之间、__start___param和__stop___param之间。

举个例子

eg: 
commandline中: 
debug=0 
驱动模块中: 
static int debug = 1; 
module_param(debug, int, 0664);

驱动中的模块参数debug在定义的时候就赋值了默认的值1,在内核被加载进内存的时候,模块参数debug的值就是1。但是在内核的启动过程中
在执行do_init_call()之前会根据commandline先处理模块参数,此时debug的值就被改为了0,然后执行do_init_call()加载这个模块,调用其
__init修饰的函数。因此此列中debug=1对驱动模块来说根本就不可见。 

参考《深入Linux内核架构》Pg988

28.从vmlinux.lds.h中看XXXX_initcall_sync(fn)也在__initcall_start 和__initcall_end之间,只不过7s在7后面而已(Xs都在X后面,从哪里体现出来sync了??)
===>目前看带sync的和不带sync的是一样的!

https://wiki.archlinux.org/index.php/Kernel_module_(%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87)


本文参考链接:https://www.cnblogs.com/hellokitty2/p/9738993.html