Linux驱动开发实战(十二):并发事件:硬件同步原语

发布于:2025-04-12 ⋅ 阅读:(74) ⋅ 点赞:(0)

Linux驱动开发实战(十二):并发事件:硬件同步原语



前言

在驱动开发中,硬件同步原语是解决并发问题的基础工具。同步原语(synchronization primitives)中的原语(primitive)一词来自计算机科学领域,指的是最基本、不可再分的操作单元。这个术语源自数学和逻辑学中的"原始概念"或"基本元素"的含义。让我从并发的本质开始,逐步展开这个话题。


一、并发问题的根源

并发是现代操作系统的必然现象,主要来源于:

  • 多线程与多进程调度:操作系统为了提高资源利用率,会在CPU上调度不同的线程和进程轮流执行
  • 各类中断机制:硬件中断、软中断和异常处理都会导致程序执行流被打断,产生并发场景

在设备驱动中,并发问题尤为突出,因为驱动代码需要处理来自用户空间的请求、内核线程以及硬件中断,这些可能同时发生并访问共享资源

二、并发带来的挑战

并发会对程序造成多种不良影响:

  • 共享数据被篡改:多个执行流同时修改同一数据,导致数据不一致
  • 动作不完整:原本需要原子完成的操作被中断,造成状态不一致
  • 系统性能问题:同步带来的调度开销、死锁风险、数据竞争等
    进程一读取地址0X30000000
    进程二读取同一地址
    进程一写入10
    进程二写入20
    最终结果是20覆盖了10,进程一的操作被丢失。在驱动开发中,这类问题可能导致设备控制错误、数据丢失或系统崩溃。

比喻

这就像厨房里有多位厨师(线程/进程)同时工作。餐厅老板(操作系统)为了提高效率,让多位厨师轮流使用同一个炉台(CPU)来完成不同的菜品。

各类中断机制

这相当于厨师正在烹饪过程中,突然电话响了(硬件中断)、有服务员进来询问菜品状态(软中断)或发现锅里的食材煮过头了(异常)。厨师必须暂停当前工作去处理这些突发事件。

并发带来的具体问题

  • 共享数据被篡改:
    想象两位厨师共用一个菜谱笔记本。厨师A查看笔记本上写着"加入10克盐",但在他去拿盐之前,厨师B看了同一页并修改为"加入20克盐"。厨师A回来后不知道已被修改,结果两个厨师都加了盐,菜品最终含有30克盐而不是预期的10克或20克。

  • 动作不完整:
    厨师正在执行"加入面粉并搅拌均匀"这个应该连续完成的步骤,但中途被叫去接电话。回来后,他忘记之前是否已经搅拌,导致面粉没有完全混合或重复搅拌。

  • 系统性能问题
    为了避免冲突,厨师们必须排队使用某些工具(锁)。但如果厨师A拿着平底锅等待铲子,而厨师B拿着铲子等待平底锅,两人都无法继续工作(死锁)。或者,厨师们花太多时间协调谁先用哪个工具,反而降低了整体效率。

解决方案

原子性操作就像是把"加盐并搅拌"定义为一个不可分割的工序,厨师必须从头到尾完成,期间不能被打断或让其他厨师插手。在您提供的代码中,atomic_t变量就像是一个特殊的记事本,确保同一时刻只有一个厨师能查看和修改它的内容,避免了多个厨师同时尝试使用同一个厨具(设备)的混乱局面。

硬件同步原语的作用

硬件同步原语是CPU架构提供的不可分割的原子操作指令,可以在不中断的情况下完成读-修改-写这样的操作序列。它们是所有高级同步机制的基础。

主要特点:

  • 原子性:操作过程中不会被中断
  • 可见性:操作结果对所有CPU核心可见
  • 顺序性:保证指令执行顺序

常用的硬件同步原语

1. 原子整型操作

Linux内核提供了一系列针对整型变量的原子操作接口:

// 定义原子变量
atomic_t counter = ATOMIC_INIT(0);  // 初始化为0

// 读取与设置
int value = atomic_read(&counter);
atomic_set(&counter, 10);

// 原子加减
atomic_add(5, &counter);  // counter += 5
atomic_sub(3, &counter);  // counter -= 3

// 自增自减
atomic_inc(&counter);  // counter++
atomic_dec(&counter);  // counter--

这些操作内部使用了CPU的原子指令(如x86的LOCK前缀指令),确保在多处理器系统中也能正确执行。

如果不能理解我们不妨想象一下:

想象一个餐厅厨房中有一个特殊的电子记账牌,这个记账牌只能由一个人在任何时刻操作,并且每次操作都必须一气呵成不能被打断:

atomic_t counter = ATOMIC_INIT(0);  // 初始化为0

安装了一个全新的电子记账牌,并将初始数值设为0。

int value = atomic_read(&counter);

查看记账牌上的数字,而且这个查看动作是"瞬间完成"的,不会有人在你看的过程中改变它。

atomic_set(&counter, 10);

将记账牌上的数字一步到位地设置为10,整个过程中没有人能插手修改。

atomic_add(5, &counter);  // counter += 5

给记账牌上的数字增加5,这个加法操作是整体完成的,不会出现"读取-计算-写回"被打断的情况。

atomic_sub(3, &counter);  // counter -= 3

从记账牌上的数字减去3,同样是一气呵成不可打断的。

atomic_inc(&counter);  // counter++

记账牌上的数字精确加1,即使在繁忙的厨房中,这个加1动作也会完整执行,不会有别的厨师同时干扰这个操作。

atomic_dec(&counter);  // counter--

记账牌上的数字精确减1,减法过程中没有人能同时操作这个记账牌。

关键是,这个电子记账牌有"魔法保护"——不管厨房多么繁忙,多少人同时想要操作记账牌,系统都能保证每次只有一个人完成一个完整操作,其他人必须等待,而且操作过程不会被任何事情打断。这就避免了常见的"多个厨师同时修改同一个数值"的混乱情况。

2. 位操作函数

对于需要按位操作的场景,内核提供了位原子操作:

unsigned long flags = 0;

// 设置第3位为1
set_bit(3, &flags);

// 清除第3位为0
clear_bit(3, &flags);

// 检查第3位的值
if (test_bit(3, &flags)) {
    // 位为1的处理
}

// 反转第3位的值
change_bit(3, &flags);

3. 比较与交换(CAS)

比较与交换是一种更强大的原语,它实现了"读取-比较-写入"的原子操作:

// 原子地比较old_val和*ptr,如果相等则将*ptr设为new_val
bool atomic_compare_exchange(int *ptr, int old_val, int new_val);

在ARM架构中,这通常通过LDREX和STREX指令实现;在x86中,通过CMPXCHG指令实现。

  • 比喻:这个智能记账牌允许厨师提出一个"有条件的更新请求",流程如下:
  1. 厨师记住当前看到的记账牌数值(假设是42)
  2. 厨师去准备食材,回来后想将数值更新为50
  3. 但厨师担心在他离开期间,其他人可能已经修改了这个数值
  4. 于是他使用CAS操作,对记账牌说:“只有当你的数值仍然是42时,才将其更改为50”
  • 实际执行过程

  • 记账牌会先检查当前显示的数值是否等于厨师提供的"期望值"(42)

  • 如果相等,就将数值更新为"新值"(50),并返回"成功"

  • 如果不相等(说明其他厨师已经修改过),就保持原值不变,并返回"失败"

4. 内存屏障

内存屏障确保内存操作的顺序性和可见性:

// 确保之前的所有内存写操作完成后,才执行后续操作
wmb();  // 写内存屏障

// 确保之前的所有内存读操作完成后,才执行后续操作
rmb();  // 读内存屏障

// 同时确保读写操作顺序
mb();   // 完全内存屏障

三、在驱动开发中的应用

1. 原子变量的定义与初始化:

static atomic_t test_atomic = ATOMIC_INIT(1);

代码定义了一个原子变量test_atomic并将其初始化为1,表示设备初始状态为可用。

2. 设备打开函数中的互斥控制:

static int led_chr_dev_open(struct inode *inode, struct file *filp)
{
    if(atomic_read(&test_atomic))
    {
        atomic_set(&test_atomic, 0);
    }
    else
    {
        printk("\n driver on using!  open failed !!!\n");
        return -EBUSY;
    }
    printk("\n open form driver \n");
    return 0;
}

这段代码通过原子操作实现了对设备的互斥访问控制:

  • atomic_read(&test_atomic)原子地读取变量值,检查设备是否可用
  • 如果设备可用(值为1),则atomic_set(&test_atomic, 0)原子地将其设置为0,表示设备已被占用
  • 如果设备已被占用(值为0),则返回-EBUSY错误,表示设备忙

设备释放函数中的状态恢复

static int led_chrdev_release(struct inode *inode, struct file *filp)
{
    atomic_set(&test_atomic, 1);
    printk("KERN_ALERT  \n finished  !!!\n");
    return 0;
}

当设备被释放时,原子地将test_atomic设置回1,表示设备重新可用。
原子操作的关键作用在于:

  • 原子性保证:读取和修改test_atomic的操作不会被中断,避免了多个进程同时通过检查并修改这个标志的情况
  • 互斥控制:确保同一时间只有一个进程能够打开并使用该设备
  • 可见性:一个进程对test_atomic的修改对其他所有尝试访问该设备的进程立即可见

四、实验现象

驱动程序为上面的那段
应用程序:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(int argc, char *argv[])
{
    /*判断输入的命令是否合法*/
    if(argc != 2)
    {
        printf(" commend error ! \n");
        return -1;
    }

    /*打开文件*/
    int fd = open("/dev/rgb_led", O_RDWR);
    if(fd < 0)
    {
		printf("\n open file : /dev/rgb_led failed !!!\n");
		return -1;
	}


    /*写入命令*/
    int error = write(fd,argv[1],sizeof(argv[1]));
    if(error < 0)
    {
        printf("write file error! \n");
        close(fd);
        /*判断是否关闭成功*/
    }


    sleep(10);  //休眠10秒

    /*关闭文件*/
    error = close(fd);
    if(error < 0)
    {
        printf("close file error! \n");
    }
    return 0;
}

当执行过快的时候并且同时双线程进行的时候(一个后台运行)
线程A读取test_atomic为1
线程B也读取test_atomic为1
线程A设置test_atomic为0
线程B设置test_atomic为0
两个线程都认为自己获得了设备访问权限

static int led_chr_dev_open(struct inode *inode, struct file *filp)
{
    if(atomic_read(&test_atomic))
    {
        atomic_set(&test_atomic, 0);
    }
    else
    {
        printk("\n driver on using!  open failed !!!\n");
        return -EBUSY;
    }
    printk("\n open form driver \n");
    return 0;
}

因为这段代码对整形原子的操作进行了保护,访问过文件之后就不能再访问多一次了!所以就会出现open file : /dev/rgb_led failed !!!的现象,要怎么解决呢,我们下一章就会讲到

总结

硬件同步原语是驱动开发中解决并发问题的基础工具。它们通过硬件支持的原子操作,为更高级的同步机制(如自旋锁、互斥锁、信号量等)提供了实现基础。在驱动开发中正确使用这些原语,可以有效避免并发带来的数据竞争、状态不一致等问题,提高驱动的可靠性和性能。本章主要讲的是原子的操作,下一章我们讲更高级的同步机制。


网站公告

今日签到

点亮在社区的每一天
去签到