前言在上一篇文章中我们实现了一个可读写的内存缓冲区设备。但它有一个严重缺陷完全没有并发保护。当多个进程同时打开设备进行读写时内核缓冲区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及相关操作mutex_init(lock);// 动态初始化互斥锁mutex_lock(lock);// 获取锁若锁已被占用则睡眠等待mutex_unlock(lock);// 释放锁本驱动有两个共享资源需要保护kernel_buf[1024]数据缓冲区data_size有效数据长度我们将用一把mutex锁住整个read和write中对共享资源访问的代码段。二、驱动代码实现带mutex保护以下代码基于第二篇文章的版本修改新增了mutex的保护其余框架不变。所有改动都用注释标明。新建chrdev_mutex.c直接复制即可编译运行。/* * chrdev_mutex.c * 带互斥锁保护的字符设备驱动。 * 使用 mutex 确保多进程并发访问时缓冲区数据一致性。 * 加载后生成 /dev/chrdev_mtx。 * 作者[你的ID] * 适配内核Linux 5.x (4.x 亦可) */#includelinux/module.h#includelinux/fs.h#includelinux/cdev.h#includelinux/device.h#includelinux/uaccess.h#includelinux/mutex.h/* 互斥锁头文件 */#defineDEVICE_NAMEchrdev_mtx#defineCLASS_NAMEchrdev_mtx_class#defineBUF_SIZE1024staticdev_tdev_num;staticstructcdevmy_cdev;staticstructclass*my_class;staticstructdevice*my_device;/* 共享资源 */staticcharkernel_buf[BUF_SIZE];staticsize_tdata_size;/* 互斥锁定义 */staticDEFINE_MUTEX(buf_mutex);// 静态定义并初始化锁/* * 打开设备无需额外操作直接返回成功。 * 注意open 本身不操作共享资源因此无需加锁。 */staticintchrdev_open(structinode*inode,structfile*file){pr_info(chrdev_mtx: device opened\n);return0;}/* 关闭设备 */staticintchrdev_release(structinode*inode,structfile*file){pr_info(chrdev_mtx: device closed\n);return0;}/* 读取设备加锁保护整个操作 */staticssize_tchrdev_read(structfile*file,char__user*buf,size_tcount,loff_t*f_pos){ssize_tret_bytes;unsignedlongnot_copied;/* 获取锁如果锁被其他进程持有当前进程会睡眠等待 */if(mutex_lock_interruptible(buf_mutex)){/* 如果在等待过程中收到致命信号返回 -ERESTARTSYS */return-ERESTARTSYS;}/* --- 临界区开始 --- */if(data_size0){pr_info(chrdev_mtx: buffer empty, read returns EOF\n);ret_bytes0;gotoout_unlock;}ret_bytes(countdata_size)?count:data_size;not_copiedcopy_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;gotoout_unlock;}pr_info(chrdev_mtx: read %zd bytes from buffer\n,ret_bytes);/* 消费模式读取后清空缓冲区 */memset(kernel_buf,0,BUF_SIZE);data_size0;/* --- 临界区结束 --- */out_unlock:mutex_unlock(buf_mutex);// 释放锁returnret_bytes;}/* 写入设备加锁保护整个操作 */staticssize_tchrdev_write(structfile*file,constchar__user*buf,size_tcount,loff_t*f_pos){unsignedlongnot_copied;size_twrite_bytes;ssize_tret;if(mutex_lock_interruptible(buf_mutex))return-ERESTARTSYS;/* --- 临界区开始 --- */write_bytes(countBUF_SIZE)?count:BUF_SIZE;not_copiedcopy_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;gotoout_unlock;}data_sizewrite_bytes;pr_info(chrdev_mtx: written %zu bytes to buffer\n,data_size);retwrite_bytes;/* --- 临界区结束 --- */out_unlock:mutex_unlock(buf_mutex);returnret;}staticstructfile_operationschrdev_fops{.ownerTHIS_MODULE,.openchrdev_open,.releasechrdev_release,.readchrdev_read,.writechrdev_write,};/* 模块初始化 */staticint__initchrdev_init(void){intret;retalloc_chrdev_region(dev_num,0,1,DEVICE_NAME);if(ret0){pr_err(chrdev_mtx: failed to allocate device number\n);returnret;}pr_info(chrdev_mtx: allocated major%d, minor%d\n,MAJOR(dev_num),MINOR(dev_num));cdev_init(my_cdev,chrdev_fops);my_cdev.ownerTHIS_MODULE;retcdev_add(my_cdev,dev_num,1);if(ret){pr_err(chrdev_mtx: cdev_add failed\n);gotoerr_cdev_add;}my_classclass_create(THIS_MODULE,CLASS_NAME);if(IS_ERR(my_class)){pr_err(chrdev_mtx: class_create failed\n);retPTR_ERR(my_class);gotoerr_class_create;}my_devicedevice_create(my_class,NULL,dev_num,NULL,DEVICE_NAME);if(IS_ERR(my_device)){pr_err(chrdev_mtx: device_create failed\n);retPTR_ERR(my_device);gotoerr_device_create;}/* 初始化共享资源 */memset(kernel_buf,0,BUF_SIZE);data_size0;/* mutex 已通过 DEFINE_MUTEX 静态初始化无需额外操作 */pr_info(chrdev_mtx: module loaded, /dev/%s created\n,DEVICE_NAME);return0;err_device_create:class_destroy(my_class);err_class_create:cdev_del(my_cdev);err_cdev_add:unregister_chrdev_region(dev_num,1);returnret;}/* 模块卸载 */staticvoid__exitchrdev_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 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 加载驱动并赋权sudoinsmod chrdev_mutex.kosudochmod666/dev/chrdev_mtx4.2 编写并发测试脚本我们使用 Shell 脚本启动多个后台进程同时对设备进行大量读写模拟竞态场景。测试脚本test_concurrent.sh#!/bin/bashDEV/dev/chrdev_mtxLOOP500echoStarting concurrent write/read test...# 后台进程1重复写入不同内容(foriin$(seq1$LOOP);doechoAAAA_$i$DEVdone)# 后台进程2重复写入另一内容(foriin$(seq1$LOOP);doechoBBBB_$i$DEVdone)# 后台进程3不断读取(foriin$(seq1$LOOP);docat$DEV/dev/nulldone)# 等待所有后台进程结束waitechoTest completed. Check dmesg for any errors.运行脚本chmodx test_concurrent.sh ./test_concurrent.sh4.3 观察内核日志dmesg|grepchrdev_mtx|tail-20你会看到大量read/written日志但不会出现任何EFAULT或内核崩溃。如果没有互斥锁这种并发测试很容易触发数据错乱或系统不稳定。4.4 卸载驱动sudormmod chrdev_mutex五、互斥锁的使用原则与注意事项持锁时间要短尽可能只锁住必须保护的代码。长时间持锁会降低系统并发性能。避免在持锁时调用可能睡眠的函数不绝对mutex允许持锁时睡眠所以调用copy_to/from_user是安全的。但如果改用自旋锁spinlock则绝对不能在持锁时睡眠。不要重复加锁mutex不支持同一进程递归加锁否则会导致死锁。锁的粒度本驱动用一把锁保护整个缓冲区是最简单的方案。若设备有多个独立的缓冲区可以考虑每个缓冲区用一把锁更细粒度来提高并发度。中断上下文不能使用mutex如果在中断处理函数或软中断中需要保护共享资源应使用spin_lock_irqsave因为中断上下文不能睡眠。六、总结与下篇预告本文通过在字符设备驱动中引入mutex有效解决了多进程并发访问导致的数据竞态问题。经过加锁保护我们的内存缓冲区设备已经具备基本的安全生产环境。从驱动框架到数据交互再到并发控制我们已经完成了一个字符设备驱动最核心的三个环节。下篇预告我们将离开虚拟的内存操作进入真实的硬件世界。下一篇将讲解如何在驱动中控制GPIO让开发板上的LED在我们的设备操作下闪烁起来。敬请期待如果本文对你有帮助欢迎点赞、收藏、关注。有任何技术疑问欢迎在评论区留言交流