在 上一篇文章中主要描述了并发与竞态的含义、引发条件以以及竞态资源的保护机制。竞态(共享)资源保护机制主要包括 屏蔽中断、原子操作、自旋锁、互斥体,屏蔽中断一般不推荐使用,其他几种是我们在编写驱动程序时常用到的。下面我将上述机制应用在实际场景中,在应用之前,我们先回顾下保护机制的参考选择。
场景 | 参考使用机制 |
---|---|
整型变量加锁 | 原子锁/自旋锁 |
低开销加锁 | 自旋锁 |
短期加锁 | 自旋锁 |
长期加锁 | 互斥体/信号量 |
中断上下文中加锁 | 自旋锁 |
线程支持睡眠 | 互斥体/信号量 |
线程资源同步 | 信号量 |
并发与竞态在驱动中
基于“【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
次数的限制比较好验证,分别open
5次后原子变量清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