第4章 Linux内核模块(宋宝华Linux设备驱动开发详解)

第4章 Linux内核模块

 看得出来,这章的内容依旧是简单介绍,浅浅过了一遍

4.1 Linux内核模块介绍

​ Linux为避免内核过大,提供了一个机制,当我需要这些功能时,再加载进去内核。这种机制叫做模块(module)

lsmod可以获得系统中已加载的所有模块以及模块间的依赖关系,lsmod命令,实际上是读取并分析“/proc/modules”文件。内核中已加载模块的信息也存在于/sys/module目录下。

​ modprobe命令比insmod命令要强大,它在加载某模块时,会同时加载该模块所依赖的其他模块。使用modprobe命令加载的模块若以“modprobe -r filename”的方式卸载,将同时卸载其他依赖的模块。模块之间的依赖关系存放在根文件系统的/lib/modules/<kernel-version>/modules.dep文件中,实际上是在整体编译内核的时候由depmod工具生成。

​ 使用modinfo<模块名>命令可以获得模块的信息。

4.2 Linux内核模块程序结构

(1)模块加载函数

(2)模块卸载函数

(3)模块许可证声明

​ 不声明LICENSE,模块被加载时,将会收到内核污染的警告

(4)模块参数(可选)

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

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

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

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

4.3 模块加载函数

​ Linux内核模块加载函数一般以__init标识声明,模块加载函数以module_init的形式被指定。

​ 在Linux内核中,可以使用request_module函数加载内核模块,驱动开发人员通过调用下列代码request_module(module_name);灵活地加载其他内核模块。

​ 对下面这块内容不太理解,问了问AI
在这里插入图片描述

  • 图片内容解释了 Linux 内核中 __init__initdata 这两个关键宏的作用和原理,核心目的是优化内核内存使用。核心概念是 “初始化区段” 及其释放机制

    以下是详细解释,特别是关于“区段”:

    1. __init 函数(初始化函数)

      • 目的: 标记那些只在系统启动初始化阶段运行一次的函数(例如,驱动初始化、硬件探测)。
      • 编译链接处理 (#define __init ...):
        • 使用 GCC 的 __attribute__((__section__(".init.text"))) 属性。
        • 这个属性告诉编译器:把这个函数编译后的代码(机器指令)放到最终内核二进制镜像(vmlinux/bzImage)中一个名叫 .init.text 的特定内存区域(区段)里
      • __initcall.init 区段:
        • 仅仅把函数代码放进 .init.text 还不够,内核需要知道有哪些初始化函数需要调用以及调用的顺序。
        • module_init(hello_init) 这个宏(或者内核内部的其他机制)不仅将 hello_init 函数放入 .init.text还会在另一个叫做 .initcall.init 的区段里创建一个指向 hello_init 的函数指针(或者包含优先级信息的结构体)
        • .initcall.init 区段本质上是一个函数指针数组(或链表),内核启动时遍历这个数组,依次调用里面指向的所有 __init 函数。
      • 生命周期:
        • 作用: 在系统启动初始化阶段执行。
        • 释放: 一旦内核完成所有模块和核心子系统的初始化,它就认定这些 __init 函数再也不会被调用。
        • 内核会释放整个 .init.text 区段所占用的物理内存。这块内存会被标记为空闲,可以被内核用于其他目的(如动态分配内存 kmalloc)。
    2. __initdata 数据(初始化数据)

      • 目的: 标记那些只在系统启动初始化阶段需要访问的全局或静态变量
      • 编译链接处理: 类似 __init,但使用 __initdata 宏(通常定义为 __attribute__((__section__(".init.data"))))。
      • 区段: 这些变量会被放入一个特定的数据区段(通常是 .init.data 或类似名称)。
      • 生命周期:
        • 作用: 在初始化阶段供 __init 函数使用(如图中的 hello_data)。
        • 释放: .init.text 同时被释放。初始化完成后,这些数据就不再需要,释放其内存。
    3. 关键概念:区段 (Section)

      • 是什么? 在编译和链接过程中,编译器(gcc)和链接器(ld)会把不同类型的内容(代码、数据、只读数据、未初始化数据等)分类组织到输出文件(内核镜像)的不同部分。这些部分就叫做“区段”或“节”。
      • 作用:
        • 组织管理: 让代码和数据更有条理(例如,把所有初始化代码放一起,所有只读数据放一起)。
        • 内存属性: 链接器脚本 (vmlinux.lds.S) 会根据区段名称,告诉操作系统加载器(或内核自身)这些区段应该被加载到内存的什么位置(地址),以及这些内存区域应该具有什么属性(可读、可写、可执行)。例如,.text 通常是可执行不可写,.data 通常是可读可写。
        • 优化释放: __init__initdata 的核心魔法就在于利用了区段! 通过 GCC 属性将它们标记到特殊的 .init.text.init.data (或 .init.rodata 等) 区段。
        • 链接器脚本 (vmlinux.lds.S): 这个文件定义了内核镜像的内存布局,其中明确指定了 .init.text, .initcall.init, .init.data 等区段的起始和结束地址,以及最重要的——哪些区段属于“初始化内存范围”
      • 内核如何释放?
        • 内核启动代码在完成所有初始化后(do_basic_setup() -> do_initcalls() 遍历完所有 .initcall.init 中的函数指针后),会调用一个函数(通常是 free_initmem())。
        • 这个函数利用链接器脚本中定义的符号(如 __init_begin__init_end,精确地知道 .init.text, .initcall.init, .init.data 等所有“初始化区段”在内核内存映像中占据的物理内存的起始地址和结束地址。
        • 内核然后调用内存管理函数,将这块从 __init_begin__init_end 的连续物理内存区域标记为空闲(释放回内核的内存池)。之后这块内存就可以被 kmalloc 等函数分配给其他内核组件使用了。

    总结图片内容的核心思想:

    1. 标记临时性:__init 标记只运行一次的初始化函数,用 __initdata 标记只初始化阶段需要的数据。
    2. 区段集中存放: 利用 GCC 的 section 属性和链接器脚本,把这些函数和数据的代码/内容集中存放到内核镜像的特定区段(.init.text, .init.data, .initcall.init)。
    3. 按需使用: 内核启动时,通过遍历 .initcall.init 区段中的函数指针列表来调用所有初始化函数。
    4. 及时释放: 初始化完成后,内核识别出所有属于“初始化范围”的区段(通过链接器脚本定义的符号边界),一次性释放这些区段占用的物理内存,节约宝贵的常驻内存(特别是嵌入式系统)。
    5. 好处: 显著减少内核初始化完成后常驻内存(RAM)的占用,提升系统可用内存。这对于内存受限的嵌入式系统尤其重要。

问题:你讲的很好,但你说的前提是内核启动时,但如果我驱动是在内核启动后用insmod去加载呢?

  • 您的问题切中了关键点!当驱动作为模块(module)在内核启动后通过 insmod 加载时,__init__initdata 的行为与直接编译进内核镜像时有显著区别。 您提到的图片内容主要针对直接编译进内核的驱动/代码。对于动态加载的模块,机制有所不同,但核心目的(优化内存)相同。

    以下是针对 insmod 加载模块的解释:

    1. 模块的加载过程 (insmod):

      • 当您执行 insmod your_module.ko 时,内核的模块加载器会工作。
      • 加载器会从磁盘读取 .ko 文件(内核对象文件)。
      • 它会分配一块新的内核内存来存放这个模块的代码、数据、符号表等信息。
      • 将模块的代码段(.text,包含 __init 函数)、数据段(.data,包含 __initdata 变量)、以及特殊的 .init.text.init.data 等区段都复制到这块新分配的内存中
      • 然后,加载器会解析模块的依赖关系(如果需要),解决符号引用(找到模块中调用的内核函数地址)。
      • 最后,加载器会直接调用模块的 __init 函数(即 module_init 指定的那个函数)。

4.4 模块卸载函数

​ __exit 修饰,用module_exit(函数名)来指定哪个函数。

​ 如果内核的相关模块被编译进去内核,则cleanup_function()函数会被省略。

4.5 模块参数

我们可以用module_param(参数名,参数类型,参数读/写权限)为模块定义一个参数。

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

​ 在/sys/module/目录下将出现以此模块名命名的目录。

​ 书中给出了实际使用的例子(标记一手,就不抄过来了,回顾的时候忘了再找书)。

4.6 导出符号

​ Linux中/proc/kallsym文件对应着内核符号表,它记录了符号以及符号所在的内存地址。符号指代驱动中的函数。

​ 导出的符号可以被其他模块使用。

EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名);

​ 可以从"/proc/kallsysms"文件中找到导出符号的相关信息。

​ 书中给出了实际使用的例子(标记一手,就不抄过来了,回顾的时候忘了再找书)。

4.7 模块声明与描述

​ 在Linux内核模块中,声明模的作者、描述、版本、设备表和别名。

4.8 模块的使用计数

try_module_get()module_put(),增加和减少模块计数

4.9 模块的编译

一个示例Makefile

# Kernel modules
KVERS = $(shell uname -r)  # 这行是关键定义!
obj-m += hello.o

# Specify flags for the module compilation.
#EXTRA_CFLAGS=g -00

build: kernel_modules

kernel_modules:
    make -C /lib/modules/$(KVERS)/build M=$ (CURDIR) modules

clean:
    make -C /lib/modules/$(KVERS)/build M=$ (CURDIR) clean
  • 第一行代码是获取了当前内核的运行版本,当运行make时,会由build跳转到kernel_modules开始执行。

  • kernel_modules:
        make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
    
    • 功能:调用内核构建系统编译模块
    • 关键参数
      • -C:切换到内核构建目录
      • $(KVERS):内核版本变量
      • M=$(CURDIR):指定模块源代码目录(当前目录)
      • modules:内核构建系统的编译目标
    • 问题$ (KVERS)$ (CURDIR) 中的空格是语法错误,应改为 $(KVERS)$(CURDIR)

4.10 使用模块绕开GPL

​ Linux内核不能使用非GPL许可权

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值