school-of-sre内核模块开发:简单驱动与系统调用实践

【免费下载链接】school-of-sre linkedin/school-of-sre: 这是一个用于培训软件可靠性工程师(SRE)的在线课程。适合用于需要学习软件可靠性工程和运维技能的场景。特点:内容丰富,涵盖多种软件可靠性工程领域知识,具有实践案例和课程资料。 【免费下载链接】school-of-sre 项目地址: https://gitcode.com/gh_mirrors/sc/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 0x80syscall指令)实现。下图展示了这一转换过程:

用户态与内核态切换

系统调用表

每个系统调用都有唯一的编号,内核通过系统调用表(sys_call_table)将编号映射到具体函数。在x86_64架构中,系统调用号通常定义在/usr/include/asm/unistd_64.h文件中。

字符设备驱动开发

字符设备是Linux中最常见的设备类型之一,如键盘、鼠标、串口等。字符设备驱动通过文件系统接口(/dev目录下的设备文件)与用户空间交互。

驱动基本框架

一个简单的字符设备驱动包含以下核心部分:

  1. 设备结构体定义
  2. 文件操作结构体实现(open/read/write等方法)
  3. 设备注册与注销

以下是一个字符设备驱动的框架代码:

#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详细介绍了系统调用的实现原理。

系统调用流程

  1. 用户程序调用C库函数(如openread
  2. C库函数将系统调用号和参数放入寄存器
  3. 执行软中断指令切换到内核态
  4. 内核根据系统调用号查找并执行相应处理函数
  5. 将结果返回给用户空间

添加自定义系统调用(实验环境)

注意:修改内核源码添加系统调用仅用于学习目的,生产环境不建议这样做。

  1. 修改syscall_64.tbl添加系统调用号
  2. syscalls.h中声明系统调用原型
  3. 实现系统调用函数
  4. 重新编译内核并测试

常见问题与调试技巧

内核调试工具

  • dmesg:查看内核日志
  • printk:内核打印函数,注意使用合适的日志级别
  • kgdb:内核调试器,支持断点调试
  • strace:跟踪用户程序的系统调用

常见错误

  • Oops:内核崩溃,通常由空指针引用或非法内存访问引起
  • 模块编译错误:确保内核源码路径正确,Makefile编写无误
  • 设备权限问题:使用chmod或udev规则设置设备文件权限

总结与进一步学习

本文介绍了内核模块的基本结构、字符设备驱动的实现方法和系统调用机制。通过school-of-sre项目的system_calls_and_signals章节,你可以深入学习系统调用和信号处理的更多知识。

内核开发是一个复杂但有趣的领域,建议结合Linux内核源码和《Linux设备驱动程序》一书继续学习。掌握这些技能将帮助你更好地理解Linux系统原理,解决复杂的系统问题。

如果你在实践中遇到问题,可以参考项目的Linux中级教程系统故障排除章节,获取更多系统调试和性能优化的知识。

【免费下载链接】school-of-sre linkedin/school-of-sre: 这是一个用于培训软件可靠性工程师(SRE)的在线课程。适合用于需要学习软件可靠性工程和运维技能的场景。特点:内容丰富,涵盖多种软件可靠性工程领域知识,具有实践案例和课程资料。 【免费下载链接】school-of-sre 项目地址: https://gitcode.com/gh_mirrors/sc/school-of-sre

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐