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

news/2025/10/4 17:17:51/文章来源:https://www.cnblogs.com/ljbguanli/p/19125752

详细介绍: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.mzph.cn/news/927373.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

河南焦作有做网站开发的公司吗连云港今天的新消息

11月12日&#xff0c;由金蝶软件&#xff08;中国&#xff09;&#xff08;以下简称“金蝶”&#xff09;主办的2022全球创见者大会之“对标世界一流管理——走进一心堂暨生命科学行业峰会”在云南昆明顺利举办。金蝶携手众多企业经营管理者&#xff0c;业界思想领袖及先锋企业…

深入解析:uniapp集成语音识别与图片识别集成方案【百度智能云】

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

sql注入和xss漏洞

1、复习信息搜集中的Google hack语法,随机找10个php网站的后台地址 2、简述sql注入漏洞的原理及危害 原理:由于后端程序员未对用户输入的字符进行严格控制和过滤,导致黑客可以在后台程序员不知情的情况下注入SQL语法…

威海建设局官方网站长沙室内设计工作室

加载指定会话最近消息 前言 上一集我们就把三个标签页的加载列表的任务给完成啦&#xff01;那么我们这一集就来完成加载指定绘画最近消息的任务。 需求分析 我们点击了某个会话之后&#xff0c;我们就会去显示我们的会话的最近的N条消息。请看下图。 我们这里涉及到两个区…

大连企业网站建设模板wordpress 设置语言

http://www.elecfans.com/article/89/92/2017/20170425510728.html转载于:https://www.cnblogs.com/jackn-crazy/p/7300228.html

数学 trick

基本不等式遇到 \(x=\dfrac{一次函数}{一次函数}\),考虑分离出一个常数: 例:(2024 浙江模拟)已知实数 \(x,y,x>3,xy+2x-3y=12,(x+y)_{\min}\)? 解:考虑分离 \(x,y\),由 \(xy+2x-3y=12\) 得到 \(x=\dfrac{12…

免费网站下载app软件免费重庆seo优

本文来自网易云社区作者&#xff1a;田亚楠须知本文主要是根据 createjs 中的 EaselJS 在 github 上的 tutorials 目录下的文章整理而来 &#xff08;原文链接&#xff09;&#xff0c;同时也包含了很多本人的理解&#xff0c;如过有叙述不当的地方&#xff0c;请联系我 :-D 本…

Python 2025:异步革命与AI驱动下的开发新范式 - 详解

Python 2025:异步革命与AI驱动下的开发新范式 - 详解2025-10-04 16:56 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; di…

完整教程:精读C++20设计模式——行为型设计模式:解释器模式

完整教程:精读C++20设计模式——行为型设计模式:解释器模式pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Cons…

js疑惑

textBox.addEventListener("keydown", function (event) { console.log(`You pressed "${event.key}".`);});这个函数接收的"keydown",到底是什么意思我还是没看懂为什么会这样写看着也…

使用 Git Submodule 管理微服务项目:从繁琐到高效 - 指南

使用 Git Submodule 管理微服务项目:从繁琐到高效 - 指南pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consola…

如何识别网页用什么网站做的女装关键词排名

一个网站&#xff0c;其实说白了就是某几个特定功能的组合&#xff0c;而更换用户头像就在这些功能之中。今天就来做个测试&#xff0c;针对不同的用户&#xff0c;实现头像上传功能。先给大家展示下成品效果图&#xff1a;思路针对不同的用户上传头像&#xff0c;我们要为每一…

邯郸专业做网站多少钱做印刷网站公司哪家好

一、接口自动化测试中&#xff0c;会用到测试账号&#xff0c;如何合理运用账号&#xff1f; 账号一般用于接口登录、接口用例传参、操作sql等&#xff0c;目前账号是写到yaml配置文件里&#xff0c;如果1个账户使用会出现资源冲突&#xff0c;可以配置多个账号使用&#xff0…

佛山专业建设网站平台兼职python做网站

STM32定时器定时及其应用 定时器概述☆定时器相关配置CubeMX工程配置及程序实现固件库程序设计及实现 定时器概述 1. 工作原理 使用精准的时基&#xff0c;通过硬件的方式&#xff0c;实现定时功能。定时器核心就是计数器 2. 定时器分类   基本定时器&#xff08;TIM6~TIM7…

深入解析:单元测试学习+AI辅助单测

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

20251004国庆模拟4

对于 20251004 CSP-S 模拟的总结Part 1 题目 点击快速下载 有两道是洛谷的: T2: P5979 [PA2014] Druzyny T3: P2371 [国家集训队] 墨墨的等式 ⚠警告: P5979 和本场的 T3 并不完全一样。 Part 2 考试重要时间线 8:00…

珂朵莉树 ODT

能干什么/局限性 高效处理区间平推(区间赋值)的问题。 在随机数据下飞快。 如果没有区间平推,或者区间平推的操作数量可以被卡得很少甚至没有,就不适用。 前置知识set没了。 建点 每个点要维护一个区间,以及这个区…

2025多校CSP模拟赛2

2025多校CSP模拟赛2 狂写大树套树通过 \(T3\) 的救赎感。 T1 查询 第一眼感觉不好做。 首先直接找绝对没前途,考虑二分 \(v\)。 问题变成了统计 \(a_j+b_j\times c_i\le v\) 的数量,变换一下变成: \[c_i\le \frac{v…

网站查询访问界面设计模式读后感

如今人们对于住宅需求早已今非昔比&#xff0c;不但需要足够大的空间&#xff0c;而且对于住宅所处位置是否交通便利&#xff0c;环境如何&#xff0c;光照情况都有要求&#xff0c;但是最关注的问题还是住宅的安全问题。如今的社会科技发达&#xff0c;不法分子的手段也层出不…

io多路复用:reactor模型的封装及与上层简单业务的搭建(webserver)

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …