Linux设备驱动并发控制总结_CSDN

Linux 设备驱动中的并发控制(总结笔记)

笔记来源:Linux 设备驱动学习笔记 ------ 第 20 章《Linux 设备驱动中的并发控制》

关键词:并发、竞态、临界区、原子操作、自旋锁、信号量、互斥体、完成量


一、并发与竞态的基本概念

并发(concurrency):多个执行单元同时、并行地被执行。

竞态(race conditions) :并发的执行单元对共享资源(硬件资源、软件上的全局变量、静态变量等)进行访问时,很容易导致的结果错误。

1. 竞态产生的原因

  1. 多个 CPU:SMP(对称多处理)是一种紧耦合、共享存储的系统模型,多核同时访问同一共享资源;
  2. 进程调度:一个进程在内核执行时,可能被另一高优先级进程打断;
  3. 中断与进程之间:硬中断、软中断、Tasklet(底半部)与进程之间并发,高优先级的中断可以打断低优先级,中断可以打断进程。

2. 解决方案

解决竞态问题的途径是保证对共享资源的互斥访问------一个执行单元在访问共享资源时,其他执行单元被禁止访问。

访问共享资源的代码区域称为临界区(Critical Sections),临界区需要以某种互斥机制加以保护。Linux 设备驱动中可采用的互斥途径有:

中断屏蔽、原子操作、自旋锁、信号量、互斥体(以及用于同步的完成量)。


二、原子操作(atomic)

原子操作可以保证对一个整型数据的修改是排他性的。

Linux 内核提供了一系列函数来实现原子操作,分为两类:

  • 针对整型变量的原子操作;
  • 针对位的原子操作。

位和整型变量的原子操作都依赖于底层 CPU 的原子指令,因此所有这些函数都与 CPU 架构密切相关 。这种排他性不仅适用于同一个核内部的并发,还适用于多核之间的并发。

1. 整型原子操作 API

函数名 功能
ATOMIC_INIT 定义原子变量并初始化为给定值,如 atomic_t t = ATOMIC_INIT(2);
atomic_set 设置原子变量的值
atomic_read 返回原子变量的值
atomic_add 原子变量增加 i
atomic_sub 原子变量减少 i
atomic_inc 原子变量增加 1
atomic_dec 原子变量减少 1
atomic_inc_and_test 原子地递增,并测试最终的值是否为零
atomic_dec_and_test 原子地递减,并测试最终的值是否为零
atomic_sub_and_test 原子地递减 i,并测试最终的值是否为零
atomic_inc_return 自增操作,并返回新的值
atomic_dec_return 自减操作,并返回新的值

2. 位原子操作 API

函数名 功能
set_bit 设置位(位图操作,输入子系统会用到)
clear_bit 清除位
change_bit 改变位
test_and_set_bit 测试并设置位
test_and_clear_bit 测试并清除位
test_and_change_bit 测试并改变位

3. 代码示例

c 复制代码
#include <asm/atomic.h>

/* 定义并初始化:设备可用标志,1 = 空闲 */
static atomic_t dev_available = ATOMIC_INIT(1);

static int dev_open(struct inode *inode, struct file *filp)
{
    /* 原子地减 1 并测试:结果不为 0 说明本来就不是 1,即已被占用 */
    if (!atomic_dec_and_test(&dev_available)) {
        atomic_inc(&dev_available);     /* 恢复计数值,让别的进程还能再尝试 */
        return -EBUSY;
    }
    return 0;                           /* 抢占成功,设备被独占 */
}

static int dev_release(struct inode *inode, struct file *filp)
{
    atomic_inc(&dev_available);         /* 释放设备,恢复可用标志 */
    return 0;
}

原子操作适合的场景:计数器、标志位这类简单变量的保护,开销极小。


三、自旋锁(spinlock)

1. 原理

自旋锁是一种经典的、对临界资源进行互斥访问的手段:

  • 在某 CPU 上运行的代码需先执行一个原子操作 ,该操作对某个内存变量进行测试并设置(Test-And-Set);
  • 由于是原子操作,在该操作完成之前其他执行单元不可能访问这个内存变量;
  • 若测试结果表明锁空闲,则获得自旋锁、继续执行;
  • 若锁被占用 ,程序将在一个小的循环内重复"测试并设置"操作 ------ 即"原地打转",故名为自旋锁(忙等待 Busy-wait)。

适用范围 :主要针对 SMP 或单 CPU 但内核可抢占的情况;对于单 CPU 且内核不支持抢占的系统,自旋锁退化为空操作。

注意 :自旋锁"忙等待"期间 CPU 不会让出,因此持有自旋锁的代码绝对不能睡眠,临界区必须极短(微秒级)。

2. API

函数名 功能
spin_lock_init 初始化自旋锁
spin_lock 获得自旋锁
spin_trylock 尝试获得自旋锁(不阻塞)
spin_unlock 释放自旋锁
中断中使用
spin_lock_irqsave 中断中申请自旋锁(保存中断状态并关中断)
spin_unlock_irqrestore 中断中释放自旋锁(恢复中断状态)

3. 代码示例

c 复制代码
#include <linux/spinlock.h>

static spinlock_t lock;
static unsigned long flags;
static int shared_data = 0;

/* 模块加载时初始化 */
static int __init dev_init(void)
{
    spin_lock_init(&lock);
    ...
}

/* ---------- 进程上下文中使用 ---------- */
spin_lock(&lock);
shared_data++;                  /* 临界区:访问共享资源 */
spin_unlock(&lock);

/* ---------- 中断上下文中使用(或临界区可能被本核中断打断时) ---------- */
spin_lock_irqsave(&lock, flags);
shared_data++;
spin_unlock_irqrestore(&lock, flags);

/* ---------- 只想试探一下,拿不到就先干别的 ---------- */
if (spin_trylock(&lock)) {
    shared_data++;
    spin_unlock(&lock);
} else {
    /* 获取失败,做别的事,不要忙等 */
}

四、信号量(semaphore)

信号量是操作系统中最经典的用于同步和互斥 的方法,与操作系统中的经典概念 PV 操作对应:

  • P(down):申请信号量,计数值减 1,若小于 0 则睡眠等待;
  • V(up):释放信号量,计数值加 1,并唤醒等待者。

初始化时计数值为 1 的信号量即可当互斥锁用;计数值 大于 1 时是计数信号量,可用于限制并发数量。

1. API

函数名 功能
sema_init 初始化信号量
down 获得信号量(不可被信号打断)
down_interruptible 获得信号量(可以被信号打断,推荐)
down_trylock 尝试获得信号量(不阻塞)
up 释放信号量

2. 代码示例

c 复制代码
#include <linux/semaphore.h>

static struct semaphore sem;

static int __init dev_init(void)
{
    /* 第二个参数:同时允许的持有者数量,1 = 互斥使用 */
    sema_init(&sem, 1);
    ...
}

static ssize_t dev_read(struct file *filp, char __user *buf,
                        size_t count, loff_t *ppos)
{
    if (down_interruptible(&sem))       /* P 操作,可被信号打断 */
        return -ERESTARTSYS;

    /* 临界区:访问共享资源 */

    up(&sem);                           /* V 操作 */
    return count;
}

计数信号量(限制并发数)示例:

c 复制代码
sema_init(&sem, 5);   /* 最多允许 5 个进程同时进入临界区 */

五、互斥体(mutex)

自旋锁和互斥锁都是解决互斥问题的基本方法,互斥体依赖于自旋锁实现。

互斥体是进程级的,用于多个进程之间对资源的互斥:

  • 如果竞争失败,会发生进程上下文切换 ,当前进程进入睡眠态,CPU 将运行其他进程;
  • 因为进程上下文切换的开销比较大,长时间的资源占用建议使用互斥体;
  • 与自旋锁相反:互斥体可以在持锁期间睡眠,但只能用于进程上下文,不能用于中断上下文。

1. API

函数名 功能
mutex_init 初始化互斥体
mutex_lock 获得互斥体
mutex_lock_interruptible 获得互斥体(可以被信号打断)
mutex_trylock 尝试获得互斥体(不阻塞)
mutex_unlock 释放互斥体

2. 代码示例

c 复制代码
#include <linux/mutex.h>

static struct mutex my_mutex;

static int __init dev_init(void)
{
    mutex_init(&my_mutex);
    ...
}

static long dev_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    mutex_lock(&my_mutex);          /* 竞争失败会睡眠,进程上下文切换 */
    /* 临界区:可能耗时的操作,持锁期间允许睡眠 */
    mutex_unlock(&my_mutex);
    return 0;
}

六、完成量(Completion)

完成量主要用于同步操作:一个执行单元等待另一个执行单元完成某件事。

  • 用"同步阻塞"的代码手段,去等待一个"异步发生"的事件;
  • 发送通知的一方可以在中断上下文,等待通知的一方不能是中断上下文(等待方会睡眠)。

典型场景:等待硬件 DMA 完成 、等待内核线程启动等。

1. API

函数名 功能
struct completion my_completion; 定义完成量
init_completion(&my_completion); 初始化完成量
reinit_completion(&my_completion); 重新初始化完成量(重复使用时)
wait_for_completion(struct completion *c); 等待完成量(阻塞)
void complete(struct completion *c); 唤醒完成量(唤醒一个等待者)
void complete_all(struct completion *c); 唤醒完成量(唤醒所有等待者)

2. 代码示例:等待内核线程启动

c 复制代码
#include <linux/completion.h>

static struct completion thread_done;

static int my_thread(void *data)
{
    /* ... 线程初始化工作 ... */
    complete(&thread_done);          /* 通知:我已就绪(可在任何上下文) */
    return 0;
}

static int __init dev_init(void)
{
    init_completion(&thread_done);          /* 初始化完成量 */

    kthread_run(my_thread, NULL, "my_kthread");

    wait_for_completion(&thread_done);      /* 等待方:必须在进程上下文 */
    /* 走到这里,保证线程已经真正跑起来了 */
    return 0;
}

若同一个 completion 需要多次"等待---唤醒",每次唤醒后用 reinit_completion() 重新初始化即可复用。


七、四种机制差异对比(重要)

特性 自旋锁 (Spinlock) 信号量 (Semaphore) 互斥体 (Mutex) 完成量 (Completion)
核心机制 忙等待 (Busy-wait) 睡眠等待 (Sleep-wait) 睡眠等待 (Sleep-wait) 事件通知 (Event-based)
能否睡眠 绝对不能 可以 可以 等待方可以睡眠
上下文限制 进程上下文、中断上下文、软中断/Tasklet 仅限进程上下文 仅限进程上下文 进程上下文 + 中断上下文 (completion 可在中断中发送)
是否可递归 不可(死锁) 可(计数信号量) 不可(死锁) 不涉及
性能开销 极小(无上下文切换) 较大(有进程调度) 较小(比信号量轻量) 中等
持有时间 极短(微秒级) 较长 较长 取决于事件发生时间
典型应用场景 中断处理、短临界区、多核间共享数据保护 遗留代码、需要计数资源的场景(如限制并发数) 进程间互斥访问共享资源、持有锁期间可能睡眠 等待某个异步操作完成(如等待硬件 DMA 完成、线程启动)

八、总结与选型建议

  1. 原子操作(atomic):对整型变量(或位图)的原子操作,适合保护简单的计数器、标志位;
  2. 自旋锁 :开销小,短时忙等待,多用于中断 中,当然非中断中也可以使用;持锁期间绝不能睡眠;
  3. 信号量 :因为互斥场景已经有互斥体了,所以信号量多用于计数信号量(限制并发数),也可以当作互斥体使用;会导致休眠,开销大;
  4. 互斥体 :在驱动中做互斥的首选之一,同一时刻只能被一个进程使用;会导致休眠,开销大,适合持锁时间长的场合;
  5. 完成量 :主要做同步 操作 ------ 一个执行单元等待另一个执行单元执行完某事;做同步会导致休眠,开销大;发送通知的一方可以在中断上下文,等待通知的一方不能是中断上下文。

一句话选型口诀:

  • 保护一个整数/位图 → 原子操作
  • 中断上下文 / 临界区极短 → 自旋锁
  • 进程间互斥、持锁时间长 → 互斥体
  • 限制最多 N 个并发 → 计数信号量
  • 等待一个异步事件完成 → 完成量

完。如果这篇文章对你有帮助,欢迎点赞、收藏、评论交流!

相关推荐
colinu1 小时前
Docker 容器端口未发布问题快速排查文档
运维·docker·容器
迪康coolmu1 小时前
迪康Defender网络准入控制系统落地实战:部署、配置与问题排查记录
运维·开发语言·网络·经验分享·php
Dovis(誓平步青云)1 小时前
一年1956亿行:智能电表场景下的时序数据存储实践
运维·前端·自动化·音视频·prometheus
Y3815326621 小时前
SerpBase + RAG 实战:用搜索 API 给知识库接上实时数据,回答不再过期
java·服务器·数据库
北风toto1 小时前
银河麒麟 V10 内网 YUM 源修复实战教程(U盘中转版)
linux·服务器
韩曙亮1 小时前
【错误记录】网络配置问题处理 ( 获取服务器的 出口 和 入口 IP | 定位网络变化 )
服务器·网络·tcp/ip
开开心心就好1 小时前
文件关联被破坏怎么修?免费工具一键修复
java·服务器·开发语言·pdf·ocr·启发式算法·宽度优先
小白鸽i1 小时前
【linux】麒麟桌面版安装人大金仓V8
linux·运维
Ruiery1 小时前
Linux 6.6内核内存管理深度解析(三):页表与缺页处理 — 虚拟地址怎么按需映射到物理页
linux·运维·服务器