Linux 内核驱动 —— 锁机制

一、先搞懂:内核为什么需要「锁」

Linux 内核是多 CPU、抢占式、并发执行的:

  • 多个进程同时调用你的驱动
  • 多个内核线程同时操作同一个全局变量
  • 中断来了会打断当前代码,也会操作共享资源

共享资源:全局变量、硬件寄存器、缓冲区、链表、设备结构体等。

如果不加锁,会出现竞态条件:多个执行流同时读改写同一份数据 → 数据错乱、崩溃、死机、死锁。

锁的核心作用 :保证同一时刻只有一个执行流访问共享资源,串行化操作。


二、内核常用锁分类(驱动必学就这 4 类)

  1. 原子操作(最轻量,简单变量防并发)
  2. 自旋锁 spinlock_t(中断 / 多核高频场景)
  3. 互斥锁 mutex(会休眠,适合耗时操作)
  4. 信号量 semaphore(比 mutex 更灵活,可允许多个并发)

从简单到复杂、使用场景逐个讲。


1. 原子操作 atomic_t

是什么

整形变量的读 - 改 - 写 变成不可分割的一步,不用加复杂锁,纯硬件指令实现。

适用场景

只做简单计数、标志位:引用计数、状态标记、并发计数。

特点

  • 不休眠、不阻塞
  • 开销极小
  • 只能操作整数,不能保护一大段代码逻辑

用法示例

cpp 复制代码
// 定义原子变量
atomic_t cnt = ATOMIC_INIT(0);

// 自增
atomic_inc(&cnt);
// 自减
atomic_dec(&cnt);
// 取值
int val = atomic_read(&cnt);

2. 自旋锁 spinlock_t(驱动最常用)

核心原理

拿不到锁时原地空转、忙等待 ,一直轮询直到拿到锁,绝不休眠

适用场景

  1. 中断上下文(中断里不能休眠)
  2. 持有锁的代码执行时间极短
  3. 多核 CPU 之间保护共享资源

关键特性

  • 不能休眠、不能调用会阻塞的函数
  • 可以保护进程上下文 + 中断上下文并发
  • 开销小,适合短临界区

常用配套接口

cpp 复制代码
// 定义自旋锁
spinlock_t lock;

// 初始化
spin_lock_init(&lock);

// 加锁 + 解锁(进程上下文用)
spin_lock(&lock);
// 临界区:操作共享变量/硬件
spin_unlock(&lock);

重点:带中断保护版本(驱动必用)

如果共享资源既被进程调用、又被中断访问,要用:

cpp 复制代码
unsigned long flags;

// 关本地中断 + 加锁
spin_lock_irqsave(&lock, flags);

// 临界区操作共享资源

// 解锁 + 恢复中断
spin_unlock_irqrestore(&lock, flags);

作用:防止中断打断当前加锁流程,避免死锁

一句话总结自旋锁

拿不到就原地转圈等,不许睡觉,适合短操作、中断里能用。


3. 互斥锁 struct mutex

核心原理

拿不到锁时进程主动休眠,让出 CPU,等锁释放后再唤醒。

适用场景

  1. 进程上下文(不能在中断、tasklet 里用!)
  2. 临界区执行时间较长
  3. 需要等待、可以休眠的场景

特点

  • 会休眠,不能用在中断上下文
  • 同一时刻只能一个人持有锁
  • 用法简单,可读性好

代码模板

cpp 复制代码
// 定义互斥体
struct mutex mtx;

// 初始化
mutex_init(&mtx);

// 加锁
mutex_lock(&mtx);

// 临界区操作共享资源

// 解锁
mutex_unlock(&mtx);

禁忌

中断里绝对不能用 mutex,一用直接内核崩溃。

一句话总结互斥锁

拿不到锁就去睡觉,不占 CPU,适合耗时操作,只能进程上下文用。


4. 信号量 semaphore

是什么

比 mutex 更通用的同步机制,可以设置允许 N 个执行流同时进入临界区

  • 设为 1:等价于互斥锁
  • 设为 N:允许 N 个并发访问(生产者消费者、限流)

特点

  • 也会休眠,同样不能在中断上下文随便用
  • 支持计数、适合资源池、缓冲区队列

简单用法

cpp 复制代码
struct semaphore sem;

// 初始值1:互斥效果
sema_init(&sem, 1);

down(&sem);   // 申请信号量
// 临界区
up(&sem);     // 释放信号量

三、一张表分清:4 种锁怎么选(背下来就能写驱动)

锁类型 是否休眠 能否用在中断 适用场景
原子操作 不休眠 可以 简单计数、标志位
自旋锁 不休眠 可以 短临界区、多核、中断共享资源
互斥锁 mutex 会休眠 不可以 进程上下文、耗时操作
信号量 sem 会休眠 谨慎用 并发限流、生产者消费者

四、驱动开发黄金选型规则(直接照抄)

  1. 只是一个整数计数 → 用 原子操作
  2. 代码很短、还要和中断共享 → 用 spinlock_irqsave
  3. 进程调用、代码有点耗时、无中断 → 用 mutex 互斥锁
  4. 要控制最大并发数、缓冲区队列 → 用 信号量

五、新手写驱动最容易犯的锁错误

  1. 中断里用 mutex / 信号量 → 内核 Oops 崩溃
  2. 自旋锁临界区里写耗时、休眠函数
  3. 加锁后忘记解锁
  4. 多个锁顺序颠倒导致死锁

带自旋锁 + 完整 read 函数的字符设备驱动

cpp 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/spinlock.h>
#include <linux/uaccess.h>  // copy_to_user 需要

#define CDEV_MAJOR 255
#define CDEV_NAME  "spinlock_demo"

static struct cdev    my_cdev;
static struct class  *my_class;
static struct device *my_dev;

// 共享资源:全局变量(必须加锁保护)
static int g_value = 100;

// 自旋锁
static spinlock_t my_lock;

// ------------------- open -------------------
static int my_cdev_open(struct inode *inode, struct file *file)
{
    pr_info("driver opened\n");
    return 0;
}

// ------------------- release -------------------
static int my_cdev_release(struct inode *inode, struct file *file)
{
    pr_info("driver closed\n");
    return 0;
}

// ------------------- read(关键!) -------------------
static ssize_t my_cdev_read(
    struct file *file,
    char __user *buf,
    size_t size,
    loff_t *ppos
)
{
    unsigned long flags;
    int value_snapshot;

    if (size < sizeof(int))
        return -EINVAL;
    if (*ppos > 0)
        return 0;

    // 自旋锁 + 关中断
    spin_lock_irqsave(&my_lock, flags);

    // 每次读取,g_value 自增 1(演示共享资源修改)
    g_value++;
    value_snapshot = g_value;

    // 解锁
    spin_unlock_irqrestore(&my_lock, flags);

    // copy_to_user 可能缺页,不能放在自旋锁临界区内
    if (copy_to_user(buf, &value_snapshot, sizeof(int))) {
        pr_err("copy_to_user failed\n");
        return -EFAULT;
    }

    *ppos += sizeof(int);
    pr_info("read: g_value = %d\n", value_snapshot);
    return sizeof(int);
}

// 文件操作集合
static struct file_operations my_fops = {
    .owner   = THIS_MODULE,
    .open    = my_cdev_open,
    .release = my_cdev_release,
    .read    = my_cdev_read,  // 现在有 read 了!
};

// ------------------- 驱动入口 -------------------
static int __init my_drv_init(void)
{
    int ret;
    dev_t devno = MKDEV(CDEV_MAJOR, 0);

    // 初始化自旋锁
    spin_lock_init(&my_lock);

    // 注册设备号
    ret = register_chrdev_region(devno, 1, CDEV_NAME);
    if (ret < 0) {
        pr_err("register chrdev failed\n");
        return ret;
    }

    // 注册字符设备
    cdev_init(&my_cdev, &my_fops);
    cdev_add(&my_cdev, devno, 1);

    // 自动创建设备节点
    my_class = class_create("spinlock_class");
    my_dev = device_create(my_class, NULL, devno, NULL, CDEV_NAME);

    pr_info("spinlock driver init ok\n");
    return 0;
}

// ------------------- 驱动出口 -------------------
static void __exit my_drv_exit(void)
{
    dev_t devno = MKDEV(CDEV_MAJOR, 0);

    device_destroy(my_class, devno);
    class_destroy(my_class);
    cdev_del(&my_cdev);
    unregister_chrdev_region(devno, 1);

    pr_info("spinlock driver exit ok\n");
}

module_init(my_drv_init);
module_exit(my_drv_exit);

MODULE_LICENSE("GPL");
相关推荐
技术钱3 小时前
OutputParser输出解析器
linux·服务器·前端·python
七七powerful3 小时前
AI+运维提效--证书有效期监控系统实施方案
运维
先知后行。4 小时前
Liunx驱动 IO 模型
linux·运维·服务器
计算机安禾4 小时前
【Linux从入门到精通】第39篇:版本控制Git服务器搭建——Gitea/GitLab私有化部署
linux·服务器·git
可视化运维管理爱好者4 小时前
pi mono操作开发指南
运维·网络·ai
浪客灿心4 小时前
Linux网络HTTP协议
linux
橙子也要努力变强4 小时前
volatile与信号
linux·服务器·c++
蜡笔小新拯救世界4 小时前
部分安全笔记总结
linux·网络·web安全
醇氧5 小时前
WSL 安装 Ubuntu 完整步骤(Windows 10/11 通用,极简无脑版)
linux·windows·ubuntu