第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这两个关键宏的作用和原理,核心目的是优化内核内存使用。核心概念是 “初始化区段” 及其释放机制。以下是详细解释,特别是关于“区段”:
-
__init函数(初始化函数)- 目的: 标记那些只在系统启动初始化阶段运行一次的函数(例如,驱动初始化、硬件探测)。
- 编译链接处理 (
#define __init ...):- 使用 GCC 的
__attribute__((__section__(".init.text")))属性。 - 这个属性告诉编译器:把这个函数编译后的代码(机器指令)放到最终内核二进制镜像(vmlinux/bzImage)中一个名叫
.init.text的特定内存区域(区段)里。
- 使用 GCC 的
__initcall.init区段:- 仅仅把函数代码放进
.init.text还不够,内核需要知道有哪些初始化函数需要调用以及调用的顺序。 module_init(hello_init)这个宏(或者内核内部的其他机制)不仅将hello_init函数放入.init.text,还会在另一个叫做.initcall.init的区段里创建一个指向hello_init的函数指针(或者包含优先级信息的结构体)。.initcall.init区段本质上是一个函数指针数组(或链表),内核启动时遍历这个数组,依次调用里面指向的所有__init函数。
- 仅仅把函数代码放进
- 生命周期:
- 作用: 在系统启动初始化阶段执行。
- 释放: 一旦内核完成所有模块和核心子系统的初始化,它就认定这些
__init函数再也不会被调用。 - 内核会释放整个
.init.text区段所占用的物理内存。这块内存会被标记为空闲,可以被内核用于其他目的(如动态分配内存kmalloc)。
-
__initdata数据(初始化数据)- 目的: 标记那些只在系统启动初始化阶段需要访问的全局或静态变量。
- 编译链接处理: 类似
__init,但使用__initdata宏(通常定义为__attribute__((__section__(".init.data"))))。 - 区段: 这些变量会被放入一个特定的数据区段(通常是
.init.data或类似名称)。 - 生命周期:
- 作用: 在初始化阶段供
__init函数使用(如图中的hello_data)。 - 释放: 与
.init.text同时被释放。初始化完成后,这些数据就不再需要,释放其内存。
- 作用: 在初始化阶段供
-
关键概念:区段 (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等函数分配给其他内核组件使用了。
- 内核启动代码在完成所有初始化后(
总结图片内容的核心思想:
- 标记临时性: 用
__init标记只运行一次的初始化函数,用__initdata标记只初始化阶段需要的数据。 - 区段集中存放: 利用 GCC 的 section 属性和链接器脚本,把这些函数和数据的代码/内容集中存放到内核镜像的特定区段(
.init.text,.init.data,.initcall.init)。 - 按需使用: 内核启动时,通过遍历
.initcall.init区段中的函数指针列表来调用所有初始化函数。 - 及时释放: 初始化完成后,内核识别出所有属于“初始化范围”的区段(通过链接器脚本定义的符号边界),一次性释放这些区段占用的物理内存,节约宝贵的常驻内存(特别是嵌入式系统)。
- 好处: 显著减少内核初始化完成后常驻内存(RAM)的占用,提升系统可用内存。这对于内存受限的嵌入式系统尤其重要。
-
问题:你讲的很好,但你说的前提是内核启动时,但如果我驱动是在内核启动后用insmod去加载呢?
-
您的问题切中了关键点!当驱动作为模块(module)在内核启动后通过
insmod加载时,__init和__initdata的行为与直接编译进内核镜像时有显著区别。 您提到的图片内容主要针对直接编译进内核的驱动/代码。对于动态加载的模块,机制有所不同,但核心目的(优化内存)相同。以下是针对
insmod加载模块的解释:-
模块的加载过程 (
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许可权
3627

被折叠的 条评论
为什么被折叠?



