一、先搞懂:内核为什么需要「锁」
Linux 内核是多 CPU、抢占式、并发执行的:
- 多个进程同时调用你的驱动
- 多个内核线程同时操作同一个全局变量
- 中断来了会打断当前代码,也会操作共享资源
共享资源:全局变量、硬件寄存器、缓冲区、链表、设备结构体等。
如果不加锁,会出现竞态条件:多个执行流同时读改写同一份数据 → 数据错乱、崩溃、死机、死锁。
锁的核心作用 :保证同一时刻只有一个执行流访问共享资源,串行化操作。
二、内核常用锁分类(驱动必学就这 4 类)
- 原子操作(最轻量,简单变量防并发)
- 自旋锁
spinlock_t(中断 / 多核高频场景) - 互斥锁
mutex(会休眠,适合耗时操作) - 信号量
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(驱动最常用)
核心原理
拿不到锁时原地空转、忙等待 ,一直轮询直到拿到锁,绝不休眠。
适用场景
- 中断上下文(中断里不能休眠)
- 持有锁的代码执行时间极短
- 多核 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,等锁释放后再唤醒。
适用场景
- 进程上下文(不能在中断、tasklet 里用!)
- 临界区执行时间较长
- 需要等待、可以休眠的场景
特点
- 会休眠,不能用在中断上下文
- 同一时刻只能一个人持有锁
- 用法简单,可读性好
代码模板
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 | 会休眠 | 谨慎用 | 并发限流、生产者消费者 |
四、驱动开发黄金选型规则(直接照抄)
- 只是一个整数计数 → 用 原子操作
- 代码很短、还要和中断共享 → 用 spinlock_irqsave
- 进程调用、代码有点耗时、无中断 → 用 mutex 互斥锁
- 要控制最大并发数、缓冲区队列 → 用 信号量
五、新手写驱动最容易犯的锁错误
- 中断里用 mutex / 信号量 → 内核 Oops 崩溃
- 自旋锁临界区里写耗时、休眠函数
- 加锁后忘记解锁
- 多个锁顺序颠倒导致死锁
带自旋锁 + 完整 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");