【Linux驱动开发】第二天:内核模块生命周期+内存分配全解

内核模块KO全解:加载卸载+生命周期

1. 什么是Linux内核模块(.ko)

内核模块(Kernel Module) 后缀为 .ko,是Linux内核的「动态插件」。

Linux内核本身是一个庞大的核心镜像vmlinux,如果把所有驱动、文件系统、网络协议都编译进去,内核会变得无比臃肿,启动极慢。而内核模块机制解决了这个问题:

  • 不需要的功能不编译进内核
  • 需要用时动态加载 ,不用时动态卸载
  • 全程无需重启系统、无需重新编译整个内核
两种内核代码形式对比
代码形式 特点 适用场景
静态编译 直接编译进内核镜像,开机常驻,无法卸载 核心功能(进程调度、内存管理)
动态模块(.ko) 按需加载卸载,独立编译 绝大多数硬件驱动、扩展功能

2. 内核模块完整生命周期(重中之重)

module_init()module_exit()是内核模块生命周期的两个核心入口。

一个内核模块从加载到卸载,会完整经历以下4个阶段:

(1)加载阶段

执行sudo insmod xxx.ko时触发:

  1. 系统读取.ko文件内容到内核态内存
  2. 内核校验模块信息:内核版本、许可证、依赖关系、函数符号
  3. 分配内核地址空间,重定位内核API地址(如printkdevice_create
  4. 执行module_init()注册的初始化函数
(2)初始化执行阶段

对应驱动里的drv_init()函数:

  • 申请设备号、创建设备类、生成/dev/xxx设备文件
  • 注册文件操作结构体、中断处理函数、硬件资源
  • 初始化完成后,模块进入常驻运行状态

🔔 重要细节:标记__init的函数,初始化完成后内存会被自动释放,节省内核空间。

(3)常驻运行阶段

模块加载完成后:

  • 驻留在内核态全局地址空间
  • 接收用户态通过系统调用下发的open/read/write/ioctl请求
  • 响应硬件中断、处理定时任务、管理设备状态
  • 无主动退出逻辑,一直运行直到被卸载
(4)卸载销毁阶段

执行sudo rmmod xxx时触发:

  1. 检查模块引用计数,若被占用则卸载失败
  2. 执行module_exit()注册的卸载函数
  3. 反向释放所有申请的资源:销毁设备、注销设备号、释放内存
  4. 从内核模块链表中移除该模块
  5. 回收模块占用的所有内核内存

⚠️ 致命禁忌:卸载时不释放资源会导致内核内存泄漏、设备占用,长期运行会造成系统卡顿甚至崩溃。

3. 模块加载与卸载底层原理

insmod加载底层做了什么
  1. 校验模块与当前内核的版本兼容性
  2. 将模块的代码段和数据段拷贝到内核态内存
  3. 解析模块的未定义符号,链接到内核全局符号表
  4. 执行模块的初始化函数
  5. 将模块添加到内核的全局模块链表中(lsmod可查)
rmmod卸载底层做了什么
  1. 检查模块的引用计数,不为0则返回错误
  2. 执行模块的退出函数
  3. 注销模块在内核中注册的所有资源
  4. 从内核模块链表中移除该模块
  5. 释放模块占用的代码段、数据段内存

4. insmod与modprobe核心区别(面试必背)

这是驱动开发面试最高频的考点之一,必须背下来:

对比项 insmod modprobe
依赖处理 ❌ 不处理任何依赖,缺少依赖直接报错 ✅ 自动解析并加载所有依赖模块
模块路径 必须写绝对/相对路径 ,如insmod ./chr_dev.ko 自动搜索系统模块路径/lib/modules/$(uname -r)/
用法 只能加载单个本地ko文件 直接用模块名加载,如modprobe e1000e
错误处理 简单报错,无自动修复 依赖缺失时会提示并尝试自动处理
常用场景 本地开发调试驱动 系统开机自动加载、生产环境
通俗理解
  • insmod:只干一件事,强行安装你指定的这一个插件,缺任何东西就直接报错摆烂
  • modprobe:智能安装管家,先查清楚需要哪些配套插件,全部自动装好,最后再装目标模块

💡 补充:modprobe -r 模块名也可以卸载模块,会反向卸载所有不再被使用的依赖模块 ,比rmmod更安全。

5. 内核模块常用全套命令

bash 复制代码
# 1. 查看所有已加载的内核模块
lsmod

# 2. 加载本地开发的驱动模块(开发调试首选)
sudo insmod ./xxx.ko

# 3. 加载系统模块(自动处理依赖)
sudo modprobe 模块名

# 4. 卸载模块
sudo rmmod 模块名
sudo modprobe -r 模块名  # 安全卸载,同时卸载无用依赖

# 5. 查看模块详细信息(作者、版本、依赖、许可证)
modinfo xxx.ko

# 6. 更新模块依赖缓存(modprobe依赖此文件)
sudo depmod -a

# 7. 查看模块打印日志
dmesg | tail -20

6. 结合Day1驱动的连贯实操

bash 复制代码
# 进入Day1的驱动目录
cd chr_demo

# 1. 编译驱动
make

# 2. 加载模块
sudo insmod chr_dev.ko

# 3. 查看模块是否加载成功
lsmod | grep chr_dev

# 4. 查看内核日志
dmesg | tail

# 5. 测试驱动
cat /dev/mychrdev

# 6. 卸载模块
sudo rmmod chr_dev

# 7. 清理编译产物
make clean

7. 模块开发两个重要细节

(1)必须声明GPL许可证
c 复制代码
MODULE_LICENSE("GPL");
  • 这行代码必须添加,否则内核会标记为"污染内核"
  • 非GPL协议的模块无法使用部分内核导出函数
  • 会导致驱动编译失败或运行异常
(2)模块引用计数机制

内核会自动维护每个模块的引用计数:

  • 当设备被用户进程打开时,引用计数+1
  • 当设备被关闭时,引用计数-1
  • 引用计数不为0时,rmmod会直接报错,防止正在使用的驱动被卸载

内核内存分配全解:kmalloc/kzalloc/vmalloc

1. 为什么驱动绝对不能用malloc/free?

作为Linux应用开发者,你天天用malloc/free,但在内核驱动中完全不能使用,核心原因有4个:

  1. 编译不通过malloc是C标准库函数,内核编译时不链接libc.so,会直接报"未定义引用"错误
  2. 环境不匹配malloc依赖用户态堆管理器,内核态没有这个基础设施
  3. 上下文不安全malloc可能因内存不足而阻塞睡眠,而内核的中断上下文绝对禁止睡眠
  4. 内存隔离malloc分配的是用户态虚拟内存,驱动需要的是内核态内存

因此,Linux内核专门提供了三套内存分配API,这是驱动开发每天都要用的核心知识点。

2. 一句话记住三者核心定位

  1. kmalloc :内核最常用、最快,分配物理地址连续的小内存
  2. kzalloc := kmalloc + 自动清零(驱动开发首选推荐
  3. vmalloc :分配虚拟地址连续、物理地址不连续的大内存

3. 核心区别对比表(开发+面试必背)

对比项 kmalloc kzalloc vmalloc
物理地址连续性 ✅ 连续 ✅ 连续 ❌ 不连续
是否自动清零 ❌ 内存是脏数据(随机值) ✅ 自动全部清零 ❌ 不清零
分配大小上限 小内存(一般128KB~4MB以内) 同kmalloc 大内存(MB/GB级别)
分配速度 ⚡ 极快(最佳性能) ⚡ 极快 🐌 较慢(需要建立页表映射)
能否睡眠 由flags参数控制 同kmalloc 可以睡眠
适用场景 高性能场景、DMA传输、硬件交互 绝大多数驱动常规场景 大内存分配、网络/文件系统缓存
对应的释放函数 kfree() kfree() vfree()
所需头文件 linux/slab.h linux/slab.h linux/vmalloc.h

4. 逐个详解+极简代码示例

(1)kmalloc
  • 内核最基础的内存分配函数
  • 分配的内存物理地址连续,这对DMA传输和硬件交互至关重要
  • 缺点:不会清零内存,里面是之前残留的随机值,容易引发bug
c 复制代码
#include <linux/slab.h>

// 分配1024字节内核内存,允许睡眠
char *buf = kmalloc(1024, GFP_KERNEL);

if (buf == NULL) {
    printk(KERN_ERR "kmalloc分配失败\n");
    return -ENOMEM;
}

// 使用内存...

// 释放内存
kfree(buf);
buf = NULL;  // 好习惯,防止野指针
(2)kzalloc(驱动开发首选)
  • k代表kernel,z代表zero
  • 功能完全等同于kmalloc,但会自动将分配的内存全部清零
  • 最安全,避免了脏数据导致的各种难以排查的bug
  • 驱动开发中90%的场景都应该优先使用kzalloc
c 复制代码
#include <linux/slab.h>

// 分配1024字节,自动清零
char *buf = kzalloc(1024, GFP_KERNEL);

if (buf == NULL) {
    printk(KERN_ERR "kzalloc分配失败\n");
    return -ENOMEM;
}

// 使用内存...

// 释放方式和kmalloc完全一样
kfree(buf);
buf = NULL;
(3)vmalloc
  • 专门用于分配大内存
  • 虚拟地址连续,但物理地址不连续
  • 速度较慢,因为需要为不连续的物理页建立页表映射
  • 不能用于DMA传输和硬件交互(这些场景要求物理地址连续)
  • 只能在进程上下文使用,不能在中断上下文使用
c 复制代码
#include <linux/vmalloc.h>

// 分配1MB大内存
char *buf = vmalloc(1024 * 1024);

if (buf == NULL) {
    printk(KERN_ERR "vmalloc分配失败\n");
    return -ENOMEM;
}

// 使用内存...

// ⚠️ 注意:必须用vfree释放,绝对不能用kfree!
vfree(buf);
buf = NULL;

5. 最重要的GFP分配标志

kmallockzalloc的第二个参数是GFP标志,用于控制内存分配的行为,最常用的有两个:

(1)GFP_KERNEL
  • 最常用的标志
  • 允许内存分配器在内存不足时阻塞睡眠,等待可用内存
  • 只能在进程上下文使用 (如驱动的openreadwrite函数)
(2)GFP_ATOMIC
  • 原子分配标志
  • 绝对不允许睡眠
  • 必须在中断上下文、定时器回调等不能睡眠的场景使用
  • 分配失败会直接返回NULL

6. 完整可运行的内存测试驱动

我把三种内存分配方式整合到一个完整的内核模块中,你可以直接编译运行测试:

mem_demo.c
c 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
#include <linux/kernel.h>

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("内核内存分配测试驱动");

#define SMALL_SIZE 1024
#define LARGE_SIZE 1024 * 1024  // 1MB

static int __init mem_test_init(void)
{
    char *km_buf = NULL;
    char *kz_buf = NULL;
    char *vm_buf = NULL;

    printk(KERN_INFO "=== 开始内核内存分配测试 ===\n");

    // 1. 测试kmalloc
    km_buf = kmalloc(SMALL_SIZE, GFP_KERNEL);
    if (km_buf) {
        printk(KERN_INFO "kmalloc分配%d字节成功\n", SMALL_SIZE);
        // 验证kmalloc分配的内存是脏数据
        printk(KERN_INFO "kmalloc内存第一个字节: 0x%02x\n", (unsigned char)km_buf[0]);
    } else {
        printk(KERN_ERR "kmalloc分配失败\n");
    }

    // 2. 测试kzalloc
    kz_buf = kzalloc(SMALL_SIZE, GFP_KERNEL);
    if (kz_buf) {
        printk(KERN_INFO "kzalloc分配%d字节成功(自动清零)\n", SMALL_SIZE);
        // 验证kzalloc分配的内存已清零
        printk(KERN_INFO "kzalloc内存第一个字节: 0x%02x\n", (unsigned char)kz_buf[0]);
    } else {
        printk(KERN_ERR "kzalloc分配失败\n");
    }

    // 3. 测试vmalloc
    vm_buf = vmalloc(LARGE_SIZE);
    if (vm_buf) {
        printk(KERN_INFO "vmalloc分配%d字节成功\n", LARGE_SIZE);
    } else {
        printk(KERN_ERR "vmalloc分配失败\n");
    }

    // 释放所有内存
    kfree(km_buf);
    kfree(kz_buf);
    vfree(vm_buf);

    printk(KERN_INFO "=== 所有内存已成功释放 ===\n");
    return 0;
}

static void __exit mem_test_exit(void)
{
    printk(KERN_INFO "内存测试模块卸载\n");
}

module_init(mem_test_init);
module_exit(mem_test_exit);
Makefile
makefile 复制代码
obj-m += mem_demo.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) clean
运行命令
bash 复制代码
make
sudo insmod mem_demo.ko
dmesg | tail
sudo rmmod mem_demo
make clean

7. 驱动开发内存使用铁律

  1. 优先使用kzalloc:自动清零,最安全,避免脏数据bug
  2. 小内存用kmalloc/kzalloc,大内存用vmalloc
  3. DMA和硬件交互必须用kmalloc/kzalloc:要求物理地址连续
  4. 释放函数绝对不能混用kfree对应kmalloc/kzalloc,vfree对应vmalloc
  5. 分配后必须检查返回值:内核内存分配可能失败,不检查会导致空指针崩溃
  6. 谁分配谁释放:在同一个函数中完成分配和释放,避免内存泄漏
  7. 中断上下文只能用GFP_ATOMIC:绝对不能使用可能睡眠的分配方式

核心知识点总结

  1. 内核模块是Linux驱动的基本存在形式,实现了功能的按需加载卸载
  2. 内核模块生命周期:加载 → 初始化 → 常驻运行 → 卸载释放
  3. module_initmodule_exit是模块的唯一入口和出口
  4. insmod适合本地开发调试,modprobe适合生产环境自动处理依赖
  5. 内核不能用malloc,必须使用kmalloc/kzalloc/vmalloc
  6. kzalloc是驱动开发首选,自动清零,安全可靠
  7. kmalloc分配物理连续的小内存,vmalloc分配物理不连续的大内存
  8. 释放函数必须配对使用,绝对不能混用
相关推荐
计算机安禾1 小时前
【Linux从入门到精通】第28篇:文本处理三剑客(中)——sed 流编辑器
linux·服务器·编辑器
Will_Ye2 小时前
Ubuntu:系统断网后自动重连指定wifi脚本
linux·运维·ubuntu
郝学胜-神的一滴2 小时前
深入epoll封装:event_set与event_add核心原理剖析
linux·服务器·开发语言·网络·c++·unix
HABuo2 小时前
【linux(四)】套接字编程--socket套接字及其接口认识
linux·运维·服务器·c语言·c++·ubuntu·centos
凤年徐2 小时前
命令行进度条完全指南:倒计时、缓冲区刷新与动态下载
linux
北山有鸟2 小时前
address-cell& size-cell
linux·网络
小则又沐风a2 小时前
基础的开发工具(Linux)
linux·运维·服务器
深邃-2 小时前
【Web安全】-Kali,Linux配置(2):Java环境配置,Python环境配置,Conda使用,PIP配置使用,SSH远程登录
java·linux·python·安全·web安全·网络安全·php
Fanfanaas2 小时前
Linux 系统编程 进程篇 (六)
linux·服务器·c语言·开发语言