Linux驱动之 互斥、信号量、完成量学习记录:
互斥概念
信号量是在并行处理环境中对多个处理器访问某个公共资源进行保护的机制,mutex锁用于互斥操作。
mutex的语义相对于信号量要简单轻便一些,在锁争用激烈的测试场景下,mutex比信号量执行速度更快,可扩展性更好,
另外mutex数据结构的定义比信号量小。
struct mutex my_mutex; //定义
mutex_init(&mu_mutex); //初始化
//临界区,执行逻辑——操作共享数据。
mutex_lock(&my_mutex);
...... //临界资源
mutex_unlock(&my_mutex);
mutex的使用注意事项
同一时刻只有一个线程可以持有mutex。
只有锁持有者可以解锁,不能在一个进程中持有mutex,在另外一个进程中释放他。
不允许递归地加锁和解锁。
当进程持有mutex时,进程不可以退出。
mutex必须使用官方API来初始化。
mutex可以睡眠,所以不允许在中断处理程序或者中断下半部中使用,例如tasklet、定时器等
区别 | 信号量 | 互斥量 |
---|---|---|
适用对象 | 线程和进程 | 线程 |
最值 | 非负整数 | 0或1 |
操作 | PV操作可由不同线程完成 | 加锁和解锁必须由同一线程使用 |
应用 | 用于线程的同步 | 用于线程的互斥 |
互斥:主要关注于资源访问的唯一性和排他性。
同步:主要关注于操作的顺序,同步以互斥为前提。
当同一时刻,有多个对象需要同时持有锁的情况下,可以使用信号量。
信号量
加强版的互斥锁。适于多个资源多个线程访问的情况。
信号量一个有用的特性是它可以同时允许任意数量的锁持有者,而自旋锁和互斥锁在一个时刻最多允许一个任务持有它。
Linux信号量类型:
用户态信号量介绍参考链接:https://blog.csdn.net/silent123go/article/details/52763420
内核态信号量介绍参考链接:https://blog.csdn.net/silent123go/article/details/52760701
信号量核心操作:
- P
将信号量S减去1,若结果小于0,则把调用P的进程置成等待信号量S的状态。即为请求资源。
如果有一个任务想要获得已经被占用的信号量时,信号量会将其放入一个等待队列然后让其睡眠。这时处理器能重获自由,从而去执行其他代码。 - V
将信号量S加上1,若结果不大于0,则释放一个等待信号量S的进程。即为释放资源。
当持有信号量的进程将信号释放后,处于等待队列中的一个任务将被唤醒(因为队列中可能不止一个任务),并让其获得信号量。
内核态信号量主要函数:
定义一个信号量
#include <linux/semaphore.h>
struct semaphore my_sem;
数据结构:
struct semaphore {
//lock标志
raw_spinlock_t lock;
//count 相当于信号量的值,大于0,资源空闲;等于0,资源忙,但没有进程等待这个保护的资源;小于0,资源不可用,并至少有一个进程等待资源。
unsigned int count;
//wait_list 存放等待队列链表的地址,当前等待资源的所有睡眠进程都会放在这个链表中。
struct list_head wait_list;
};
信号量同时允许的持有者数量可以在声明信号量时指定。这个值称为使用者数量(usage count)或简单的叫做数量(count)。
通常情况下,信号量和自旋锁一样,在一个时刻仅允许有一个锁持有者。
这时计数等于1,这样的信号量被称为二值信号量或者称为互斥信号量。
初始化时也可以把数量设置为大于1的非0值。这种情况,信号量被称为计数信号量(counting semaphore),它允许在同一时刻至多有count个锁持有者。
初始化信号量
//初始化信号量,并设置信号量sem的值为val,尽管信号量可以被初始化为大于1的值而成为一个计数信号量,但是通常不建议这么去做。
void sema_init(struct semaphore *sem, int val);
//相关宏:
#define init_MUTEX(sem) sema_init(sem, 1)
#define init_MUTEX_LOCKED(sem) sema_init(sem, 0)
DELCEAR_MUTEX(my_sem); //初始化一个互斥的信号量,把sem的值设置为1,表示资源空闲
DELCEAR_MUTEX_LOCKED(my_sem); //初始化一个信号量,值设置为0,表示资源忙
获得一个信号量
//该函数用于获得信号量sem,它会导致睡眠,且不会被信号(signal)打断,因此不能再中断上下文中使用;
void down(struct semaphore *sem);
//该函数与down类似,因为down进入睡眠状态的进程不能被信号打断,
//但因为down_interruptible而进入睡眠状态的进程能被信号打断,信号也会导致该函数返回,返回非0;
int down_interruptible(struct semaphore *sem);
//睡眠的进程可以因为受到致命信号而被唤醒,中断获取信号量的操作。
int down_killable(struct semaphore *sem);
//该函数尝试获得信号量sem,如果能立即获得返回0,失败返回非0,不会导致调用者睡眠,可以再中断上下文中使用。
int down_trylock(struct semaphore *sem);
//该函数在一定时间内尝试获得信号量sem,如果能立即获得返回0,失败返回非0
int down_timeout(struct semaphore *sem, long jiffies);
down_interruptible常用写法:
//返回值一般会进行检查,如果非0,通常立即返回 -ERESTARTSYS
if(down_interruptible(&my_sem))
return -ERESTARTSYS;
释放信号,LINUX内核只提供了一个up函数
//该函数释放信号量sem,唤醒等待者。
void up(struct semaphore *sem);
在读写之前使用down_interruptible检查是否可以获得信号量,若不能直接返回,在读写完成后使用up来释放信号量。
信号量实现设备只能被一个进程打开,互斥
static DECLARE_MUTEX(my_sem); //declare mutex lock 1
static int xxx_open(struct ...)
{
...
if(down_trylock(&my_sem))
return -EBUSY;
...
return 0;
}
static int xxx_release(struct ...)
{
up(&my_sem);
return 0;
}
信号量用于同步 ,实现顺序执行
如果信号量初始化为0, 则它可以用于同步,同步意味着一个执行单元的继续执行需要另外一个执行单元完成某事,保证执行的先后顺序。
执行顺序: 代码区域a --> 代码区域c --> 代码区域b
执行单元A 执行单元B
struct semphore sem;
init_MUTEX_LOCKED(&sem); 代码区域c
代码区域a
down(&sem); //得到 <---------激活------ up(&sem);//释放
代码区域b
在执行单元A中,因为互斥量被设置为0,所以在down的时候试图获得信号量,因为得不到而进入休眠,所以代码区域b一直无法执行到,当执行单元B 发生,执行up来释放互斥量,使得执行单元A被唤醒,从而继续往下执行。
完成量
Linux系统还提供了一种比信号量更好的同步机制,即completion,
completion是一种轻量级的机制,它允许一个线程告诉另一个线程某个工作已经完成。
- 定义、初始化:
#include <linux/completion.h>
struct completion my_compeltion;
init_completion(&my_completion);
DECLARE_COMPLETION(my_completion);
//这个宏可用来快速执行重新初始化:
INIT_COMPLETION(my_completion);
- 等待完成量
等待一个完成量被唤醒
void wait_for_completion(struct completion *c);
- 唤醒完成量
//前者只唤醒一个等待的执行单元
void completion(struct completion *c);
//释放所有等待同意完成量的执行单元
void completion_all(struct completion *c);
完成量用于同步,实现顺序执行:
执行单元A 执行单元B
struct completion com;
init_completion(&com); 代码区域c
代码区域a
wait_for_completion(&com); <---------激活------ completion(&com);
代码区域b
参考学习视频:B站 一口Linux: https://space.bilibili.com/661326452/
参考文章链接:https://blog.csdn.net/vv0_0vv/article/details/7403123