Linux 内核模块编写

FreeGuideOnline 最新 2026-07-13

bash sudo apt update sudo apt install build-essential linux-headers-$(uname -r)


**CentOS/RHEL/Fedora:**
```bash
sudo dnf install kernel-devel kernel-headers gcc make

或对于旧版本:

sudo yum install kernel-devel kernel-headers gcc make

验证安装:

ls /lib/modules/$(uname -r)/build

如果该目录存在,说明内核构建环境已就绪。

注意:当前使用的内核版本可通过 uname -r 查看,部分系统可能需要将 linux-headers 替换为 linux-headers-generic


第一个内核模块:Hello World

内核模块必须包含两个入口函数:初始化函数(模块加载时调用)和 退出函数(模块卸载时调用)。下面是一个最简单的示例。

创建文件 hello.c

#include <linux/init.h>    // 包含 module_init 和 module_exit 宏
#include <linux/module.h>  // 包含 MODULE_* 宏和基本模块函数
#include <linux/kernel.h>  // 包含 printk 等内核日志函数

// 初始化函数
static int __init hello_init(void)
{
    printk(KERN_INFO "Hello, kernel module loaded!\n");
    return 0;  // 返回 0 表示加载成功
}

// 退出函数
static void __exit hello_exit(void)
{
    printk(KERN_INFO "Goodbye, kernel module unloaded.\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");              // 许可证,必选
MODULE_AUTHOR("Your Name");         // 模块作者
MODULE_DESCRIPTION("A simple hello world module");  // 描述

代码解读:

  • module_init(fn):指定模块初始化函数,模块被插入内核时执行。
  • module_exit(fn):指定模块清理函数,模块被移除时执行。
  • printk:内核日志打印函数。KERN_INFO 是日志级别,可通过 dmesg 查看。
  • MODULE_LICENSE 必须存在,否则加载时会产生内核污染警告。常用许可证有 "GPL""GPL v2""Dual BSD/GPL" 等。

编写 Makefile

内核模块的编译依赖于内核构建系统,不能使用普通的 gcc 命令。需要编写一个专用的 Makefile。

创建 Makefile

obj-m := hello.o

# 当前内核源码的路径,由系统自动获取
KDIR := /lib/modules/$(shell uname -r)/build
PWD  := $(shell pwd)

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

clean:
	$(MAKE) -C $(KDIR) M=$(PWD) clean
  • obj-m := hello.o 告诉内核构建系统要编译的模块目标。
  • -C $(KDIR) 切换到内核源码目录,M=$(PWD) 让构建系统回到模块源码目录进行编译。
  • 执行 make 时会生成 hello.ko 等文件。

编译与加载模块

在包含 hello.cMakefile 的目录下执行 make

make

如果一切正常,会生成 hello.ko(内核模块对象文件)。

加载模块:

sudo insmod hello.ko

查看模块是否加载:

lsmod | grep hello

查看内核日志:

dmesg | tail

应能看到 Hello, kernel module loaded!

卸载模块:

sudo rmmod hello

再次查看 dmesg 会显示退出信息。


模块信息与许可证

MODULE_LICENSEMODULE_AUTHORMODULE_DESCRIPTION 等宏会自动将信息嵌入模块。可以通过 modinfo 命令查看:

modinfo hello.ko

输出示例:

filename:       /path/to/hello.ko
description:    A simple hello world module
author:         Your Name
license:        GPL
srcversion:     ...
depends:
retpoline:      Y
name:           hello
vermagic:       ...

为模块添加参数

模块可以接受命令行参数,方便用户动态配置行为。使用 module_param 宏来声明参数。

修改 hello.c,添加一个整数参数和字符串参数:

#include <linux/moduleparam.h>

static int repeat = 1;
module_param(repeat, int, S_IRUSR | S_IWUSR);
MODULE_PARM_DESC(repeat, "Number of times to print hello");

static char *who = "world";
module_param(who, charp, 0000);
MODULE_PARM_DESC(who, "Whom to greet");

在初始化函数中使用这些参数:

static int __init hello_init(void)
{
    int i;
    for (i = 0; i < repeat; i++)
        printk(KERN_INFO "Hello, %s!\n", who);
    return 0;
}

参数说明:

  • module_param(变量名, 类型, 权限)int, charp(字符串指针), bool 等是常用类型。
  • 权限用于在 sysfs 中创建对应的文件节点,普通用户可设为 S_IRUSR | S_IWUSR0000 表示不可见。

测试带参数加载:

sudo insmod hello.ko who="LKM" repeat=3

查看 dmesg 会看到三次问候。


创建简单的 /proc 接口

很多时候模块需要与用户空间交换数据,最简单的方式之一是通过 /proc 文件系统。利用 proc_create 和相关函数可以创建一个可读写的 proc 文件。

在模块中加入 proc 文件示例:

#include <linux/proc_fs.h>
#include <linux/uaccess.h>  // copy_from_user / copy_to_user

#define PROC_NAME "hello_proc"
static struct proc_dir_entry *proc_entry;

// 用户读取 proc 文件时调用
static ssize_t proc_read(struct file *file, char __user *buf,
                         size_t count, loff_t *ppos)
{
    const char *msg = "Hello from kernel module via /proc!\n";
    int len = strlen(msg);

    if (*ppos > 0 || count < len)
        return 0;

    if (copy_to_user(buf, msg, len))
        return -EFAULT;

    *ppos = len;
    return len;
}

static const struct proc_ops proc_fops = {
    .proc_read = proc_read,
};

// 在初始化函数中创建 proc 条目
static int __init hello_init(void)
{
    proc_entry = proc_create(PROC_NAME, 0444, NULL, &proc_fops);
    if (!proc_entry)
        return -ENOMEM;
    printk(KERN_INFO "/proc/%s created\n", PROC_NAME);
    return 0;
}

// 退出时删除
static void __exit hello_exit(void)
{
    proc_remove(proc_entry);
    printk(KERN_INFO "/proc/%s removed\n", PROC_NAME);
}

编译加载后,执行:

cat /proc/hello_proc