Bootstrap

【Linux】内核驱动模块

在这里插入图片描述

Linux内核模块是一种特殊的内核组件,它们可以被动态地加载到正在运行的内核中以扩展其功能,或者在不需要时从内核中卸载。这种动态特性使得Linux内核能够保持精简,同时又可以根据需要加载不同的功能模块。本文将详细介绍Linux内核模块的相关知识。

1. 引言

Linux内核模块为开发者提供了一种灵活的方式来扩展内核的功能,而无需重新编译整个内核。模块可以用来实现各种硬件驱动、网络协议、文件系统以及其他内核功能。理解内核模块的编写和使用是Linux系统编程中的一个重要方面。

2. Linux内核模块基础

2.1 模块的概念

Linux内核模块是独立于内核主体的代码段,它们可以被动态加载到内核中,并且在不需要时可以从内核中卸载。模块可以提供新的功能或增强现有的功能,例如新的设备驱动程序、文件系统支持或网络协议。

2.2 模块的生命周期

模块在其生命周期中有几个重要的阶段:

  1. 编译:模块首先需要被编译成一个独立的文件(通常以.ko作为扩展名)。
  2. 加载:模块被加载到内核中,此时会调用模块的初始化函数。
  3. 运行:模块在内核中运行,提供其定义的功能。
  4. 卸载:当不再需要该模块时,可以将其从内核中卸载,此时会调用模块的退出函数。

2.3 模块的接口

为了能够被内核正确加载和卸载,模块必须实现两个特殊的函数:module_initmodule_exit

2.3.1 module_init函数

这是模块初始化函数,在模块加载时被调用。在这个函数中,模块应该初始化它所使用的任何数据结构,并注册任何需要的服务。

// 定义模块初始化函数
static int __init mod_init(void)
{
    // 输出初始化信息
    printk(KERN_INFO "Module initialized.\n");

    // 其他初始化代码
    // ...

    // 返回0表示成功
    return 0;
}
2.3.2 module_exit函数

这是模块退出函数,在模块卸载时被调用。在这个函数中,模块应该释放它所使用的任何资源,并注销之前注册的服务。

// 定义模块退出函数
static void __exit mod_exit(void)
{
    // 输出退出信息
    printk(KERN_INFO "Module exited.\n");

    // 清理和释放资源
    // ...
}

2.4 模块的编译

模块可以单独编译,也可以作为内核的一部分编译。通常,模块是作为一个单独的目标文件编译的,这样可以在不重新编译整个内核的情况下更新模块。

2.4.1 编译命令

编译模块通常使用gcc命令,并且需要包含内核的头文件路径。

gcc -Wall -Wstrict-prototypes -O2 -fno-strict-aliasing -I /usr/src/linux/include -D__KERNEL__ -c -o mymodule.o mymodule.c

然后使用ld链接器将模块对象文件链接成模块文件。

ld -r -m elf_i386 -o mymodule.ko mymodule.o
2.4.2 使用Makefile

更常见的是使用Makefile来编译模块,这样可以更容易地管理和构建多个模块。

# Makefile for building a simple kernel module

# Define the list of object files
obj-m += mymodule.o

# Define the directory where the kernel headers are installed
KERNELDIR := /lib/modules/$(shell uname -r)/build

# Define the default target
default:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

# Define the clean target
clean:
	rm -rf *.o *.ko *.mod.c .*.cmd

3. Linux内核模块编写

3.1 模块编写流程

编写内核模块通常遵循以下步骤:

  1. 定义模块:定义模块的数据结构和函数。
  2. 实现初始化函数:实现module_init函数。
  3. 实现退出函数:实现module_exit函数。
  4. 导出符号:如果模块需要导出符号供其他模块使用,需要使用EXPORT_SYMBOL宏。
  5. 指定许可证:指定模块的许可证类型,通常使用MODULE_LICENSE宏。
3.1.1 示例代码

下面是一个简单的内核模块示例:

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

// 定义一个全局变量,用于存储模块参数
static int my_param = 1;

// 模块参数宏
module_param(my_param, int, S_IRUGO);

// 描述模块参数的作用
MODULE_PARAM_DESC(my_param, "A sample parameter.");

// 模块初始化函数
static int __init mod_init(void)
{
    // 输出模块参数的值
    printk(KERN_INFO "Parameter value: %d\n", my_param);

    // 其他初始化代码
    // ...

    // 返回0表示成功
    return 0;
}

// 模块退出函数
static void __exit mod_exit(void)
{
    // 输出退出信息
    printk(KERN_INFO "Module exited.\n");

    // 清理和释放资源
    // ...
}

// 模块初始化函数声明
module_init(mod_init);

// 模块退出函数声明
module_exit(mod_exit);

// 指定模块的许可证
MODULE_LICENSE("GPL");

3.2 模块的加载与卸载

模块可以通过insmodmodprobe命令加载到内核中,通过rmmod命令从内核中卸载。

3.2.1 加载模块
sudo insmod mymodule.ko

或者使用modprobe

sudo modprobe mymodule
3.2.2 卸载模块
sudo rmmod mymodule

3.3 模块参数

模块可以接受参数,在模块加载时通过命令行传递给模块。这些参数可以用来配置模块的行为。

3.3.1 模块参数示例
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

// 定义一个全局变量,用于存储模块参数
static int my_param = 1;

// 模块参数宏
module_param(my_param, int, S_IRUGO);

// 描述模块参数的作用
MODULE_PARAM_DESC(my_param, "A sample parameter.");

// 模块初始化函数
static int __init mod_init(void)
{
    // 输出模块参数的值
    printk(KERN_INFO "Parameter value: %d\n", my_param);

    // 其他初始化代码
    // ...

    // 返回0表示成功
    return 0;
}

// 模块退出函数
static void __exit mod_exit(void)
{
    // 输出退出信息
    printk(KERN_INFO "Module exited.\n");

    // 清理和释放资源
    // ...
}

// 模块初始化函数声明
module_init(mod_init);

// 模块退出函数声明
module_exit(mod_exit);

// 指定模块的许可证
MODULE_LICENSE("GPL");

加载模块时传递参数:

sudo insmod mymodule.ko my_param=5

3.4 模块依赖

模块之间可以有依赖关系,一个模块可能依赖于另一个模块提供的功能。这种依赖关系可以通过depends_on宏指定。

3.4.1 模块依赖示例
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>

// 定义模块依赖
MODULE_DEPENDS("other_module");

// 模块初始化函数
static int __init mod_init(void)
{
    // 输出初始化信息
    printk(KERN_INFO "Module initialized.\n");

    // 其他初始化代码
    // ...

    // 返回0表示成功
    return 0;
}

// 模块退出函数
static void __exit mod_exit(void)
{
    // 输出退出信息
    printk(KERN_INFO "Module exited.\n");

    // 清理和释放资源
    // ...
}

// 模块初始化函数声明
module_init(mod_init);

// 模块退出函数声明
module_exit(mod_exit);

// 指定模块的许可证
MODULE_LICENSE("GPL");

4. Linux内核模块的内部机制

4.1 模块的加载过程

模块加载的过程包括以下几个步骤:

  1. 加载模块:通过insmodmodprobe命令加载模块到内核中。
  2. 解析模块:内核解析模块文件,提取符号表和其他信息。
  3. 初始化模块:调用模块的module_init函数来初始化模块。
4.1.1 加载模块的命令
sudo insmod mymodule.ko
4.1.2 解析模块

内核会解析模块文件,提取模块的符号表、依赖关系和其他元数据。

4.1.3 初始化模块

内核调用模块的module_init函数,让模块进行初始化。

static int __init mod_init(void)
{
    // 模块初始化代码
    return 0;
}

4.2 模块的卸载过程

模块卸载的过程包括以下几个步骤:

  1. 卸载模块:通过rmmod命令卸载模块。
  2. 清理模块:调用模块的module_exit函数来释放资源。
4.2.1 卸载模块的命令
sudo rmmod mymodule
4.2.2 清理模块

内核调用模块的module_exit函数,让模块进行清理。

static void __exit mod_exit(void)
{
    // 模块清理代码
}

4.3 模块间的通信

模块间可以通过导出符号的方式进行通信。一个模块可以导出符号供其他模块使用,而其他模块则可以通过request_module函数获取这些符号。

4.3.1 导出符号
EXPORT_SYMBOL(my_function);
4.3.2 获取符号
extern void (*my_function)(int);

5. Linux内核模块的高级特性

5.1 模块的调试

模块的调试可以通过内核的日志机制来进行,使用printk函数输出调试信息。

5.1.1 使用printk
printk(KERN_DEBUG "Debug message: %s\n", str);

5.2 模块的版本兼容性

模块需要与内核版本兼容,否则可能会导致加载失败。可以通过检查内核版本来确保兼容性。

5.2.1 检查内核版本
if (!capable(CAP_SYS_ADMIN)) {
    printk(KERN_ERR "Module requires CAP_SYS_ADMIN capability.\n");
    return -EPERM;
}

if (kernel_version < KERNEL_VERSION(2, 6, 32)) {
    printk(KERN_ERR "Module requires kernel version 2.6.32 or later.\n");
    return -EINVAL;
}

5.3 模块的动态加载

模块可以被动态加载,这意味着模块的加载时机可以在运行时决定。

5.3.1 使用modprobe
modprobe mymodule

5.4 模块的静态链接

模块也可以被静态链接到内核中,这种方式通常用于那些频繁加载和卸载的模块。

5.4.1 静态链接模块

在内核配置过程中选择将模块静态链接到内核。

5.5 模块的热插拔支持

模块可以支持热插拔设备,即在系统运行时插入或拔出设备而不需重启系统。

5.5.1 支持热插拔设备
static int __init mod_init(void)
{
    register_hotplug_notifier(&my_notifier);
    return 0;
}

static void __exit mod_exit(void)
{
    unregister_hotplug_notifier(&my_notifier);
}

6. Linux内核模块示例:字符设备驱动

下面是一个字符设备驱动模块的例子,展示了如何创建一个简单的字符设备,并实现基本的读写操作。

6.1 设备初始化

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/interrupt.h>

// 定义设备号
static dev_t dev_num = MKDEV(240, 0);
static struct cdev c_dev;
static struct class *class;
static struct device *device;
static char buf[PAGE_SIZE] = {0};

// 设备打开操作
static int dev_open(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Device opened.\n");
    return 0;
}

// 设备关闭操作
static int dev_release(struct inode *inode, struct file *file)
{
    printk(KERN_INFO "Device closed.\n");
    return 0;
}

// 设备读操作
static ssize_t dev_read(struct file *file, char __user *buf, size_t count, loff_t *ppos)
{
    if (*ppos >= PAGE_SIZE)
        return 0;
    if (copy_to_user(buf, &buf[*ppos], count))
        return -EFAULT;
    *ppos += count;
    return count;
}

// 设备写操作
static ssize_t dev_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos)
{
    if (*ppos >= PAGE_SIZE)
        return -ENOSPC;
    if (copy_from_user(&buf[*ppos], buf, count))
        return -EFAULT;
    *ppos += count;
    return count;
}

// 设备文件操作结构
static const struct file_operations fops = {
    .owner          = THIS_MODULE,
    .read           = dev_read,
    .write          = dev_write,
    .open           = dev_open,
    .release        = dev_release,
};

// 模块初始化函数
static int __init dev_init(void)
{
    // 注册字符设备
    register_chrdev_region(dev_num, 1, "my_char_dev");

    // 初始化字符设备结构
    cdev_init(&c_dev, &fops);

    // 添加字符设备到设备类
    class = class_create(THIS_MODULE, "my_char_class");
    device = device_create(class, NULL, dev_num, NULL, "my_char_dev");

    // 注册字符设备
    cdev_add(&c_dev, dev_num, 1);

    return 0;
}

// 模块退出函数
static void __exit dev_exit(void)
{
    // 删除字符设备
    cdev_del(&c_dev);

    // 移除设备
    device_destroy(class, dev_num);

    // 销毁设备类
    class_unregister(class);

    // 注销字符设备区域
    unregister_chrdev_region(dev_num, 1);
}

// 模块初始化函数声明
module_init(dev_init);

// 模块退出函数声明
module_exit(dev_exit);

// 指定模块的许可证
MODULE_LICENSE("GPL");

6.2 测试字符设备

测试字符设备可以通过用户空间程序来读写设备节点。

echo "Hello, World!" > /dev/my_char_dev
cat /dev/my_char_dev

7. 总结

Linux内核模块是内核编程中的重要组成部分,它们允许在不重启系统的情况下扩展内核的功能。通过理解模块的编写、加载和卸载流程,以及如何在模块间传递参数和建立依赖关系,可以更好地利用Linux内核模块来满足各种需求。希望本文能帮助读者更好地掌握Linux内核模块的相关知识。

;