当前位置: 首页 > news >正文

详细介绍:Linux字符设备驱动开发全攻略

详细介绍:Linux字符设备驱动开发全攻略

目标与读者

这篇 面向小白、由浅入深的超详细教程,带你从最基础的概念到可以动手运行的 字符设备驱动(char driver)示例、调试与常见坑位。假如你是 Linux 驱动新手、刚接触内核/嵌入式开发,跟着本文走一遍,你能:

  • 理解内核空间 vs 用户空间;
  • 掌握内核与用户程序的常见通信方式(read/writeioctlmmapsysfsproc 等);
  • 能写出一个最小可运行的字符设备驱动,知道如何编译/加载/测试和清理;
  • 能进行基本调试与排错、理解内存/中断/同步等核心概念。

下面开始,慢慢来,每一部分都有实例与操作步骤,可直接照着做。


为什么要学驱动(从工程角度)


最基础的概念(必须搞清楚)

内核空间(Kernel Space)与用户空间(User Space)

原则:永远不要在内核中直接使用用户空间指针或自行解引用用户指针 —— 一定要用内核提供的安全接口(copy_from_user / copy_to_user 等)。

设备节点(/dev)

  • 在 Linux 中,设备由设备节点表示(例如 /dev/ttyS0)。
  • 每个设备节点有 主设备号 (major)次设备号 (minor)。主号告诉内核使用哪个驱动,次号区分同驱动下的不同设备实例。

file_operations 与字符设备

驱动通过实现 struct file_operations(open/read/write/ioctl/mmap 等回调)与用户交互。内核在用户调用 open()/read() 时,会调用对应的驱动回调。

动态注册 vs 静态主设备号

  • 推荐用 alloc_chrdev_region 动态分配主设备号(避免冲突)。
  • 早期可用 register_chrdev(major, name, fops) 指定静态主号(不推荐)。

内核与用户空间通信方式概览


从零写一个最小、可运行的字符设备驱动(完整示例)

下面给出一个完整的可编译内核模块(字符设备),并配套用户态测试程序与 Makefile。代码已尽量注释以便小白理解。

驱动源码:mychardev.c

// mychardev.c
#include <linux/module.h>      // module init/exit macros#include <linux/init.h>#include <linux/fs.h>          // alloc_chrdev_region, struct file_operations#include <linux/cdev.h>        // cdev utilities#include <linux/uaccess.h>     // copy_to_user, copy_from_user#include <linux/slab.h>        // kmalloc, kfree#include <linux/device.h>      // class_create, device_create#define DEVICE_NAME "mychardev"   // /dev/mychardev#define BUF_SIZE    4096          // 内核缓冲区大小static dev_t dev_number;          // 保存分配到的设备号(包含主次号)static struct cdev my_cdev;       // cdev 结构static struct class *my_class;    // device class(用于自动创建设备节点)static char *kbuffer;             // 内核缓冲区static size_t data_len = 0;       // 缓冲区中有效数据长度// open 回调(每次用户 open() 时调用)static int my_open(struct inode *inode, struct file *file){pr_info("mychardev: device opened\n");return 0;}// release/close 回调(每次用户 close() 时调用)static int my_release(struct inode *inode, struct file *file){pr_info("mychardev: device closed\n");return 0;}// read 回调,将内核缓冲区数据拷贝到用户空间static ssize_t my_read(struct file *file, char __user *ubuf, size_t count, loff_t *ppos){size_t available;size_t to_copy;int ret;// 如果偏移到文件末尾,返回0表示 EOFif (*ppos >= data_len)return 0;available = data_len - *ppos;to_copy = (count < available) ? count : available;// 从内核缓冲区复制到用户空间ret = copy_to_user(ubuf, kbuffer + *ppos, to_copy);if (ret != 0) { // ret 是未复制的字节数,非 0 表示失败pr_err("mychardev: copy_to_user failed, ret=%d\n", ret);return -EFAULT;}*ppos += to_copy; // 更新文件偏移pr_info("mychardev: read %zu bytes\n", to_copy);return to_copy;}// write 回调,将用户数据写入内核缓冲区(覆盖写)static ssize_t my_write(struct file *file, const char __user *ubuf, size_t count, loff_t *ppos){int ret;size_t to_copy;// 限制写入长度(防止溢出)to_copy = (count < BUF_SIZE - 1) ? count : (BUF_SIZE - 1);// 将用户空间数据复制到内核缓冲区ret = copy_from_user(kbuffer, ubuf, to_copy);if (ret != 0) {pr_err("mychardev: copy_from_user failed, ret=%d\n", ret);return -EFAULT;}kbuffer[to_copy] = '\0'; // 保证以 '\0' 结尾,便于以字符串处理data_len = to_copy;      // 更新数据长度pr_info("mychardev: wrote %zu bytes\n", to_copy);return to_copy;}// 定义文件操作结构体,把我们的回调关联上来static const struct file_operations my_fops = {.owner = THIS_MODULE,.open = my_open,.release = my_release,.read = my_read,.write = my_write,};// module init:注册设备号、注册 cdev、创建类与设备节点、分配内存static int __init mychardev_init(void){int ret;// 动态分配主设备号(0 表示内核分配)ret = alloc_chrdev_region(&dev_number, 0, 1, DEVICE_NAME);if (ret < 0) {pr_err("mychardev: alloc_chrdev_region failed\n");return ret;}pr_info("mychardev: alloc_chrdev_region ok, major=%d minor=%d\n",MAJOR(dev_number), MINOR(dev_number));// 初始化 cdev 并添加到内核cdev_init(&my_cdev, &my_fops);my_cdev.owner = THIS_MODULE;ret = cdev_add(&my_cdev, dev_number, 1);if (ret) {pr_err("mychardev: cdev_add failed\n");unregister_chrdev_region(dev_number, 1);return ret;}// 创建类,配合 udev 自动创建设备节点 /dev/mychardevmy_class = class_create(THIS_MODULE, "mychardev_class");if (IS_ERR(my_class)) {pr_err("mychardev: class_create failed\n");cdev_del(&my_cdev);unregister_chrdev_region(dev_number, 1);return PTR_ERR(my_class);}device_create(my_class, NULL, dev_number, NULL, DEVICE_NAME);// 分配内核缓冲区kbuffer = kmalloc(BUF_SIZE, GFP_KERNEL);if (!kbuffer) {pr_err("mychardev: kmalloc failed\n");device_destroy(my_class, dev_number);class_destroy(my_class);cdev_del(&my_cdev);unregister_chrdev_region(dev_number, 1);return -ENOMEM;}data_len = 0;pr_info("mychardev: module loaded\n");return 0;}// module exit:释放资源static void __exit mychardev_exit(void){kfree(kbuffer);device_destroy(my_class, dev_number);class_destroy(my_class);cdev_del(&my_cdev);unregister_chrdev_region(dev_number, 1);pr_info("mychardev: module unloaded\n");}MODULE_LICENSE("GPL");MODULE_AUTHOR("示例教程");MODULE_DESCRIPTION("简单字符设备驱动示例");module_init(mychardev_init);module_exit(mychardev_exit);

说明(高层):这个模块分配了设备号、注册了 cdev,创建了 /dev/mychardev(如果系统上有 udev 会自动创建),并分配了一个 4KB 的内核缓冲区用于 read/writewrite 会覆盖缓冲区,read 从缓冲区读取。


Makefile(用于编译内核模块)

# Makefile
obj-m += mychardev.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:$(MAKE) -C $(KDIR) M=$(PWD) clean

用户态测试程序:test.c

// test.c - 用于测试 /dev/mychardev
#include <stdio.h>#include <fcntl.h>#include <unistd.h>#include <string.h>#include <errno.h>int main(void){int fd = open("/dev/mychardev", O_RDWR);if (fd < 0) {perror("open");return 1;}const char *msg = "Hello from user space!\n";ssize_t w = write(fd, msg, strlen(msg));if (w < 0) {perror("write");close(fd);return 1;}printf("wrote %zd bytes\n", w);// 从头开始读lseek(fd, 0, SEEK_SET);char buf[256];ssize_t r = read(fd, buf, sizeof(buf) - 1);if (r < 0) {perror("read");} else {buf[r] = '\0';printf("read %zd bytes: %s", r, buf);}close(fd);return 0;}

编译、加载、测试步骤(逐条可复制执行)

  1. mychardev.cMakefile 放到一个目录,运行:

    make
  2. 加载模块(需要 root):

    sudo insmod mychardev.ko
  3. 查看 dmesg(查看模块输出与分配到的主设备号):

    dmesg | tail -n 20
    # 你会看到类似:
    # mychardev: alloc_chrdev_region ok, major=249 minor=0
    # mychardev: module loaded
  4. 查看设备节点(如果 udev 自动创建):

    ls -l /dev/mychardev
    # 如果没有自动创建(较老系统),可以手动:
    # sudo mknod /dev/mychardev c <major> 0# sudo chmod 666 /dev/mychardev

    如果需要手动 mknod,用 dmesg 中的 major 替换 <major>

  5. 编译用户程序并运行:

    gcc -o test test.c
    sudo ./test
    # 期望输出:
    # wrote 21 bytes
    # read 21 bytes: Hello from user space!
  6. 卸载模块并清理:

    sudo rmmod mychardev
    dmesg | tail -n 10
    make clean

代码逐步讲解(重要点,面向小白)

  • alloc_chrdev_region(&dev_number, 0, 1, DEVICE_NAME);

    • 动态分配一个设备号,保存到 dev_number(包含主/次号)。1 表示注册 1 个连续设备编号。
  • cdev_init(&my_cdev, &my_fops); cdev_add(&my_cdev, dev_number, 1);

    • 初始化 cdev 结构并把它添加到内核中,使内核知道当访问该设备号时要调用哪个 file_operations
  • class_createdevice_create

    • 通过 sysfs / udev 创建 /dev 下的设备节点(如果系统运行 udev,会自动创建设备节点)。这样用户侧就可以通过 /dev/mychardev 打开驱动。
  • kmalloc(BUF_SIZE, GFP_KERNEL)

    • 在内核中分配一段内存作为缓冲区。GFP_KERNEL 表示可以睡眠等待内存分配(通常在进程上下文使用)。
  • copy_from_user / copy_to_user

    • 内核与用户数据拷贝的安全接口:永远不要直接访问用户指针,必须通过这些 API。
    • 这些函数返回未成功复制的字节数(0 表示成功)。检查返回值非常重要。
  • pr_info / pr_err

    • 内核日志打印(等价于 printk),便于 dmesg 查看调试信息。
  • 清理顺序:

    • 释放内存 kfreedevice_destroyclass_destroycdev_delunregister_chrdev_region

常见错误与排查(小白最容易踩的坑)


内存分配与 I/O 映射(常见函数对比)

  • kmalloc(size, GFP_KERNEL):分配内核连续的虚拟内存(物理上可能不连续),用于小块内存(几 KB ~ 数百 KB)。
  • vmalloc(size):分配虚拟连续但物理不连续的大块内存(用于很大内存)。
  • ioremap(phys, size):把设备物理寄存器地址映射到内核虚拟地址,供 CPU 读写外设寄存器。
  • dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL):分配用于 DMA 的内存(既保证物理连续又可映射到设备)。用于驱动需要 DMA 的场景。

中断处理(简介,配合设备需要了解)

注意:ISR 中不能睡眠,不能调用会睡的 API(例如 kmallocGFP_KERNEL 可能睡,尽量使用 GFP_ATOMIC,并尽量少做重工作)。


并发/同步(核心概念,小白常迷糊)

  • mutex:用于可睡眠上下文(普通进程上下文),当持有者做 I/O 或会休眠时选择 mutex。
  • spinlock:用于中断或不可睡眠上下文,获取锁不会睡眠(自旋),持有时间要尽量短。
  • atomic_t:对单一整数的原子操作(计数器等)。
  • semaphoresrw_semaphore:更复杂的同步原语。

原则:在 IRQ 或死忙场景不能睡眠时用 spinlock;在可以睡眠时用 mutex。


调试技巧(必会)


安全与最佳实践(写驱动的约定俗成)

  • 总是检查返回值(每个内核 API 都可能失败)。
  • 出错时按申请资源的逆序释放。
  • 不要在中断上下文睡眠;不要在可睡眠上下文使用 spin_lock 导致死锁。
  • 使用 dev_*ptrIS_ERR() 等内核辅助宏规范化代码。
  • 测试环境请使用虚拟机会更安全,避免主机宕机损失。

推荐学习路线与资料(按难度递进)


小练习(建议做 5 个小练手项目)

  • 改造示例驱动,使写入数据追加(不覆盖)并支持文件偏移。
  • 添加 ioctl 实现设备控制命令(比如获取/设置缓冲区大小)。
  • 实现 mmap,把内核缓冲区映射给用户程序,比较性能差异(有无拷贝)。
  • 在驱动中添加一个 sysfs 属性(device_create_file),在用户空间通过 echo/ cat 操作查看与设置。
  • 为虚拟设备实现简单中断模拟(使用 tasklet / workqueue 完成较耗时工作)。

总结(简明版)


http://www.hskmm.com/?act=detail&tid=24326

相关文章:

  • sql注入和xss漏洞
  • 数学 trick
  • 完整教程:精读C++20设计模式——行为型设计模式:解释器模式
  • js疑惑
  • 关于我
  • 20251004国庆模拟4
  • 珂朵莉树 ODT
  • 2025多校CSP模拟赛2
  • 详细介绍:深入了解linux网络—— 基于UDP实现翻译和聊天功能
  • Rewind: Codeforces Round 1055 (Div.1+Div.2)
  • 10.4模拟赛总结
  • 01.linux基础
  • 英语完形填空
  • 2025整体橱柜厂家TOP企业品牌推荐排行榜,云南昆明整体橱柜全瓷砖,开放式厨房,经济型,一站式无烟柴火灶,嵌入式,智能,多功能,全屋无烟柴火灶整体橱柜公司推荐
  • Centos7安装mysql8
  • vite-vue3脚手架(参考帝莎编程-后台管理系统开发)
  • 上传文件的后端程序handleFileUpload()、getOriginalFilename()、UUID
  • 从模拟入侵到渗透测试:我摸清了黑客的套路,也懂了企业的软肋 - 详解
  • 同样的Python代码,在Windows上运行没有错误,在Linux Centos上运行出行错误。
  • FreeBSD 14发布后的技术问题解析
  • handleFileUpload()
  • 实用指南:Typescript高级类型详解
  • 集合幂级数,FMT 与 FWT 学习笔记
  • 2025多校CSP模拟赛1
  • 上传文件前端需要注意的三个点:
  • AT_arc189_b [ARC189B] Minimize Sum
  • Jenkins安装与配备
  • 2025-10-04 60S读世界
  • 适合新手的PPT模板网站,简单操作但效果好!
  • 2025多校冲刺CSP模拟赛2 总结