1. 什么是 Linux 内核模块?
1.1 定义
Linux 内核模块(Kernel Module)是 可动态加载和卸载的内核代码,它允许开发者在 无需重启系统 的情况下扩展内核功能,例如:
- 设备驱动(Drivers)
- 文件系统(File Systems)
- 网络协议(Network Protocols)
- 安全增强(Security Extensions)
1.2 为什么需要内核模块?
在没有内核模块的情况下,所有功能都需要编译进内核,这会导致:
- 大而臃肿:系统包含许多不必要的代码,占用内存。
- 不灵活:任何修改都需要重新编译并重启内核,影响系统稳定性。
内核模块的优势:
✅ 动态加载/卸载,提高灵活性
✅ 减少内存占用,按需加载
✅ 降低内核维护成本,方便调试
2. 内核模块的基本结构
2.1 一个最简单的内核模块
#include <linux/module.h> // 内核模块头文件
#include <linux/init.h> // __init 和 __exit 相关宏MODULE_LICENSE("GPL"); // 指定模块的许可证
MODULE_AUTHOR("Your Name"); // 作者
MODULE_DESCRIPTION("A Simple Linux Kernel Module"); // 模块描述// 模块加载函数
static int __init my_module_init(void)
{printk(KERN_INFO "Hello, Kernel! My module is loaded.\n");return 0;
}// 模块卸载函数
static void __exit my_module_exit(void)
{printk(KERN_INFO "Goodbye, Kernel! My module is removed.\n");
}// 注册模块入口和退出函数
module_init(my_module_init);
module_exit(my_module_exit);
2.2 关键宏解析
MODULE_LICENSE("GPL")
规定模块的许可证,否则可能会触发taint
(污染)警告。module_init()
指定模块加载时执行的函数。module_exit()
指定模块卸载时执行的函数。
2.3 编译、加载与卸载
创建 Makefile
:
obj-m += my_module.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)all:$(MAKE) -C $(KDIR) M=$(PWD) modulesclean:$(MAKE) -C $(KDIR) M=$(PWD) clean
编译:
make
加载模块:
sudo insmod my_module.ko
查看日志:
dmesg | tail -n 10
卸载模块:
sudo rmmod my_module
3. 设备驱动与字符设备
3.1 字符设备基础
字符设备是按字节流方式访问的设备,例如串口、键盘等。
3.2 字符设备驱动框架
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>#define DEVICE_NAME "mychardev"static dev_t dev_num;
static struct cdev my_cdev;static int my_open(struct inode *inode, struct file *file) {printk(KERN_INFO "Device opened\n");return 0;
}static int my_release(struct inode *inode, struct file *file) {printk(KERN_INFO "Device closed\n");return 0;
}static struct file_operations fops = {.owner = THIS_MODULE,.open = my_open,.release = my_release,
};static int __init my_module_init(void) {alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);cdev_init(&my_cdev, &fops);cdev_add(&my_cdev, dev_num, 1);printk(KERN_INFO "Char device registered\n");return 0;
}static void __exit my_module_exit(void) {cdev_del(&my_cdev);unregister_chrdev_region(dev_num, 1);printk(KERN_INFO "Char device unregistered\n");
}module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
4. Linux 内核模块面试高频问题
Q1: 你能解释一下 printk
和 dmesg
的作用吗?
解答:
printk()
:内核日志打印函数,用于调试。dmesg
:查看printk
输出的命令。
示例:
printk(KERN_INFO "This is an informational message.\n");
常见的 KERN_
级别:
KERN_EMERG
:紧急情况KERN_ALERT
:需要立即采取措施KERN_CRIT
:严重错误KERN_ERR
:错误信息KERN_WARNING
:警告KERN_NOTICE
:普通重要消息KERN_INFO
:信息KERN_DEBUG
:调试信息
Q2: module_init()
和 module_exit()
的作用是什么?
解答:
module_init()
:定义内核模块加载时执行的函数。module_exit()
:定义内核模块卸载时执行的函数。
Q3: 设备号是什么?如何分配?
解答:
设备号(Device Number)用于唯一标识设备,包括:
- 主设备号(Major Number):表示设备类型(如磁盘、串口)。
- 次设备号(Minor Number):区分同类设备。
分配方式:
dev_t dev_num;
alloc_chrdev_region(&dev_num, 0, 1, "my_device");
或者使用 register_chrdev()
动态注册。
Q4: 你能解释一下 file_operations
结构体的作用吗?
解答:
file_operations
结构体用于定义设备驱动的操作,例如:
struct file_operations fops = {.open = my_open,.release = my_release,.read = my_read,.write = my_write,
};
它的作用类似于 系统调用接口,用于应用层和内核交互。
5. 结论
这篇文章介绍了 Linux 内核模块 的基本概念、代码示例、常见面试题及其解答。
对于初学者,建议从 加载和卸载模块 开始练习;对于高阶工程师,建议研究 字符设备驱动 和 面试考点,以深入理解 Linux 内核模块的运作机制。
欢迎留言交流!😊加粗样式