Skip to content

LV050-互斥锁

怎么保护共享资源,防止竞争?

一、互斥锁简介

比如公司部门里, 我在使用着打印机打印东西的同时(还没有打印完) , 别人刚好也在此刻使用打印机打印东西, 如果不做任何处理的话,打印出来的东西肯定是错乱的。 那么怎么解决这种情况呢? 只要我在打印着的时候别人是不允许打印的, 只有等我打印结束后别人才允许打印。 这个过程有点类似于, 把打印机放在一个房间里, 给这个房间安把锁, 这个锁默认是打开的。 当 A 需要打印时, 他先过来检查这把锁有没有锁着, 没有的话就进去, 同时上锁在房间里打印。 而在这时, 刚好 B 也需要打印, B 同样先检查锁, 发现锁是锁住的, 他就在门外等着。 而当 A 打印结束后, 他会开锁出来, 这时候 B 才进去上锁打印。

现在应该就知道互斥锁是什么了,也可以叫互斥体,后面我还是叫互斥锁吧。互斥锁会导致休眠, 所以在中断里面不能用互斥锁。 同一时刻只能有一个线程持有互斥锁,并且只有持有者才可以解锁, 并且不允许递归上锁和解锁。

我们将信号量量值设置为 1, 最终实现的就是互斥效果,虽然两者功能相同但是具体的实现方式是不同的, 但是使用互斥锁效率更高、更简洁, 所以如果使用到的信号量“量值”为 1, 一般将其修改为使用互斥锁实现。

当有多个线程几乎同时修改某一个共享数据的时候, 需要进行同步控制。 线程同步能够保证多个线程安全访问竞争资源, 最简单的同步机制是引入互斥锁。 互斥锁为资源引入一个状态:锁定或者非锁定。 某个线程要更改共享数据时, 先将其锁定, 此时资源的状态为“锁定” , 其他线程不能更改; 直到该线程释放资源, 将资源的状态变成“非锁定” , 其他的线程才能再次锁定该资源。 互斥锁保证了每次只有一个线程进行写入操作, 从而保证了多线程情况下数据的正确性, 能够保证多个线程访问共享数据不会出现资源竞争及数据错误。

二、相关数据结构与 API

1. struct mutex

Linux 内核使用 struct mutex 来表示互斥锁:

c
struct mutex {
	atomic_long_t		owner;
	spinlock_t		wait_lock;
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
	struct optimistic_spin_queue osq; /* Spinner MCS lock */
#endif
	struct list_head	wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
	void			*magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
	struct lockdep_map	dep_map;
#endif
};

2. 互斥锁相关 API

相关的 API 在 mutex.h - include/linux/mutex.h 中有对应的声明或者定义:

函数描述
DEFINE_MUTEX(name)定义并初始化一个 mutex 变量。
void mutex_init(mutex *lock)初始化 mutex。
void mutex_lock(struct mutex *lock)获取 mutex,也就是给 mutex 上锁。如果获取不到就进休眠。
void mutex_unlock(struct mutex *lock)释放 mutex,也就给 mutex 解锁。
int mutex_trylock(struct mutex *lock)尝试获取 mutex,如果成功就返回 1,如果失败就返回 0。
int mutex_is_locked(struct mutex *lock)判断 mutex 是否被获取,如果是的话就返回 1,否则返回 0。
int mutex_lock_interruptible(struct mutex *lock)使用此函数获取信号量失败进入休眠以后可以被信号打断。

3. 使用示例

c
struct mutex lock; /* 定义一个互斥体 */
mutex_init(&lock); /* 初始化互斥体 */

mutex_lock(&lock); /* 上锁 */
/* 临界区 */
mutex_unlock(&lock); /* 解锁 */

三、注意事项

在使用 mutex 的时候要注意如下几点:

①、 mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。

②、和信号量一样, mutex 保护的临界区可以调用引起阻塞的 API 函数。

③、因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁。

四、互斥锁 demo

1. demo 源码

c
#include <linux/init.h> /* module_init module_exit */
#include <linux/kernel.h>
#include <linux/module.h> /* MODULE_LICENSE */

#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>

#include <linux/delay.h> /* ssleep */

#include <linux/mutex.h>

#include "./timestamp_autogenerated.h"
#include "./version_autogenerated.h"
#include "./sdrv_common.h"

// #undef PRT
// #undef PRTE
#ifndef PRT
#define PRT printk
#endif
#ifndef PRTE
#define PRTE printk
#endif

#define CHRDEV_NAME "sdev"    /* 设备名, cat /proc/devices 查看与设备号的对应关系 */
#define CLASS_NAME  "sclass"  /* 类名,在 /sys/class 中显示的名称 */
#define DEVICE_NAME "sdevchr" /* 设备节点名,在 /sys/class/class_name/ 中显示的名称以及 /dev/ 下显示的节点名 */
#define BUFSIZE     32        /* 设置最大偏移量为 32 */
#define MUTEX_FLAG  0         /* 用于有无信号量的现象对比 */
// ioctl 支持的命令定义
#define CMD_TEST0 _IO('S', 0)
#define CMD_TEST1 _IOW('S', 1, int)
#define CMD_TEST2 _IOR('S', 2, int)
#define CMD_TEST3 _IOW('S', 3, int)

struct __CMD_TEST{
	int a;
	int b;
	int c;
};

typedef struct __CHAR_DEVICE
{
    char dev_name[32];     // 设备名称, /dev/dev-name
    dev_t dev_num;         // 定义 dev_t 类型(32 位大小)的变量 dev_num, 用来存放设备号
    struct cdev s_cdev;    // 定义 cdev 结构体类型的变量 scdev
    struct class *class;   // 定于 struct class *类型结构体变量 class,表示要创建的类
    struct device *device; // 设备
    char buf[BUFSIZE];     // 设置数据存储数组 mem
    struct mutex mutex_lock;  // 互斥锁
} _CHAR_DEVICE;

_CHAR_DEVICE g_chrdev = {0};  //定义一个 device_test 结构体变量

static int scdev_ops_open(struct inode *pInode, struct file *pFile)
{

    PRT("This is scdev_ops_open!dev name=%s\n", g_chrdev.dev_name);
    pFile->private_data = &g_chrdev;  // 设置私有数据
    #if MUTEX_FLAG == 1
    /* 获取互斥锁, 可以被信号打断 */
    if (mutex_lock_interruptible(&g_chrdev.mutex_lock))
    {
        return -ERESTARTSYS;
    }
    #if 0
    mutex_lock(&g_chrdev.lock); /* 不能被信号打断 */
    #endif
    #endif
    return 0;
}

static ssize_t scdev_ops_read(struct file *pFile, char __user *buf, size_t size, loff_t *off)
{
    
    loff_t offset = *off; // 将读取数据的偏移量赋值给 loff_t 类型变量 p
    size_t count = size;
    _CHAR_DEVICE *p_chrdev=(_CHAR_DEVICE *)pFile->private_data; //在 write 函数中读取 private_data

    if (offset > BUFSIZE)
    {
        return 0;
    }

    if (count > BUFSIZE - offset)
    {
        count = BUFSIZE - offset;
    }

    if (copy_to_user(buf, p_chrdev->buf + offset, count))
    { 
        // 将 mem 中的值写入 buf,并传递到用户空间
        PRT("copy_to_user error!\n");
        return -1;
    }
    #if 0
    int i = 0;
    for (i = 0; i < BUFSIZE; i++)
    {
        PRT("buf[%d] %c\n", i, p_chrdev->buf[i]); // 将 mem 中的值打印出来
    }
    
    PRT("read offset is %llu, count is %d\n", offset, count);
    #endif
    *off = *off + count; // 更新偏移值

    return count;
}

static ssize_t scdev_ops_write(struct file *pFile, const char __user *buf, size_t size, loff_t *off)
{
    loff_t offset = *off; // 将读取数据的偏移量赋值给 loff_t 类型变量 p
    size_t count = size;
    _CHAR_DEVICE *p_chrdev=(_CHAR_DEVICE *)pFile->private_data; //在 write 函数中读取 private_data

    if (offset > BUFSIZE)
    {
        return 0;
    }

    if (count > BUFSIZE - offset)
    {
        count = BUFSIZE - offset;
    }

    if (copy_from_user(p_chrdev->buf + offset, buf, count))
    { 
        // 将 buf 中的值,从用户空间传递到内核空间
        PRT("copy_to_user error \n");
        return -1;
    }
    PRT("copy_from_user buf is %s\n", p_chrdev->buf + offset);
#if 0
    int i = 0;
    for (i = 0; i < BUFSIZE; i++)
    {
        PRT("buf[%d] %c\n", i, p_chrdev->buf[i]); // 将 mem 中的值打印出来
    }
    PRT("write offset is %llu, count is %d\n", offset, count); // 打印写入的值
    #endif
    *off = *off + count;                         // 更新偏移值

    return count;
}

static int scdev_ops_release(struct inode *pInode, struct file *pFile)
{
    _CHAR_DEVICE *p_chrdev=(_CHAR_DEVICE *)pFile->private_data; //在 write 函数中读取 private_data
    #if MUTEX_FLAG == 1
    mutex_unlock(&p_chrdev->mutex_lock);// 释放互斥锁
    #endif
    PRT("This is scdev_ops_release!dev_name=%s\n", p_chrdev->dev_name);
    return 0;
}

static loff_t scdev_ops_llseek(struct file *pFile, loff_t offset, int whence)
{
    loff_t new_offset = 0; // 定义 loff_t 类型的新的偏移值
    switch (whence)    // 对 lseek 函数传递的 whence 参数进行判断
    {
        case SEEK_SET:
            if (offset < 0 || offset > BUFSIZE)
            {
                return -EINVAL; // EINVAL = 22 表示无效参数
            }
            new_offset = offset; // 如果 whence 参数为 SEEK_SET,则新偏移值为 offset
            break;
        case SEEK_CUR:
            if ((pFile->f_pos + offset < 0) || (pFile->f_pos + offset > BUFSIZE))
            {
                return -EINVAL;
            }
            new_offset = pFile->f_pos + offset; // 如果 whence 参数为 SEEK_CUR,则新偏移值为 pFile-> f_pos + offset,pFile-> f_pos 为当前的偏移值
            break;
        case SEEK_END:
            if (pFile->f_pos + offset < 0)
            {
                return -EINVAL;
            }
            new_offset = BUFSIZE + offset; // 如果 whence 参数为 SEEK_END,则新偏移值为 BUFSIZE + offset,BUFSIZE 为最大偏移量
            break;
        default:
            break;
    }
    pFile->f_pos = new_offset; // 更新 pFile-> f_pos 偏移值

    return new_offset;
}

static long scdev_ops_ioctl(struct file *pFile, unsigned int cmd, unsigned long arg)
{
    int val = 0;//定义 int 类型向应用空间传递的变量 val
    switch(cmd)
    {
        case CMD_TEST0:
            PRT("this is CMD_TEST0\n");
            break;
        case CMD_TEST1:
            PRT("this is CMD_TEST1\n");
            PRT("arg is %ld\n",arg);//打印应用空间传递来的 arg 参数
            break;
        case CMD_TEST2:
            val = 1;//将要传递的变量 val 赋值为 1
            PRT("this is CMD_TEST2\n");
            if(copy_to_user((int *)arg, &val, sizeof(val)) != 0)
            {
                //通过 copy_to_user 向用户空间传递数据
                PRT("copy_to_user error \n");
            }
            break;
        case CMD_TEST3:
        {
            struct __CMD_TEST cmd_test3 = {0};
            if (copy_from_user(&cmd_test3, (int *)arg, sizeof(cmd_test3)) != 0)
            {
                PRT("copy_from_user error\n");
            }
            PRT("cmd_test3.a = %d\n", cmd_test3.a);
            PRT("cmd_test3.b = %d\n", cmd_test3.b);
            PRT("cmd_test3.c = %d\n", cmd_test3.c);
            break;
        }
        default:
            break;
    }

    return 0;
}

static struct file_operations g_scdev_ops = {
    .owner = THIS_MODULE,//将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
	.open = scdev_ops_open,
	.read = scdev_ops_read,
	.write = scdev_ops_write,
	.release = scdev_ops_release,
    .llseek = scdev_ops_llseek,
    .unlocked_ioctl = scdev_ops_ioctl,
}; // 定义 file_operations 结构体类型的变量 g_cdev_dev_ops

static int scdev_create(_CHAR_DEVICE *p_chrdev)
{
    int ret;          // 定义 int 类型的变量 ret,用来判断函数返回值
    int major, minor; // 定义 int 类型的主设备号 major 和次设备号 minor
    #if MUTEX_FLAG == 1
	mutex_init(&p_chrdev->mutex_lock);	/* 初始化互斥锁 */
    #endif
    ret = alloc_chrdev_region(&p_chrdev->dev_num, 0, 1, CHRDEV_NAME); // 自动获取设备号,设备名为 chrdev_name
    if (ret < 0)
    {
        PRTE("alloc_chrdev_region is error!ret=%d\n", ret);
        goto err_alloc_devno;
    }
    major = MAJOR(p_chrdev->dev_num); // 使用 MAJOR()函数获取主设备号
    minor = MINOR(p_chrdev->dev_num); // 使用 MINOR()函数获取次设备号
    //PRT("major is %d, minor is %d !\n", major, minor);

    cdev_init(&p_chrdev->s_cdev, &g_scdev_ops); // 使用 cdev_init()函数初始化 p_chrdev-> s_cdev 结构体,并链接到 cdev_ops 结构体
    p_chrdev->s_cdev.owner = THIS_MODULE;          // 将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
    ret = cdev_add(&p_chrdev->s_cdev, p_chrdev->dev_num, 1); // 使用 cdev_add()函数进行字符设备的添加
    if (ret < 0)
    {
        PRTE("cdev_add is error !ret=%d\n", ret);
        goto err_cdev_add;
    }

    p_chrdev->class = class_create(THIS_MODULE, CLASS_NAME); // 使用 class_create 进行类的创建,类名称为 class_dev
    if(IS_ERR(p_chrdev->class))
    {
        ret = PTR_ERR(p_chrdev->class);
        goto err_class_create;
    }
    p_chrdev->device = device_create(p_chrdev->class, NULL, p_chrdev->dev_num, NULL, "%s", DEVICE_NAME); // 使用 device_create 进行设备的创建,设备名称为 device_dev
    if(IS_ERR(p_chrdev->device))
    {
        ret = PTR_ERR(p_chrdev->class);
        goto err_device_create;
    }
    snprintf (p_chrdev->dev_name, sizeof(p_chrdev->dev_name), "/dev/%s", DEVICE_NAME);
    PRT("scdev_create %s success!\n", p_chrdev->dev_name);
    return 0;
    // 一些列的错误处理
err_device_create:
    class_destroy(p_chrdev->class);   // 删除创建的类
err_class_create:
    cdev_del(&p_chrdev->s_cdev);      // 使用 cdev_del()函数进行字符设备的删除
err_cdev_add:
    unregister_chrdev_region(p_chrdev->dev_num, 1); //注销设备号
err_alloc_devno:
    return ret;
}

static void scdev_destroy(_CHAR_DEVICE *p_chrdev)
{
    // 需要注意的是, 字符设备的注册要放在申请字符设备号之后,
    // 字符设备的删除要放在释放字符驱动设备号之前。
    cdev_del(&p_chrdev->s_cdev);                    // 使用 cdev_del()函数进行字符设备的删除
    unregister_chrdev_region(p_chrdev->dev_num, 1); // 释放字符驱动设备号
    device_destroy(p_chrdev->class, p_chrdev->dev_num); // 删除创建的设备
    class_destroy(p_chrdev->class);                 // 删除创建的类
    PRT("scdev_destroy success!\n");
}

/**
 * @brief  sdrv_demo_init
 * @note   注册驱动
 * @param [in]
 * @param [out]
 * @retval 
 */
static __init int sdrv_demo_init(void)
{
    int ret = 0;
	printk("*** [%s:%d]Build Time: %s %s, git version:%s ***\n", __FUNCTION__,
           __LINE__, KERNEL_KO_DATE, KERNEL_KO_TIME, KERNEL_KO_VERSION);

	// 注册字符设备
    ret = scdev_create(&g_chrdev);
    if (ret < 0) 
    {
        PRTE("Failed to scdev_create!ret=%d\n", ret);
        goto err_scdev_create;
    }
    PRT("sdrv_demo module init success!\n");
	return 0;

err_scdev_create:
    return ret;
}

/**
 * @brief  sdrv_demo_exit
 * @note   注销驱动
 * @param [in]
 * @param [out]
 * @retval 
 */
static __exit void sdrv_demo_exit(void)
{
    // 注销字符设备
    scdev_destroy(&g_chrdev);
    PRT("sdrv_demo module exit!\n");
}

module_init(sdrv_demo_init); // 将__init 定义的函数指定为驱动的入口函数
module_exit(sdrv_demo_exit); // 将__exit 定义的函数指定为驱动的出口函数

/* 模块信息(通过 modinfo xxx.ko 查看) */
MODULE_LICENSE("GPL v2");            /* 源码的许可证协议 */
MODULE_AUTHOR("sumu");               /* 字符串常量内容为模块作者说明 */
MODULE_DESCRIPTION("Description");   /* 字符串常量内容为模块功能说明 */
MODULE_ALIAS("module's other name"); /* 字符串常量内容为模块别名 */

2. 开发板验证

其实这个现象和信号量是一样的,我们将编译得到的 sdriver_demo.ko、app_demo.out 拷贝到开发板。

  • (1)加载驱动
shell
insmod sdriver_demo.ko
  • (2)app_demo.out 会创建子进程运行
shell
./app_demo.out /dev/sdevchr 2
image-20250122151244602

可以看到是一个执行完再执行另一个。没有互斥锁保护的时候是这样的:

image-20250122151319872

参考资料:

自旋锁在抢占(或非抢占)单核和多核中的作用_多核系统中, 任务可以通过自旋锁或者中断的方式独占 cpu-CSDN 博客