04-并发同步与原子操作.md 64 KB


title: 并发同步与原子操作 tags: [Linux驱动, 并发, 锁, 原子操作, 嵌入式, 自旋锁, 信号量, 互斥体, 读写锁, 顺序锁, RCU] created: 2026-09-16 updated: 2026-09-17 pdf_ref:

  • "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第四十七章 Linux并发与竞争"
  • "【正点原子】I.MX6U嵌入式Linux驱动开发指南V2.0.1 - 第四十八章 Linux并发与竞争实验"

    并发同步与原子操作

💡 关联知识: [[FreeRTOS学习笔记/07-中断管理与临界段]] | [[03-Linux驱动开发核心/05-中断下半部处理]] | [[03-Linux驱动开发核心/07-platform总线模型]]


一、并发问题概述

1.1 什么是竞态条件

竞态条件(Race Condition) 是指多个执行单元(线程、中断、进程)同时访问共享资源,且最终结果依赖于执行时序的现象。

💡 生活类比:公司只有一台打印机,小明和小红同时发送打印任务。如果小明的文档先发送但打印较慢,小红的文档后发送但打印较快,最终小明可能拿到小红的文档——这就是竞态条件。

sequenceDiagram
    participant P1 as 进程A
    participant P2 as 进程B
    participant R as 共享资源

    Note over P1,R: 理想执行顺序
    P1->>R: 读取资源
    P1->>R: 修改资源
    P2->>R: 读取资源
    P2->>R: 修改资源

    Note over P1,R: 实际可能的执行顺序
    P1->>R: 读取资源
    P2->>R: 读取资源
    P1->>R: 修改资源
    P2->>R: 修改资源(覆盖P1的结果)

1.2 竞态条件产生场景

场景 说明 危害程度
中断与进程 进程访问共享数据时被中断,中断处理函数也访问同一数据 ⭐⭐⭐⭐
多进程/多线程 多个进程或线程同时读写共享内存 ⭐⭐⭐
多CPU(SMP) 多核处理器上多个CPU核心同时执行访问同一数据 ⭐⭐⭐⭐⭐
抢占调度 进程在内核态执行时被高优先级任务抢占 ⭐⭐⭐

1.3 竞态条件的危害

  1. 数据不一致:共享变量值被意外覆盖
  2. 逻辑错误:程序执行流程偏离预期
  3. 系统崩溃:内核数据结构被破坏导致kernel panic
  4. 难以复现:时序相关的问题极难调试

1.4 并发问题示意图

flowchart TD
    A[并发访问共享资源] --> B{资源类型}
    B --> C[简单变量]
    B --> D[复杂数据结构]
    B --> E[硬件寄存器]

    C --> F[原子操作]
    D --> G[自旋锁/互斥体]
    E --> H[原子操作/自旋锁]

    F --> I[保证操作不可中断]
    G --> J[保证临界区互斥]
    H --> K[保证硬件访问原子性]

    style A fill:#fee2e2,stroke:#dc2626
    style F fill:#dcfce7,stroke:#16a34a
    style G fill:#dcfce7,stroke:#16a34a
    style H fill:#dcfce7,stroke:#16a34a

1.5 为什么需要关注并发

Linux是多任务操作系统,以下因素导致并发不可避免:

  • 多线程/多进程:用户空间程序并发执行
  • 抢占式调度:2.6版本后Linux内核支持抢占
  • 中断程序并发:硬件中断随时可能发生
  • SMP多核并行:多CPU核心同时执行内核代码

⚠️ 注意:编写驱动程序时必须考虑并发保护,否则会导致难以定位的bug。

1.6 Linux并发控制机制总览

Linux内核提供的同步机制从"轻"到"重"排列如下,选择哪一种是驱动开发中最基础的判断:

机制 典型类型 是否可睡眠 中断上下文 适用场景
原子操作 atomic_t 可用 单个整数计数、状态标志
自旋锁 spinlock_t 可用 极短临界区、中断上下文
读写锁 rwlock_t 可用 读多写少、临界区短
顺序锁 seqlock_t 可用(写者不可被中断) 读多写极少、读侧不能加锁
RCU rcu_read_lock 读侧可用 链表遍历为主、读多写极少
信号量 struct semaphore 不可用 长临界区、允许并发数 > 1
互斥体 struct mutex 不可用 长临界区、需要互斥且可睡眠
完成量 struct completion 不可用 一个执行单元等待另一个完成

💡 选择原则:能不用锁就不用锁(无锁设计优先,如 per-CPU 变量),必须加锁时按"临界区长度 + 是否可能睡眠 + 读多还是写多"三个维度决策。本文档按此顺序逐个讲解。


二、原子操作

2.1 什么是原子操作

原子操作(Atomic Operation) 是指在执行过程中不会被中断的操作,要么完全执行,要么完全不执行。适用于简单的变量操作。

/* 原子变量定义 - include/linux/types.h */
typedef struct {
    int counter;
} atomic_t;

2.2 原子变量操作API

函数 说明
ATOMIC_INIT(int i) 定义原子变量时初始化值
int atomic_read(atomic_t *v) 读取原子变量的值
void atomic_set(atomic_t *v, int i) 设置原子变量的值
void atomic_add(int i, atomic_t *v) 原子加法
void atomic_sub(int i, atomic_t *v) 原子减法
void atomic_inc(atomic_t *v) 原子自增
void atomic_dec(atomic_t *v) 原子自减
int atomic_inc_and_test(atomic_t *v) 自增并测试是否为0
int atomic_dec_and_test(atomic_t *v) 自减并测试是否为0
int atomic_sub_and_test(int i, atomic_t *v) 减法并测试是否为0

2.3 原子操作使用示例

#include <linux/atomic.h>

/* 定义并初始化原子变量 */
static atomic_t led_status = ATOMIC_INIT(0);

/* 在驱动初始化时设置 */
atomic_set(&led_status, 1);  /* 设置为1,表示LED可用 */

/* 检查并获取设备 */
static int led_open(struct inode *inode, struct file *filp)
{
    /* 原子减1并测试是否为0 */
    if (!atomic_dec_and_test(&led_status)) {
        atomic_inc(&led_status);  /* 恢复原值 */
        return -EBUSY;           /* 设备被占用 */
    }
    return 0;
}

/* 关闭设备时释放 */
static int led_release(struct inode *inode, struct file *filp)
{
    atomic_inc(&led_status);  /* 原子加1,释放设备 */
    return 0;
}

2.4 原子位操作API

函数 说明
void set_bit(int nr, void *p) 设置第nr位为1
void clear_bit(int nr, void *p) 清除第nr位
void change_bit(int nr, void *p) 反转第nr位
int test_bit(int nr, void *p) 读取第nr位的值
int test_and_set_bit(int nr, void *p) 设置第nr位并返回原值
int test_and_clear_bit(int nr, void *p) 清除第nr位并返回原值

三、自旋锁

3.1 什么是自旋锁

自旋锁(Spinlock) 是一种忙等待的锁机制。当一个线程尝试获取锁时,如果锁已被持有,该线程会在原地"自旋"等待,直到锁被释放。

stateDiagram-v2
    [*] --> 空闲: 初始状态
    空闲 --> 已锁定: spin_lock()
    已锁定 --> 空闲: spin_unlock()
    已锁定 --> 已锁定: 其他CPU尝试获取锁(自旋等待)

    note right of 已锁定
        持有锁的CPU执行临界区代码
        其他CPU在原地循环等待
    end note

3.2 自旋锁类型定义

/* include/linux/spinlock_types.h */
typedef struct spinlock {
    union {
        struct raw_spinlock rlock;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
        struct {
            u8 __padding[LOCK_PADSIZE];
            struct lockdep_map dep_map;
        };
#endif
    };
} spinlock_t;

3.3 自旋锁API

函数 说明 使用场景
DEFINE_SPINLOCK(spinlock_t lock) 静态定义并初始化 全局锁
void spin_lock_init(spinlock_t *lock) 动态初始化 运行时初始化
void spin_lock(spinlock_t *lock) 获取锁(忙等待) 进程上下文
void spin_unlock(spinlock_t *lock) 释放锁 进程上下文
int spin_trylock(spinlock_t *lock) 尝试获取锁,失败返回0 非阻塞场景
int spin_is_locked(spinlock_t *lock) 检查锁状态 调试

3.4 中断安全的自旋锁API

函数 说明
void spin_lock_irq(spinlock_t *lock) 禁止本地中断并获取锁
void spin_unlock_irq(spinlock_t *lock) 释放锁并开启本地中断
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags) 保存中断状态、禁止中断、获取锁
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) 恢复中断状态、释放锁
void spin_lock_bh(spinlock_t *lock) 禁止下半部并获取锁
void spin_unlock_bh(spinlock_t *lock) 释放锁并开启下半部

3.5 自旋锁使用场景

flowchart TD
    A[需要保护共享资源] --> B{访问上下文}
    B -->|进程上下文| C{是否可能被中断访问?}
    B -->|中断上下文| D[使用spin_lock]
    C -->|是| E[使用spin_lock_irqsave]
    C -->|否| F[使用spin_lock]

    E --> G[临界区执行]
    D --> G
    F --> G

    G --> H{需要保护下半部?}
    H -->|是| I[使用spin_lock_bh]
    H -->|否| J[使用spin_unlock]

    style A fill:#fef3c7,stroke:#f59e0b
    style G fill:#dbeafe,stroke:#2563eb

3.6 自旋锁使用注意事项

综合前面关于自旋锁的信息,我们需要在使用自旋锁的时候要注意以下几点(原书 47.3.4):

  1. 锁的持有时间不能太长,一定要短:因为在等待自旋锁的时候处于"自旋"状态,会降低系统性能。如果临界区比较大、运行时间比较长的话,要选择其他的并发处理方式,比如信号量和互斥体。
  2. 自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数:否则可能导致死锁。
  3. 不能递归申请自旋锁:因为一旦通过递归的方式申请一个你正在持有的锁,那么你就必须"自旋",等待锁被释放,然而你正处于"自旋"状态,根本没法释放锁。结果就是自己把自己锁死了!
  4. 在编写驱动程序时必须考虑驱动的可移植性:因此不管你用的是单核的还是多核的 SOC,都将其当做多核 SOC 来编写驱动程序。

💡 补充:若临界区可能被中断访问,需使用 spin_lock_irqsave / spin_unlock_irqrestore 保证中断安全(原书 47.3.2 自旋锁 API 中的 _irqsave 变体)。


四、信号量

4.1 什么是信号量

信号量(Semaphore) 是一种可睡眠的同步机制。当进程获取信号量失败时会进入睡眠状态,释放CPU给其他任务使用。

sequenceDiagram
    participant P1 as 进程A
    participant S as 信号量
    participant P2 as 进程B

    P1->>S: down() - 获取信号量
    S-->>P1: 成功,继续执行
    P2->>S: down() - 尝试获取
    S-->>P2: 失败,进入睡眠
    P1->>S: up() - 释放信号量
    S->>P2: 唤醒进程B
    P2->>S: 获取成功

4.2 信号量API

#include <linux/semaphore.h>

struct semaphore {
    raw_spinlock_t lock;
    unsigned int count;
    struct list_head wait_list;
};

/* 初始化信号量 */
sem_init(&sem, 1);      /* 互斥信号量(计数为1) */
sem_init(&sem, 10);     /* 计数信号量(允许10个并发) */

/* 获取信号量 */
down(&sem);              /* 不可中断,可能睡眠 */
down_trylock(&sem);      /* 非阻塞,失败返回非0 */
down_interruptible(&sem); /* 可被信号中断 */

/* 释放信号量 */
up(&sem);

4.3 信号量使用示例

static struct semaphore led_sem;

static int __init led_init(void)
{
    sema_init(&led_sem, 1);  /* 初始化为互斥信号量 */
    return 0;
}

static ssize_t led_write(struct file *filp, const char __user *buf,
                         size_t cnt, loff_t *offt)
{
    down(&led_sem);          /* 获取信号量,可能睡眠 */
    /* 临界区(可以睡眠的操作) */
    gpiod_set_value(dev->led_gpio, ledstat);
    up(&led_sem);            /* 释放信号量 */
    return 0;
}

4.4 自旋锁 vs 信号量对比表

特性 自旋锁 信号量
等待方式 忙等待(自旋) 睡眠等待
CPU占用 占用CPU 不占用CPU
可睡眠 ❌ 不可以 ✅ 可以
中断上下文 ✅ 可以使用 ❌ 不可以
临界区长度 短(μs级) 长(ms级)
性能开销 低(忙等) 高(上下文切换)
适用场景 中断处理、短临界区 长时间操作、I/O操作
quadrantChart
    title 自旋锁与信号量选择
    x-axis 短临界区 --> 长临界区
    y-axis 不可睡眠 --> 可睡眠
    "中断处理": [0.2, 0.1]
    "短临界区": [0.3, 0.2]
    "驱动初始化": [0.7, 0.8]
    "文件操作": [0.6, 0.9]
    "网络传输": [0.8, 0.7]
    "磁盘I/O": [0.9, 0.9]

五、互斥体

5.1 什么是互斥体

互斥体(Mutex) 是Linux内核中最常用的互斥锁机制。它是一种特殊的信号量,只能由一个线程持有,提供了更严格的互斥保证。

5.2 互斥体API

#include <linux/mutex.h>

/* 定义互斥体 */
struct mutex my_mutex;

/* 初始化 */
mutex_init(&my_mutex);

/* 获取锁 */
mutex_lock(&my_mutex);           /* 可能睡眠 */
mutex_trylock(&my_mutex);        /* 非阻塞,失败返回0 */

/* 释放锁 */
mutex_unlock(&my_mutex);

/* 销毁 */
mutex_destroy(&my_mutex);

5.3 互斥体使用示例

static struct mutex led_mutex;

static int __init led_init(void)
{
    mutex_init(&led_mutex);
    return 0;
}

static ssize_t led_write(struct file *filp, const char __user *buf,
                         size_t cnt, loff_t *offt)
{
    mutex_lock(&led_mutex);      /* 获取互斥锁 */
    /* 临界区 */
    gpiod_set_value(dev->led_gpio, ledstat);
    mutex_unlock(&led_mutex);    /* 释放互斥锁 */
    return 0;
}

5.4 互斥体 vs 自旋锁对比

特性 互斥体 自旋锁
睡眠 ✅ 可以睡眠 ❌ 不能睡眠
持有时长 可以较长 必须很短
中断上下文 ❌ 不能使用 ✅ 可以使用
优先级继承 ✅ 支持 ❌ 不支持
递归锁 ❌ 不支持 ❌ 不支持
调试支持 ✅ 丰富 ⚠️ 有限

六、读写自旋锁(rwlock)

本节内容来自《I.MX6U嵌入式Linux驱动开发指南》47.3.3 其他类型的锁。原书指出:这些锁在驱动中其实用的不多,更多的是在 Linux 内核中使用

6.1 什么是读写自旋锁

原书背景:现在有个学生信息表,此表存放着学生的年龄、家庭住址、班级等信息,此表可以随时被修改和读取。此表肯定是数据,那么必须要对其进行保护。如果我们现在使用自旋锁对其进行保护,每次只能一个读操作或者写操作。但是,实际上此表是可以并发读取的,只需要保证在修改此表的时候没人读取,或者在其他人读取此表的时候没有人修改此表就行了。也就是此表的读和写不能同时进行,但是可以多人并发的读取此表。像这样,当某个数据结构符合读/写生产者/消费者模型的时候就可以使用读写自旋锁。

核心规则

  • 一次只能允许一个写操作:也就是只能一个线程持有写锁,而且不能进行读操作
  • 没有写操作时允许一个或多个线程持有读锁:可以进行并发的读操作

    flowchart TD
    A[读写锁状态] --> B{请求类型}
    B -->|读锁| C{当前有写者?}
    B -->|写锁| D{当前有任何持有者?}
    C -->|无写者| E[✅ 获取成功<br/>可多个读者共存]
    C -->|有写者| F[⏳ 自旋等待]
    D -->|无| G[✅ 获取成功<br/>排他]
    D -->|有读者或写者| H[⏳ 自旋等待]
    
    style E fill:#dcfce7,stroke:#16a34a
    style G fill:#dcfce7,stroke:#16a34a
    style F fill:#fef3c7,stroke:#f59e0b
    style H fill:#fef3c7,stroke:#f59e0b
    

💡 一句话总结:读写锁 = "允许 N 个读者 + 1 个写者,读者和写者互斥"。

6.2 读写锁类型定义

Linux 内核使用 rwlock_t 结构体表示读写锁,结构体定义如下(原书示例删除了条件编译):

/* 示例代码 47.3.3.1 rwlock_t 结构体 */
typedef struct {
    arch_rwlock_t raw_lock;
} rwlock_t;

完整的内核定义(include/linux/rwlock_types.h)在开启调试选项时会附加调试字段:

typedef struct {
    arch_rwlock_t raw_lock;
#ifdef CONFIG_DEBUG_SPINLOCK
    unsigned int magic, owner_cpu;
    void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map dep_map;
#endif
} rwlock_t;

rwlock_t 的本质与 spinlock_t 相同,都是基于架构相关的原子指令实现(ARM 上使用 LDREX/STREX),区别在于内部额外维护了读者计数和写者标志。

6.3 读写锁 API

#include <linux/rwlock.h>

/* 初始化方式一:动态初始化 */
rwlock_t my_rwlock;
rwlock_init(&my_rwlock);

/* 初始化方式二:静态初始化 */
DEFINE_RWLOCK(my_rwlock);

读锁 API(共享)——原书表 47.3.3.1:

函数 描述
void read_lock(rwlock_t *lock) 获取读锁
void read_unlock(rwlock_t *lock) 释放读锁
void read_lock_irq(rwlock_t *lock) 禁止本地中断,并且获取读锁
void read_unlock_irq(rwlock_t *lock) 打开本地中断,并且释放读锁
void read_lock_irqsave(rwlock_t *lock, unsigned long flags) 保存中断状态,禁止本地中断,并获取读锁
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags) 将中断状态恢复到以前的状态,并且激活本地中断,释放读锁
void read_lock_bh(rwlock_t *lock) 关闭下半部,并获取读锁
void read_unlock_bh(rwlock_t *lock) 打开下半部,并释放读锁

写锁 API(排他)——原书表 47.3.3.1:

函数 描述
void write_lock(rwlock_t *lock) 获取写锁
void write_unlock(rwlock_t *lock) 释放写锁
void write_lock_irq(rwlock_t *lock) 禁止本地中断,并且获取写锁
void write_unlock_irq(rwlock_t *lock) 打开本地中断,并且释放写锁
void write_lock_irqsave(rwlock_t *lock, unsigned long flags) 保存中断状态,禁止本地中断,并获取写锁
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags) 将中断状态恢复到以前的状态,并且激活本地中断,释放写锁
void write_lock_bh(rwlock_t *lock) 关闭下半部,并获取写锁
void write_unlock_bh(rwlock_t *lock) 打开下半部,并释放写锁

💡 补充:内核另外还提供 read_trylock()write_trylock(),尝试获取锁,成功返回非 0、失败返回 0 且不等待——原书表格未列出。

⚠️ 命名规则与自旋锁一致irq 版本禁止本地中断,irqsave 版本保存并恢复中断状态,bh 版本关闭/打开下半部(软中断)。

6.4 读写锁使用示例

以"一个全局配置表"为例,配置项读多写少:

#include <linux/rwlock.h>
#include <linux/module.h>

/* 共享的配置数据 */
struct device_config {
    int sample_rate;
    int gain;
    int channel;
};

static struct device_config g_config;
static rwlock_t config_lock;    /* 读写锁 */

/* 读操作:多个进程可并发进入 */
static int get_sample_rate(void)
{
    unsigned long flags;
    int rate;

    read_lock_irqsave(&config_lock, flags);
    rate = g_config.sample_rate;        /* 只读,不修改 */
    read_unlock_irqrestore(&config_lock, flags);

    return rate;
}

/* 写操作:排他,与所有读写者互斥 */
static void set_sample_rate(int rate)
{
    unsigned long flags;

    write_lock_irqsave(&config_lock, flags);
    g_config.sample_rate = rate;        /* 修改共享数据 */
    write_unlock_irqrestore(&config_lock, flags);
}

static int __init rwlock_demo_init(void)
{
    rwlock_init(&config_lock);
    g_config.sample_rate = 8000;
    g_config.gain = 10;
    g_config.channel = 1;

    pr_info("read rate = %d\n", get_sample_rate());
    set_sample_rate(16000);
    pr_info("read rate = %d\n", get_sample_rate());
    return 0;
}

static void __exit rwlock_demo_exit(void)
{
    pr_info("rwlock demo exit\n");
}

module_init(rwlock_demo_init);
module_exit(rwlock_demo_exit);
MODULE_LICENSE("GPL");

6.5 读写锁 vs 自旋锁对比

特性 读写锁(rwlock_t) 自旋锁(spinlock_t)
并发性 读者可并发 完全互斥
实现开销 稍大(维护读者计数) 更小
写者开销 稍大
中断上下文 ✅ 可用 ✅ 可用
可睡眠 ❌ 不可以 ❌ 不可以
适用场景 读多写少(读:写 > 10:1) 读写均衡或写为主
饥饿风险 ⚠️ 读者持续涌入时写者可能饥饿
优先级继承 ❌ 不支持 ❌ 不支持

6.6 读写锁注意事项

⚠️ 避坑指南

  1. 不要试图升级锁:持有读锁时调用 write_lock()死锁(自己等自己释放读锁)。
  2. 不要试图降级锁:持有写锁时调用 read_lock() 同样死锁。需要降级时必须先 write_unlock()read_lock(),但中间存在"空窗期",另一写者可能插入。
  3. 写者饥饿:Linux 的 rwlock 实现中,新来的读者可以"插队"到等待的写者之前,读压力极大时写者可能长时间得不到锁。若写者不能忍受饥饿,应改用顺序锁RCU
  4. 临界区必须短:读写锁本质仍是自旋锁,持锁期间不能睡眠,也不能调用可能睡眠的函数(如 kmalloc(GFP_KERNEL)copy_from_user())。
  5. 中断场景用 irqsave:读写锁保护的临界区若可能被中断访问,必须使用 read_lock_irqsave / write_lock_irqsave
  6. 优先考虑 RCU:如果共享数据是"读多写极少"的链表或指针结构,RCU 的性能远优于读写锁。

七、顺序锁(seqlock)

7.1 什么是顺序锁

原书表述:顺序锁在读写锁的基础上衍生而来的,使用读写锁的时候读操作和写操作不能同时进行。使用顺序锁的话可以允许在写的时候进行读操作,也就是实现同时读写,但是不允许同时进行并发的写操作。

虽然顺序锁的读和写操作可以同时进行,但是如果在读的过程中发生了写操作,最好重新进行读取,保证数据完整性

⚠️ 原书重点警告顺序锁保护的资源不能是指针,因为如果在写操作的时候可能会导致指针无效,而这个时候恰巧有读操作访问指针的话就可能导致意外发生,比如读取野指针导致系统崩溃

实现机制

  1. 有一个序号(sequence number),初始为偶数
  2. 写者进入:序号 +1(变为奇数,表示"正在写")→ 修改数据 → 序号 +1(变回偶数)
  3. 读者

    • 进入前读序号 s1,若为奇数说明正在写,等待/重试
    • 读取数据
    • 读完再读序号 s2,比较 s1 == s2,相等说明读取期间没有写者,数据有效;否则重读

      sequenceDiagram
      participant R as 读者(无锁)
      participant W as 写者
      participant S as 序号
      
      R->>S: s1 = 读序号 (偶数=2)
      W->>S: 序号+1 (3, 奇数, 开始写)
      W->>W: 修改数据
      R->>R: 读取数据 (可能读到半成品)
      W->>S: 序号+1 (4, 偶数, 写完)
      R->>S: s2 = 读序号 (4)
      Note over R: s1(2) != s2(4)<br/>本次读取无效 → 重试
      R->>S: s1 = 读序号 (4)
      R->>R: 读取数据 (完整)
      R->>S: s2 = 读序号 (4)
      Note over R: s1 == s2 → 数据有效 ✅
      

💡 本质:顺序锁牺牲了读者的"确定性延迟"(可能重试),换来了读者零加锁开销。所以它适合"读极多、写极少、且读操作非常快"的场景。

7.2 顺序锁类型与 API

Linux 内核使用 seqlock_t 结构体表示顺序锁,结构体定义如下(原书示例代码 47.3.3.2):

/* 示例代码 47.3.3.2 seqlock_t 结构体 */
typedef struct {
    struct seqcount seqcount;
    spinlock_t lock;
} seqlock_t;

💡 结构体解读:顺序锁内部由两部分组成——seqcount 是序号计数器(读者用来判断是否被写者打断),spinlock_t lock 是写者之间的互斥锁(保证不会并发写)。这也解释了为什么"允许写时读,但不允许并发写"。

初始化方式:

#include <linux/seqlock.h>

/* 初始化方式一:动态 */
seqlock_t my_seqlock;
seqlock_init(&my_seqlock);

/* 初始化方式二:静态 */
DEFINE_SEQLOCK(my_seqlock);

顺序锁写操作 API(原书表 47.3.3.2):

函数 描述
void write_seqlock(seqlock_t *sl) 获取写顺序锁
void write_sequnlock(seqlock_t *sl) 释放写顺序锁
void write_seqlock_irq(seqlock_t *sl) 禁止本地中断,并且获取写顺序锁
void write_sequnlock_irq(seqlock_t *sl) 打开本地中断,并且释放写顺序锁
void write_seqlock_irqsave(seqlock_t *sl, unsigned long flags) 保存中断状态,禁止本地中断,并获取写顺序锁
void write_sequnlock_irqrestore(seqlock_t *sl, unsigned long flags) 将中断状态恢复到以前的状态,并且激活本地中断,释放写顺序锁
void write_seqlock_bh(seqlock_t *sl) 关闭下半部,并获取写顺序锁
void write_sequnlock_bh(seqlock_t *sl) 打开下半部,并释放写顺序锁

顺序锁读操作 API(原书表 47.3.3.2):

函数 描述
unsigned read_seqbegin(const seqlock_t *sl) 读单元访问共享资源的时候调用此函数,此函数会返回顺序锁的顺序号
unsigned read_seqretry(const seqlock_t *sl, unsigned start) 读结束以后调用此函数检查在读的过程中有没有对资源进行写操作,如果有的话就要重读

7.3 顺序锁使用示例

标准的"读侧重试"写法:

#include <linux/seqlock.h>
#include <linux/module.h>

struct sensor_data {
    int temperature;
    int pressure;
    int humidity;
};

static struct sensor_data g_sensor;
static seqlock_t sensor_lock;

/* 写侧:中断中更新传感器数据(写极少) */
static irqreturn_t sensor_irq_handler(int irq, void *dev_id)
{
    unsigned long flags;

    write_seqlock_irqsave(&sensor_lock, flags);
    g_sensor.temperature = read_temp_reg();
    g_sensor.pressure    = read_press_reg();
    g_sensor.humidity    = read_humid_reg();
    write_sequnlock_irqrestore(&sensor_lock, flags);

    return IRQ_HANDLED;
}

/* 读侧:用户进程高频读取(读极多),无锁 + 重试 */
static void get_sensor_data(struct sensor_data *out)
{
    unsigned int seq;

    do {
        seq = read_seqbegin(&sensor_lock);

        out->temperature = g_sensor.temperature;
        out->pressure    = g_sensor.pressure;
        out->humidity    = g_sensor.humidity;

    } while (read_seqretry(&sensor_lock, seq));   /* 不一致则重读 */
}

static ssize_t sensor_read(struct file *filp, char __user *buf,
                           size_t count, loff_t *ppos)
{
    struct sensor_data data;

    get_sensor_data(&data);      /* 读到的一定是完整快照 */

    if (copy_to_user(buf, &data, sizeof(data)))
        return -EFAULT;

    return sizeof(data);
}

7.4 顺序锁 vs 读写锁对比

特性 顺序锁(seqlock) 读写锁(rwlock)
读者加锁 ❌ 完全不加锁 ✅ 需要加读锁
读者开销 极低(2 次读序号) 中(原子操作)
读者可能重试 ✅ 会被写者打断后重试 ❌ 不会
写者饥饿 ❌ 不会(写者优先) ⚠️ 可能
可保护的数据 仅限不含指针的简单数据 任意数据(含链表/指针)
保护指针的危险 ⚠️ 读侧可能读到野指针 ✅ 安全
典型场景 jiffies、时间戳、统计计数 配置表、缓存
是否可睡眠 ❌ 写侧不可睡眠 ❌ 不可睡眠

⚠️ 关键限制:顺序锁的读侧不能保护指针。因为读者在无锁读取指针时,写者可能已经释放(free)了指针指向的内存,读者解引用就是野指针访问。所以顺序锁只能用于"纯值类型"的数据(整数、结构体内全是整数)。

7.5 顺序锁在内核中的典型应用

内核里最著名的顺序锁使用者是 jiffies

/* 内核中的实际用法:读 jiffies_64 */
u64 get_jiffies_64(void)
{
    unsigned int seq;
    u64 ret;

    do {
        seq = read_seqbegin(&jiffies_lock);
        ret = jiffies_64;
    } while (read_seqretry(&jiffies_lock, seq));

    return ret;
}

jiffies 被每个时钟中断更新(写少),而被大量内核代码读取(读极多),完美契合顺序锁的适用场景。


八、RCU(扩展知识)

⚠️ 来源说明:本节不属于《I.MX6U嵌入式Linux驱动开发指南》的内容——第四十七章 47.3.3 只讲到读写自旋锁顺序锁这两种"其他类型的锁",没有涉及 RCU 和完成量。此处作为扩展知识补充,便于阅读内核源码与面试准备。

8.1 什么是 RCU

RCU 是 Linux 内核中性能最高的读侧同步机制,全称 Read-Copy Update(读-复制-更新)。它把"读"的代价降到了接近零:

读者:只是标记进入/退出临界区(在非抢占内核里甚至是一条编译器屏障,无原子操作)。 写者:不直接修改数据,而是"复制一份 → 修改副本 → 替换指针 → 等待宽限期 → 释放旧数据"。

flowchart TD
    A[写者要修改数据] --> B[1. 复制一份新数据]
    B --> C[2. 修改副本]
    C --> D[3. 原子替换指针<br/>rcu_assign_pointer]
    D --> E[4. 等待宽限期<br/>synchronize_rcu]
    E --> F[5. 释放旧数据]

    G[读者] --> H[rcu_read_lock<br/>标记进入]
    H --> I[通过指针访问数据]
    I --> J[rcu_read_unlock<br/>标记退出]

    style D fill:#dbeafe,stroke:#2563eb
    style E fill:#fef3c7,stroke:#f59e0b

宽限期(Grace Period) 是 RCU 的核心概念:写者替换指针后,必须等到所有已经进入读侧临界区的读者都退出,才能安全释放旧数据。此时保证没有任何读者还持有旧数据的引用。

8.2 RCU 核心 API

#include <linux/rcupdate.h>

/* ===== 读侧(极低开销)===== */
rcu_read_lock();          /* 标记进入读侧临界区 */
/* 访问数据(注意:rcu_read_lock 不会阻塞写者) */
rcu_read_unlock();        /* 标记退出读侧临界区 */

/* ===== 写侧 ===== */
rcu_assign_pointer(ptr, new);      /* 释放语义地替换指针 */
struct foo *p = rcu_dereference(ptr);   /* 读侧带屏障地解引用 */

synchronize_rcu();                 /* 同步等待一个宽限期(会睡眠) */
void call_rcu(struct rcu_head *head, rcu_callback_t func);  /* 异步回调 */
kfree_rcu(ptr, rcu_head_field);    /* 延迟释放 */

RCU 链表 API

函数 说明
list_add_rcu(new, head) RCU 版链表插入
list_del_rcu(entry) RCU 版链表删除(不释放内存,仅断链)
list_replace_rcu(old, new) RCU 版节点替换
list_for_each_entry_rcu(pos, head, member) RCU 版遍历(需在rcu_read_lock 内)

8.3 RCU 使用示例

一个典型的"读多写少链表"场景:

#include <linux/rcupdate.h>
#include <linux/list.h>
#include <linux/slab.h>

struct my_node {
    struct list_head list;
    int value;
    struct rcu_head rcu;        /* RCU 延迟释放用 */
};

static LIST_HEAD(g_node_list);
static DEFINE_MUTEX(g_write_lock);  /* 写者之间还需互斥 */

/* 读侧:无锁遍历,多个读者可并发 */
static int lookup_max_value(void)
{
    struct my_node *node;
    int max = INT_MIN;

    rcu_read_lock();                                    /* 进入 */
    list_for_each_entry_rcu(node, &g_node_list, list) { /* 无锁遍历 */
        if (node->value > max)
            max = node->value;
    }
    rcu_read_unlock();                                  /* 退出 */

    return max;
}

/* 写侧:复制-修改-替换 */
static void add_node(int value)
{
    struct my_node *new_node;

    new_node = kmalloc(sizeof(*new_node), GFP_KERNEL);
    if (!new_node)
        return;
    new_node->value = value;

    mutex_lock(&g_write_lock);          /* 写者之间互斥 */
    list_add_rcu(&new_node->list, &g_node_list);
    mutex_unlock(&g_write_lock);
}

/* 写侧:删除节点 */
static void del_first_node(void)
{
    struct my_node *node;

    mutex_lock(&g_write_lock);
    node = list_first_entry_or_null(&g_node_list, struct my_node, list);
    if (node) {
        list_del_rcu(&node->list);      /* 断链,但不释放 */
        kfree_rcu(node, rcu);           /* 等宽限期后自动释放 */
    }
    mutex_unlock(&g_write_lock);
}

8.4 RCU vs 读写锁 vs 顺序锁

特性 RCU 顺序锁 读写锁
读者开销 极低(近乎零) 极低(读序号) 中(原子操作)
读者会重试 ❌ 不会 ✅ 会 ❌ 不会
读者是否阻塞写者 ❌ 完全不阻塞 ❌ 不阻塞 ✅ 阻塞
可保护指针/链表 ✅ 是主要用途 ❌ 不可 ✅ 可
写者开销 高(需等宽限期)
写者语义 必须"复制-替换" 原地修改 原地修改
内存开销 高(新旧副本共存一段时间) 无额外 无额外
典型场景 网络路由表、文件描述符表、模块链表 jiffies、时间戳 配置表、缓存

💡 一句话记忆

  • 读多写少 + 保护指针 → RCU
  • 读多写少 + 纯值 + 写者不能被饿死 → 顺序锁
  • 读写均衡 → 读写锁自旋锁
  • 临界区长且可睡眠 → 互斥体

8.5 RCU 注意事项

⚠️ 避坑指南

  1. rcu_read_lock() 不会睡眠也不会阻塞:它只是标记,所以读侧临界区不能睡眠(不能调用 copy_to_user()kmalloc(GFP_KERNEL) 等可能睡眠的函数)。
  2. synchronize_rcu() 会睡眠:不能在中断上下文、原子上下文或持有自旋锁时调用。
  3. 写者必须"复制-替换":绝不能在读侧可能引用它时原地修改数据,否则读者会读到不一致的中间状态。
  4. 写者之间仍需互斥:RCU 只解决"读写"冲突,多个写者同时操作同一链表仍需额外的锁(如互斥体)保护。
  5. 读侧指针必须用 rcu_dereference() 解引用:否则编译器和 CPU 可能重排指令,导致读到未初始化的数据。
  6. RCU 不保证实时性:宽限期可能持续较长时间,写者释放旧数据的时机不确定。

九、完成量(扩展知识)

⚠️ 来源说明:同第八节,本节也不属于正点原子教材内容,作为扩展知识补充。

9.1 什么是完成量

完成量(completion) 用于解决一种特定的同步需求:一个执行单元等待另一个执行单元完成某件事

它和信号量的区别在于:

  • 信号量可以用于"资源计数"(初值 > 1),也可以用于"互斥"(初值 = 1)
  • 完成量专门用于"等待某事件发生",是最轻量的"等待-唤醒"原语,语义更清晰

典型场景:

  • 驱动初始化时等待硬件就绪(中断到来后 complete()
  • 一个线程等待另一个线程完成 DMA 传输
  • 等待内核线程退出

💡 生活类比:信号量像"停车场车位"(可以有很多个,谁先来谁停),完成量像"等人到齐"(一个人等另一个人做完某事,只关心"完成了没有")。

9.2 完成量 API

#include <linux/completion.h>

/* 初始化方式一:动态 */
struct completion my_completion;
init_completion(&my_completion);

/* 初始化方式二:静态 */
DECLARE_COMPLETION(my_completion);
DECLARE_COMPLETION_ONSTACK(my_completion);   /* 栈上定义,用于局部等待 */
函数 说明
void init_completion(struct completion *x) 初始化完成量(计数=0)
void reinit_completion(struct completion *x) 重置完成量(允许重复使用)
void wait_for_completion(struct completion *x) 等待完成,不可被信号打断
int wait_for_completion_interruptible(struct completion *x) 可被信号打断,被打断返回-ERESTARTSYS
int wait_for_completion_killable(struct completion *x) 可被致命信号打断
unsigned long wait_for_completion_timeout(struct completion *x, unsigned long timeout) 带超时等待,超时返回 0
void complete(struct completion *x) 唤醒一个等待者(计数+1)
void complete_all(struct completion *x) 唤醒所有等待者

9.3 完成量使用示例

场景:等待一次中断到来(典型的"硬件就绪"同步):

#include <linux/completion.h>
#include <linux/interrupt.h>
#include <linux/module.h>

static struct completion irq_done;
static int irq_number;

static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    pr_info("IRQ %d triggered\n", irq);
    complete(&irq_done);        /* 通知等待者:事件已发生 */
    return IRQ_HANDLED;
}

static int __init completion_demo_init(void)
{
    int ret;

    init_completion(&irq_done);

    irq_number = gpio_to_irq(/* ... */);
    ret = request_irq(irq_number, my_irq_handler,
                      IRQF_TRIGGER_FALLING, "my_irq", NULL);
    if (ret)
        return ret;

    pr_info("waiting for interrupt...\n");

    /* 等待中断到来,最长等 1 秒(1000 * 10ms) */
    if (!wait_for_completion_timeout(&irq_done, msecs_to_jiffies(1000))) {
        pr_err("timeout: IRQ not received\n");
        free_irq(irq_number, NULL);
        return -ETIMEDOUT;
    }

    pr_info("interrupt received, continue init\n");
    free_irq(irq_number, NULL);
    return 0;
}

9.4 完成量 vs 信号量对比

特性 完成量(completion) 信号量(semaphore)
语义 "等待事件完成" "资源计数 / 互斥"
初始值 固定为 0 可任意(sema_init(&s, n)
主要用途 一对一/一对多事件通知 资源池、互斥
能否重复使用 reinit_completion() 天然支持
是否可睡眠 ✅ 可以 ✅ 可以
中断上下文使用 ✅ 只能complete() ✅ 只能up()
代码可读性 语义更明确 需要看初值才知道意图
典型场景 等待硬件就绪、等待线程退出 限制并发数、简单互斥

💡 选择建议:如果只是想"等某件事做完",用完成量比信号量语义更清晰、更不容易出错;如果需要"限制最多 N 个并发"或"互斥",用信号量或互斥体。


十、完整源码分析

10.1 原子操作实验 - atomic.c

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define GPIOLED_CNT     1           /* 设备号个数 */
#define GPIOLED_NAME    "gpioled"   /* 名字 */
#define LEDOFF          0           /* 关灯 */
#define LEDON           1           /* 开灯 */

/* gpioled设备结构体 */
struct gpioled_dev {
    dev_t devid;                /* 设备号 */
    struct cdev cdev;           /* cdev */
    struct class *class;        /* 类 */
    struct device *device;      /* 设备 */
    int major;                  /* 主设备号 */
    int minor;                  /* 次设备号 */
    struct device_node *nd;     /* 设备节点 */
    int led_gpio;               /* led所使用的GPIO编号 */
    atomic_t lock;              /* 原子变量 */
};

struct gpioled_dev gpioled;     /* led设备 */

/*
 * @description  : 打开设备
 * @param - inode : 传递给驱动的inode
 * @param - filp  : 设备文件,file结构体有个叫做private_data的成员变量
 *                    一般在open的时候将private_data指向设备结构体
 * @return       : 0 成功; 其他 失败
 */
static int led_open(struct inode *inode, struct file *filp)
{
    /* 通过判断原子变量的值来检查LED有没有被别的应用使用 */
    if (!atomic_dec_and_test(&gpioled.lock)) {
        atomic_inc(&gpioled.lock);  /* 小于0的话就加1,使其原子变量等于0 */
        return -EBUSY;              /* LED被使用,返回忙 */
    }

    filp->private_data = &gpioled;  /* 设置私有数据 */
    return 0;
}

/*
 * @description  : 向设备写数据
 * @param - filp  : 设备文件,表示打开的文件描述符
 * @param - buf   : 要写给设备写入的数据
 * @param - cnt   : 要写入的数据长度
 * @param - offt  : 相对于文件首地址的偏移
 * @return        : 写入的字节数,如果为负值,表示写入失败
 */
static ssize_t led_write(struct file *filp, const char __user *buf,
                         size_t cnt, loff_t *offt)
{
    int retvalue;
    unsigned char databuf[1];
    unsigned char ledstat;
    struct gpioled_dev *dev = filp->private_data;

    retvalue = copy_from_user(databuf, buf, cnt);
    if (retvalue < 0) {
        printk("kernel write failed!\r\n");
        return -EFAULT;
    }

    ledstat = databuf[0];       /* 获取状态值 */

    if (ledstat == LEDON) {
        gpio_set_value(dev->led_gpio, 0);   /* 打开LED灯 */
    } else if (ledstat == LEDOFF) {
        gpio_set_value(dev->led_gpio, 1);   /* 关闭LED灯 */
    }
    return 0;
}

/*
 * @description  : 关闭/释放设备
 * @param - filp  : 要关闭的设备文件(文件描述符)
 * @return        : 0 成功; 其他 失败
 */
static int led_release(struct inode *inode, struct file *filp)
{
    struct gpioled_dev *dev = filp->private_data;

    /* 关闭驱动文件的时候释放原子变量 */
    atomic_inc(&dev->lock);
    return 0;
}

/* 设备操作函数 */
static struct file_operations gpioled_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release = led_release,
};

/*
 * @description : 驱动入口函数
 * @return       : 无
 */
static int __init led_init(void)
{
    int ret = 0;

    /* 初始化原子变量 */
    atomic_set(&gpioled.lock, 1);   /* 原子变量初始值为1 */

    /* 设置LED所使用的GPIO */
    /* 1、获取设备节点:gpioled */
    gpioled.nd = of_find_node_by_path("/gpioled");
    if (gpioled.nd == NULL) {
        printk("gpioled node not find!\r\n");
        return -EINVAL;
    } else {
        printk("gpioled node find!\r\n");
    }

    /* 2、获取设备树中的gpio属性,得到LED所使用的LED编号 */
    gpioled.led_gpio = of_get_named_gpio(gpioled.nd, "led-gpio", 0);
    if (gpioled.led_gpio < 0) {
        printk("can't get led-gpio");
        return -EINVAL;
    }
    printk("led-gpio num = %d\r\n", gpioled.led_gpio);

    /* 3、设置GPIO1_IO03为输出,并且输出高电平,默认关闭LED灯 */
    ret = gpio_direction_output(gpioled.led_gpio, 1);
    if (ret < 0) {
        printk("can't set gpio!\r\n");
    }

    /* 注册字符设备驱动 */
    /* 1、创建设备号 */
    if (gpioled.major) {
        gpioled.devid = MKDEV(gpioled.major, 0);
        register_chrdev_region(gpioled.devid, GPIOLED_CNT, GPIOLED_NAME);
    } else {
        alloc_chrdev_region(&gpioled.devid, 0, GPIOLED_CNT, GPIOLED_NAME);
        gpioled.major = MAJOR(gpioled.devid);
        gpioled.minor = MINOR(gpioled.devid);
    }
    printk("gpioled major=%d,minor=%d\r\n", gpioled.major, gpioled.minor);

    /* 2、初始化cdev */
    gpioled.cdev.owner = THIS_MODULE;
    cdev_init(&gpioled.cdev, &gpioled_fops);

    /* 3、添加一个cdev */
    cdev_add(&gpioled.cdev, gpioled.devid, GPIOLED_CNT);

    /* 4、创建类 */
    gpioled.class = class_create(THIS_MODULE, GPIOLED_NAME);
    if (IS_ERR(gpioled.class)) {
        return PTR_ERR(gpioled.class);
    }

    /* 5、创建设备 */
    gpioled.device = device_create(gpioled.class, NULL,
                                   gpioled.devid, NULL, GPIOLED_NAME);
    if (IS_ERR(gpioled.device)) {
        return PTR_ERR(gpioled.device);
    }

    return 0;
}

/*
 * @description : 驱动出口函数
 * @return       : 无
 */
static void __exit led_exit(void)
{
    /* 注销字符设备驱动 */
    cdev_del(&gpioled.cdev);
    unregister_chrdev_region(gpioled.devid, GPIOLED_CNT);

    device_destroy(gpioled.class, gpioled.devid);
    class_destroy(gpioled.class);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("zuozhongkai");

10.2 代码逐行解析

行号 代码 说明
atomic_t lock 设备结构体中的原子变量 用于实现设备互斥访问
atomic_set(&gpioled.lock, 1) 初始化原子变量为1 表示设备可用
atomic_dec_and_test(&gpioled.lock) 原子减1并测试是否为0 如果为0表示获取成功
atomic_inc(&gpioled.lock) 原子加1 释放设备

10.3 自旋锁实验 - spinlock.c

/* 使用自旋锁保护的设备结构体 */
struct gpioled_dev {
    dev_t devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    int major;
    int minor;
    struct device_node *nd;
    int led_gpio;
    spinlock_t lock;            /* 自旋锁 */
    int dev_stats;              /* 设备使用统计 */
};

static int led_open(struct inode *inode, struct file *filp)
{
    unsigned long flags;
    struct gpioled_dev *dev = filp->private_data;

    spin_lock_irqsave(&dev->lock, flags);   /* 获取自旋锁 */

    if (dev->dev_stats != 0) {
        spin_unlock_irqrestore(&dev->lock, flags);
        return -EBUSY;
    }
    dev->dev_stats++;                       /* 标记设备被使用 */

    spin_unlock_irqrestore(&dev->lock, flags);
    return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
    unsigned long flags;
    struct gpioled_dev *dev = filp->private_data;

    spin_lock_irqsave(&dev->lock, flags);   /* 获取自旋锁 */

    dev->dev_stats--;                       /* 释放设备 */

    spin_unlock_irqrestore(&dev->lock, flags);
    return 0;
}

10.4 信号量实验 - semaphore.c

struct gpioled_dev {
    dev_t devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    int major;
    int minor;
    struct device_node *nd;
    int led_gpio;
    struct semaphore sem;       /* 信号量 */
};

static int led_open(struct inode *inode, struct file *filp)
{
    struct gpioled_dev *dev = filp->private_data;

    down(&dev->sem);           /* 获取信号量,可能睡眠 */

    return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
    struct gpioled_dev *dev = filp->private_data;

    up(&dev->sem);             /* 释放信号量 */

    return 0;
}

10.5 互斥体实验 - mutex.c

struct gpioled_dev {
    dev_t devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    int major;
    int minor;
    struct device_node *nd;
    int led_gpio;
    struct mutex lock;         /* 互斥体 */
};

static int led_open(struct inode *inode, struct file *filp)
{
    struct gpioled_dev *dev = filp->private_data;

    mutex_lock(&dev->lock);   /* 获取互斥锁 */

    return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
    struct gpioled_dev *dev = filp->private_data;

    mutex_unlock(&dev->lock); /* 释放互斥锁 */

    return 0;
}

十一、实验验证

11.1 编译方法

# 1. 编译驱动模块
# 修改Makefile中obj-m变量
obj-m := atomic.o
# 或
obj-m := spinlock.o

# 编译
make -j32

# 2. 编译测试APP
arm-linux-gnueabihf-gcc atomicApp.c -o atomicApp

11.2 测试步骤

# 1. 加载驱动模块
depmod                  # 首次加载需要
modprobe atomic.ko      # 加载驱动

# 2. 运行测试程序(后台运行)
./atomicApp /dev/gpioled 1 &   # 打开LED

# 3. 尝试第二个程序访问
./atomicApp /dev/gpioled 1     # 应该失败,返回EBUSY

# 4. 卸载驱动
rmmod atomic.ko

11.3 结果分析

测试场景 预期结果 实际结果
首次打开设备 成功 ✅ 成功
25秒内再次打开 失败(EBUSY) ✅ 失败
25秒后再次打开 成功 ✅ 成功
关闭设备后再打开 成功 ✅ 成功
gantt
    title 设备访问时序测试
    dateFormat X
    axisFormat %s

    section 进程A
    打开设备     :a1, 0, 5
    占用设备     :a2, 5, 25
    关闭设备     :a3, 25, 30

    section 进程B
    尝试打开     :b1, 10, 11
    失败返回     :b2, 11, 11
    等待         :b3, 11, 30
    打开成功     :b4, 30, 35

十二、跨平台对比

12.1 IMX6ULL vs STM32 vs RK3568并发机制差异

特性 IMX6ULL STM32 RK3568
CPU架构 Cortex-A7 单核 Cortex-M4/M7 Cortex-A55 四核
并发模型 单核+中断 单核+中断 多核SMP+中断
自旋锁 ✅ 支持 ❌ 不需要 ✅ 核间同步
原子操作 ✅ 支持 ⚠️ 部分支持 ✅ 支持
互斥体 ✅ 支持 ❌ 不支持 ✅ 支持
信号量 ✅ 支持 ✅ 支持 ✅ 支持
读写锁 ✅ 支持 ❌ 不需要 ✅ 支持
顺序锁 ✅ 支持 ❌ 不需要 ✅ 支持
RCU ✅ 支持 ❌ 不需要 ✅ 支持(重点)
中断优先级 32级 16级 32级
典型锁选择 spinlock_irqsave 临界区 mutex/spinlock
其他类型锁的使用 内核为主,驱动少用 无对应机制 RCU 广泛用于网络/文件系统

12.2 多核 vs 单核的锁选择

flowchart TD
    A{是否为SMP多核系统?} -->|是| B{临界区是否可能被中断访问?}
    A -->|否| C{临界区是否可能被中断访问?}

    B -->|是| D[spin_lock_irqsave]
    B -->|否| E[spin_lock]

    C -->|是| F[local_irq_disable + spin_lock]
    C -->|否| G{临界区长度}

    G -->|短| H[自旋锁/原子操作]
    G -->|长| I[信号量/互斥体]

    style D fill:#dbeafe,stroke:#2563eb
    style E fill:#dbeafe,stroke:#2563eb
    style F fill:#fef3c7,stroke:#f59e0b
    style H fill:#dcfce7,stroke:#16a34a
    style I fill:#dcfce7,stroke:#16a34a

十三、面试精选

题目1:自旋锁和信号量有什么区别?

考察点:锁机制理解

参考答案

特性 自旋锁 信号量
等待方式 忙等待(占用CPU) 睡眠(释放CPU)
可睡眠 ❌ 不可以 ✅ 可以
中断上下文 ✅ 可以使用 ❌ 不可以
临界区长度 短(μs级) 长(ms级)
CPU占用 高(忙等) 低(上下文切换)
适用场景 中断处理、短临界区 长时间操作、I/O

题目2:为什么要在中断处理函数中使用spin_lock_irqsave?

考察点:中断安全

参考答案

  1. 中断可能打断持有锁的进程
  2. 如果中断也尝试获取同一个锁,会导致死锁
  3. spin_lock_irqsave会禁止本地中断,避免中断与进程竞争同一把锁
  4. 使用irqsave版本是因为它可以保存并恢复中断状态,比spin_lock_irq更安全

题目3:原子操作适用于什么场景?

考察点:原子操作理解

参考答案

  • 简单变量操作:整数的加减、位操作
  • 状态标志:设备占用标志、错误标志
  • 引用计数:模块引用计数、文件引用计数
  • 不适合复杂操作:涉及多个变量的操作不能用原子操作

题目4:如何避免死锁?

考察点:死锁预防

参考答案

  1. 锁的顺序:多把锁按固定顺序获取
  2. 超时机制:使用trylock避免永久等待
  3. 锁粒度:锁的范围尽可能小
  4. 避免嵌套:尽量减少锁的嵌套使用
  5. 中断安全:在可能被中断访问的临界区使用spin_lock_irqsave

题目5:写一个简单的设备互斥访问示例

考察点:综合应用

参考答案

struct my_device {
    struct mutex lock;
    int in_use;
};

static int device_open(struct inode *inode, struct file *filp)
{
    struct my_device *dev = &my_dev;

    mutex_lock(&dev->lock);
    if (dev->in_use) {
        mutex_unlock(&dev->lock);
        return -EBUSY;
    }
    dev->in_use = 1;
    mutex_unlock(&dev->lock);

    return 0;
}

static int device_release(struct inode *inode, struct file *filp)
{
    struct my_device *dev = &my_dev;

    mutex_lock(&dev->lock);
    dev->in_use = 0;
    mutex_unlock(&dev->lock);

    return 0;
}

题目6:读写锁和自旋锁有什么区别?什么场景下用读写锁?

考察点:读写锁理解(教材 47.3.3)

参考答案

特性 读写锁(rwlock_t) 自旋锁(spinlock_t)
读操作并发 ✅ 允许多个读者同时持有 ❌ 完全互斥
写操作 排他,与所有读写者互斥 排他
读写能否同时 ❌ 不能 ❌ 不能
实现开销 稍大(维护读者计数) 更小
适用场景 读多写少,且读操作可并发 读写均衡、临界区极短

使用场景:当数据结构符合读/写生产者/消费者模型时使用,例如学生信息表、设备配置表这类"读远多于写"的数据。

追问

  • 追问1:读写锁能否把写锁升级为读锁(降级)?→ 不能,持有写锁时再获取读锁会死锁(自己等自己)。必须先 write_unlock()read_lock(),但中间存在空窗期。
  • 追问2:读写锁会导致写者饥饿吗?→ 。Linux 的实现中读者可以插队到等待的写者之前,读压力极大时写者可能长时间得不到锁。

题目7:顺序锁的原理是什么?它为什么不能保护指针?

考察点:顺序锁理解(教材 47.3.3)

参考答案

顺序锁允许写的时候同时读,但不允许并发写。读者不加锁,而是靠序号判断:

  1. 读之前读序号 s1
  2. 读取数据
  3. 读之后再读序号 s2
  4. s1 == s2 说明读取期间没有写者,数据有效;否则重读

对应 API:read_seqbegin() / read_seqretry(),写侧用 write_seqlock() / write_sequnlock()

为什么不能保护指针:原书明确指出——如果在写操作的时候可能会导致指针无效(例如写者把指针指向的旧内存 kfree 掉了),而此时恰巧有读操作访问该指针,就会读取野指针导致系统崩溃。所以顺序锁只能保护纯值类型数据(如 jiffies),不能保护含指针的结构。

追问

  • 追问1:顺序锁和读写锁相比,谁的性能更好?→ 读者侧顺序锁更好(无加锁开销),但可能重试;写者侧顺序锁不会饥饿。
  • 追问2:内核中顺序锁的典型使用者?→ jiffies_64(时钟中断频繁写、大量代码频繁读)。

题目8:RCU 与读写锁的区别?(扩展)

考察点:RCU 理解(超出教材范围)

参考答案

特性 RCU 读写锁
读者开销 近乎零(只标记) 中(原子操作)
读者会重试 ❌ 不会 ❌ 不会
读者阻塞写者 ❌ 完全不阻塞 ✅ 阻塞
可保护指针/链表 ✅ 主要用途 ✅ 可
写者语义 复制-替换-等宽限期-释放 原地加锁修改
内存开销 高(新旧副本共存) 无额外

核心区别:RCU 的写者不是"原地加锁修改",而是"复制一份 → 改副本 → 原子替换指针 → 等宽限期 → 释放旧数据"。宽限期保证所有已进入读侧临界区的读者都退出后,旧数据才被释放。

追问

  • 追问1:rcu_read_lock() 会阻塞写者吗?→ 不会,它只是标记进入临界区,所以读侧临界区不能睡眠
  • 追问2:synchronize_rcu() 能在中断里调用吗?→ 不能,它会睡眠。

内容来源: 《I.MX6U嵌入式Linux驱动开发指南》第四十七章 Linux并发与竞争(含 47.3.3 其他类型的锁)+ 第四十八章 并发与竞争实验;例程 07_atomic, 08_spinlock, 09_semaphore, 10_mutex 扩展内容: 第八、九节(RCU、完成量)超出教材范围,为补充知识 最后更新: 2026-09-17