16-Linux驱动开发-多核通信中的并发控制

物理架构与竞态

物理内存布局 (SRAM)

html 复制代码
STM32MP157 内部有一块 SRAM,它被映射到了两个地址空间:
MPU (Microprocessor Unit,微处理器) A7 (Linux): 虚拟地址 -> 物理地址 0x10040000
MCU (Microcontroller Unit,微控制器) M4 (RTOS): 物理地址 0x10040000

三级竞态

html 复制代码
核间竞态 (Inter-Core): A7 和 M4 同时写这块 SRAM。
	会导致数据损坏(Tearing)。
	VirtIO 环形缓冲区 (软件解法) 或 HSEM (硬件解法)。
本地中断竞态 (Local Interrupt): A7 正在向 SRAM 填数据,突然 IPCC 接收中断来了。
	会导致死锁或数据不一致。
	Spinlock (自旋锁) + 关中断。
本地多进程竞态 (Local Process): 两个 Linux APP 同时通过驱动发消息。
	会导致逻辑混乱。
	Mutex (信号量)。
核心数据结构 ------ VirtIO Ring (解决核间竞态)

为了避免 A7 和 M4 竞态,VirtIO 采用了单向流设计:

html 复制代码
vring (Virtual Ring): 由三部分组成:
	Descriptor Table (描述符表): 存放实际数据的地址和长度。
	Available Ring (可用环): 发送方(Driver)只写这一部分,告诉接收方"我有数据给你"。
	Used Ring (已用环): 接收方(Device)只写这一部分,告诉发送方"我处理完了"。

VirtIO 的设计使得 A7 只修改 Available Ring 的索引,M4 只修改 Used Ring 的索引。读写分离,从算法上消除了核间竞态。

Linux 驱动层的并发 (解决本地竞态)

Linux 内核模块(.ko)的保护

html 复制代码
A.发送数据时:处于"进程上下文" (Process Context)
	Linux 应用层程序(APP)调用了 write(fd, buf, len)。
	此时,CPU 从用户态切换到内核态,开始执行驱动里的 my_ipcc_write 函数。
	某个具体的进程执行(PID=100 的 demo_app),内核知道这个进程的所有信息(task_struct)。
	休眠目的:
		调用了 copy_from_user。
		用户传进来的内存地址可能是虚拟地址。
		如果这块内存当时不在物理 RAM 里(被交换到了硬盘 swap 区),就会触发 "缺页异常 (Page Fault)"。
		由于CPU 需要去硬盘读数据,这非常慢。
		为了不浪费 CPU,内核会把当前进程"置为睡眠状态 (Sleep)",把 CPU 让给别的进程用,等数据读好了再唤醒它。
	由于休眠,所以用 Mutex(互斥体)。如果拿不到锁,当前进程就去休眠,不占用 CPU 资源。
	
B.接收数据时:处于"中断上下文" (Interrupt Context)
	M4 核心向 A7 发了一个信号,硬件触发了 IPCC 中断。
	A7 的 CPU 立即停止手头的一切工作(无论当时在运行哪个进程),强行跳转到 my_ipcc_rx_handler 执行。
	CPU 响应硬件的紧急呼叫。
	此时内核代表的是"硬件",没有对应的 task_struct(就是借用了被打断进程的壳,但无权限使用)。
	为什么严禁休眠:
		Linux 调度器只能调度"进程"。中断处理程序不属于任何进程。
		如果你在中断里休眠(调用了会导致休眠的函数,或者使用了 Mutex),调度器就把这个"不知名"的执行流挂起。
		调度器再也找不到它了,因为它没有进程 ID。
		导致系统崩溃(Kernel Panic),或者整个 CPU 核心死锁。
	因为不能休眠,所以用 Spinlock(自旋锁)。
	如果拿不到锁,CPU 就在那里"原地空转(Spin)"死等。因为中断必须极快结束,稍微等几微秒是可以接受的,但绝不能休眠。
	

1.发送数据 (进程上下文)互斥体

当APP 调用 write() 发送数据时,你处于进程上下文,可能会休眠,也可能会被其他进程抢占。

c 复制代码
struct ipcc_client_dev {
    struct mutex tx_lock;      // 互斥体:用于进程间互斥
    spinlock_t rx_lock;        // 自旋锁:用于中断保护
    void __iomem *shm_base;    // 共享内存基地址
};

// 发送函数:APP调用 write 时触发
ssize_t my_ipcc_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
    struct ipcc_client_dev *dev = filp->private_data;
    
    // 【步骤1】获取互斥锁 (解决:进程竞态)
    // 为什么要用 mutex?因为 copy_from_user 可能会休眠(导致缺页异常),
    // 只有 mutex 允许休眠,spinlock 不允许。
    if (mutex_lock_interruptible(&dev->tx_lock)) {
        return -ERESTARTSYS;
    }

    // 【步骤2】拷贝数据到内核缓冲区
    // 此时其他进程想发数据会被堵塞在 mutex_lock
    copy_from_user(local_buffer, buf, count);

    // 【步骤3】写入共享内存 & 触发 IPCC
    // 这里如果涉及到底层寄存器操作,且必须极快,可能需要关中断
    // 但通常 OpenAMP 内部处理了,这里假设我们在操作裸机共享内存
    writel(data, dev->shm_base + OFFSET); 
    
    // 触发 IPCC 硬件门铃
    stm32_ipcc_set_channel(ipcc_handle, CHANNEL_ID);

    // 【步骤4】释放互斥锁
    mutex_unlock(&dev->tx_lock);
    
    return count;
}

2.接收数据 (中断上下文)自旋锁

当 M4 回复消息时,A7 会触发中断。此时严禁休眠。

c 复制代码
// 中断服务程序 (ISR):当 IPCC 硬件中断触发时执行
static irqreturn_t my_ipcc_rx_handler(int irq, void *data)
{
    struct ipcc_client_dev *dev = (struct ipcc_client_dev *)data;
    unsigned long flags;

    // 【步骤1】获取自旋锁并关中断 (解决:中断竞态)
    // 为什么用 spin_lock_irqsave?
    // 1. 我们在中断里,绝不能休眠,所以不能用 mutex。
    // 2. irqsave 会保存当前中断状态并禁止本地中断,防止嵌套中断打断我们。
    spin_lock_irqsave(&dev->rx_lock, flags);

    // --- 临界区开始 (Critical Section) ---
    // 必须快进快出!不要在这里做耗时操作(如打印大量日志、延时)
    
    // 从共享内存读取 M4 发来的状态/数据
    uint32_t status = readl(dev->shm_base + STATUS_OFFSET);
    
    // 如果数据量大,通常只把数据拷贝到环形缓冲区,然后唤醒等待队列
    // 让下半部(Bottom Half)去慢慢处理
    
    // --- 临界区结束 ---

    // 【步骤2】释放自旋锁并恢复中断
    spin_unlock_irqrestore(&dev->rx_lock, flags);

    return IRQ_HANDLED;
}
轻量级并发:原子操作

原子操作的作用: 它是CPU指令级别的保证,保证代码执行一气呵成,中间绝对插不进任何东西。
A. 整型原子操作 (操作 atomic_t 结构体)

不能直接对 atomic_t 里的 int counter 赋值,必须用专用函数。

操作 函数/宏 解释
定义 atomic_t v = ATOMIC_INIT(1); 定义 v 并初始化为 1
atomic_set(&v, 5); 把 v 设为 5
val = atomic_read(&v); 读取 v 的值
加减 atomic_add(2, &v); v = v + 2
自增减 atomic_inc(&v); v = v + 1
测试 atomic_dec_and_test(&v); 先减1,如果结果为0返回真。常用于引用计数。

B. 位原子操作 (操作 unsigned long)

用于控制硬件寄存器的标志位

操作 函数 解释
置位 set_bit(nr, &addr); 把第 nr 位 置1
清零 clear_bit(nr, &addr); 把第 nr 位 置0
翻转 change_bit(nr, &addr); 0变1,1变0
测试 test_bit(nr, &addr); 查看第 nr 位是 0 还是 1
测试并设置 test_and_set_bit(...); 原子级操作:读出旧值,写入1。用于实现"抢锁"。

实现一次只能打开一次的驱动

c 复制代码
static atomic_t test_atomic = ATOMIC_INIT(1); // 初始为1

static int led_chrdev_open(...) {
    // atomic_dec_and_test 执行步骤:
    // 1. 值减1 (变为0)
    // 2. 检查结果是否为0 (是)
    // 3. 返回 true
    // 整个过程绝对不会被打断
    /*
    atomic_read(&test_atomic) 直接这样就是错误的,因为这样放在if里面就不是原子的了
    if(atomic_read(&test_atomic)) {  // 第一步:读,如果是1 (空闲)
        atomic_set(&test_atomic, 0); // 第二步:设为0 (占用)
    }
    */
    if (atomic_dec_and_test(&test_atomic)) {
        printk("Open success!\n");
        return 0; 
    } else {
        // 如果原本是0,减1变成-1,test失败
        // 如果原本是-1,减1变成-2,test失败
        // 为了恢复逻辑,我们需要把它加回去 (虽然此时可能已经乱了,但在只有两个竞争者时通常配合 atomic_inc 使用)
        atomic_inc(&test_atomic); 
        return -EBUSY;
    }
}

static int led_chrdev_release(...) {
    atomic_set(&test_atomic, 1); // 恢复为1
    return 0;
}
IPCC 中使用原子操作

多核通信项目中,原子操作非常适合做统计和轻量级标志位。

原子操作非常适合做统计和轻量级标志位。
统计接收包的数量

因为在中断里收消息,这是极快的过程,用锁太重了。

c 复制代码
// 定义全局统计变量
static atomic_t rx_count = ATOMIC_INIT(0);

// 中断服务函数 (ISR)
static irqreturn_t my_ipcc_rx_handler(...) {
    // ... 读取数据 ...
    
    // 原子自增,绝对安全,不会因为并发中断导致计数丢失
    atomic_inc(&rx_count); 
    
    return IRQ_HANDLED;
}

// 在 APP 读取统计时
ssize_t read_count(...) {
    int count = atomic_read(&rx_count);
    // ... copy_to_user ...
}

判断 M4 是否存活

可以用一个原子变量作为心跳计数。

c 复制代码
// A7 每秒检查一次
if (atomic_read(&m4_heartbeat) == last_value) {
    printk("M4 死机了!\n");
}
总结说明
比较维度 原子操作 (Atomic Operations) 自旋锁 (Spinlock) 互斥体 (Mutex)
定义与核心概念 最小的不可分割指令序列。 由硬件保证操作在执行过程中绝不会被中断,中间状态对外不可见。 一种基于忙等待 (Busy-Wait) 的多线程同步锁机制。 当获取锁失败时,执行单元不会挂起,而是在循环中不断检测锁状态。 一种基于调度机制 (Sleep-Wait) 的互斥锁机制。 当获取锁失败时,执行单元会主动放弃 CPU,进入睡眠状态等待唤醒。
底层实现原理 依赖 CPU 指令集架构 (ISA)。 如 ARM 的 LDREX / STREX (独占加载/存储) 和总线锁。 依赖原子操作 + 循环判断。 通常使用 "Test-and-Set" 原子指令修改锁标志位。 依赖原子操作 + 操作系统调度器。 维护一个等待队列 (Wait Queue),涉及进程状态切换。
保护粒度 极小 仅限单个整型变量或位 (Bit)。 保护极短的代码片段或临界区。 保护复杂的代码块、大数据结构或长逻辑。
竞争时的行为 无等待 硬件指令立即执行成功或重试,无"锁"的概念。 忙等待 (Spinning) CPU 保持运行状态(空转),占用 100% 算力,直到拿到锁。 挂起 (Blocking) 当前进程从运行队列移除,发生上下文切换,CPU 转去执行其他任务。
上下文限制 无限制 可在中断上下文 (ISR) 和进程上下文中使用。 无限制 (但在中断中是唯一选择) 可在中断上下文和进程上下文中使用。 仅限进程上下文 严禁在中断上下文中使用 (因为中断不能休眠)。
临界区要求 不可休眠 指令级操作,瞬间完成。 严禁休眠 持有锁期间绝对不能调用导致调度的函数 (如 kmalloc, copy_from_user)。 允许休眠 持有锁期间可以执行 I/O 操作或内存分配。
适用场景 引用计数、状态标志位、统计变量。 中断服务程序 (ISR)、多核间共享数据的极短操作。 文件读写、用户空间内存拷贝、设备配置等长耗时操作。
相关推荐
SEP50101 小时前
STM32 Bit-Bang I2C
stm32·i2c·bit-bang
liweiweili1262 小时前
Linux 中查询指定目录下查找包含特定文本的文件
linux·运维·chrome
编码追梦人2 小时前
基于 ESP32 的多人无线答题竞赛系统设计与实现
单片机·嵌入式硬件
YFLICKERH2 小时前
【Linux系统】安装包 deb | rpm
linux·运维·安装包
yyycqupt2 小时前
蓝牙协议栈的学习(二)
stm32·单片机·嵌入式硬件·mcu·物联网·51单片机·iot
CodeByV2 小时前
【Linux】线程从内核到实战:本质、控制逻辑与封装指南
linux
Xの哲學2 小时前
Linux设备管理:从内核驱动到用户空间的完整架构解析
linux·服务器·算法·架构·边缘计算
ACP广源盛139246256732 小时前
GSV2125D@ACP#GSV6125#HDMI 2.0 转 DisplayPort 1.4 转换器(带嵌入式 MCU)
嵌入式硬件·计算机外设·音视频
繁华似锦respect3 小时前
C++ unordered_map 底层实现与详细使用指南
linux·开发语言·c++·网络协议·设计模式·哈希算法·散列表