0、前言:
- 这是一篇以问题为导向,的技术贴!
- 作为字符设备学习的基础完结知识,这篇技术贴算是画上一个小句号,但是如果是深入学习开发的知识,还得补充学习以下知识:
- 1、在掌握mknod之后,在开发中,更多的是用class_creat+device_create自动创建;
- 2、linux的5.15内核已广泛使用设备树(Device Tree),用于描述硬件信息,即使没有开发板,也可以在虚拟机中学习“驱动如何解析设备树属性”(从设备树读取缓冲区大小、定时器周期);
基础概念库:
一、并发与同步基础知识
并发、竞态、同步机制 的 基础知识:
- 并发是多个执行单元(进程、中断、多核 CPU 线程)同时访问共享资源(如全局变量、硬件寄存器、链表)的现象。
- 并发的风险(竞态):并发访问共享资源时,若操作不原子(如 “读、改、写” 三步),会导致数据不一致(比如 A 进程读count=5,还没修改,中断触发把count改成 6,A 进程最终写回 5,覆盖了正确值)。
- 竞态产生的典型场景
1、多进程访问驱动(如多个cat /dev/xxx调用驱动的read函数);
2、进程与中断(顶半部 / 底半部)同时访问共享资源;
3、SMP 多核 CPU 上,不同 CPU 的执行单元访问共享资源(虚拟机可开启多核模拟)。 - 解决竞态的思路:同步机制,通过内核提供的工具,保证共享资源的 “互斥访问”(同一时间只有一个执行单元能操作)或 “原子操作”(操作不可分割)。
- 临界资源:是多个进程或线程并发访问时,需要互斥使用的共享资源;
- 临界区:进程或者线程访问共享资源(全局变量、硬件寄存器、链表等)的代码段,这段代码不能被打断,否则会导致数据不一致;读-改-写(Read-Modify-Write)是最典型的临界区操作,也是并发问题的重灾区。
- 总结:并发的时候,如果有中断发生,就可能出现竞态,导致访问的数据出错,所以要通过同步机制规避竞态时数据访问出错的问题;
- 注意:并发会读文件,这就需要创建硬件节点作为中间媒介;
常用同步机制:
- 1、原子操作:不可分割的CPU指令,无锁,效率最高,适用于简单变量的读、写、改;核心接口如下:【本质:通过单条 CPU 指令完成操作,不会被任何执行单元打断。】
#include <linux/types.h>
static atomic_t irq_count = ATOMIC_INIT(0);atomic_inc(&irq_count);
atomic_dec(&irq_count);
atomic_read(&irq_count);
atomic_dec_and_test(&irq_count);
- 2、自旋锁:忙等待锁,不释放CPU,不能睡眠,适用于多核场景;核心接口如下:【申请锁时若被占用,会循环等待(自旋),直到拿到锁,期间不释放 CPU】,案例5就体现了自旋锁的特点: “关中断 + 互斥访问”
#include <linux/spinlock.h>
static spinlock_t count_lock;
spin_lock_init(&count_lock);
unsigned long flags;
spin_lock_irqsave(&count_lock, flags);
...
spin_unlock_irqrestore(&count_lock, flags);
- 3、互斥体:阻塞等待锁,释放CPU,可睡眠,适用于进程上下文;核心接口如下:【申请锁时若被占用,当前进程会进入休眠(释放 CPU),锁释放后被唤醒】
#include <linux/mutex.h>
static struct mutex dev_mutex;
mutex_init(&dev_mutex);
mutex_lock(&dev_mutex);
...
mutex_unlock(&dev_mutex);
add_补充问题
- 实时打印内核驱动日志:dmesg -w
- 内核无法卸载驱动的做法:查看使用驱动的进程、杀死这些进程;

二、阻塞IO与非阻塞IO
- 阻塞 I/O 中:I = Input(输入)、O = Output(输出);
- 在字符设备驱动中,阻塞与非阻塞 IO 是处理设备与用户态交互的核心机制,尤其适用于需要 “等待资源就绪” 的场景(如数据生成、外部事件响应等)。
阻塞 IO:给你机会,等等你
- 当用户进程调用 read()/write() 等操作时,若设备资源未就绪(如无数据可读),进程会主动进入睡眠状态(释放 CPU),直到资源就绪后被驱动唤醒,再继续执行。
- 优点:不占用 CPU 资源,适合等待时间较长的场景。
- 关键机制:等待队列(wait_queue_head_t),用于管理睡眠的进程。
非阻塞 IO:就问一次
- 当用户进程调用 IO 操作时,无论资源是否就绪,驱动都会立即返回:
- 资源就绪:返回操作结果(如读取的数据长度)。
- 资源未就绪:返回错误码(通常是 -EAGAIN 或 -EWOULDBLOCK),进程可轮询重试。
- 优点:进程不阻塞,适合需要快速响应的场景(但可能占用 CPU 轮询)。
等待队列与阻塞队列:Linux阻塞机制的核心
- 等待队列 是Linux内核实现进程挂起和唤醒的基础数据结构,本质是双向链表,节点是等待特定事件的task_struct(进程控制块);等待队列是通过内核操作的;
- 阻塞队列是基于等待队列实现的生产者-消费者模型,当队列满时生产者阻塞,队列空时消费者阻塞;
等待队列的核心代码
#include <linux/wait.h>
wait_queue_head_t wq;
init_waitqueue_head(&wq);
wait_event_interruptible(wq, condition);
wake_up_interruptible(&wq);
wake_up_interruptible_nr(&wq, 1);
具体问题解决:
案例5:并发
- 基于案例4,新增read接口让用户进程访问irq_count,用 自旋锁 保护 “中断顶半部” 与 “进程” 的并发访问。
- 先明确并发场景的核心冲突:
1、demo_read 是用户进程调用的读函数(运行在进程上下文)
2、irq_handler(中断顶半部,中断上下文)会每秒修改 irq_count
3、tasklet_func(底半部,中断上下文)会每秒读取 irq_count。
4、如果没有自旋锁,会出现 “读一半被写打断” 的情况(比如进程刚读到 irq_count=3,中断就把它改成 4,进程最终返回 3,数据错误)。自旋锁就是要阻止这种情况。
5、自旋锁要添加在所有可能对同一临界资源访问的区域。 - 注意:当存在多个临界资源时,核心原则是:为每个独立的临界资源分配一把独立的自旋锁,避免 “一把锁保护所有资源” 导致的不必要阻塞,同时通过 “锁的有序性” 避免死锁。
架构

源码
#include <linux/module.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/timer.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/spinlock.h> #define DEV_NAME "concurrency_demo"
static int major;
static int irq_count = 0;
static spinlock_t count_lock;
static struct timer_list my_timer;
static struct tasklet_struct my_tasklet;
static void tasklet_func(unsigned long data)
{unsigned long flags;spin_lock_irqsave(&count_lock, flags);printk(KERN_INFO "Bottom half: irq_count = %d\n", irq_count);spin_unlock_irqrestore(&count_lock, flags);
}
static void irq_handler(struct timer_list *timer)
{unsigned long flags;spin_lock_irqsave(&count_lock, flags);irq_count++; spin_unlock_irqrestore(&count_lock, flags);tasklet_schedule(&my_tasklet);mod_timer(&my_timer, jiffies + HZ);
}
static ssize_t demo_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{char kbuf[32];unsigned long flags;int len;spin_lock_irqsave(&count_lock, flags);len = snprintf(kbuf, sizeof(kbuf), "irq_count: %d\n", irq_count); spin_unlock_irqrestore(&count_lock, flags);if (copy_to_user(buf, kbuf, len) != 0)return -EFAULT;return len;
}
static struct file_operations fops = {.owner = THIS_MODULE,.read = demo_read,
};
static int __init concurrency_init(void