前言
在上一篇文章中,我们实现了一个可读写的内存缓冲区设备。但它有一个严重缺陷:完全没有并发保护 。当多个进程同时打开设备进行读写时,内核缓冲区 kernel_buf 和 data_size 可能被交叉修改,导致数据错乱甚至内核崩溃。
本文将在现有驱动的基础上,引入内核互斥锁(mutex),让驱动在多进程环境下也能安全稳定地运行。你将掌握:
- 竞态条件的概念及其危害
- Linux内核互斥锁
mutex的初始化、加锁与解锁 - 如何用
mutex保护字符设备驱动的临界区 - 多进程并发测试的方法

一、为什么要引入并发控制?
1.1 什么是竞态条件?
以我们的内存缓冲区设备为例,假设有两个进程A和B同时执行写操作:
- 进程A调用
chrdev_write,准备写入 500 字节。 - 内核调度器在
copy_from_user之后、更新data_size之前将CPU交给进程B。 - 进程B调用
chrdev_write,写入 200 字节,将data_size更新为 200。 - 调度器切回进程A,进程A将
data_size更新为 500。
此时 data_size = 500,但内核缓冲区中的实际内容是进程B写入的 200 字节加上未知数据。之后读取时会读到脏数据,逻辑彻底混乱。
这就是典型的 竞态条件(Race Condition):多个执行单元同时访问共享资源,而最终结果依赖于执行顺序。
1.2 解决方案:互斥锁
互斥锁(mutex)保证同一时刻 只有一个执行单元 能进入临界区(访问共享资源)。其他竞争者必须等待锁释放。
Linux内核提供了 struct mutex 及相关操作:
c
mutex_init(&lock); // 动态初始化互斥锁
mutex_lock(&lock); // 获取锁(若锁已被占用则睡眠等待)
mutex_unlock(&lock); // 释放锁
本驱动有两个共享资源需要保护:
kernel_buf[1024]数据缓冲区data_size有效数据长度
我们将用一把 mutex 锁住整个 read 和 write 中对共享资源访问的代码段。
二、驱动代码实现(带mutex保护)
以下代码基于第二篇文章的版本修改,新增了 mutex 的保护,其余框架不变。所有改动都用注释标明。
新建 chrdev_mutex.c,直接复制即可编译运行。
c
/*
* chrdev_mutex.c
* 带互斥锁保护的字符设备驱动。
* 使用 mutex 确保多进程并发访问时缓冲区数据一致性。
* 加载后生成 /dev/chrdev_mtx。
* 作者:[你的ID]
* 适配内核:Linux 5.x (4.x 亦可)
*/
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/uaccess.h>
#include <linux/mutex.h> /* 互斥锁头文件 */
#define DEVICE_NAME "chrdev_mtx"
#define CLASS_NAME "chrdev_mtx_class"
#define BUF_SIZE 1024
static dev_t dev_num;
static struct cdev my_cdev;
static struct class *my_class;
static struct device *my_device;
/* 共享资源 */
static char kernel_buf[BUF_SIZE];
static size_t data_size;
/* 互斥锁定义 */
static DEFINE_MUTEX(buf_mutex); // 静态定义并初始化锁
/*
* 打开设备:无需额外操作,直接返回成功。
* 注意:open 本身不操作共享资源,因此无需加锁。
*/
static int chrdev_open(struct inode *inode, struct file *file)
{
pr_info("chrdev_mtx: device opened\n");
return 0;
}
/* 关闭设备 */
static int chrdev_release(struct inode *inode, struct file *file)
{
pr_info("chrdev_mtx: device closed\n");
return 0;
}
/* 读取设备:加锁保护整个操作 */
static ssize_t chrdev_read(struct file *file, char __user *buf,
size_t count, loff_t *f_pos)
{
ssize_t ret_bytes;
unsigned long not_copied;
/* 获取锁:如果锁被其他进程持有,当前进程会睡眠等待 */
if (mutex_lock_interruptible(&buf_mutex)) {
/* 如果在等待过程中收到致命信号,返回 -ERESTARTSYS */
return -ERESTARTSYS;
}
/* --- 临界区开始 --- */
if (data_size == 0) {
pr_info("chrdev_mtx: buffer empty, read returns EOF\n");
ret_bytes = 0;
goto out_unlock;
}
ret_bytes = (count < data_size) ? count : data_size;
not_copied = copy_to_user(buf, kernel_buf, ret_bytes);
if (not_copied != 0) {
pr_err("chrdev_mtx: copy_to_user failed, %lu bytes not copied\n",
not_copied);
ret_bytes = -EFAULT;
goto out_unlock;
}
pr_info("chrdev_mtx: read %zd bytes from buffer\n", ret_bytes);
/* 消费模式:读取后清空缓冲区 */
memset(kernel_buf, 0, BUF_SIZE);
data_size = 0;
/* --- 临界区结束 --- */
out_unlock:
mutex_unlock(&buf_mutex); // 释放锁
return ret_bytes;
}
/* 写入设备:加锁保护整个操作 */
static ssize_t chrdev_write(struct file *file, const char __user *buf,
size_t count, loff_t *f_pos)
{
unsigned long not_copied;
size_t write_bytes;
ssize_t ret;
if (mutex_lock_interruptible(&buf_mutex))
return -ERESTARTSYS;
/* --- 临界区开始 --- */
write_bytes = (count < BUF_SIZE) ? count : BUF_SIZE;
not_copied = copy_from_user(kernel_buf, buf, write_bytes);
if (not_copied != 0) {
pr_err("chrdev_mtx: copy_from_user failed, %lu bytes not copied\n",
not_copied);
ret = -EFAULT;
goto out_unlock;
}
data_size = write_bytes;
pr_info("chrdev_mtx: written %zu bytes to buffer\n", data_size);
ret = write_bytes;
/* --- 临界区结束 --- */
out_unlock:
mutex_unlock(&buf_mutex);
return ret;
}
static struct file_operations chrdev_fops = {
.owner = THIS_MODULE,
.open = chrdev_open,
.release = chrdev_release,
.read = chrdev_read,
.write = chrdev_write,
};
/* 模块初始化 */
static int __init chrdev_init(void)
{
int ret;
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret < 0) {
pr_err("chrdev_mtx: failed to allocate device number\n");
return ret;
}
pr_info("chrdev_mtx: allocated major=%d, minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
cdev_init(&my_cdev, &chrdev_fops);
my_cdev.owner = THIS_MODULE;
ret = cdev_add(&my_cdev, dev_num, 1);
if (ret) {
pr_err("chrdev_mtx: cdev_add failed\n");
goto err_cdev_add;
}
my_class = class_create(THIS_MODULE, CLASS_NAME);
if (IS_ERR(my_class)) {
pr_err("chrdev_mtx: class_create failed\n");
ret = PTR_ERR(my_class);
goto err_class_create;
}
my_device = device_create(my_class, NULL, dev_num, NULL, DEVICE_NAME);
if (IS_ERR(my_device)) {
pr_err("chrdev_mtx: device_create failed\n");
ret = PTR_ERR(my_device);
goto err_device_create;
}
/* 初始化共享资源 */
memset(kernel_buf, 0, BUF_SIZE);
data_size = 0;
/* mutex 已通过 DEFINE_MUTEX 静态初始化,无需额外操作 */
pr_info("chrdev_mtx: module loaded, /dev/%s created\n", DEVICE_NAME);
return 0;
err_device_create:
class_destroy(my_class);
err_class_create:
cdev_del(&my_cdev);
err_cdev_add:
unregister_chrdev_region(dev_num, 1);
return ret;
}
/* 模块卸载 */
static void __exit chrdev_exit(void)
{
device_destroy(my_class, dev_num);
class_destroy(my_class);
cdev_del(&my_cdev);
unregister_chrdev_region(dev_num, 1);
pr_info("chrdev_mtx: module unloaded\n");
}
module_init(chrdev_init);
module_exit(chrdev_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A mutex-protected char device driver");
MODULE_VERSION("1.0");
代码关键点说明
-
静态定义互斥锁
DEFINE_MUTEX(buf_mutex)等价于struct mutex buf_mutex;加mutex_init(&buf_mutex)。因为锁的生命周期与模块相同,静态定义最简洁。 -
使用
mutex_lock_interruptible- 普通
mutex_lock在等待锁时进程处于 不可中断睡眠(TASK_UNINTERRUPTIBLE),无法被信号唤醒,若死锁只能重启。 mutex_lock_interruptible使进程处于 可中断睡眠 ,当接收到信号(如kill)时会返回-EINTR,我们在驱动中转为-ERESTARTSYS,符合内核惯例。这增加了系统的健壮性。
- 普通
-
临界区保护范围
读写函数中将 所有访问
kernel_buf和data_size的代码 都放在lock和unlock之间,包括copy_to/from_user。因为这些函数内部可能触发缺页异常而导致睡眠,但它们与共享数据相关,且mutex允许持锁睡眠(与自旋锁不同)。这样保证拷贝过程中缓冲区内容不被其他进程修改。 -
错误路径释放锁
使用
goto out_unlock确保任何提前返回的路径都会执行mutex_unlock,防止锁泄漏。
三、Makefile
makefile
# Makefile for chrdev_mutex
KERNEL_DIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
obj-m := chrdev_mutex.o
all:
make -C $(KERNEL_DIR) M=$(PWD) modules
clean:
make -C $(KERNEL_DIR) M=$(PWD) clean
编译:make
四、并发测试与验证
4.1 加载驱动并赋权
bash
sudo insmod chrdev_mutex.ko
sudo chmod 666 /dev/chrdev_mtx
4.2 编写并发测试脚本
我们使用 Shell 脚本启动多个后台进程,同时对设备进行大量读写,模拟竞态场景。
测试脚本 test_concurrent.sh:
bash
#!/bin/bash
DEV="/dev/chrdev_mtx"
LOOP=500
echo "Starting concurrent write/read test..."
# 后台进程1:重复写入不同内容
(
for i in $(seq 1 $LOOP); do
echo "AAAA_$i" > $DEV
done
) &
# 后台进程2:重复写入另一内容
(
for i in $(seq 1 $LOOP); do
echo "BBBB_$i" > $DEV
done
) &
# 后台进程3:不断读取
(
for i in $(seq 1 $LOOP); do
cat $DEV > /dev/null
done
) &
# 等待所有后台进程结束
wait
echo "Test completed. Check dmesg for any errors."
运行脚本:
bash
chmod +x test_concurrent.sh
./test_concurrent.sh
4.3 观察内核日志
bash
dmesg | grep chrdev_mtx | tail -20
你会看到大量 read/written 日志,但不会出现任何 EFAULT 或内核崩溃。如果没有互斥锁,这种并发测试很容易触发数据错乱或系统不稳定。
4.4 卸载驱动
bash
sudo rmmod chrdev_mutex
五、互斥锁的使用原则与注意事项
-
持锁时间要短
尽可能只锁住必须保护的代码。长时间持锁会降低系统并发性能。
-
避免在持锁时调用可能睡眠的函数?不绝对
mutex允许持锁时睡眠,所以调用copy_to/from_user是安全的。但如果改用自旋锁(spinlock),则绝对不能在持锁时睡眠。 -
不要重复加锁
mutex不支持同一进程递归加锁,否则会导致死锁。 -
锁的粒度
本驱动用一把锁保护整个缓冲区,是最简单的方案。若设备有多个独立的缓冲区,可以考虑每个缓冲区用一把锁(更细粒度)来提高并发度。
-
中断上下文不能使用
mutex如果在中断处理函数或软中断中需要保护共享资源,应使用
spin_lock_irqsave,因为中断上下文不能睡眠。
六、总结与下篇预告
本文通过在字符设备驱动中引入 mutex,有效解决了多进程并发访问导致的数据竞态问题。经过加锁保护,我们的内存缓冲区设备已经具备基本的安全生产环境。
从驱动框架到数据交互,再到并发控制,我们已经完成了一个字符设备驱动最核心的三个环节。
下篇预告 :我们将离开虚拟的内存操作,进入真实的硬件世界。下一篇将讲解如何在驱动中控制GPIO,让开发板上的LED在我们的设备操作下闪烁起来。敬请期待!
如果本文对你有帮助,欢迎点赞、收藏、关注。有任何技术疑问,欢迎在评论区留言交流!