同步与互斥
什么是同步与互斥
一、互斥
互斥是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的
二、同步
同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。
三、同步与互斥的联系与区别
同步其实已经实现了互斥,所以同步是一种更为复杂的互斥。所以可以说,互斥是一种特殊的同步
四、线程和进程的理解
线程:比如我们常说我们电脑是几核几核,其实就是说我们计算机里有几个核心的逻辑处理单元,比如说我们电脑是8核的,那么他最大就支持16线程。
进程:而每打开一个app就相当于打开了一个进程,而每个进程又可以被不同的线程切分为很多片。程是一个拷贝的 流程,而线程只是把一条河流截成很多条小溪。它没有拷贝这些额外的开销,但是仅仅是现存的一条河流,就被多线程技术几乎无开销地转成很多条小流程,它的伟 大就在于它少之又少的系统开销。
多线程和多进程就会带来多个进程或者多个线程同时访问同一资源的问题,也就是并发,如果我们想要在某个进程或者线程使用该资源的过程中不让其他进程或者线程打断或使用该资源,那么我们就可以使用同步或者互斥的技术来解决该问题。
五、同步与互斥失败的例子
例一:
static int valid = 1; static ssize_t gpio_key_drv_open (struct inode *node, struct file *file) { if (!valid) { return -EBUSY; } else { valid = 0; } return 0; //成功 } static int gpio_key_drv_close (struct inode *node, struct file *file) { valid = 1; return 0; }
程序A执行到第11行之前,被程序B抢占了,这时valid尚未被改成0;
程序B调用gpio_key_drv_open时,发现valid等于1,所以成功返回0;
当程序A继续从第11行执行时,它最终也成功返回0;
这样程序A、B都成功打开了驱动程序。
注意:在内核态,程序A不是主动去休眠、主动放弃CPU资源;而是被优先级更高的程序B抢占了,这种行为被称为“preempt”(抢占)。
例二:
static int valid = 1; static ssize_t gpio_key_drv_open(struct inode *node,struct file *file) { if(--valid){ valid++; return -EBUSY; } return 0; } static int gpio_key_drv_close (struct inode *node, struct file *file) { valid = 1; return 0; }
进程A在读出valid时发现它是1,减1后为0,这时if不成立;但是修改后的值尚未写回内存;
假设这时被程序B抢占,程序B读出valid仍为1,减1后为0,这时if不成立,最后成功返回;
轮到A继续执行,它把0值写到valid变量,最后也成功返回。
这样程序A、B都成功打开了驱动程序。
六、原子变量
1、原理与使用
在上面的第2个失败例子里,问题在于对valid变量的修改被打断了。如果对valid变量的操作不能被打断,就解决这个问题了。
这可以使用原子操作,所谓“原子操作”就是这个操作不会被打断。Linux有2种原子操作:原子变量、原子位。
2、原子变量的内核操作数
原子变量的操作函数在Linux内核文件arch/arm/include/asm/atomic.h中。
原子变量类型如下,实际上就是一个结构体(内核文件include/linux/types.h):
typedef struct{ int counter; }atomic_t;
函数名 | 作用 |
---|---|
atomic_read(v) | 读出原子变量的值,即v->counter |
atomic_set(v,i) | 设置原子变量的值,即v->counter = i |
atomic_inc(v) | v->counter++ |
atomic_dec(v) | v->counter-- |
atomic_add(i,v) | v->counter += i |
atomic_sub(i,v) | v->counter -= i |
atomic_inc_and_test(v) | 先加1,再判断新值是否等于0;等于0的话,返回值为1 |
atomic_dec_and_test(v) | 先减1,再判断新值是否等于0;等于0的话,返回值为1 |
以上是内核提供的操作函数
3、原子变量使用实例
static atomic_t valid = ATOMIC_INIT(1); static struct ssize_t gpio_keys_drv_open(struct inode *node, struct file *file){ if(atomic_dec_and_test(&valid)){ open_drv(); return 0; } atomic_inc(&valid); return -EBUSY; } static int gpio_keys_drv_open(struct inode *node, struct file *file){ atomic_inc(&valid); return 0; }
原子位,因为不常用就不做介绍了。
七、linux锁
1、锁的类型
Linux内核提供了很多类型的锁,它们可以分为两类:
① 自旋锁(spinning lock);
② 睡眠锁(sleeping lock)。
2、自旋锁
简单地说就是无法获得锁时,不会休眠,会一直循环等待。
自旋锁的加锁、解锁函数是:spin_lock、spin_unlock,还可以加上各种后缀,这表示在加锁或解锁的同时,还会做额外的事情:
后缀 | 描述 |
---|---|
_bh() | 加锁时禁止下半部(软中断),解锁时使能下半部(软中断) |
_irq() | 加锁时禁止中断,解锁时使能中断 |
_irqsave/restore() | 加锁时禁止并中断并记录状态,解锁时恢复中断为所记录的状态 |
3、睡眠锁
简单地说就是无法获得锁时,当前线程就会休眠。有这些休眠锁:
休眠锁 | 描述 |
---|---|
mutex | mutual exclusion,彼此排斥,即互斥锁(后面讲解) |
rt_mutex | |
semaphore | 信号量、旗语(后面讲解) |
rw_semaphore | 读写信号量,读写互斥,但是可以多人同时读 |
ww_mutex | |
percpu_rw_semaphore | 对rw_semaphore的改进,性能更优 |
4、锁的内核函数
spinlock函数在内核文件include\linux\spinlock.h中声明,如下表:
自旋锁
函数名 | 作用 |
---|---|
spin_lock_init(_lock) | 初始化自旋锁为unlock状态 |
void spin_lock(spinlock_t *lock) | 获取自旋锁(加锁),返回后肯定获得了锁 |
int spin_trylock(spinlock_t *lock) | 尝试获得自旋锁,成功获得锁则返回1,否则返回0 |
void spin_unlock(spinlock_t *lock) | 释放自旋锁,或称解锁 |
int spin_is_locked(spinlock_t *lock) | 返回自旋锁的状态,已加锁返回1,否则返回0 |
自旋锁的加锁、解锁函数是:spin_lock、spin_unlock,还可以加上各种后缀,这表示在加锁或解锁的同时,还会做额外的事情:
后缀 | 描述 |
---|---|
_bh() | 加锁时禁止下半部(软中断),解锁时使能下半部(软中断) |
_irq() | 加锁时禁止中断,解锁时使能中断 |
_irqsave/restore() | 加锁时禁止并中断并记录状态,解锁时恢复中断为所记录的状态 |
信号量
semaphore函数在内核文件include\linux\semaphore.h中声明,如下表:
函数名 | 作用 |
---|---|
DEFINE_SEMAPHORE(name) | 定义一个struct semaphore name结构体,count值设置为1 |
void sema_init(struct semaphore *sem, int val) | 初始化semaphore |
void down(struct semaphore *sem) | 获得信号量,如果暂时无法获得就会休眠返回之后就表示肯定获得了信号量在休眠过程中无法被唤醒,即使有信号发给这个进程也不处理 |
int down_interruptible(struct semaphore *sem) | 获得信号量,如果暂时无法获得就会休眠,休眠过程有可能收到信号而被唤醒,要判断返回值:0:获得了信号量-EINTR:被信号打断 |
int down_killable(struct semaphore *sem) | 跟down_interruptible类似,down_interruptible可以被任意信号唤醒,但down_killable只能被“fatal signal”唤醒,返回值:0:获得了信号量-EINTR:被信号打断 |
int down_trylock(struct semaphore *sem) | 尝试获得信号量,不会休眠,返回值:0:获得了信号量1:没能获得信号量 |
int down_timeout(struct semaphore *sem, long jiffies) | 获得信号量,如果不成功,休眠一段时间返回值:0:获得了信号量-ETIME:这段时间内没能获取信号量,超时返回down_timeout休眠过程中,它不会被信号唤醒 |
void up(struct semaphore *sem) | 释放信号量,唤醒其他等待信号量的进程 |
互斥锁
mutex函数在内核文件include\linux\mutex.h中声明,如下表:
函数名 | 作用 |
---|---|
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_lock_interruptible(struct mutex *lock) | 获得mutex,如果暂时无法获得,休眠;休眠过程中可以被信号唤醒,返回值:0:成功获得了mutex-EINTR:被信号唤醒了 |
int mutex_lock_killable(struct mutex *lock) | 跟mutex_lock_interruptible类似,mutex_lock_interruptible可以被任意信号唤醒,但mutex_lock_killable只能被“fatal signal”唤醒,返回值:0:获得了mutex-EINTR:被信号打断 |
int mutex_trylock(struct mutex *lock) | 尝试获取mutex,如果无法获得,不会休眠,返回值:1:获得了mutex,0:没有获得注意,这个返回值含义跟一般的mutex函数相反, |
void mutex_unlock(struct mutex *lock) | 释放mutex,会唤醒其他等待同一个mutex的线程 |
int atomic_dec_and_mutex_lock(atomic_t *cnt, struct mutex *lock) | 让原子变量的值减1,如果减1后等于0,则获取mutex,返回值:1:原子变量等于0并且获得了mutex0:原子变量减1后并不等于0,没有获得mutex |
自旋锁和信号量的区别
semaphore中可以指定count为任意值,比如有10个厕所,所以10个人都可以使用厕所。
而mutex的值只能设置为1或0,只有一个厕所。
是不是把semaphore的值设置为1后,它就跟mutex一样了呢?不是的。
看一下mutex的结构体定义,如下:
它里面有一项成员“struct task_struct *owner”,指向某个进程。一个mutex只能在进程上下文中使用:谁给mutex加锁,就只能由谁来解锁。
而semaphore并没有这些限制,它可以用来解决“读者-写者”问题:程序A在等待数据──想获得锁,程序B产生数据后释放锁,这会唤醒A来读取数据。semaphore的锁定与释放,并不限定为同一个进程。
主要区别列表如下:
semaphore | mutex | |
---|---|---|
几把锁 | 任意,可设置 | 1 |
谁能解锁 | 别的程序、中断等都可以 | 谁加锁,就得由谁解锁 |
多次解锁 | 可以 | 不可以,因为只有1把锁 |
循环加锁 | 可以 | 不可以,因为只有1把锁 |
任务在持有锁的期间可否退出 | 可以 | 不建议,容易导致死锁 |
硬件中断、软件中断上下文中使用 | 可以 | 不可以 |
5、使用锁的时机
你可能看不懂上面这个表格,请学习完后面的章节再回过头来看这个表格。
举例简单介绍一下,上表中第一行“IRQ Handler A”和第一列“Softirq A”的交叉点是“spin_lock_irq()”,意思就是说如果“IRQ Handler A”和“Softirq A”要竞争临界资源,那么需要使用“spin_lock_irq()”函数。为什么不能用spin_lock而要用spin_lock_irq?也就是为什么要把中断给关掉?假设在Softirq A中获得了临界资源,这时发生了IRQ A中断,IRQ Handler A去尝试获得自旋锁,这就会导致死锁:所以需要关中断。
6、内核抢占(preempt)等额外的概念
早期的的Linux内核是“不可抢占”的,假设有A、B两个程序在运行,当前是程序A在运行,什么时候轮到程序B运行呢?
① 程序A主动放弃CPU:
比如它调用某个系统调用、调用某个驱动,进入内核态后执行了schedule()主动启动一次调度。
② 程序A调用系统函数进入内核态,从内核态返回用户态的前夕:
这时内核会判断是否应该切换程序。
③ 程序A正在用户态运行,发生了中断:
内核处理完中断,继续执行程序A的用户态指令的前夕,它会判断是否应该切换程序。
从这个过程可知,对于“不可抢占”的内核,当程序A运行内核态代码时进程是无法切换的(除非程序A主动放弃),比如执行某个系统调用、执行某个驱动时,进程无法切换。
这会导致2个问题:
① 优先级反转:
一个低优先级的程序,因为它正在内核态执行某些很耗时的操作,在这一段时间内更高优先级的程序也无法运行。
② 在内核态发生的中断不会导致进程切换
为了让系统的实时性更佳,Linux内核引入了“抢占”(preempt)的功能:进程运行于内核态时,进程调度也是可以发生的。
回到上面的例子,程序A调用某个驱动执行耗时的操作,在这一段时间内系统是可以切换去执行更高优先级的程序。
对于可抢占的内核,编写驱动程序时要时刻注意:你的驱动程序随时可能被打断、随时是可以被另一个进程来重新执行。对于可抢占的内核,在驱动程序中要考虑对临界资源加锁。
7、使用场景
7.1、只在用户上下文加锁
假设只有程序A、程序B会抢占资源,这2个程序都是可以休眠的,所以可以使用信号量,代码如下:
static DEFINE_SPINLOCK(clock_lock); // 或 struct semaphore sem; sema_init(&sem, 1); if (down_interruptible(&sem)) // if (down_trylock(&sem)) { /* 获得了信号量 */ } /* 释放信号量 */ up(&sem);
对于down_interruptible函数,如果信号量暂时无法获得,此函数会令程序进入休眠;别的程序调用up()函数释放信号量时会唤醒它。
在down_interruptible函数休眠过程中,如果进程收到了信号,则会从down_interruptible中返回;对应的有另一个函数down,在它休眠过程中会忽略任何信号。
*注意*:“信号量”(semaphore),不是“信号”(signal)。
也可以使用mutex,代码如下:
static DEFINE_MUTEX(mutex); //或 static struct mutex mutex; mutex_init(&mutex); mutex_lock(&mutex); /* 临界区 */ mutex_unlock(&mutex);
*注意*:一般来说在同一个函数里调用mutex_lock或mutex_unlock,不会长期持有它。这只是惯例,如果你使用mutex来实现驱动程序只能由一个进程打开,在drv_open中调用mutex_lock,在drv_close中调用mutex_unlock,这也完全没问题。
7.2、在用户上下文与Softirqs之间加锁
假设这么一种情况:程序A运行到内核态时,正在访问一个临界资源;这时发生了某个硬件中断,在硬件中断处理完后会处理Softirq,而某个Softirq也会访问这个临界资源。
怎么办?
在程序A访问临界资源之前,干脆禁止Softirq好了!
可以使用spin_lock_bh函数,它会先禁止本地CPU的中断下半部即Softirq,这样本地Softirq就不会跟它竞争了;假设别的CPU也想获得这个资源,它也会调用spin_lock_bh禁止它自己的Softirq。这2个CPU都禁止自己的Softirq,然后竞争spinlock,谁抢到谁就先执行。可见,在执行临界资源的过程中,本地CPU的Softirq、别的CPU的Softirq都无法来抢占当前程序的临界资源。
释放锁的函数是spin_unlock_bh。
spin_lock_bh/spin_unlock_bh的后缀是“_bh”,表示“Bottom Halves”,中断下半部,这是软件中断的老名字。这些函数改名为spin_lock_softirq也许更恰当,请记住:spin_lock_bh会禁止Softirq,而不仅仅是禁止“中断下半部”(timer、tasklet里等都是Softirq,中断下半部只是Softirq的一种)。
示例代码如下:
static DEFINE_SPINLOCK(lock); // static spinlock_t lock; spin_lock_init(&lock); spin_lock_bh(&lock); /* 临界区 */ spin_unlock_bh(&lock);
7.3、在用户上下文与Tasklet之间加锁
Tasklet也是Softirq的一种,所以跟前面是“在用户上下文与Softirqs之间加锁”完全一样。
7.4、在用户上下文与timer之间加锁
Timer也是Softirq的一种,所以跟前面是“在用户上下文与Softirqs之间加锁”完全一样。
7.5、在tasklet与timer之间加锁
假设在Tasklet中访问临界资源,另一个CPU会不会同时运行这个Tasklet?不会的,所以如果只是在某个Tasklet中访问临界资源,无需上锁。
假设在Timer中访问临界资源,另一个CPU会不会同时运行这个timer?不会的,所以如果只是在某个Timer中访问临界资源,无需上锁。
如果在有2个不同的Tasklet或Timer都会用到一个临界资源,那么可以使用spin_lock()、spin_unlock()来保护临界资源。不需要用spin_lock_bh(),因为一旦当前CPU已经处于Tasklet或Timer中,同一个CPU不会同时再执行其他Tasklet或Timer。
7.6、在softirq之间加锁
这里讲的softirq不含tasklet、timer。
同一个Softirq是有可能在不同CPU上同时运行的,所以可以使用spin_lock()、spin_unlock()来访问临界区。如果追求更高的性能,可以使用“per-CPU array”,本章不涉及。
不同的Softirq之间,可以使用spin_lock()、spin_unlock()来访问临界区。
总结起来,在Softirq之间(含timer、tasklet、相同的Softirq、不同的Softirq),都可以使用spin_lock()、spin_unlock()来访问临界区。
static DEFINE_SPINLOCK(lock); // static spinlock_t lock; spin_lock_init(&lock); spin_lock(&lock); /* 临界区 */ spin_unlock(&lock);
7.7、硬中断上下文
假设一个硬件中断服务例程与一个Softirq共享数据,需要考虑2点:
① Softirq执行的过程中,可能会被硬件中断打断;
② 临界区可能会被另一个CPU上的硬件中断进入。
怎么办?
在Softirq获得锁之前,禁止当前CPU的中断。
在硬件中断服务例程中不需要使用spin_lock_irq(),因为当它在执行的时间Softirq是不可能执行的;它可以使用spin_lock()用来防止别的CPU抢占。
如果硬件中断A、硬件中断B都要访问临界资源,怎么办?这篇文章里说要使用spin_lock_irq():
https://mirrors.edge.kernel.org/pub/linux/kernel/people/rusty/kernel-locking/
*但是*我认为使用spin_lock()就足够了。因为Linux不支持中断嵌套,即当前CPU正在处理中断A时,中断B不可能在当前CPU上被处理,不需要再次去禁止中断;当前CPU正在处理中断A时,假如有另一个CPU正在处理中断B,它们使用spin_lock()实现互斥访问临界资源就可以了。
spin_lock_irq()/spin_unlock_irq()会禁止/使能中断,另一套函数是spin_lock_irqsave()/spin_unlock_irqrestore(),spin_lock_irqsave()会先保存当前中断状态(使能还是禁止),再禁止中断;spin_unlock_irqrestore()会恢复之前的中断状态(不一定是使能中断,而是恢复成之前的状态)。
示例代码如下:
static DEFINE_SPINLOCK(lock); // static spinlock_t lock; spin_lock_init(&lock); spin_lock_irq(&lock); /* 临界区 */ spin_unlock_irq(&lock);
标签:同步,struct,中断,lock,互斥,加锁,mutex,spin From: https://www.cnblogs.com/hlprogrammer/p/17125855.html