深入理解 Linux 内核同步机制

文章目录

    • 前言
    • 一、竞态产生的根源:三个核心诱因与两个必要条件
      • 1.1 竞态的三个核心诱因
        • (1)SMP 多核架构的多核并发
        • (2)内核抢占机制的进程抢占
        • (3)中断上下文对进程上下文的打断
      • 1.2 竞态产生的两个必要条件
    • 二、原子操作:最简单的同步机制,针对整数与位操作
      • 2.1 原子整数操作:保护简单整数共享资源
        • (1)`atomic_t`类型定义
        • (2)常用原子整数操作接口
        • (3)原子整数操作实战示例:设备引用计数管理
        • (4)示例解析
      • 2.2 原子位操作:保护单个比特位状态
        • (1)常用原子位操作接口
        • (2)原子位操作实战示例:设备状态标志管理
        • (3)示例解析
      • 2.3 原子操作的局限性
    • 三、自旋锁:短临界区与中断上下文的首选
      • 3.1 自旋锁的核心特性
      • 3.2 自旋锁的基本使用流程
      • 3.3 常用自旋锁接口
        • (1)基本接口(无中断参与)
        • (2)中断安全接口(处理中断上下文竞态)
          • ① 处理硬件中断(顶半部):`spin_lock_irqsave()` / `spin_unlock_irqrestore()`
          • ② 处理软中断(底半部):`spin_lock_bh()` / `spin_unlock_bh()`
      • 3.4 自旋锁实战示例
        • 示例 1:进程上下文之间的同步(多核 SMP,无中断参与)
        • 示例 2:进程上下文与中断上下文的同步(带中断安全接口)
      • 3.5 自旋锁的注意事项与局限性
        • (1)注意事项
        • (2)局限性
    • 四、互斥机制:长临界区与进程上下文的首选
      • 4.1 互斥锁(`mutex_t`):严格互斥的首选
        • (1)互斥锁的核心特性
        • (2)互斥锁的基本使用流程
        • (3)常用互斥锁接口
        • (4)互斥锁实战示例:保护复杂链表(长临界区)
        • (5)示例解析
      • 4.2 信号量(`struct semaphore`):支持多执行流并发的互斥机制
        • (1)信号量的核心特性
        • (2)信号量的基本使用流程
        • (3)常用信号量接口
        • (4)信号量实战示例:计数信号量实现缓冲区池管理
        • (5)示例解析
      • 4.3 互斥锁与信号量的核心区别
    • 五、其他高级同步手段:应对特殊场景
      • 5.1 读写锁(`rwlock_t`):读多写少场景的优化
        • (1)读写锁的核心特性
        • (2)常用读写锁接口
        • (3)适用场景
      • 5.2 RCU 机制:高并发读场景的极致优化
        • (1)RCU 机制的核心特性
        • (2)RCU 的核心接口
        • (3)适用场景
      • 5.3 内存屏障:解决指令重排与内存可见性问题
        • (1)内存屏障的核心背景
        • (2)内存屏障的分类与常用接口
        • (3)内存屏障的注意事项
    • 六、死锁的避免:从根源上规避同步陷阱
      • 6.1 死锁产生的四个必要条件
      • 6.2 死锁避免的核心策略
        • (1)资源有序分配:破坏循环等待条件
        • (2)避免嵌套锁:破坏持有并等待条件
        • (3)使用超时机制:破坏不可剥夺条件
        • (4)其他辅助策略
    • 七、内核同步机制选择指南:按场景精准匹配
    • 八、总结

前言

在现代计算机系统中,多核 SMP(对称多处理)架构已成为主流,Linux 内核也早已支持内核抢占和高效中断处理。这些特性极大提升了系统的并发性能和响应速度,但同时也引入了一个核心问题 ------竞态(Race Condition)。当多个执行流(多核 CPU 上的进程、内核抢占产生的高优先级进程、中断上下文)同时访问共享资源(全局变量、设备寄存器、链表等),且至少有一个执行流执行写操作时,就可能导致数据不一致、系统崩溃、功能异常等严重问题。

一、竞态产生的根源:三个核心诱因与两个必要条件

在学习同步机制之前,我们必须先搞清楚:竞态到底是如何产生的?只有抓住根源,才能正确选择同步机制解决问题。

1.1 竞态的三个核心诱因

Linux 内核中的执行流并非单一的,存在进程上下文、中断上下文、软中断上下文等多种执行流,这些执行流的并发执行是竞态产生的直接诱因,具体可归纳为三类:

(1)SMP 多核架构的多核并发

SMP(Symmetric Multi-Processing,对称多处理)架构的核心特征是多个 CPU 核心共享系统主存、系统总线等硬件资源,每个 CPU 核心都可以独立执行内核代码。当多个 CPU 核心同时执行同一段内核代码,且操作同一份共享内存资源时,就会产生竞态。

举个简单的例子:两个 CPU 核心同时对一个全局变量count执行count++操作。我们知道,count++在汇编层面并非原子操作,而是拆解为三个步骤:

  1. 从主存中将count的值读取到 CPU 寄存器中;
  2. 在寄存器中对count的值执行加 1 操作;
  3. 将寄存器中的新值写回主存中的count

假设count的初始值为 1,CPU 0 和 CPU 1 同时执行这三个步骤,可能出现如下执行序列:

  • CPU 0 读取count=1到寄存器 R0;
  • CPU 1 读取count=1到寄存器 R1;
  • CPU 0 执行 R0=R0+1(R0=2),并写回主存,此时count=2
  • CPU 1 执行 R1=R1+1(R1=2),并写回主存,此时count=2

最终count的值为 2,而预期结果应为 3,这就是多核并发导致的竞态问题 ------ 两个 CPU 核心的操作相互覆盖,导致数据不一致。

(2)内核抢占机制的进程抢占

Linux 2.6 及以后版本的内核支持内核抢占(Kernel Preemption),与用户态抢占不同,内核抢占允许在进程处于内核态执行时,若有更高优先级的进程就绪,调度器可以抢占当前进程的 CPU 执行权,切换到高优先级进程执行。

内核抢占极大提升了系统的实时响应能力,但也引入了新的竞态风险。例如:

  • 进程 A(低优先级)在内核态执行,正在修改全局变量count,已完成 "读取值到寄存器" 和 "寄存器加 1" 步骤,尚未将新值写回主存;
  • 此时,高优先级进程 B 就绪,调度器抢占进程 A 的 CPU 执行权,切换到进程 B 执行;
  • 进程 B 同样修改全局变量count,完成完整的 "读取 - 加 1 - 写回" 操作,count的值更新为预期值;
  • 进程 B 执行完毕后,调度器切换回进程 A 继续执行,进程 A 将之前寄存器中计算的旧值写回主存,覆盖了进程 B 的修改,导致数据不一致。

需要注意的是,内核抢占并非在所有场景下都允许,例如持有自旋锁、处于中断上下文时,内核会自动关闭抢占,避免竞态。

(3)中断上下文对进程上下文的打断

中断(包括硬件中断和软中断)是硬件设备与内核通信的重要方式,中断上下文的执行优先级高于任何进程上下文(即使是实时进程),当中断发生时,CPU 会立即暂停当前正在执行的进程上下文,切换到中断处理程序执行,这就可能导致竞态。

中断引发的竞态有两种典型场景:

  1. 硬件中断(顶半部)打断进程上下文:进程 A 正在修改共享资源,突然发生硬件中断(如网卡接收数据、定时器超时),CPU 切换到中断处理程序,若中断处理程序也修改同一共享资源,就会导致数据不一致;
  2. 软中断(底半部)打断进程上下文:软中断是内核的延迟处理机制,优先级高于进程上下文、低于硬件中断,若进程上下文和软中断上下文同时访问同一共享资源,也会产生竞态。

例如,进程 A 正在向全局缓冲区buffer写入数据,此时定时器中断触发,中断处理程序从buffer中读取数据并修改,中断返回后,进程 A 继续执行写入操作,就会覆盖中断处理程序的修改,导致缓冲区数据错乱。

1.2 竞态产生的两个必要条件

无论是上述哪种诱因,竞态的产生都必须满足两个必要条件,缺少其中任何一个,都不会产生竞态:

  1. 存在共享资源:共享资源是指可以被多个执行流访问的资源,包括全局变量、静态变量、设备寄存器、内核链表、共享内存等。只读的共享资源不会产生竞态,只有可写的共享资源才存在竞态风险;
  2. 多个执行流同时访问共享资源:"同时访问" 是指执行流的操作在时间上存在重叠(多核并发的真正同时,或单核上的抢占 / 打断导致的逻辑同时),且至少有一个执行流执行写操作。

理解这两个必要条件对后续选择同步机制至关重要 ------ 同步机制的核心就是通过限制 "多个执行流同时访问共享资源",来破坏竞态产生的条件,从而保证数据一致性。

二、原子操作:最简单的同步机制,针对整数与位操作

原子操作是 Linux 内核中最简单、最基础的同步机制,其核心特性是不可被打断的操作------ 要么全部执行完成,要么完全不执行,没有中间状态,从而避免竞态问题。

Linux 内核中的原子操作主要分为两类:原子整数操作 (针对整数变量)和原子位操作(针对单个比特位),它们都由内核封装为跨架构接口,屏蔽了不同 CPU 架构的底层实现差异。

2.1 原子整数操作:保护简单整数共享资源

原子整数操作的操作对象是内核封装的atomic_t类型,而不是普通的int类型,这是因为普通int可能被编译器优化(寄存器缓存、指令重排),且在不同架构下大小不一致,而atomic_t是专门为原子操作设计的跨架构类型。

(1)atomic_t类型定义

atomic_t的定义位于<linux/types.h>头文件中,以 32 位 Linux 内核为例:

复制代码
typedef struct {
    int counter;  // 存储整数值的计数器
} atomic_t;

需要特别注意:atomic_t变量不能直接使用赋值、加减等普通操作符操作,必须使用内核提供的专用接口函数,否则会破坏原子性。

(2)常用原子整数操作接口

原子整数操作接口按功能可分为初始化、赋值、增减、比较交换、取值五类,以下是最常用的接口:

接口函数 功能说明
ATOMIC_INIT(i) 定义atomic_t变量时静态初始化,将计数器值设为i
void atomic_set(atomic_t *v, int i) 运行时初始化,将atomic_t变量v的计数器值设为i
void atomic_inc(atomic_t *v) 原子地将v的计数器值加 1,无返回值
void atomic_dec(atomic_t *v) 原子地将v的计数器值减 1,无返回值
int atomic_inc_return(atomic_t *v) 原子地将v的计数器值加 1,返回修改后的新值
int atomic_dec_return(atomic_t *v) 原子地将v的计数器值减 1,返回修改后的新值
int atomic_add_return(int i, atomic_t *v) 原子地将v的计数器值加i,返回修改后的新值
int atomic_sub_return(int i, atomic_t *v) 原子地将v的计数器值减i,返回修改后的新值
int atomic_dec_and_test(atomic_t *v) 原子地将v的计数器值减 1,若结果为 0 返回true,否则返回false(常用于引用计数释放)
int atomic_cmpxchg(atomic_t *v, int old, int new) 比较并交换:若v的当前值等于old,则原子地替换为new,返回旧值
int atomic_read(const atomic_t *v) 原子地读取v的计数器值,返回当前值
(3)原子整数操作实战示例:设备引用计数管理

在设备驱动开发中,引用计数是常用的资源管理手段,用于记录设备被多少进程使用,当引用计数为 0 时,才能释放设备资源。使用atomic_t可以保证引用计数的原子性,避免竞态问题。

完整示例代码:

复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/cdev.h>

// 定义设备结构体,包含原子引用计数
struct my_device {
    dev_t devno;          // 设备号
    struct cdev cdev;     // 字符设备结构体
    atomic_t ref_count;   // 原子引用计数,记录设备被使用的次数
};

static struct my_device my_dev;

// 设备打开函数
static int my_dev_open(struct inode *inode, struct file *filp)
{
    // 原子地增加引用计数
    atomic_inc(&my_dev.ref_count);
    printk(KERN_INFO "my_dev open: ref_count = %d\n", atomic_read(&my_dev.ref_count));
    
    // 将设备结构体指针存入文件私有数据
    filp->private_data = &my_dev;
    return 0;
}

// 设备释放函数
static int my_dev_release(struct inode *inode, struct file *filp)
{
    // 原子地减少引用计数,并判断是否为0
    if (atomic_dec_and_test(&my_dev.ref_count)) {
        printk(KERN_INFO "my_dev release: ref_count = 0, device is idle\n");
    } else {
        printk(KERN_INFO "my_dev release: ref_count = %d\n", atomic_read(&my_dev.ref_count));
    }
    return 0;
}

// 定义文件操作结构体
static const struct file_operations my_dev_fops = {
    .owner = THIS_MODULE,
    .open = my_dev_open,
    .release = my_dev_release,
};

// 模块初始化函数
static int __init my_dev_init(void)
{
    int ret;
    
    // 1. 分配设备号(动态分配)
    ret = alloc_chrdev_region(&my_dev.devno, 0, 1, "my_dev");
    if (ret < 0) {
        printk(KERN_ERR "alloc_chrdev_region failed\n");
        return ret;
    }
    
    // 2. 初始化字符设备
    cdev_init(&my_dev.cdev, &my_dev_fops);
    my_dev.cdev.owner = THIS_MODULE;
    
    // 3. 注册字符设备
    ret = cdev_add(&my_dev.cdev, my_dev.devno, 1);
    if (ret < 0) {
        unregister_chrdev_region(my_dev.devno, 1);
        printk(KERN_ERR "cdev_add failed\n");
        return ret;
    }
    
    // 4. 初始化原子引用计数(初始值为0)
    atomic_set(&my_dev.ref_count, 0);
    printk(KERN_INFO "my_dev init success, devno = %d:%d\n", 
           MAJOR(my_dev.devno), MINOR(my_dev.devno));
    return 0;
}

// 模块退出函数
static void __exit my_dev_exit(void)
{
    // 注销字符设备
    cdev_del(&my_dev.cdev);
    // 释放设备号
    unregister_chrdev_region(my_dev.devno, 1);
    printk(KERN_INFO "my_dev exit success\n");
}

module_init(my_dev_init);
module_exit(my_dev_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Developer");
MODULE_DESCRIPTION("Atomic Operation Example for Device Ref Count");
(4)示例解析
  1. 定义了my_device结构体,其中包含atomic_t类型的引用计数ref_count,用于记录设备被打开的次数;
  2. 设备打开时,调用atomic_inc()原子地增加引用计数,避免多进程同时打开设备导致计数错误;
  3. 设备释放时,调用atomic_dec_and_test()原子地减少引用计数,并判断计数是否为 0,若为 0 则说明设备已无进程使用,进入空闲状态;
  4. 模块初始化时,调用atomic_set()将引用计数初始化为 0,保证初始状态的正确性。

该示例中,原子操作保证了引用计数的修改不会被打断,即使多个进程同时打开 / 关闭设备,也能保证引用计数的准确性。

2.2 原子位操作:保护单个比特位状态

原子位操作是针对内存中单个比特位的原子操作,无需封装特殊类型,直接操作普通的unsigned long类型内存地址,适用于管理设备状态标志位、开关等简单场景。

(1)常用原子位操作接口

原子位操作接口按功能可分为设置、清除、翻转、测试四类,以下是最常用的接口:

接口函数 功能说明
void set_bit(int nr, volatile unsigned long *addr) 原子地将addr指向的内存的第nr位设为 1
void clear_bit(int nr, volatile unsigned long *addr) 原子地将addr指向的内存的第nr位设为 0
void change_bit(int nr, volatile unsigned long *addr) 原子地翻转addr指向的内存的第nr位(0 变 1,1 变 0)
int test_bit(int nr, const volatile unsigned long *addr) 测试addr指向的内存的第nr位,返回 1(置位)或 0(未置位)(只读操作,无竞态)
int test_and_set_bit(int nr, volatile unsigned long *addr) 原子地测试第nr位,然后将其设为 1,返回该位原来的值
int test_and_clear_bit(int nr, volatile unsigned long *addr) 原子地测试第nr位,然后将其设为 0,返回该位原来的值

其中,nr表示比特位的编号(从 0 开始,最低位为第 0 位),addr表示内存地址,volatile关键字用于防止编译器优化,保证操作的是内存中的实际值。

(2)原子位操作实战示例:设备状态标志管理
复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>

// 定义设备状态标志位(使用unsigned long存储,可管理64个标志位(64位系统))
#define DEV_FLAG_BUSY    0  // 第0位:设备忙碌标志
#define DEV_FLAG_READY   1  // 第1位:设备就绪标志
#define DEV_FLAG_ERROR   2  // 第2位:设备错误标志

static volatile unsigned long dev_flags = 0;  // 设备状态标志变量

// 设备启动函数:设置就绪标志,清除错误标志
static int dev_start(void)
{
    // 原子地设置就绪标志(第1位)
    set_bit(DEV_FLAG_READY, &dev_flags);
    // 原子地清除错误标志(第2位)
    clear_bit(DEV_FLAG_ERROR, &dev_flags);
    
    printk(KERN_INFO "dev start: ready = %d, error = %d\n",
           test_bit(DEV_FLAG_READY, &dev_flags),
           test_bit(DEV_FLAG_ERROR, &dev_flags));
    return 0;
}

// 设备操作函数:获取设备,设置忙碌标志
static int dev_acquire(void)
{
    // 原子地测试并设置忙碌标志(第0位),若原来为0(未忙碌),则设置为1并返回0
    if (test_and_set_bit(DEV_FLAG_BUSY, &dev_flags) == 0) {
        printk(KERN_INFO "dev acquire success: busy = 1\n");
        return 0;
    } else {
        printk(KERN_ERR "dev acquire failed: device is busy\n");
        return -EBUSY;
    }
}

// 设备释放函数:清除忙碌标志
static void dev_release(void)
{
    // 原子地清除忙碌标志(第0位)
    clear_bit(DEV_FLAG_BUSY, &dev_flags);
    printk(KERN_INFO "dev release: busy = 0\n");
}

// 模块初始化函数
static int __init bit_op_init(void)
{
    dev_start();
    dev_acquire();
    dev_release();
    return 0;
}

// 模块退出函数
static void __exit bit_op_exit(void)
{
    printk(KERN_INFO "bit op module exit\n");
}

module_init(bit_op_init);
module_exit(bit_op_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Developer");
MODULE_DESCRIPTION("Atomic Bit Operation Example for Device Flags");
(3)示例解析
  1. 定义了三个设备状态标志位,使用unsigned long类型的dev_flags存储,通过原子位操作管理;
  2. 设备启动时,调用set_bit()设置就绪标志,clear_bit()清除错误标志,保证状态修改的原子性;
  3. 设备获取时,调用test_and_set_bit()原子地测试并设置忙碌标志,避免多个执行流同时获取设备导致冲突;
  4. 设备释放时,调用clear_bit()清除忙碌标志,允许其他执行流获取设备。

2.3 原子操作的局限性

原子操作虽然简单高效,但也存在明显的局限性,决定了它只能用于简单场景:

  1. 操作对象有限:仅支持整数和单个比特位,无法保护复杂数据结构(如链表、结构体、缓冲区等);
  2. 功能单一:仅支持简单的增减、赋值、位操作,无法实现复杂的同步逻辑(如互斥、等待等);
  3. 不支持睡眠:原子操作执行期间虽然短暂,但也不支持睡眠,若在原子操作期间调用睡眠函数,可能导致数据不一致;
  4. 64 位原子操作依赖架构:32 位架构下,64 位原子操作需要借助自旋锁模拟,开销较大,而 64 位架构下原生支持 64 位原子操作。

因此,原子操作仅适用于简单共享变量(引用计数、标志位)的同步,对于复杂场景,需要使用更强大的同步机制。

三、自旋锁:短临界区与中断上下文的首选

自旋锁(spinlock_t)是 Linux 内核中最常用的同步机制之一,专门用于保护短临界区 ,其核心特性是忙等待------ 当执行流尝试获取自旋锁时,若锁已被其他执行流持有,该执行流不会睡眠,而是不断循环(自旋)检查锁是否被释放,直到获取到锁为止。

3.1 自旋锁的核心特性

  1. 忙等待,不放弃 CPU:自旋锁获取失败时,执行流持续自旋等待,不放弃 CPU 执行权,因此适合临界区执行时间极短的场景(自旋等待的开销小于进程切换的开销);
  2. 不可睡眠 :持有自旋锁期间,执行流不能睡眠(如调用kmalloc(GFP_KERNEL)schedule()等函数),否则会导致死锁 ------ 睡眠后执行流放弃 CPU,而自旋锁仍被持有,其他想要获取该锁的执行流会一直自旋等待,无法推进;
  3. 关闭内核抢占:在进程上下文获取自旋锁时,内核会自动关闭当前 CPU 的内核抢占,防止同一 CPU 上的高优先级进程抢占当前进程,导致竞态;
  4. 中断上下文可用:自旋锁提供了中断安全的接口,可用于保护中断上下文与进程上下文共享的资源;
  5. 严格互斥:自旋锁是排他锁,同一时刻只能有一个执行流持有锁,不支持共享访问;
  6. 跨 CPU 同步:自旋锁的主要作用是防止多核 SMP 架构下,多个 CPU 上的执行流并发访问临界区。

3.2 自旋锁的基本使用流程

自旋锁的使用流程非常规范,分为定义、初始化、获取、释放四个步骤:

  1. 定义自旋锁变量spinlock_t lock;
  2. 初始化自旋锁
    • 静态初始化:DEFINE_SPINLOCK(lock);(定义时直接初始化,推荐)
    • 动态初始化:spin_lock_init(&lock);(运行时初始化,适用于动态分配的自旋锁)
  3. 获取自旋锁:调用相应的锁获取接口,进入临界区;
  4. 释放自旋锁:调用锁释放接口,退出临界区。

3.3 常用自旋锁接口

自旋锁接口分为基本接口 (适用于无中断参与的进程上下文)和中断安全接口(适用于中断上下文与进程上下文共享资源的场景),以下是核心接口:

(1)基本接口(无中断参与)
接口函数 功能说明
void spin_lock(spinlock_t *lock) 获取自旋锁,若获取失败则自旋等待,不可中断
void spin_unlock(spinlock_t *lock) 释放自旋锁,唤醒等待该锁的执行流
int spin_trylock(spinlock_t *lock) 尝试获取自旋锁,获取成功返回 1,失败返回 0(不自旋等待)
int spin_is_locked(spinlock_t *lock) 检查锁是否被持有,返回 1(被持有)或 0(未被持有)
(2)中断安全接口(处理中断上下文竞态)

中断上下文的优先级高于进程上下文,若进程上下文持有自旋锁时被中断上下文打断,而中断上下文也尝试获取同一自旋锁,会导致死锁(进程上下文被中断打断,自旋锁未释放,中断上下文自旋等待该锁,无法返回进程上下文)。

为了解决这个问题,自旋锁提供了中断安全接口,核心是获取锁时关闭本地 CPU 的中断,释放锁时恢复中断状态,主要分为两类:

① 处理硬件中断(顶半部):spin_lock_irqsave() / spin_unlock_irqrestore()

这是最安全、最常用的中断安全接口,函数原型:

复制代码
unsigned long spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);

功能说明:

  1. spin_lock_irqsave():获取自旋锁,关闭当前 CPU 的本地中断,保存当前中断状态到flags变量中;
  2. spin_unlock_irqrestore():释放自旋锁,根据flags变量恢复当前 CPU 的中断状态;
  3. flags变量必须是unsigned long类型,且在获取锁和释放锁之间不能修改;
  4. 两个函数必须成对使用,且在同一个函数中调用,不能跨函数拆分。
② 处理软中断(底半部):spin_lock_bh() / spin_unlock_bh()

函数原型:

复制代码
void spin_lock_bh(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);

功能说明:

  1. spin_lock_bh():获取自旋锁,关闭当前 CPU 的软中断,防止软中断上下文打断进程上下文;
  2. spin_unlock_bh():释放自旋锁,恢复当前 CPU 的软中断状态;
  3. 适用于进程上下文与软中断上下文共享资源的场景。

3.4 自旋锁实战示例

示例 1:进程上下文之间的同步(多核 SMP,无中断参与)

该示例实现两个内核线程并发修改一个全局计数器,使用自旋锁保护临界区,避免多核并发导致的计数错误。

复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spinlock.h>
#include <linux/kthread.h>
#include <linux/delay.h>

// 全局共享资源:计数器
static unsigned long global_count = 0;
// 定义并静态初始化自旋锁
static DEFINE_SPINLOCK(count_lock);

// 内核线程函数1:递增全局计数器
static int kthread_count_inc(void *data)
{
    unsigned long i;
    unsigned long flags;  // 自旋锁标志位(本示例无中断,可不用,仅为演示)
    
    for (i = 0; i < 1000000; i++) {
        // 1. 获取自旋锁
        spin_lock(&count_lock);
        
        // 2. 进入临界区:修改全局计数器
        global_count++;
        
        // 3. 释放自旋锁
        spin_unlock(&count_lock);
        
        // 轻微延迟,模拟其他操作
        udelay(1);
    }
    
    printk(KERN_INFO "kthread_inc finished: global_count = %lu\n", global_count);
    return 0;
}

// 内核线程函数2:递增全局计数器
static int kthread_count_inc2(void *data)
{
    unsigned long i;
    
    for (i = 0; i < 1000000; i++) {
        // 1. 获取自旋锁
        spin_lock(&count_lock);
        
        // 2. 进入临界区:修改全局计数器
        global_count++;
        
        // 3. 释放自旋锁
        spin_unlock(&count_lock);
        
        // 轻微延迟,模拟其他操作
        udelay(1);
    }
    
    printk(KERN_INFO "kthread_inc2 finished: global_count = %lu\n", global_count);
    return 0;
}

// 保存内核线程PID
static struct task_struct *kthread1;
static struct task_struct *kthread2;

// 模块初始化函数
static int __init spinlock_proc_init(void)
{
    // 创建内核线程1
    kthread1 = kthread_run(kthread_count_inc, NULL, "kthread_count_inc");
    if (IS_ERR(kthread1)) {
        printk(KERN_ERR "create kthread1 failed\n");
        return PTR_ERR(kthread1);
    }
    
    // 创建内核线程2
    kthread2 = kthread_run(kthread_count_inc2, NULL, "kthread_count_inc2");
    if (IS_ERR(kthread2)) {
        printk(KERN_ERR "create kthread2 failed\n");
        kthread_stop(kthread1);
        return PTR_ERR(kthread2);
    }
    
    printk(KERN_INFO "spinlock proc module init success\n");
    return 0;
}

// 模块退出函数
static void __exit spinlock_proc_exit(void)
{
    // 停止内核线程
    kthread_stop(kthread1);
    kthread_stop(kthread2);
    
    printk(KERN_INFO "spinlock proc module exit: final global_count = %lu\n", global_count);
}

module_init(spinlock_proc_init);
module_exit(spinlock_proc_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Developer");
MODULE_DESCRIPTION("Spinlock Example for Process Context Synchronization");
示例 2:进程上下文与中断上下文的同步(带中断安全接口)

该示例实现进程上下文向全局缓冲区写入数据,定时器中断上下文从缓冲区读取数据,使用spin_lock_irqsave()/spin_unlock_irqrestore()保护临界区,避免中断打断导致的数据错乱。

复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spinlock.h>
#include <linux/timer.h>
#include <linux/kernel.h>
#include <linux/types.h>

// 全局共享缓冲区大小
#define BUFFER_SIZE 1024
// 全局共享缓冲区
static char global_buffer[BUFFER_SIZE];
// 缓冲区数据长度
static unsigned long buffer_len = 0;
// 定义并静态初始化自旋锁
static DEFINE_SPINLOCK(buffer_lock);
// 定义定时器结构体
static struct timer_list my_timer;

// 定时器中断处理函数(中断上下文)
static void timer_handler(unsigned long data)
{
    unsigned long flags;
    char buffer_copy[BUFFER_SIZE] = {0};
    
    // 1. 获取自旋锁,关闭本地中断,保存中断状态
    spin_lock_irqsave(&buffer_lock, flags);
    
    // 2. 进入临界区:拷贝缓冲区数据(只读操作,也需要保护,防止写操作打断)
    if (buffer_len > 0) {
        memcpy(buffer_copy, global_buffer, buffer_len);
        printk(KERN_INFO "timer handler: read buffer data = %s, len = %lu\n",
               buffer_copy, buffer_len);
        
        // 清空缓冲区
        memset(global_buffer, 0, BUFFER_SIZE);
        buffer_len = 0;
    }
    
    // 3. 释放自旋锁,恢复中断状态
    spin_unlock_irqrestore(&buffer_lock, flags);
    
    // 重新设置定时器(5秒后再次触发)
    mod_timer(&my_timer, jiffies + msecs_to_jiffies(5000));
}

// 向缓冲区写入数据的函数(进程上下文)
static void write_buffer(const char *data, unsigned long len)
{
    unsigned long flags;
    
    if (len >= BUFFER_SIZE) {
        printk(KERN_ERR "write buffer: data too long\n");
        return;
    }
    
    // 1. 获取自旋锁,关闭本地中断,保存中断状态
    spin_lock_irqsave(&buffer_lock, flags);
    
    // 2. 进入临界区:写入缓冲区数据
    memcpy(global_buffer, data, len);
    buffer_len = len;
    printk(KERN_INFO "write buffer: write data = %s, len = %lu\n", data, len);
    
    // 3. 释放自旋锁,恢复中断状态
    spin_unlock_irqrestore(&buffer_lock, flags);
}

// 模块初始化函数
static int __init spinlock_irq_init(void)
{
    // 1. 初始化定时器
    setup_timer(&my_timer, timer_handler, 0);
    
    // 2. 设置定时器(5秒后首次触发)
    mod_timer(&my_timer, jiffies + msecs_to_jiffies(5000));
    
    // 3. 向缓冲区写入测试数据
    write_buffer("Hello, Linux Kernel Spinlock!", 28);
    
    printk(KERN_INFO "spinlock irq module init success\n");
    return 0;
}

// 模块退出函数
static void __exit spinlock_irq_exit(void)
{
    // 1. 删除定时器
    del_timer(&my_timer);
    
    // 2. 清空缓冲区
    memset(global_buffer, 0, BUFFER_SIZE);
    buffer_len = 0;
    
    printk(KERN_INFO "spinlock irq module exit success\n");
}

module_init(spinlock_irq_init);
module_exit(spinlock_irq_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Developer");
MODULE_DESCRIPTION("Spinlock Example for Process and IRQ Context Synchronization");

3.5 自旋锁的注意事项与局限性

(1)注意事项
  1. 临界区必须尽可能短:自旋锁是忙等待,临界区越长,自旋等待的时间就越长,会浪费大量 CPU 资源,降低系统吞吐量。一般来说,临界区的执行时间应小于进程切换的时间(约几微秒);
  2. 持有锁期间不能睡眠 :禁止调用任何可能导致睡眠的函数(如kmalloc(GFP_KERNEL)copy_from_user()schedule()等),否则会导致死锁;
  3. 不能嵌套获取同一个自旋锁:同一个执行流多次获取同一个自旋锁,会导致自己自旋等待自己,陷入死锁;
  4. 中断安全接口的成对使用spin_lock_irqsave()spin_unlock_irqrestore()spin_lock_bh()spin_unlock_bh()必须成对使用,且flags变量不能被修改;
  5. 单 CPU 架构下的优化:单 CPU 架构下,自旋锁的忙等待逻辑会被优化掉,核心作用是关闭内核抢占,防止进程抢占导致的竞态。
(2)局限性
  1. 不适合长临界区:临界区执行时间较长时,自旋等待的开销远大于进程睡眠 / 唤醒的开销,此时应使用互斥锁或信号量;
  2. 不支持优先级继承:在实时系统中,低优先级进程持有自旋锁,高优先级进程自旋等待,会导致高优先级进程无法执行(优先级反转),自旋锁无法解决该问题;
  3. 开销高于原子操作:自旋锁涉及锁的获取、释放和抢占开关,开销高于原子操作,简单场景优先使用原子操作。

四、互斥机制:长临界区与进程上下文的首选

与自旋锁的忙等待不同,互斥机制的核心特性是睡眠等待 ------ 当执行流尝试获取互斥资源失败时,会放弃 CPU 执行权,进入睡眠状态,直到资源被释放,调度器唤醒该执行流。这种特性使得互斥机制适合保护长临界区,且仅能在进程上下文使用。

Linux 内核中的互斥机制主要包括互斥锁(mutex_t)**和**信号量(struct semaphore,其中互斥锁用于严格互斥场景,信号量用于支持多执行流并发的场景。

4.1 互斥锁(mutex_t):严格互斥的首选

互斥锁是一种严格的排他锁,同一时刻只能有一个执行流持有锁,主要用于解决进程上下文之间的互斥问题,是长临界区的首选同步机制。

(1)互斥锁的核心特性
  1. 严格互斥:同一时刻只能有一个执行流持有互斥锁,不支持共享访问,等价于二值信号量(计数为 0 或 1);
  2. 睡眠等待 :获取不到锁时,执行流进入TASK_UNINTERRUPTIBLETASK_INTERRUPTIBLE睡眠状态,不浪费 CPU 资源,适合长临界区;
  3. 支持优先级继承:当高优先级进程等待低优先级进程持有的互斥锁时,低优先级进程会临时继承高优先级进程的优先级,防止优先级反转,这是互斥锁与二值信号量的核心区别;
  4. 仅能在进程上下文使用:中断上下文不能睡眠,因此无法使用互斥锁;
  5. 持有锁期间可睡眠 :互斥锁支持睡眠,持有锁期间可以调用可能导致睡眠的函数(如kmalloc(GFP_KERNEL)copy_from_user()等);
  6. 不可嵌套获取:同一个执行流不能多次获取同一个互斥锁,否则会导致死锁;
  7. 必须成对使用:获取锁和释放锁必须成对,且通常在同一个函数中调用。
(2)互斥锁的基本使用流程
  1. 定义互斥锁变量mutex_t mutex;

  2. 初始化互斥锁

    • 静态初始化:DEFINE_MUTEX(mutex);(定义时直接初始化,推荐)
    • 动态初始化:mutex_init(&mutex);(运行时初始化,适用于动态分配的互斥锁)
  3. 获取互斥锁:调用相应的锁获取接口,进入临界区;

  4. 释放互斥锁:调用锁释放接口,退出临界区。

(3)常用互斥锁接口
接口函数 功能说明
void mutex_lock(mutex_t *mutex) 获取互斥锁,获取失败则进入TASK_UNINTERRUPTIBLE睡眠(不可被信号打断)
int mutex_lock_interruptible(mutex_t *mutex) 获取互斥锁,获取失败则进入TASK_INTERRUPTIBLE睡眠(可被信号打断,返回 0 成功,-EINTR 被打断)
int mutex_trylock(mutex_t *mutex) 尝试获取互斥锁,获取成功返回 1,失败返回 0(不睡眠,直接返回)
void mutex_unlock(mutex_t *mutex) 释放互斥锁,唤醒等待该锁的执行流

其中,mutex_lock_interruptible()是最常用的接口,因为它允许睡眠被信号打断,避免执行流永久睡眠,调用者需要处理返回值,判断是否获取锁成功。

(4)互斥锁实战示例:保护复杂链表(长临界区)

该示例实现一个复杂链表的插入、删除和遍历操作,临界区包含内存分配、数据拷贝等耗时操作(长临界区),使用互斥锁保护,避免多进程并发访问导致的链表错乱。

复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/mutex.h>
#include <linux/slab.h>
#include <linux/list.h>
#include <linux/kthread.h>
#include <linux/delay.h>

// 定义链表节点结构体
struct my_list_node {
    int id;                 // 节点ID
    char name[32];          // 节点名称
    struct list_head list;  // 内核链表节点
};

// 定义全局链表头和互斥锁
static LIST_HEAD(my_global_list);
static DEFINE_MUTEX(list_mutex);

// 向链表中插入节点(长临界区:包含kmalloc内存分配)
static int list_node_insert(int id, const char *name)
{
    struct my_list_node *node;
    int ret = 0;
    
    // 1. 获取互斥锁(可被信号打断)
    ret = mutex_lock_interruptible(&list_mutex);
    if (ret != 0) {
        printk(KERN_ERR "list_node_insert: mutex lock interrupted\n");
        return ret;
    }
    
    // 2. 进入临界区:分配节点内存(可能睡眠,互斥锁支持)
    node = kmalloc(sizeof(struct my_list_node), GFP_KERNEL);
    if (!node) {
        printk(KERN_ERR "list_node_insert: kmalloc failed\n");
        mutex_unlock(&list_mutex);
        return -ENOMEM;
    }
    
    // 初始化节点数据
    node->id = id;
    strncpy(node->name, name, sizeof(node->name) - 1);
    node->name[sizeof(node->name) - 1] = '\0';
    
    // 将节点插入链表头部
    list_add(&node->list, &my_global_list);
    printk(KERN_INFO "list_node_insert: insert node (id: %d, name: %s)\n",
           node->id, node->name);
    
    // 模拟长临界区:耗时操作(10毫秒)
    msleep(10);
    
    // 3. 释放互斥锁
    mutex_unlock(&list_mutex);
    
    return 0;
}

// 从链表中删除节点
static int list_node_delete(int id)
{
    struct my_list_node *node, *tmp;
    int ret = 0;
    int found = 0;
    
    // 1. 获取互斥锁
    ret = mutex_lock_interruptible(&list_mutex);
    if (ret != 0) {
        printk(KERN_ERR "list_node_delete: mutex lock interrupted\n");
        return ret;
    }
    
    // 2. 进入临界区:遍历链表,查找并删除节点
    list_for_each_entry_safe(node, tmp, &my_global_list, list) {
        if (node->id == id) {
            // 从链表中删除节点
            list_del(&node->list);
            printk(KERN_INFO "list_node_delete: delete node (id: %d, name: %s)\n",
                   node->id, node->name);
            
            // 释放节点内存
            kfree(node);
            found = 1;
            break;
        }
    }
    
    if (!found) {
        printk(KERN_WARNING "list_node_delete: node (id: %d) not found\n", id);
    }
    
    // 模拟长临界区:耗时操作(10毫秒)
    msleep(10);
    
    // 3. 释放互斥锁
    mutex_unlock(&list_mutex);
    
    return found ? 0 : -ENOENT;
}

// 遍历链表并打印节点信息
static void list_node_traverse(void)
{
    struct my_list_node *node;
    int ret = 0;
    
    // 1. 获取互斥锁
    ret = mutex_lock_interruptible(&list_mutex);
    if (ret != 0) {
        printk(KERN_ERR "list_node_traverse: mutex lock interrupted\n");
        return;
    }
    
    // 2. 进入临界区:遍历链表
    printk(KERN_INFO "list_node_traverse: start traversing list\n");
    list_for_each_entry(node, &my_global_list, list) {
        printk(KERN_INFO "list_node_traverse: node (id: %d, name: %s)\n",
               node->id, node->name);
        // 轻微延迟,模拟遍历开销
        udelay(100);
    }
    printk(KERN_INFO "list_node_traverse: finish traversing list\n");
    
    // 3. 释放互斥锁
    mutex_unlock(&list_mutex);
}

// 内核线程函数:插入节点
static int kthread_insert(void *data)
{
    int i;
    for (i = 1; i <= 5; i++) {
        char name[32];
        snprintf(name, sizeof(name), "Node_%d", i);
        list_node_insert(i, name);
        msleep(100);  // 每100毫秒插入一个节点
        if (kthread_should_stop()) {
            break;
        }
    }
    return 0;
}

// 内核线程函数:删除节点
static int kthread_delete(void *data)
{
    int i;
    msleep(500);  // 等待插入线程插入部分节点
    for (i = 1; i <= 5; i++) {
        list_node_delete(i);
        msleep(100);  // 每100毫秒删除一个节点
        if (kthread_should_stop()) {
            break;
        }
    }
    return 0;
}

// 保存内核线程PID
static struct task_struct *kthread_ins;
static struct task_struct *kthread_del;

// 模块初始化函数
static int __init mutex_example_init(void)
{
    // 创建插入节点内核线程
    kthread_ins = kthread_run(kthread_insert, NULL, "kthread_insert");
    if (IS_ERR(kthread_ins)) {
        printk(KERN_ERR "create kthread_ins failed\n");
        return PTR_ERR(kthread_ins);
    }
    
    // 创建删除节点内核线程
    kthread_del = kthread_run(kthread_delete, NULL, "kthread_delete");
    if (IS_ERR(kthread_del)) {
        printk(KERN_ERR "create kthread_del failed\n");
        kthread_stop(kthread_ins);
        return PTR_ERR(kthread_del);
    }
    
    // 遍历链表
    msleep(1000);
    list_node_traverse();
    
    printk(KERN_INFO "mutex example module init success\n");
    return 0;
}

// 模块退出函数
static void __exit mutex_example_exit(void)
{
    struct my_list_node *node, *tmp;
    
    // 停止内核线程
    kthread_stop(kthread_ins);
    kthread_stop(kthread_del);
    
    // 清理链表剩余节点
    mutex_lock(&list_mutex);
    list_for_each_entry_safe(node, tmp, &my_global_list, list) {
        list_del(&node->list);
        kfree(node);
    }
    mutex_unlock(&list_mutex);
    
    printk(KERN_INFO "mutex example module exit success\n");
}

module_init(mutex_example_init);
module_exit(mutex_example_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Developer");
MODULE_DESCRIPTION("Mutex Example for Protecting Complex Linked List");
(5)示例解析
  1. 定义了内核链表和互斥锁,互斥锁用于保护链表的插入、删除和遍历操作;
  2. 插入节点时,调用kmalloc()分配内存(可能睡眠),互斥锁支持持有锁期间睡眠,这是自旋锁无法实现的;
  3. 临界区包含内存分配、数据拷贝、链表操作等耗时操作(长临界区),使用互斥锁的睡眠等待特性,避免了自旋锁的 CPU 资源浪费;
  4. 所有链表操作都通过mutex_lock_interruptible()获取锁,mutex_unlock()释放锁,保证了操作的原子性和数据一致性;
  5. 模块退出时,清理链表剩余节点,同样需要获取互斥锁,避免并发修改。

4.2 信号量(struct semaphore):支持多执行流并发的互斥机制

信号量是一种计数型同步机制,分为二值信号量 (计数为 0 或 1,等价于互斥锁)和计数信号量(计数大于 1,支持多个执行流同时持有信号量),主要用于共享资源可被多个执行流同时访问的场景。

(1)信号量的核心特性
  1. 计数型:信号量包含一个原子计数值,当计数值 > 0 时,获取信号量成功,计数值减 1;当计数值 = 0 时,获取信号量失败,执行流进入睡眠状态;
  2. 睡眠等待:获取不到信号量时,执行流进入睡眠状态,不浪费 CPU 资源,适合长临界区;
  3. 支持多执行流并发:计数信号量允许多个执行流同时持有信号量,访问共享资源,适合 "资源池" 场景(如缓冲区池、设备池);
  4. 仅能在进程上下文使用:中断上下文不能睡眠,无法使用信号量;
  5. 支持嵌套获取(谨慎使用):与互斥锁不同,信号量允许同一个执行流嵌套获取同一个信号量,但需要保证嵌套释放,否则会导致计数值错误;
  6. 不支持优先级继承:信号量没有实现优先级继承机制,可能出现优先级反转问题;
  7. 开销高于互斥锁:信号量涉及计数值的原子操作、进程睡眠和唤醒,开销比互斥锁更大。
(2)信号量的基本使用流程
  1. 定义信号量变量struct semaphore sem;

  2. 初始化信号量

    • 静态初始化(二值信号量):DEFINE_SEMAPHORE(sem);(计数值 = 1)
    • 动态初始化:sema_init(&sem, val);val为计数值,val>=0,计数信号量常用)
  3. 获取信号量:调用相应的获取接口,进入临界区;

  4. 释放信号量:调用释放接口,退出临界区。

(3)常用信号量接口
接口函数 功能说明
void down(struct semaphore *sem) 获取信号量,计数值减 1,获取失败则进入TASK_UNINTERRUPTIBLE睡眠(不可被信号打断)
int down_interruptible(struct semaphore *sem) 获取信号量,计数值减 1,获取失败则进入TASK_INTERRUPTIBLE睡眠(可被信号打断,返回 0 成功,-EINTR 被打断)
int down_trylock(struct semaphore *sem) 尝试获取信号量,获取成功返回 0,失败返回非 0(不睡眠,直接返回)
void up(struct semaphore *sem) 释放信号量,计数值加 1,唤醒等待该信号量的执行流
(4)信号量实战示例:计数信号量实现缓冲区池管理

该示例实现一个包含 5 个缓冲区的缓冲区池,使用计数信号量(计数值 = 5)管理,允许多个进程同时获取缓冲区(最多 5 个),当缓冲区被耗尽时,后续进程进入睡眠等待,直到有缓冲区被释放。

复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/semaphore.h>
#include <linux/slab.h>
#include <linux/kthread.h>
#include <linux/delay.h>

// 缓冲区大小
#define BUFFER_SIZE 1024
// 缓冲区池大小
#define BUFFER_POOL_SIZE 5

// 定义缓冲区结构体
struct buffer_node {
    int id;                 // 缓冲区ID
    char data[BUFFER_SIZE];  // 缓冲区数据
    int in_use;             // 是否被使用(1:使用中,0:空闲)
};

// 定义缓冲区池和计数信号量
static struct buffer_node *buffer_pool[BUFFER_POOL_SIZE];
static struct semaphore buffer_sem;

// 初始化缓冲区池
static int buffer_pool_init(void)
{
    int i;
    
    // 初始化计数信号量(计数值=缓冲区池大小)
    sema_init(&buffer_sem, BUFFER_POOL_SIZE);
    
    // 分配并初始化每个缓冲区
    for (i = 0; i < BUFFER_POOL_SIZE; i++) {
        buffer_pool[i] = kmalloc(sizeof(struct buffer_node), GFP_KERNEL);
        if (!buffer_pool[i]) {
            printk(KERN_ERR "buffer_pool_init: kmalloc failed for buffer %d\n", i);
            // 释放已分配的缓冲区
            for (int j = 0; j < i; j++) {
                kfree(buffer_pool[j]);
            }
            return -ENOMEM;
        }
        
        buffer_pool[i]->id = i;
        memset(buffer_pool[i]->data, 0, BUFFER_SIZE);
        buffer_pool[i]->in_use = 0;
    }
    
    printk(KERN_INFO "buffer_pool_init: buffer pool init success, size = %d\n",
           BUFFER_POOL_SIZE);
    return 0;
}

// 获取一个空闲缓冲区
static struct buffer_node *buffer_get(void)
{
    int ret;
    int i;
    struct buffer_node *buffer = NULL;
    
    // 1. 获取信号量(计数值减1),可被信号打断
    ret = down_interruptible(&buffer_sem);
    if (ret != 0) {
        printk(KERN_ERR "buffer_get: down interrupted\n");
        return NULL;
    }
    
    // 2. 查找空闲缓冲区(临界区:修改缓冲区in_use标志)
    for (i = 0; i < BUFFER_POOL_SIZE; i++) {
        if (buffer_pool[i]->in_use == 0) {
            buffer_pool[i]->in_use = 1;
            buffer = buffer_pool[i];
            printk(KERN_INFO "buffer_get: get buffer %d, sem count = %d\n",
                   buffer->id, buffer_sem.count.counter);
            break;
        }
    }
    
    // 理论上不会进入该分支(信号量保证有空闲缓冲区)
    if (!buffer) {
        printk(KERN_ERR "buffer_get: no free buffer\n");
        up(&buffer_sem);  // 释放信号量,计数值加1
    }
    
    return buffer;
}

// 释放一个缓冲区
static void buffer_put(struct buffer_node *buffer)
{
    if (!buffer || buffer->id < 0 || buffer->id >= BUFFER_POOL_SIZE) {
        printk(KERN_ERR "buffer_put: invalid buffer\n");
        return;
    }
    
    // 1. 标记缓冲区为空闲(临界区)
    buffer->in_use = 0;
    memset(buffer->data, 0, BUFFER_SIZE);
    printk(KERN_INFO "buffer_put: put buffer %d, sem count = %d\n",
           buffer->id, buffer_sem.count.counter);
    
    // 2. 释放信号量(计数值加1),唤醒等待的执行流
    up(&buffer_sem);
}

// 内核线程函数:使用缓冲区
static int kthread_buffer_use(void *data)
{
    int thread_id = (int)(unsigned long)data;
    struct buffer_node *buffer;
    
    for (int i = 0; i < 3; i++) {
        // 获取缓冲区
        buffer = buffer_get();
        if (!buffer) {
            continue;
        }
        
        // 模拟使用缓冲区(耗时操作:500毫秒)
        snprintf(buffer->data, BUFFER_SIZE, "Thread %d, Use %d", thread_id, i);
        msleep(500);
        
        // 释放缓冲区
        buffer_put(buffer);
        
        // 轻微延迟
        msleep(100);
        
        if (kthread_should_stop()) {
            break;
        }
    }
    
    printk(KERN_INFO "kthread %d: finished using buffer\n", thread_id);
    return 0;
}

// 保存内核线程PID
static struct task_struct *kthreads[10];

// 模块初始化函数
static int __init semaphore_example_init(void)
{
    int ret;
    int i;
    
    // 初始化缓冲区池
    ret = buffer_pool_init();
    if (ret != 0) {
        return ret;
    }
    
    // 创建10个内核线程,并发使用缓冲区(缓冲区池大小为5,最多5个线程同时使用)
    for (i = 0; i < 10; i++) {
        kthreads[i] = kthread_run(kthread_buffer_use, (void *)(unsigned long)i,
                                  "kthread_buffer_%d", i);
        if (IS_ERR(kthreads[i])) {
            printk(KERN_ERR "create kthread %d failed\n", i);
            // 停止已创建的线程
            for (int j = 0; j < i; j++) {
                kthread_stop(kthreads[j]);
            }
            return PTR_ERR(kthreads[i]);
        }
    }
    
    printk(KERN_INFO "semaphore example module init success\n");
    return 0;
}

// 模块退出函数
static void __exit semaphore_example_exit(void)
{
    int i;
    
    // 停止所有内核线程
    for (i = 0; i < 10; i++) {
        if (!IS_ERR(kthreads[i])) {
            kthread_stop(kthreads[i]);
        }
    }
    
    // 释放缓冲区池内存
    for (i = 0; i < BUFFER_POOL_SIZE; i++) {
        kfree(buffer_pool[i]);
        buffer_pool[i] = NULL;
    }
    
    printk(KERN_INFO "semaphore example module exit success\n");
}

module_init(semaphore_example_init);
module_exit(semaphore_example_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Developer");
MODULE_DESCRIPTION("Semaphore Example for Buffer Pool Management");
(5)示例解析
  1. 初始化计数信号量,计数值设为缓冲区池大小(5),表示最多允许 5 个执行流同时获取缓冲区;
  2. 线程获取缓冲区时,先调用down_interruptible()获取信号量(计数值减 1),若计数值 > 0 则获取成功,否则进入睡眠等待;
  3. 线程释放缓冲区时,调用up()释放信号量(计数值加 1),唤醒等待缓冲区的线程;
  4. 10 个线程并发访问仅有 5 个缓冲区的缓冲区池,信号量保证了最多 5 个线程同时使用缓冲区,避免了资源耗尽导致的冲突;
  5. 信号量的计数值与空闲缓冲区数量一致,通过计数机制实现了多执行流的并发访问。

4.3 互斥锁与信号量的核心区别

特性 互斥锁(mutex_t 信号量(struct semaphore
锁类型 排他锁(严格互斥) 计数型(支持共享)
优先级继承 支持 不支持
嵌套获取 不允许(死锁) 允许(谨慎使用)
适用场景 严格互斥、长临界区、实时系统 资源池、多执行流并发、非实时系统
开销 较低 较高
等价关系 等价于二值信号量(计数 = 1) 二值信号量是信号量的特例

五、其他高级同步手段:应对特殊场景

除了原子操作、自旋锁、互斥锁和信号量,Linux 内核还提供了一些高级同步机制,用于应对特殊场景(如读多写少、高并发读),包括读写锁、RCU 机制和内存屏障。

5.1 读写锁(rwlock_t):读多写少场景的优化

读写锁是一种优化的自旋锁,将访问者分为读者写者,允许多个读者同时访问共享资源(只读操作),但写者必须排他访问(同一时刻只能有一个写者,且写者访问时不允许任何读者访问)。

(1)读写锁的核心特性
  1. 读者共享,写者排他:多个读者可同时持有读锁,写者必须独占写锁;
  2. 写者优先级高于读者:大部分内核版本中,写者优先级高于读者,当有写者等待时,新的读者会被阻塞,防止写者被 "饿死";
  3. 不可睡眠,支持中断上下文:基于自旋锁实现,持有锁期间不能睡眠,提供中断安全接口;
  4. 适合读多写少场景:并发性能远高于自旋锁和互斥锁。
(2)常用读写锁接口
接口类型 读者接口 写者接口
基本接口 read_lock() / read_unlock() write_lock() / write_unlock()
中断安全接口 read_lock_irqsave() / read_unlock_irqrestore() write_lock_irqsave() / write_unlock_irqrestore()
软中断安全接口 read_lock_bh() / read_unlock_bh() write_lock_bh() / write_unlock_bh()
(3)适用场景

适合 "读多写少" 的场景,如系统配置、设备状态查询、只读链表遍历等,例如内核中的文件系统目录项缓存、网络协议栈的路由表等。

5.2 RCU 机制:高并发读场景的极致优化

RCU(Read-Copy-Update,读 - 拷贝 - 更新)是一种高效的同步机制,专门针对 "读多写极少" 的场景,其核心思想是读者无锁访问,写者拷贝更新,延迟释放旧资源

(1)RCU 机制的核心特性
  1. 读者无锁:读者访问共享资源时不需要加锁,几乎没有开销,并发性能极高;
  2. 写者拷贝更新:写者不直接修改原资源,而是拷贝一份副本,在副本上修改,修改完成后原子地替换原资源;
  3. 延迟释放:写者替换资源后,等待所有正在访问旧资源的读者完成访问(宽限期),再释放旧资源;
  4. 适合高并发读:读者可以无限制并发,写者开销较大,适用于写操作极少的场景。
(2)RCU 的核心接口
  1. 读者接口:rcu_read_lock()(进入读临界区)、rcu_read_unlock()(退出读临界区);
  2. 写者接口:synchronize_rcu()(等待宽限期)、call_rcu()(注册延迟释放回调)。
(3)适用场景

适合高并发读、写极少的场景,如内核中的链表、进程描述符、文件系统的 inode 缓存等。

5.3 内存屏障:解决指令重排与内存可见性问题

内存屏障(Memory Barrier)是一种底层同步手段,不直接保护共享资源 ,而是通过约束 CPU 和编译器的指令执行顺序,解决多核架构下的指令重排内存可见性问题,保障同步机制的正确性。

(1)内存屏障的核心背景

现代 CPU 为了提升执行效率,会对指令进行乱序执行 (Out-of-Order Execution);编译器为了优化性能,也会对代码进行指令重排。这些优化在单线程下是透明的,但在多核并发场景下,会导致严重的逻辑错误。

举个例子:

复制代码
// CPU 0执行的代码
int a = 1;    // 写操作1
int flag = 1; // 写操作2

// CPU 1执行的代码
while (flag != 1); // 读操作1
printk("a = %d\n", a); // 读操作2

理想情况下,CPU 1 会打印a = 1,但由于指令重排,CPU 0 可能先执行flag=1,再执行a=1。此时 CPU 1 检测到flag=1后,读取a的值可能还是 0,导致逻辑错误。

内存屏障的作用就是插入一个 "屏障点",强制屏障前后的指令不能重排,且屏障前的内存操作必须全部完成后,才能执行屏障后的操作。

(2)内存屏障的分类与常用接口

Linux 内核提供了三类内存屏障接口,分别对应不同的约束范围:

屏障类型 核心作用 常用接口 适用场景
编译器屏障 阻止编译器对屏障前后的指令重排,不影响 CPU 乱序执行 barrier() 单 CPU 内核,仅需约束编译器优化
CPU 读屏障 保证屏障前的读操作全部完成后,再执行屏障后的读操作 rmb() 多核架构下,读操作的顺序约束
CPU 写屏障 保证屏障前的写操作全部完成后,再执行屏障后的写操作 wmb() 多核架构下,写操作的顺序约束
CPU 读写屏障 同时约束读、写操作的顺序,最强约束 mb() 多核架构下,读写混合操作的顺序约束

实战示例:用内存屏障解决指令重排问题

复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/delay.h>
#include <linux/compiler.h>

static int a = 0;
static int flag = 0;

// CPU 0对应的内核线程
static int kthread_cpu0(void *data)
{
    a = 1;
    // 插入写屏障:强制a=1的写操作完成后,再执行flag=1
    wmb();
    flag = 1;
    printk(KERN_INFO "kthread_cpu0: a = %d, flag = %d\n", a, flag);
    return 0;
}

// CPU 1对应的内核线程
static int kthread_cpu1(void *data)
{
    // 等待flag被设置为1
    while (flag != 1) {
        cpu_relax(); // 降低CPU占用
    }
    // 插入读屏障:强制flag=1的读操作完成后,再读取a的值
    rmb();
    printk(KERN_INFO "kthread_cpu1: a = %d, flag = %d\n", a, flag);
    return 0;
}

static struct task_struct *thread0, *thread1;

static int __init memory_barrier_init(void)
{
    thread0 = kthread_run(kthread_cpu0, NULL, "kthread_cpu0");
    if (IS_ERR(thread0)) return PTR_ERR(thread0);

    thread1 = kthread_run(kthread_cpu1, NULL, "kthread_cpu1");
    if (IS_ERR(thread1)) {
        kthread_stop(thread0);
        return PTR_ERR(thread1);
    }

    printk(KERN_INFO "memory barrier module init success\n");
    return 0;
}

static void __exit memory_barrier_exit(void)
{
    kthread_stop(thread0);
    kthread_stop(thread1);
    printk(KERN_INFO "memory barrier module exit success\n");
}

module_init(memory_barrier_init);
module_exit(memory_barrier_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Memory Barrier Example");

示例解析

  • kthread_cpu0中,wmb()屏障确保a=1的写操作先于flag=1完成,避免编译器或 CPU 将两个写操作重排;
  • kthread_cpu1中,rmb()屏障确保读取flag的值为 1 后,再读取a的值,避免 CPU 乱序执行导致的读取错误;
  • 最终kthread_cpu1会稳定打印a=1,解决了指令重排带来的问题。
(3)内存屏障的注意事项
  1. 内存屏障是架构相关的,不同 CPU 架构(x86、ARM、RISC-V)的内存屏障实现和强度不同,Linux 内核封装的接口是跨架构的,推荐使用内核接口而非直接使用 CPU 指令;
  2. 内存屏障仅约束指令顺序,不提供互斥保护,通常需要和自旋锁、互斥锁等同步机制配合使用;
  3. 内存屏障会降低系统性能,应尽量减少使用,仅在必要场景下插入。

六、死锁的避免:从根源上规避同步陷阱

死锁是多执行流同步过程中最棘手的问题之一,指多个执行流因互相等待对方持有的资源,而陷入永久阻塞的状态。例如:执行流 A 持有锁 1,等待锁 2;执行流 B 持有锁 2,等待锁 1,此时两个执行流会无限等待,无法推进。

6.1 死锁产生的四个必要条件

死锁的产生必须同时满足以下四个条件,只要破坏其中任何一个,就能避免死锁:

  1. 互斥条件:资源是排他的,同一时刻只能被一个执行流持有(如自旋锁、互斥锁);
  2. 持有并等待条件:执行流持有一个资源的同时,等待另一个资源;
  3. 不可剥夺条件:资源不能被强制剥夺,只能由持有资源的执行流主动释放;
  4. 循环等待条件:多个执行流形成环形的资源等待链(如 A→B→C→A)。

6.2 死锁避免的核心策略

(1)资源有序分配:破坏循环等待条件

这是最常用、最有效的死锁避免策略,核心思想是为所有资源分配一个全局唯一的序号 ,执行流必须按照序号递增 的顺序申请资源,按照序号递减的顺序释放资源。

实战示例:有序申请自旋锁避免死锁

复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/spinlock.h>
#include <linux/kthread.h>

// 定义两个自旋锁,并分配序号:lock1(序号1) < lock2(序号2)
static DEFINE_SPINLOCK(lock1);
static DEFINE_SPINLOCK(lock2);

// 执行流A:按序号递增顺序申请锁(lock1 → lock2)
static int kthread_a(void *data)
{
    spin_lock(&lock1);
    printk(KERN_INFO "kthread_a: hold lock1, waiting for lock2\n");
    // 模拟持有lock1后,执行一些操作
    udelay(100);
    spin_lock(&lock2);
    printk(KERN_INFO "kthread_a: hold lock1 and lock2\n");

    // 按序号递减顺序释放锁(lock2 → lock1)
    spin_unlock(&lock2);
    spin_unlock(&lock1);
    return 0;
}

// 执行流B:按序号递增顺序申请锁(lock1 → lock2)
static int kthread_b(void *data)
{
    spin_lock(&lock1);
    printk(KERN_INFO "kthread_b: hold lock1, waiting for lock2\n");
    udelay(100);
    spin_lock(&lock2);
    printk(KERN_INFO "kthread_b: hold lock1 and lock2\n");

    spin_unlock(&lock2);
    spin_unlock(&lock1);
    return 0;
}

static struct task_struct *thread_a, *thread_b;

static int __init deadlock_avoid_init(void)
{
    thread_a = kthread_run(kthread_a, NULL, "kthread_a");
    if (IS_ERR(thread_a)) return PTR_ERR(thread_a);

    thread_b = kthread_run(kthread_b, NULL, "kthread_b");
    if (IS_ERR(thread_b)) {
        kthread_stop(thread_a);
        return PTR_ERR(thread_b);
    }

    printk(KERN_INFO "deadlock avoid module init success\n");
    return 0;
}

static void __exit deadlock_avoid_exit(void)
{
    kthread_stop(thread_a);
    kthread_stop(thread_b);
    printk(KERN_INFO "deadlock avoid module exit success\n");
}

module_init(deadlock_avoid_init);
module_exit(deadlock_avoid_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Deadlock Avoid Example with Ordered Lock");

示例解析

  • lock1lock2分配序号 1 和 2,规定必须先申请序号小的锁,再申请序号大的锁;
  • 执行流 A 和 B 都严格遵守这个顺序,即使并发执行,也不会形成循环等待链,从而避免死锁;
  • 若执行流 B 违反顺序,先申请lock2再申请lock1,则可能与执行流 A 形成死锁。
(2)避免嵌套锁:破坏持有并等待条件

嵌套锁指执行流持有一个锁的同时,申请另一个锁,这是死锁的高发场景。尽量避免嵌套锁,如果必须嵌套,务必遵守资源有序分配原则。

如果临界区可以拆分,建议将嵌套锁拆分为多个独立的临界区,减少锁的持有时间和嵌套层级:

复制代码
// 不推荐的嵌套锁写法
spin_lock(&lock1);
// 临界区1
spin_lock(&lock2);
// 临界区2(同时持有lock1和lock2)
spin_unlock(&lock2);
spin_unlock(&lock1);

// 推荐的拆分写法(无嵌套)
spin_lock(&lock1);
// 临界区1
spin_unlock(&lock1);

spin_lock(&lock2);
// 临界区2
spin_unlock(&lock2);
(3)使用超时机制:破坏不可剥夺条件

对于支持超时的同步接口(如mutex_lock_interruptible()down_timeout()),可以设置超时时间,当执行流等待资源超过阈值时,主动放弃等待,释放已持有的资源,从而避免死锁。

实战示例:信号量超时机制避免死锁

复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/semaphore.h>
#include <linux/kthread.h>
#include <linux/delay.h>

static DEFINE_SEMAPHORE(sem1);
static DEFINE_SEMAPHORE(sem2);

static int kthread_timeout(void *data)
{
    // 申请sem1
    if (down_interruptible(&sem1) != 0) {
        return -EINTR;
    }
    printk(KERN_INFO "kthread_timeout: hold sem1, waiting for sem2\n");

    // 申请sem2,设置超时时间为1秒
    if (down_timeout(&sem2, msecs_to_jiffies(1000)) != 0) {
        printk(KERN_ERR "kthread_timeout: wait sem2 timeout\n");
        // 超时后释放已持有的sem1,避免死锁
        up(&sem1);
        return -ETIMEDOUT;
    }

    printk(KERN_INFO "kthread_timeout: hold sem1 and sem2\n");
    up(&sem2);
    up(&sem1);
    return 0;
}

static struct task_struct *thread;

static int __init sem_timeout_init(void)
{
    thread = kthread_run(kthread_timeout, NULL, "kthread_timeout");
    if (IS_ERR(thread)) return PTR_ERR(thread);
    printk(KERN_INFO "semaphore timeout module init success\n");
    return 0;
}

static void __exit sem_timeout_exit(void)
{
    kthread_stop(thread);
    printk(KERN_INFO "semaphore timeout module exit success\n");
}

module_init(sem_timeout_init);
module_exit(sem_timeout_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Semaphore Timeout for Deadlock Avoid");

示例解析

  • 执行流先获取sem1,再通过down_timeout()申请sem2,并设置 1 秒超时;
  • 若 1 秒内无法获取sem2,则超时返回,执行流主动释放sem1,避免因永久等待导致的死锁;
  • 超时机制适用于对响应时间有要求的场景,是破坏 "不可剥夺条件" 的有效手段。
(4)其他辅助策略
  1. 减少锁的持有时间:临界区越小,持有锁的时间越短,发生死锁的概率越低;
  2. 避免在中断上下文持有锁:中断上下文无法睡眠,若持有锁时被更高优先级的中断打断,容易导致死锁;
  3. 使用死锁检测工具 :Linux 内核提供了lockdep工具,可在编译内核时启用,用于检测代码中的死锁隐患。

七、内核同步机制选择指南:按场景精准匹配

选择正确的同步机制是内核开发的核心技能,错误的选择会导致系统性能低下甚至崩溃。以下是基于临界区长度执行流类型访问模式的选择指南:

场景特征 推荐同步机制 禁止使用的机制
简单整数 / 位操作(如引用计数、标志位) 原子操作(atomic_t、位操作) 自旋锁、互斥锁(开销过大)
短临界区(< 10us)、中断上下文参与 自旋锁(spin_lock_irqsave 互斥锁、信号量(无法睡眠)
长临界区(> 10us)、仅进程上下文 互斥锁(mutex_t 自旋锁(CPU 忙等浪费资源)
资源池管理、多执行流并发访问 计数信号量(struct semaphore 自旋锁、互斥锁(不支持共享)
读多写少、进程 / 中断上下文 读写锁(rwlock_t 普通自旋锁(并发读性能差)
高并发读、写极少(如路由表、inode 缓存) RCU 机制 互斥锁、信号量(读操作有锁开销)
多核架构、指令重排问题 内存屏障(mb()/wmb()/rmb() 无(需配合其他同步机制)

八、总结

Linux 内核同步机制是解决竞态问题的核心工具,从简单的原子操作到复杂的 RCU 机制,每种机制都有其适用场景和核心特性:

  • 原子操作是基础,适用于简单整数和位操作;
  • 自旋锁是短临界区和中断上下文的首选,核心是忙等待;
  • 互斥锁和信号量适用于长临界区,核心是睡眠等待;
  • 读写锁和 RCU是特殊场景的优化,分别针对读多写少和高并发读;
  • 内存屏障解决指令重排和内存可见性问题,是同步机制的 "基石"。

死锁的避免是同步编程的必修课,通过资源有序分配避免嵌套锁超时机制等策略,可从根源上规避死锁风险。

在实际内核开发中,需结合场景特征,精准选择同步机制,在数据一致性系统性能之间找到最佳平衡点。

相关推荐
郝学胜-神的一滴1 小时前
Python数据封装与私有属性:保护你的数据安全
linux·服务器·开发语言·python·程序人生
٩( 'ω' )و2602 小时前
linux--库的制作与原理
linux
海盗12342 小时前
VMware 中 CentOS 7 无法使用 yum 安装 wget 的完整解决方案
linux·运维·centos
gtr20203 小时前
Ubuntu24.04 基于 EtherCAT 的 SVD60N 主站
linux·ethercat
weixin_462446233 小时前
ubuntu真机安装tljh jupyterhub支持跨域iframe
linux·运维·ubuntu
小码吃趴菜3 小时前
select/poll/epoll 核心区别
linux
Ghost Face...3 小时前
深入解析网卡驱动开发与移植
linux·驱动开发
a41324473 小时前
在CentOS系统上挂载硬盘到ESXi虚拟机
linux·运维·centos
MMME~3 小时前
Linux下的软件管理
linux·运维·服务器