开发环境:迅为3568开发板 + ubuntu18.04
前文【linux驱动】讲解linux驱动开发中的并发与并行,并且给出解决驱动开发中资源竞争的解决方案(上)
解决资源竞争的方法
自旋锁
自旋锁(spin lock)是为了保护共享资源提出的一种非阻塞锁机制,也就是说,如果某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗 CPU 的时间,不停的试图获取锁。
举个形象生动的例子,以现实生活中银行 ATM 机办理业务为例,ATM 机防护舱在同一时间内只允许一个人进入,当有人进入 ATM 机防护舱之后,两秒钟之后自动上锁,其他也想要存取款的人员,只能在外部等待,办理完相应的存取款业务之后,舱内人员需要手动打开防护锁,其他人才能进入其中,办理业务。
自旋锁的特点:
- 被自旋锁保护的临界区代码执行时不能进入休眠。
- 被自旋锁保护的临界区代码执行时是不能被被其他中断中断。
- 被自旋锁保护的临界区代码执行时,内核不能被抢占。
- 由于在自旋锁忙等期间,因为并没有进入临界区,所以内核抢占还是有效的,因此,等待自旋锁释放的进程有可能被更高优先级的所取代。
- 不能递归申请自旋锁,因为一旦通过递归的方式申请一个你正在持有的锁,那么你就必须“自旋”,等待锁被释放,然而你正处于“自旋”状态,根本没法释放锁。结果就是自己把自己锁死了!
自旋锁的死锁:
- 在单核条件下,拥有自旋锁的进程 A 处于内核态阻塞状态,内核调度 B 进程,碰巧 B 进程也要获得自旋锁,此时 B 只能自旋转。并且由于此时不会调度 A 进程,B 永远自旋,产生死锁。
- 进程 A 拥有自旋锁,但由于中断到来,CPU 会中断当前操作去处理中断函数,中断处理函数需要获得自旋锁,访问共享资源,此时无法获得锁,只能自旋,从而产生死锁。
内核中以 spinlock_t
结构体来表示自旋锁,具体定义如下:
typedef struct spinlock {
union {
struct raw_spinlock rlock;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
struct {
u8 __padding[LOCK_PADSIZE];
struct lockdep_map dep_map;
};
#endif
};
} spinlock_t;
自旋锁的API如下:
/tr>API | 描述 |
---|---|
DEFINE_SPINLOCK(spinlock_t lock) | 定义并初始化一个自选变量。 |
int spin_lock_init(spinlock_t *lock) | 初始化自旋锁。 |
void spin_lock(spinlock_t *lock) | 获取指定的自旋锁,也叫做加锁。 |
void spin_unlock(spinlock_t *lock) | 释放指定的自旋锁。 |
int spin_trylock(spinlock_t *lock) | 尝试获取指定的自旋锁,如果没有获取到就返回 0 |
int spin_is_locked(spinlock_t *lock) | 检查指定的自旋锁是否被获取,如果没有被获取就返回非 0,否则返回 0。 |
void spin_lock_irq(spinlock_t *lock) | 禁止本地中断,并获取自旋锁。 |
void spin_unlock_irq(spinlock_t *lock) | 激活本地中断,并释放自旋锁。 |
void spin_lock_irqsave(spinlock_t *lock,unsigned long flags) | 保存中断状态,禁止本地中断,并获取自旋锁。 |
void spin_unlock_irqrestore(spinlock_t*lock, unsigned long flags) | 将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁 |
自旋锁的使用:使用自旋锁完成:同一个驱动仅允许一个应用程序打开。
-
首先,定义一个自旋锁
spinlock_t mylock
。 -
其次,当驱动被打开时,获取自旋锁
spin_lock(&mylock)
。 -
最后,当驱动关闭时,解除自旋锁
spin_unlock(&mylock)
。
完成代码如下:
#include <linux/init.h> //初始化头文件
#include <linux/module.h> //最基本的文件,支持动态添加和卸载模块。
#include <linux/miscdevice.h> //注册杂项设备头文件
#include <linux/fs.h> //注册设备节点的文件结构体
#include <linux/uaccess.h>
/* 含自旋锁操作驱动 */
// 驱动名
#define DEVICE_NAME "mytest"
// 数据buffer
static char kbuff[32] ;
// 定义一个自旋锁
spinlock_t mylock;
// 打开设备
int misc_open(struct inode *inode,struct file*file)
{
// 给自旋锁加锁
spin_lock(&mylock);
printk("misc_open is ok.\r\n");
return 0;
}
// 关闭设备
int misc_close(struct inode * inode, struct file *file)
{
// 给自旋锁解锁
spin_unlock(&mylock);
printk("misc_close\r\n");
return 0;
}
// 读取设备中信息
ssize_t misc_read (struct file *file, char __user *buff, size_t size, loff_t *loff)
{
int len = strlen(kbuff);
if(copy_to_user(buff,kbuff,len) != 0) // 将内核中的数据给应用
{
printk("copy_to_user error\r\n");
return -1;
}
printk("copy_to_user is successful\r\n");
return len;
}
// 写入设备信息
ssize_t misc_write (struct file *file, const char __user *buff, size_t size, loff_t *loff)
{
int ret = copy_from_user(kbuff,buff,size);
if( ret != 0) // 从应用那儿获取数据
{
printk("ret of error is %d.\r\n",ret);
return ret;
}
printk("copy_from_user data:%s.\r\n",kbuff);
return size;
}
// 设备文件操作集
struct file_operations misc_fops ={
.owner = THIS_MODULE,
.open = misc_open,
.release = misc_close,
.read = misc_read,
.write = misc_write
};
// 设备文件信息描述集
struct miscdevice misc_dev = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME,
.fops = &misc_fops
};
// 驱动的入口函数
static int __init misc_init(void)
{
// 申请杂项设备
int ret = 0;
ret = misc_register(&misc_dev);
printk("--------------------------------------------\r\n");
if(ret < 0)
{
printk("misc register is error.\r\n");
}
else
printk("misc register is seccussful.\r\n");
return ret;
}
//驱动的出口函数
static void __exit misc_exit(void)
{
// 注销杂项设备
misc_deregister(&misc_dev);
printk("misc_exit\r\n");
}
module_init(misc_init);
module_exit(misc_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("zxj");
使用insmod
命令加载驱动到内核后,就可编写测试程序进行测试,由于此处的此时程序仅仅只需要打开关闭设备文件,因此此处就不过多赘述。测试结果如下:
通过测试结果可看出,在第一次打开设备文件后,如果在进行第二次打开就需要等待第一次完成关闭后才能再次打开设备文件。
信号量
信号量,本质上是一个全局变量,信号量的值表示控制访问资源的线程数,可以根据实际情况来自行设置:
- 如果在初始化的时候将信号量量值设置为大于 1,那么这个信号量就是计数型信号量,允许多个线程同时访问共享资源。
- 如果将信号量量值设置为 1,那么这个信号量就是二值信号量,同一时间内只允许一个线程访问共享资源。
- 当信号量的值为 0 时,想访问共享资源的线程必须等待,直到信号量大于 0 时,等待的线程才可以访问。
一般,当访问共享资源时,信号量先执行“减一”操作,访问完成后再执行“加一”操作。
注意!信号量的值不能小于 0 。
与自旋锁的比较:
- 相比于自旋锁,信号量具有休眠特性,因此适用长时间占用资源的场合,但由于信号量会引起休眠,所以不能用在中断函数中。
- 如果共享资源的持有时间比较短,使用信号量的话会造成频繁的休眠,反而带来更多资源的消耗,使用自旋锁反而效果更好。
- 在同时使用信号量和自旋锁的时候,要先获取信号量,再使用自旋锁,因为信号量会导致睡眠,而自旋锁不支持休眠操作。
信号量的结构体定义如下:
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
不过需要注意的是:不能直接使用结构体中的任何内容。
信号量的API如下:
函数名 | 作用 |
---|---|
DEFINE_SEMAPHORE(name) | 定义一个 struct semaphore name 结构体, count 值设置为 1 |
void sema_init(struct semaphore *sem, int val) | 初始化 semaphore |
void down(struct semaphore *sem) | 获得信号量,如果暂时无法获得就会休眠返回之后就表示肯定获得了信号量在休眠过程中无法被唤醒,即使有信号发给这个进程也不处理 |
int down_trylock(struct semaphore *sem) | 尝试获得信号量,不会休眠,返回值:0:获得了信号量 1:没能获得信号量 |
void up(struct semaphore *sem) | 释放信号量,唤醒其他等待信号量的进程 |
信号量的使用:使用信号量完成:同一个驱动仅允许一个应用程序打开。
- 首先,定义一个信号量,可使用宏定义
DEFINE_SEMAPHORE(mysemaphore)
完成定义。或者使用先定义一个型号量变量,然后再使用函数void sema_init(struct semaphore *sem, int val)
完成初始化。可以看到宏定义的实质也是调用函数void sema_init(struct semaphore *sem, int val)
完成初始化操作。
#define DEFINE_SEMAPHORE(name) \
struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)
- 其次,打开设备时,使用函数
down(&mysemaphore)
执行信号量减一操作。
// 打开设备
int misc_open(struct inode *inode,struct file*file)
{
// 加锁
down(&mysemaphore);
printk("misc_open is ok.\r\n");
return 0;
}
- 最后,关闭设备时,使用函数
up(&mysemaphore)
完成信号量加一操作。
// 关闭设备
int misc_close(struct inode * inode, struct file *file)
{
// 解锁
up(&mysemaphore);
printk("misc_close\r\n");
return 0;
}
由于此处的实现驱动较为简答,因此,不再过多描述。大家直接将此处的misc_open
、misc_close
替换自旋锁的函数即可。并且两者的测试程序也是一样的。测试结果如下:
互斥锁
互斥锁,可以将之当成初值为1的信号量来理解。虽然两者功能相同但是具体实现方式不同,但是当需要完成互斥功能时,直接使用互斥锁效率更高、更简洁,所以如果使用到的信号量“量值”为 1,一般将其修改为使用互斥锁。
互斥锁会导致休眠,所以在中断里面不能用互斥锁。同一时刻只能有一个线程持有互斥锁,并且只有持有者才可以解锁,并且不允许递归上锁和解锁。
互斥锁需要注意:
-
一次只能有一个任务持有互斥对象
-
只有所有者才能解锁互斥对象
-
不允许多次解锁
-
不允许递归锁定
-
必须通过API初始化互斥对象
-
互斥对象不能通过memset或复制进行初始化
-
任务不能在持有互斥对象的情况下退出
-
不得释放持有锁所在的内存区域
-
持有的互斥对象不能重新初始化
-
互斥不能用于硬件或软件中断
互斥锁定义结构体如下:
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
};
实现互斥锁相关的API有:
函数名 | 作用 |
---|---|
mutex_init(mutex) | 初始化一个 struct mutex 指针 |
DEFINE_MUTEX(mutexname) | 初始化 struct mutex mutexname |
int mutex_is_locked(struct mutex *lock) | 判断 mutex 的状态1:被锁了(locked)0:没有被锁 |
void mutex_lock(struct mutex *lock) | 获得 mutex,如果暂时无法获得,休眠返回之时必定是已经获得了 mutex |
int mutex_trylock(struct mutex *lock) | 尝试获取 mutex,如果无法获得,不会休眠,返回值:1:获得了 mutex,0:没有获得注意,这个返回值含义跟一般的 mutex 函数相反 |
void mutex_unlock(struct mutex *lock) | 释放 mutex,会唤醒其他等待同一个 mutex 的线程 |
互斥锁的使用:使用互斥锁完成:同一个驱动仅允许一个应用程序打开。
- 先使用宏定义定义一个互斥锁
DEFINE_MUTEX(mymutex)
,参数即为互斥锁的名字。或者也可先定义一个互斥锁变量struct mutex
,然后再使用函数mutex_init
初始化。宏定义的原型为:
#define DEFINE_MUTEX(mutexname) \
struct mutex mutexname = __MUTEX_INITIALIZER(mutexname)
- 其次,打开设备时,使用函数
mutex_lock(&mylock)
执行加锁操作。
// 打开设备
int misc_open(struct inode *inode,struct file*file)
{
// 加锁
mutex_lock(&mylock);
printk("misc_open is ok.\r\n");
return 0;
}
- 最后,关闭设备时,使用函数
mutex_unlock(&mylock)
完成解锁操作。
// 关闭设备
int misc_close(struct inode * inode, struct file *file)
{
// 解锁
mutex_unlock(&mylock);
printk("misc_close\r\n");
return 0;
}
由于此处的实现驱动较为简答,因此,不再过多描述。大家直接将此处的misc_open
、misc_close
替换自旋锁的函数即可。并且两者的测试程序也是一样的。测试结果如下:
(是不是感觉三次的测试结果差不多,哈哈哈!😂😂😂)
互斥锁与信号量的区别:
-
信号量一般以同步的方式对共享资源进行控制,而互斥锁通过互斥实现共享资源的控制;
-
信号量可以对进程的共享资源进行控制,而互斥锁不行;
-
信号量的值为非负整数,而互斥锁的值只能为0或1;
-
互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到;mutex和二值信号量的区别在于mutex必须是同一个进程来释放
自旋锁与互斥锁的区别:
-
因为自旋锁不会引起调用者睡眠,所以效率比较高
-
自旋锁比较适用于锁使用者保持锁时间比较短的情况。文章来源:https://www.toymoban.com/news/detail-827013.html
-
自旋锁容易造成死锁,所以需要安全使用它;文章来源地址https://www.toymoban.com/news/detail-827013.html
到了这里,关于【linux驱动】讲解linux驱动开发中的并发与并行,并且给出解决驱动开发中资源竞争的解决方案(下)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!