Linux字符设备驱动开发(三):引入并发控制——使用mutex保护共享数据

前言

上一篇文章中,我们实现了一个可读写的内存缓冲区设备。但它有一个严重缺陷:完全没有并发保护 。当多个进程同时打开设备进行读写时,内核缓冲区 kernel_bufdata_size 可能被交叉修改,导致数据错乱甚至内核崩溃。

本文将在现有驱动的基础上,引入内核互斥锁(mutex),让驱动在多进程环境下也能安全稳定地运行。你将掌握:

  • 竞态条件的概念及其危害
  • Linux内核互斥锁 mutex 的初始化、加锁与解锁
  • 如何用 mutex 保护字符设备驱动的临界区
  • 多进程并发测试的方法

一、为什么要引入并发控制?

1.1 什么是竞态条件?

以我们的内存缓冲区设备为例,假设有两个进程A和B同时执行写操作:

  1. 进程A调用 chrdev_write,准备写入 500 字节。
  2. 内核调度器在 copy_from_user 之后、更新 data_size 之前将CPU交给进程B。
  3. 进程B调用 chrdev_write,写入 200 字节,将 data_size 更新为 200。
  4. 调度器切回进程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 锁住整个 readwrite 中对共享资源访问的代码段。


二、驱动代码实现(带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");

代码关键点说明

  1. 静态定义互斥锁
    DEFINE_MUTEX(buf_mutex) 等价于 struct mutex buf_mutex;mutex_init(&buf_mutex)。因为锁的生命周期与模块相同,静态定义最简洁。

  2. 使用 mutex_lock_interruptible

    • 普通 mutex_lock 在等待锁时进程处于 不可中断睡眠(TASK_UNINTERRUPTIBLE),无法被信号唤醒,若死锁只能重启。
    • mutex_lock_interruptible 使进程处于 可中断睡眠 ,当接收到信号(如 kill)时会返回 -EINTR,我们在驱动中转为 -ERESTARTSYS,符合内核惯例。这增加了系统的健壮性。
  3. 临界区保护范围

    读写函数中将 所有访问 kernel_bufdata_size 的代码 都放在 lockunlock 之间,包括 copy_to/from_user。因为这些函数内部可能触发缺页异常而导致睡眠,但它们与共享数据相关,且 mutex 允许持锁睡眠(与自旋锁不同)。这样保证拷贝过程中缓冲区内容不被其他进程修改。

  4. 错误路径释放锁

    使用 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

五、互斥锁的使用原则与注意事项

  1. 持锁时间要短

    尽可能只锁住必须保护的代码。长时间持锁会降低系统并发性能。

  2. 避免在持锁时调用可能睡眠的函数?不绝对
    mutex 允许持锁时睡眠,所以调用 copy_to/from_user 是安全的。但如果改用自旋锁(spinlock),则绝对不能在持锁时睡眠。

  3. 不要重复加锁
    mutex 不支持同一进程递归加锁,否则会导致死锁。

  4. 锁的粒度

    本驱动用一把锁保护整个缓冲区,是最简单的方案。若设备有多个独立的缓冲区,可以考虑每个缓冲区用一把锁(更细粒度)来提高并发度。

  5. 中断上下文不能使用 mutex

    如果在中断处理函数或软中断中需要保护共享资源,应使用 spin_lock_irqsave,因为中断上下文不能睡眠。


六、总结与下篇预告

本文通过在字符设备驱动中引入 mutex,有效解决了多进程并发访问导致的数据竞态问题。经过加锁保护,我们的内存缓冲区设备已经具备基本的安全生产环境。

从驱动框架到数据交互,再到并发控制,我们已经完成了一个字符设备驱动最核心的三个环节。

下篇预告 :我们将离开虚拟的内存操作,进入真实的硬件世界。下一篇将讲解如何在驱动中控制GPIO,让开发板上的LED在我们的设备操作下闪烁起来。敬请期待!


如果本文对你有帮助,欢迎点赞、收藏、关注。有任何技术疑问,欢迎在评论区留言交流!

相关推荐
盟接之桥1 小时前
什么是电子数据交换(EDI)|AS2 协议详解
运维·服务器·网络·安全·低代码·汽车·制造
慵懒的猫mi1 小时前
从 Windows 到 deepin:Electron 软件无损移植实战
linux·windows·deepin
网安情报局1 小时前
抗 DDoS 的核心:黑白名单、限速、流量牵引技术对比分析
运维·服务器·网络
坤昱1 小时前
cfs调度类深入解刨——最新内核细节分析1
linux·cfs·cfs调度·linux 7.1·eevdf·核心调度结构·linux最新调度分析
huohaiyu1 小时前
深入解析JVM核心原理与运行机制
运维·服务器·jvm
MC皮蛋侠客1 小时前
Perf 火焰图深度实战:CPU 性能分析与异常排查完全指南
linux·c++·性能分析·perf·火焰图
风曦Kisaki1 小时前
Nginx代理与LVS(NAT/DR)全方位对比
运维·nginx·lvs
maosheng11462 小时前
NFS服务器的搭建有多种类型linux-linux
linux·运维·服务器
普通young man2 小时前
Linux基础开发工具集合
linux·运维·服务器