school-of-sre内核模块开发:简单驱动与系统调用实践
你是否在Linux系统管理中遇到过硬件设备无法识别、需要定制系统功能的情况?作为运维或开发人员,掌握内核模块开发基础能帮你解决这些问题。本文将通过school-of-sre项目中的系统调用知识,带你从零构建一个简单字符设备驱动,理解内核与用户空间通信的底层原理。读完本文你将掌握:内核模块的基本结构、系统调用的工作机制、字符设备驱动的实现方法。## 内核模块基础内核模块(Kernel Mod...
school-of-sre内核模块开发:简单驱动与系统调用实践
你是否在Linux系统管理中遇到过硬件设备无法识别、需要定制系统功能的情况?作为运维或开发人员,掌握内核模块开发基础能帮你解决这些问题。本文将通过school-of-sre项目中的系统调用知识,带你从零构建一个简单字符设备驱动,理解内核与用户空间通信的底层原理。读完本文你将掌握:内核模块的基本结构、系统调用的工作机制、字符设备驱动的实现方法。
内核模块基础
内核模块(Kernel Module)是一种可以动态加载到Linux内核中的代码,允许在不重启系统的情况下扩展内核功能。与传统驱动相比,内核模块具有轻量化、按需加载的特点,非常适合开发调试阶段。
模块基本结构
一个标准的内核模块包含初始化函数、清理函数和模块信息:
#include <linux/init.h>
#include <linux/module.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple kernel module");
static int __init hello_init(void) {
printk(KERN_INFO "Hello, Kernel Module!\n");
return 0;
}
static void __exit hello_exit(void) {
printk(KERN_INFO "Goodbye, Kernel Module!\n");
}
module_init(hello_init);
module_exit(hello_exit);
这段代码定义了模块加载时执行的hello_init函数和卸载时执行的hello_exit函数。printk是内核空间的打印函数,KERN_INFO指定日志级别。
编译与加载
编译内核模块需要Linux内核源码和Makefile:
obj-m += hello.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
使用make命令编译后,通过以下命令加载模块:
sudo insmod hello.ko
dmesg | tail # 查看内核日志
sudo rmmod hello # 卸载模块
系统调用机制
系统调用(System Call)是用户空间程序与内核空间通信的桥梁,所有对硬件或敏感资源的访问都必须通过系统调用完成。school-of-sre项目的system_calls_and_signals/intro.md详细介绍了这一机制。
用户态与内核态切换
当用户程序执行系统调用时,CPU会从用户态切换到内核态,这个过程通过软中断(如x86的int 0x80或syscall指令)实现。下图展示了这一转换过程:
系统调用表
每个系统调用都有唯一的编号,内核通过系统调用表(sys_call_table)将编号映射到具体函数。在x86_64架构中,系统调用号通常定义在/usr/include/asm/unistd_64.h文件中。
字符设备驱动开发
字符设备是Linux中最常见的设备类型之一,如键盘、鼠标、串口等。字符设备驱动通过文件系统接口(/dev目录下的设备文件)与用户空间交互。
驱动基本框架
一个简单的字符设备驱动包含以下核心部分:
- 设备结构体定义
- 文件操作结构体实现(open/read/write等方法)
- 设备注册与注销
以下是一个字符设备驱动的框架代码:
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/module.h>
#define DEVICE_NAME "simple_char"
#define CLASS_NAME "simple_class"
static dev_t dev_num;
static struct cdev simple_cdev;
static struct class* simple_class = NULL;
static struct device* simple_device = NULL;
static int simple_open(struct inode *inode, struct file *file) {
printk(KERN_INFO "Simple char device opened\n");
return 0;
}
static ssize_t simple_read(struct file *file, char __user *buf, size_t count, loff_t *pos) {
char msg[] = "Hello from kernel!\n";
int len = strlen(msg);
if (*pos >= len) return 0;
if (count > len - *pos) count = len - *pos;
if (copy_to_user(buf, msg + *pos, count)) return -EFAULT;
*pos += count;
return count;
}
static struct file_operations fops = {
.open = simple_open,
.read = simple_read,
.owner = THIS_MODULE,
};
static int __init simple_init(void) {
// 分配设备号
if (alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME) < 0) {
printk(KERN_ALERT "Failed to allocate device number\n");
return -1;
}
// 初始化字符设备
cdev_init(&simple_cdev, &fops);
if (cdev_add(&simple_cdev, dev_num, 1) < 0) {
unregister_chrdev_region(dev_num, 1);
printk(KERN_ALERT "Failed to add cdev\n");
return -1;
}
// 创建设备类
simple_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(simple_class)) {
cdev_del(&simple_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_ALERT "Failed to create class\n");
return PTR_ERR(simple_class);
}
// 创建设备节点
simple_device = device_create(simple_class, NULL, dev_num, NULL, DEVICE_NAME);
if (IS_ERR(simple_device)) {
class_destroy(simple_class);
cdev_del(&simple_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_ALERT "Failed to create device\n");
return PTR_ERR(simple_device);
}
printk(KERN_INFO "Simple char device initialized\n");
return 0;
}
static void __exit simple_exit(void) {
device_destroy(simple_class, dev_num);
class_unregister(simple_class);
class_destroy(simple_class);
cdev_del(&simple_cdev);
unregister_chrdev_region(dev_num, 1);
printk(KERN_INFO "Simple char device exited\n");
}
module_init(simple_init);
module_exit(simple_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple character device driver");
用户空间测试程序
编译加载驱动后,可以通过以下用户空间程序测试:
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd;
char buf[1024];
fd = open("/dev/simple_char", O_RDONLY);
if (fd < 0) {
perror("Failed to open device");
return 1;
}
read(fd, buf, sizeof(buf));
printf("Read from device: %s", buf);
close(fd);
return 0;
}
系统调用实践
系统调用是内核提供给用户空间的接口,school-of-sre项目的system_calls_and_signals/system_calls.md详细介绍了系统调用的实现原理。
系统调用流程
- 用户程序调用C库函数(如
open、read) - C库函数将系统调用号和参数放入寄存器
- 执行软中断指令切换到内核态
- 内核根据系统调用号查找并执行相应处理函数
- 将结果返回给用户空间
添加自定义系统调用(实验环境)
注意:修改内核源码添加系统调用仅用于学习目的,生产环境不建议这样做。
- 修改
syscall_64.tbl添加系统调用号 - 在
syscalls.h中声明系统调用原型 - 实现系统调用函数
- 重新编译内核并测试
常见问题与调试技巧
内核调试工具
- dmesg:查看内核日志
- printk:内核打印函数,注意使用合适的日志级别
- kgdb:内核调试器,支持断点调试
- strace:跟踪用户程序的系统调用
常见错误
- Oops:内核崩溃,通常由空指针引用或非法内存访问引起
- 模块编译错误:确保内核源码路径正确,Makefile编写无误
- 设备权限问题:使用
chmod或udev规则设置设备文件权限
总结与进一步学习
本文介绍了内核模块的基本结构、字符设备驱动的实现方法和系统调用机制。通过school-of-sre项目的system_calls_and_signals章节,你可以深入学习系统调用和信号处理的更多知识。
内核开发是一个复杂但有趣的领域,建议结合Linux内核源码和《Linux设备驱动程序》一书继续学习。掌握这些技能将帮助你更好地理解Linux系统原理,解决复杂的系统问题。
更多推荐


所有评论(0)