互斥锁可以说是“量值” 为 1 的
信号量, 最终实现的效果相同, 既然有了信号量, 那为什么还要有互斥锁呢? 这就是我们这里需要了解并掌握的
文章目录
参考资料
前面了解了原子操作和自旋锁,当然还有之前的字符设备相关操作,前面基础知识还是需要重点掌握的,才能将知识点串联起来:
接下来还是以前面字符设备 动态参数传递实验为基础,打开访问字符设备实验。 所以以前知识点 建议了解
在字符设备这块内容,所有知识点都是串联起来的,需要整体来理解,缺一不可,建议多了解一下基础知识
驱动-申请字符设备号
驱动-注册字符设备
驱动-创建设备节点
驱动-字符设备驱动框架
驱动-杂项设备
驱动-内核空间和用户空间数据交换
驱动-文件私有数据
Linux驱动之 原子操作
Linux驱动—原子操作
驱动-自旋锁
驱动-自旋锁死锁
驱动-信号量
互斥锁的介绍
将信号量量值设置为 1, 最终实现的就是互斥效果, 这里要了解的互斥锁功能相同, 虽然两者功能相同但是具体的实现方式是不同的, 但是使用互斥锁效率更高、更简洁, 所以如果使用到的信号量“量值”为 1,一般将其修改为使用互斥锁实现。当有多个线程几乎同时修改某一个共享数据的时候, 需要进行同步控制。线程同步能够保证多个线程安全访问竞争资源, 最简单的同步机制是引入互斥锁。
互斥锁为资源引入一个状态: 锁定或者非锁定。 某个线程要更改共享数据时, 先将其锁定, 此时资源的状态为“锁定” , 其他线程不能更改;直到该线程释放资源, 将资源的状态变成“非锁定” , 其他的线程才能再次锁定该资源。 互斥锁保证了每次只有一个线程进行写入操作,从而保证了多线程情况下数据的正确性, 能够保证多个线程访问共享数据不会出现资源竞争及数据错误
互斥锁结构体 - mutex
struct mutex {
atomic_t count; // 锁计数器:1-未锁,0-已锁,负值-有等待者
spinlock_t wait_lock; // 保护等待队列的自旋锁
struct list_head wait_list; // 等待该锁的进程队列
};
互斥锁 API
互斥锁实验
源码程序-mutex.c
这个源码程序,用到的还是访问字符设备的最基本内容来讲解,另外添加了 互斥锁api 来规避并发和竞争
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/delay.h>
#include <linux/atomic.h>
#include <linux/errno.h>
#include <linux/semaphore.h>
#include <linux/mutex.h>
struct mutex mutex_test;//定义mutex类型的互斥锁结构体变量mutex_test
static int open_test(struct inode *inode,struct file *file)
{
printk("\n this is open_test \n");
mutex_lock(&mutex_test);//互斥锁加锁
return 0;
};
static ssize_t read_test(struct file *file,char __user *ubuf,size_t len,loff_t *off)
{
int ret;
char kbuf[10] = "topeet";//定义char类型字符串变量kbuf
printk("\nthis is read_test \n");
ret = copy_to_user(ubuf,kbuf,strlen(kbuf));//使用copy_to_user接收用户空间传递的数据
if (ret != 0){
printk("copy_to_user is error \n");
}
printk("copy_to_user is ok \n");
return 0;
}
static char kbuf[10] = {0};//定义char类型字符串全局变量kbuf
static ssize_t write_test(struct file *file,const char __user *ubuf,size_t len,loff_t *off)
{
int ret;
ret = copy_from_user(kbuf,ubuf,len);//使用copy_from_user接收用户空间传递的数据
if (ret != 0){
printk("copy_from_user is error\n");
}
if(strcmp(kbuf,"topeet") == 0 ){//如果传递的kbuf是topeet就睡眠四秒钟
ssleep(4);
}
else if(strcmp(kbuf,"itop") == 0){//如果传递的kbuf是itop就睡眠两秒钟
ssleep(2);
}
printk("copy_from_user buf is %s \n",kbuf);
return 0;
}
static int release_test(struct inode *inode,struct file *file)
{
printk("\nthis is release_test \n");
mutex_unlock(&mutex_test);//互斥锁解锁
return 0;
}
struct chrdev_test
{
dev_t dev_num; //定义dev_t类型变量来表示设备号
int major,minor; //定义int 类型的主设备号和次设备号
struct cdev cdev_test; //定义字符设备
struct class *class_test; //定义结构体变量class 类
};
struct chrdev_test dev1; //创建chardev_test类型结构体变量
static struct file_operations fops_test = {
.owner=THIS_MODULE,//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
.open = open_test,//将open字段指向chrdev_open(...)函数
.read = read_test,//将open字段指向chrdev_read(...)函数
.write = write_test,//将open字段指向chrdev_write(...)函数
.release = release_test,//将open字段指向chrdev_release(...)函数
};//定义file_operations结构体类型的变量cdev_test_ops
static int __init chrdev_fops_init(void)//驱动入口函数
{
mutex_init(&mutex_test);//对互斥体进行初始化
if(alloc_chrdev_region(&dev1.dev_num,0,1,"chrdev_name") < 0){
printk("alloc_chrdev_region is error\n");
}
printk("alloc_chrdev_region is ok\n");
dev1.major=MAJOR(dev1.dev_num);//通过MAJOR()函数进行主设备号获取
dev1.minor=MINOR(dev1.dev_num);//通过MINOR()函数进行次设备号获取
printk("major is %d\n",dev1.major);
printk("minor is %d\n",dev1.minor);
使用cdev_init()函数初始化cdev_test结构体,并链接到cdev_test_ops结构体
cdev_init(&dev1.cdev_test,&fops_test);
dev1.cdev_test.owner = THIS_MODULE;//将owner字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
cdev_add(&dev1.cdev_test,dev1.dev_num,1);
printk("cdev_add is ok\n");
dev1.class_test = class_create(THIS_MODULE,"class_test");//使用class_create进行类的创建,类名称为class_test
device_create(dev1.class_test,NULL,dev1.dev_num,NULL,"device_test");//使用device_create进行设备的创建,设备名称为device_test
return 0;
}
static void __exit chrdev_fops_exit(void)//驱动出口函数
{
cdev_del(&dev1.cdev_test);//使用cdev_del()函数进行字符设备的删除
unregister_chrdev_region(dev1.dev_num,1);//释放字符驱动设备号
device_destroy(dev1.class_test,dev1.dev_num);//删除创建的设备
class_destroy(dev1.class_test);//删除创建的类
printk("module exit \n");
}
module_init(chrdev_fops_init);//注册入口函数
module_exit(chrdev_fops_exit);//注册出口函数
MODULE_LICENSE("GPL v2");//同意GPL开源协议
MODULE_AUTHOR("wang fang chen "); //作者信息
部分源码解读
字符设备操作这里不再赘述,重点看看互斥锁怎么用的。
在之前学习过原子操作设置标志位, 在同一时间内只允许一个任务对共享资源进行访问的方式所不
同, 这里将采用互斥锁的方式避免竞争的产生。 由于互斥体在同一时间内只允许一个任务对共享资源进行, 所以除了在 atomic_init()函数内加入初始化互斥锁函数之外,只需要在 open()函数中加入互斥锁加锁函数, 在 release()函数中加入互斥锁解锁函数即可
- 定义结构体 - mutex
struct mutex mutex_test;//定义mutex类型的互斥锁结构体变量mutex_test
- 驱动入口函数 init 中初始化 互斥锁结构体,设置值 - mutex_init
static int __init chrdev_fops_init(void)//驱动入口函数
{
mutex_init(&mutex_test);//对互斥体进行初始化
...
}
- 在open 中加锁 - mutex_lock
static int open_test(struct inode *inode,struct file *file)
{
printk("\n this is open_test \n");
mutex_lock(&mutex_test);//互斥锁加锁
return 0;
};
- 程序释放资源时候,解锁- mutex_unlock
static int release_test(struct inode *inode,struct file *file)
{
printk("\nthis is release_test \n");
mutex_unlock(&mutex_test);//互斥锁解锁
return 0;
}
编译脚本 Makefile
#!/bin/bash
export ARCH=arm64
export CROSS_COMPILE=aarch64-linux-gnu-
obj-m += mutex.o
KDIR :=/home/wfc123/Linux/rk356x_linux/kernel
PWD ?= $(shell pwd)
all:
make -C $(KDIR) M=$(PWD) modules
clean:
make -C $(KDIR) M=$(PWD) clean
测试程序 app.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(int argc, char *argv[])
{
int fd; // 定义int类型的文件描述符
char str1[10] = {0}; // 定义读取缓冲区str1
fd = open(argv[1], O_RDWR, 0666); // 调用open函数,打开输入的第一个参数文件,权限为可读可写
// fd=open("/dev/device_test",O_RDWR,0666);//调用open函数,打开输入的第一个参数文件,权限为可读可写
if (fd < 0)
{
printf("open is error\n");
return -1;
}
printf("open is ok\n");
if (strcmp(argv[2], "topeet") == 0)
{
write(fd, "topeet", sizeof(str1));
}
else if (strcmp(argv[2], "itop") == 0)
{
write(fd, "itop", sizeof(str1));
}
close(fd); // 调用close函数,对取消文件描述符到文件的映射
return 0;
}
编译 测试程序 app
aarch64-linux-gnu-gcc -o app app.c -static
准备测试命令和测试脚本-app.sh
测试命令
同时后台执行两个命令
./app /dev/device_test topeet &
./app /dev/device_test itop
测试脚本
这里准备签名驱动自选死锁的脚本,方便看看 互斥锁的作用和效果。 app.sh
[root@topeet:/mnt/sdcard]# cat app.sh
#!/bin/bash
taskset -c 0 ./app /dev/device_test topeet &
taskset -c 1 ./app /dev/device_test topeet &
taskset -c 2 ./app /dev/device_test topeet &
taskset -c 3 ./app /dev/device_test topeet &
taskset -c 0 ./app /dev/device_test topeet &
taskset -c 1 ./app /dev/device_test topeet &
taskset -c 2 ./app /dev/device_test topeet &
taskset -c 3 ./app /dev/device_test topeet &
加载驱动 insmod - 查看 dev 下生成的设备
加载驱动后,看一下字符相关操作是否有相关打印,从结果上看打印OK,逻辑正常在走。
字符设备都已经生成了,说明测试程序没有问题的。
测试验证互斥锁程序
直接命令后台验证
./app /dev/device_test topeet &
./app /dev/device_test itop
看实验结果如下:文件操作是一个等着一个执行的呢
脚本批量执行后台任务 测试验证
实际结果是,打印一个接着一个打印,会按照程序里面的逻辑 等待几秒,执行完后才会执行下一个任务命令。 而且最重要的是 这里用的是自旋锁死锁的 脚本来验证,在互斥锁这里不会死机。 这样更方便理解 互斥锁的原理了。
总结
- 互斥锁也是解决并发、竞争问题的一种方案
- 浅显的看: 互斥锁原理就是一个全局的变量,类似于原子操作。会让线程、进程去处理其它事情,不用想自旋锁原地等待。大量频繁使用会增加切换资源消耗。
互斥锁与信号量的区别与联系
基本概念对比
特性 | 互斥锁(Mutex) | 内核态信号量(Semaphore) |
---|---|---|
本质 | 特殊的二进制信号量 | 更通用的同步机制 |
持有者 | 有明确的持有者(必须由获取者释放) | 无持有者概念 |
计数 | 只能是0或1(二进制) | 可以是任意正整数(计数信号量) |
性能 | 更高(优化过的实现) | 相对较低 |
优先级继承 | 支持(防止优先级反转) | 不支持 |
使用场景 | 短期临界区保护 | 资源计数管理 |
关键区别详解
所有权机制
互斥锁具有严格的所有权概念:
只有锁定mutex的线程才能解锁它
内核会跟踪当前持有者
这种设计有助于调试和死锁检测
- 信号量没有所有权概念:
任何线程都可以对信号量执行up操作
更灵活但也更容易出错
计数方式
- 互斥锁是二进制锁:
只有锁定/未锁定两种状态
一次只允许一个线程进入临界区
- 信号量是计数信号量:
初始化时可设置任意正整数值
允许多个线程同时访问资源(当计数>1时)
性能特点
- 互斥锁经过高度优化:
快速路径(fast path)通常只需几条原子指令
在无竞争情况下性能接近无锁
- 信号量开销较大:
总是涉及上下文切换
即使在无竞争情况下也需要更多操作
联系与共同点
- 同步基础:两者都基于内核的等待队列机制实现
- 睡眠特性:当资源不可用时,都会使调用者睡眠
- 不可中断上下文使用:都不能在原子上下文(如中断处理程序)中使用
- 解决竞态条件:都可用于保护共享资源,防止竞态条件
使用场景建议
使用互斥锁的情况
- 需要严格互斥访问的共享资源
- 临界区执行时间较短
- 需要防止优先级反转的实时应用
使用信号量的情况
- 需要限制并发访问数量的资源
- 允许多个读者同时访问的情况
- 需要跨多个模块释放锁的复杂场景
选择指南
优先使用互斥锁
- 只需要二进制锁定
- 性能是关键考量
- 需要调试支持(如死锁检测)
- 在实时系统中需要优先级继承
考虑使用信号量
- 需要计数功能
- 锁定可能被不同模块释放
- 需要允许多个并发访问(如读者)