【Linux】内核驱动模块

devtools/2024/11/25 5:06:16/

在这里插入图片描述

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内核模块的相关知识。


http://www.ppmy.cn/devtools/136751.html

相关文章

Web3的核心技术:区块链如何确保信息安全与共享

在互联网不断迭代的进程中&#xff0c;Web3被视为下一代互联网的核心发展方向&#xff0c;其目标是构建更加开放、安全、去中心化的数字生态。在这一过程中&#xff0c;区块链作为核心技术&#xff0c;为信息安全与共享提供了全新解决方案。本文将深入探讨区块链如何在Web3中实…

Firewall防火墙配置

文章目录 一、firewalld简介二、firewalld特性三、firewalld相关文件及目录四、firewalld配置五、firewalld配置实例一、firewalld简介 firewalld 提供了支持网络/防火墙区域(zone)定义网络链接以及接口安全等级的动态防火墙管理工具。它支持 ipv4, ipv6 防火墙设置以及以太网…

力扣—面试题 17.14. 最小K个数

面试题 17.14. 最小K个数 设计一个算法&#xff0c;找出数组中最小的k个数。以任意顺序返回这k个数均可。 示例&#xff1a; 输入&#xff1a; arr [1,3,5,7,2,4,6,8], k 4 输出&#xff1a; [1,2,3,4]提示&#xff1a; 0 < len(arr) < 1000000 < k < min(10…

AFC自动售检票系统ACC清分系统交易数据对账差异解决方案

1. 清分系统交易数据差异解决方案 1.1 历史差异清理与修复 数据差异来源排查 日志级差异分析: 终端设备日志(ES/SLE):从设备侧提取原始交易日志。站级控制系统日志(SC):对比站级与终端数据的完整性和一致性。中心系统日志(ACC):检查交易数据从站级到中心的丢失、重…

MySQL 8.0与PostgreSQL 15.8的性能对比

根据搜索结果&#xff0c;以下是MySQL 8.0与PostgreSQL 15.8的性能对比&#xff1a; MySQL 8.0性能特点&#xff1a; MySQL在处理大量读操作时表现出色&#xff0c;其存储引擎InnoDB提供了行级锁定和高效的事务处理&#xff0c;适用于并发读取的场景。MySQL通过查询缓存来提高读…

深度学习:神经网络中线性层的使用

深度学习&#xff1a;神经网络中线性层的使用 在神经网络中&#xff0c;线性层&#xff08;也称为全连接层或密集层&#xff09;是基础组件之一&#xff0c;用于执行输入数据的线性变换。通过这种变换&#xff0c;线性层可以重新组合输入数据的特征&#xff0c;并将其映射到新…

爬虫开发工具与环境搭建——使用Postman和浏览器开发者工具

第三节&#xff1a;使用Postman和浏览器开发者工具 在网络爬虫开发过程中&#xff0c;我们经常需要对HTTP请求进行测试、分析和调试。Postman和浏览器开发者工具&#xff08;特别是Network面板和Console面板&#xff09;是两种最常用的工具&#xff0c;能够帮助开发者有效地捕…

Cmakelist.txt之Linux-redis配置

1.cmakelist.txt cmake_minimum_required(VERSION 3.16) ​ project(redis_linux_test LANGUAGES C) ​ ​ ​ add_executable(redis_linux_test main.c) ​ # 设置hiredis库的头文件路径和库文件路径 set(Hiredis_INCLUDE_DIR /usr/local/include/hiredis) set(Hiredis_LIBRA…