内核同步
在使用共享内存的应用程序中,程序员必须留意保护共享资源,防止对共享资源的并发访问,内核也不例外。如果多个执行线程同时访问和操作数据,就有可能发生各线程之间相互覆盖共享数据的情况,造成被访问的数据处于不一致状态。
1.临界区和竞争条件
临界区就是访问和操作共享数据的代码段。多个执行线程并发访问同一个资源是不安全的,因此通常对临界区采用原子操作(顾名思义,这些操作看作一个原子,不可分的单元)。若两个执行线程处于同一个临界区同时执行,这是一个bug,称这种情况为竞争条件。避免并发和防止竞争条件称为同步(synchronization)。
2.造成并发执行的原因
内核中造成并发执行的原因有:
- 中断,中断可能随时打断当前执行的代码
- 软中断和tasklet
- 内核抢占,内核中的任务可能被另一个任务抢占
- 睡眠以及与用户空间的同步,内核执行的进程可能睡眠,就会唤醒调度程序,从而导致调度一个新的用户进程执行
- 对此多处理,多个处理器同时执行代码
3.死锁
死锁需要四个必要条件:
- 互斥条件:一个资源每次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,不释放已获得资源
- 不剥夺条件:进程已获得的资源,在未使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
4.内核同步方法
Linux内核提供了一组相当完备的同步方法。
原子操作
内核提供了两组原子操作接口——一组针对整数进行操作,另一组针对单独的位进行操作。
自旋锁
Linux内核中最常见的锁是自旋锁,自旋锁只能被一个可执行线程持有。若一个执行线程试图获得一个被占用的自旋锁,那么该线程就会一直忙循环——旋转——等待锁重新可用。
用现实生活例子解释,你去上厕所,厕所只有一个坑,先去的那人把门锁了,你只有抽根烟,再去看看出来没,没出来再去抽根烟,在回来看看...只有持锁的人才能开门,所以自旋锁不适合长时间占用临界区的执行线程。
Linux中自旋锁接口定义在<linux/spinlock.h>
中,基本形式如下:
DEFINE_SPINLOCK(mr_lock);
spin_lock(&mr_lock);
/*临界区*/
spin_unlock(&mr_lock);
对于读写操作,还存在读-写自旋锁:
- 一个或多个读任务可以并发持有读者锁
- 写的锁最多只能被一个写任务持有,而且此时不能有并发读操作
信号量
Linux中的信号量是一种睡眠锁。如果一个任务试图获得一个不可用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。此时处理器能重获自由,从而执行其他代码。
同样是上厕所,这个厕所升级了,具有一个记录和通知功能,当发现厕所坑被占用之后,到来的人就在册子末尾添加自己的电话,你就可以继续去抽烟泡妞。等蹲坑那哥们一出来,就挑选册子上第一个电话号码,打电话通知你,兄弟来上厕所。
计数信号量是高级版信号量,就是说允许多个执行线程(多个但是有上限)同时访问临界区。通过PV操作执行计数器的加减操作。
继续上厕所,厕所扩容了,不止一个坑!!!因此这里就有一个计数器,最大值就是坑数,来一个人就减一,走一个人就加一。计数器为0时,嘿嘿,小伙,再夹一会吧,有坑,哥会打电话给你的。
互斥体(mutex)
互斥体和信号量类似,但是互斥体的上锁者必须负责给其解锁,因此互斥体不适合内核同用户空间复杂的同步场景,更多用于同一上下文的上锁和解锁。
小结
同步是是Linux以及多线程编程等场景下非常重要的内容,需要进一步了解同步的知识,需要动手实验一下。