【Linux驱动编程】并发与竞态实例


  在 上一篇文章中主要描述了并发与竞态的含义、引发条件以以及竞态资源的保护机制。竞态(共享)资源保护机制主要包括 屏蔽中断、原子操作、自旋锁、互斥体,屏蔽中断一般不推荐使用,其他几种是我们在编写驱动程序时常用到的。下面我将上述机制应用在实际场景中,在应用之前,我们先回顾下保护机制的参考选择。


场景 参考使用机制
整型变量加锁 原子锁/自旋锁
低开销加锁 自旋锁
短期加锁 自旋锁
长期加锁 互斥体/信号量
中断上下文中加锁 自旋锁
线程支持睡眠 互斥体/信号量
线程资源同步 信号量
竞态保护机制选择参考

并发与竞态在驱动中

  基于“【Linux驱动编程】mmap方法”文章中的字符设备驱动源码,原驱动只是实现基本功能,为提高程序的稳定性和健壮性,现增加共享资源保护机制。简单回顾下该字符驱动的作用:

  • 实现一个“软驱动”,通过内核一片物理内存交换多进程数据,实现进程通信(IPC)

  • 支持read/write/ioctl函数访问

  • 支持mmap内存映射


实现预期功能

【1】限制驱动最大支持5个进程open

  可以通过一个整型变量记录open次数来实现,因此可考虑使用原子锁和自旋锁。

  • 原子锁实现

  • 自旋锁+普通变量实现

  原子锁是最方便的机制,采用原子锁实现。


【2】对共享资源进行互斥访问

  • 自旋锁

  • 互斥锁

  代码中使用到"copy_from_user"“copy_to_user”函数,两者函数是阻塞函数,可能引起线程睡眠,因此只能采用互斥锁。


【3】读、写同步

  • 信号量

驱动代码实现

初始化

atomic_set(&s_atomic, 5);	/* 初始化原子变量值为5,最大支持5个进程打开该驱动 */
sema_init(&s_sema, 0);		/* 初始化信号量 */
mutex_init(&s_mutex);		/* 初始化互斥体 */

驱动open限制

static int memory_drv_open(struct inode * inode , struct file * pfile)
{
	int state = 0;

	
	if (atomic_read(&s_atomic) == 0)	
	{/* 原子变量为0表示不可用 */
		return -EBUSY;
	}
	atomic_dec(&s_atomic);	/* 原子变量自减 */
	......
}

static int memory_drv_close(struct inode * inode , struct file * pfile)
{
	int state = 0;
	struct memory_device *p;

	p = pfile->private_data;

	if(p->mem_buf)
	{
		kfree(p->mem_buf);
		p->mem_size = 0;
	}
	atomic_inc(&s_atomic);		/* close驱动后原子变量自增 */
    return state;
}

写互斥

static ssize_t memory_drv_write(struct file * pfile, const char __user *buffer, size_t size, loff_t *offset)
{
	unsigned long of = 0;
	struct memory_device *p;
	int ret = 0;
	
	p = pfile->private_data;
	of = *offset;
	if(of > p->mem_size)
	{
		return 0;
	}
   	if (size > (p->mem_size - of))
    {
    	size = p->mem_size -of;
    }

	mutex_lock(&s_mutex);	/* 上锁 */
	ret = copy_from_user(p->mem_buf+of, buffer, size);/* 该函数可能引起线程休眠 */
	mutex_unlock(&s_mutex);	/* 解锁 */
    if (ret)
    {
    	printk("write memory falied.\n");
        return -EFAULT;
    }
	else
	{
		*offset += size;
		up(&s_sema);	/* 释放信号量 */
	}

    return size;
}

读互斥

static ssize_t memory_drv_read(struct file * pfile, char __user *buffer, size_t size, loff_t *offset)
{
	unsigned long of = 0;
	struct memory_device *p;
	int ret = 0;
	
	p = pfile->private_data;
	of = *offset;
	if(of > p->mem_size)
	{
		return 0;
	}
    if (size > (p->mem_size - of))
    {
    	size = p->mem_size - of;
    }
	
	down(&s_sema);			/* 获取信号量 */
	mutex_lock(&s_mutex);	/* 上锁 */
	ret = copy_to_user(buffer, p->mem_buf+of, size);
	mutex_unlock(&s_mutex);	/* 解锁 */
    if (ret)
    {
    	printk("read memory falied.\n");
        return -EFAULT;
    }
	else
	{
		*offset -= size;
	}
    return size;
}

读写同步

  通过信号量持有来同步读写,当有数据写入时释放信号量,读函数通过持有信号量来同步。

down(&s_sema);	/* 获取信号量 */
/* todo read */

up(&s_sema);	/* 释放信号量 */

app程序

  针对驱动程序三个互斥功能的验证,通过原子锁现在驱动的open次数的限制比较好验证,分别open5次后原子变量清0。读写互斥,比较难以通过表象验证,内核多线程执行时,不一定能恰好遇到并行访问的情况,单核cpu不一定会出现。养成良好习惯和提高程序健壮性,应以多核cpu为规范编写代码。

  读写同步可以直观地通过现象观察和理解,当内存区域无数据时,用户进程试图去读取,则挂起用户进程,可以通过一个应用程序验证。fork两个进程,让父进程先执行;父进程任务是读取软驱动(fd)的内存数据,子进程则是向软驱动写数据,在写入数据前,如果用户进程调用读接口试图读取数据,驱动底层读函数会被信号量挂起,进而用户进程也被挂起,直至数据可读(获取到信号量)才重新加入调度,即是有数据写入时(释放信号量)。

  原有的app通过用户态的信号量进行读写同步,现我们把用户态的信号量去掉,因为内核驱动已经实现信号读写同步。

   /* 应用程序伪代码 */
	pid = fork();
    if (pid > 0)
    {
        printf("Parent procsss run\n");
        read(fd, buf, 7);
        printf("Read message from dev_mem:%s\n", buf);
        close(fd);
    }
    else if (pid == 0)
    {
        sleep(2);	/* 保证父进程先执行,父进程执行read会被内核挂起,因为没有数据可读 */
        printf("Child procsss run\n");
        printf("Write [Message] to dev_mem\n");
        write(fd, "Message", 7);	/* 写入数据后,释放信号量,父进程重新加入调度 */
        wait(NULL);
    }
    else
    {
        close(fd);
    }

编译执行

  • 进入源码目录,执行"make -j4"编译生成"devmem.ko"驱动模块

  • 执行"sudo insmod devmem.ko"加载驱动,加载成功后在"/dev"目录生成"dev_mem"设备

  • 执行"gcc app.c -o app -lpthread"编程测试应用程序,编译成生成"app"执行文件

  • 执行"sudo ./app"执行测程序

  • 执行结果如下图

    在这里插入图片描述


代码仓库

【1】https://github.com/Prry/linux-drivers/tree/master/devmem_platform_mutex


原创文章 128 获赞 147 访问量 36万+

猜你喜欢

转载自blog.csdn.net/qq_20553613/article/details/105689387
今日推荐