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

ARM《10》_03_字符设备驱动进阶(并发→竞态→同步机制、阻塞io和非阻塞io)

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>
// 定义并初始化原子变量(初始值0)
static atomic_t irq_count = ATOMIC_INIT(0);atomic_inc(&irq_count);    // 原子加1(count++)
atomic_dec(&irq_count);    // 原子减1(count--)
atomic_read(&irq_count);   // 原子读(返回当前值)
atomic_dec_and_test(&irq_count); // 减1后判断是否为0(返回bool)
  • 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);  // 初始化(也可通过 DECLARE_WAIT_QUEUE_HEAD 静态定义)
  • 进程进入等待状态
// 若 condition 为假,进程进入 TASK_INTERRUPTIBLE 状态并加入等待队列,condition:必须是全局变量(需用锁保护),表示 “资源是否就绪”(如 “是否有新数据”)
wait_event_interruptible(wq, condition);
// 返回值:0(被正常唤醒);-ERESTARTSYS(被信号唤醒)
  • 唤醒等待队列中的进程
// 唤醒等待队列中所有处于可中断状态的进程
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、自旋锁要添加在所有可能对同一临界资源访问的区域。
  • 注意:当存在多个临界资源时,核心原则是:为每个独立的临界资源分配一把独立的自旋锁,避免 “一把锁保护所有资源” 导致的不必要阻塞,同时通过 “锁的有序性” 避免死锁。

架构

在这里插入图片描述

源码

  • spinlock_t.c
#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;
// 共享资源:被中断顶半部和进程read函数同时访问
static int irq_count = 0;
// 自旋锁:保护irq_count
static spinlock_t count_lock; //当有多个临界资源时,就给每个临界资源都定义一个自旋锁;
// 定时器(模拟中断源)
static struct timer_list my_timer;
// 底半部(tasklet)
static struct tasklet_struct my_tasklet;// 底半部处理函数(中断上下文,用自旋锁保护共享资源):每秒读取irq_count
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;// 加锁保护临界区(修改irq_count)spin_lock_irqsave(&count_lock, flags);irq_count++; // 共享资源修改spin_unlock_irqrestore(&count_lock, flags);tasklet_schedule(&my_tasklet);mod_timer(&my_timer, jiffies + HZ); // 1秒后再次触发
}// 进程上下文:read函数(用户进程访问共享资源):在终端中每秒读取
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;// 加锁保护临界区(读取irq_count)spin_lock_irqsave(&count_lock, flags);len = snprintf(kbuf, sizeof(kbuf), "irq_count: %d\n", irq_count); // 将 irq_count 的整数值格式化为字符串,存入内核缓存区 kbuf,并保证不会越界。/*1、 snprintf的由来:Print Formatted to String with Size Limit;2、snprintf的返回值:成功时返回 本应写入的字符数(可能 > size),失败返回负值;*/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
http://www.dtcms.com/a/601748.html

相关文章:

  • 【DaisyUI]】dropdown在点击菜单项之后不关闭,怎么破?
  • 常德公司做网站手机网站怎么做的好
  • 网站建设的可行性报告范文城市宣传片制作公司
  • Go语言编译型:高效的编程语言选择|深入探讨Go语言的编译特性与优势
  • 邓州网站制作企业网站设计思路
  • 区县政府税务数据分析能力建设DID(2007-2025)
  • Python图像处理基础(十九)
  • 全国产5G+WiFi6工业路由,适配工业4.0多元场景需求
  • LitJSON 轻量级、高效易用的 .NET JSON 库 深度解析与实战指南
  • 什么是虚拟现实(VR)?
  • 织梦手机电影网站模板创意平面设计公司
  • Hadess入门到精通 - 如何管理Maven制品
  • 一文分清:零样本、小样本、微调,使用 LLM 的三种方式
  • 网站建设用什么科目黔东南购物网站开发设计
  • 数字化转型绕不开的“地基”:IT基础架构运维如何破局?
  • Go 语言编译优化与性能提升
  • Ansible安装与常用模块
  • Linux新
  • 建站平台哪个好承德做网站的公司
  • wordpress 用户投稿吉林百度seo
  • 通过网络调试,上位机电脑控制下位机单片机板载灯的亮灭
  • 学习笔记八:对数几率回归
  • Linux如何从docker hub下载arm镜像
  • 分析仪器数据处理软件开发
  • LASSO框架(Belloni高维估计微课笔记)
  • 自己开发一款游戏怎么做明港seo公司
  • 公司做二手网站的用意微帮推广平台有哪些
  • 小迪安全v2023学习笔记(一百四十七讲)—— C2远控篇CC++ShellCode定性分析生成提取Loader加载模式编译执行
  • .NET 10发布和它的新增功能
  • (Spring)Spring Boot 自动装配原理总结