Linux内核中的同步
如果一个进程正处于临界区时被非自愿抢占了,而且这个新调度的进程随后也进入同一个临界区,前后两个进程之间就会产生竞争,其实两者并不真是同时发生的,但它们相互交叉进行,所以叫做伪并发。如果有一台支持对称多处理器(SMP)的机器,那么两个进程就可以真正地在临界区中同时执行,这就叫真并发。
并发产生的原因有:第一,中断;第二,软中断和tasklet;第三,内核抢占;第四,睡眠及与用户空间的同步;第五,对称多处理。
设计锁在开始阶段都很粗,但当锁的争用问题变得严重时,设计就向更加细的加锁方向发展。那么什么样的数据需要加锁呢?其一,如果有其他执行线程可以访问这些数据,那么就给这些数据加上某种形式的锁;。其二,如果任何其他什么东西都能看到它,那么就要锁住它。其三,几乎访问所有的内核全局变量和共享数据都需要某种形式的同步方法。
每个线程都在等待其中的一个资源,但所有的资源都已经被占用了,所有线程都在相互等待,但它们永远不会释放已经占有的资源,于是任何线程都无法继续,这就是死锁。
死锁产生的原因主要有三点:第一,使用不可剥夺设备(比如打印机、扫描仪);第二,进程在已经占有一个设备的基础上又提出使用新设备,而该新设备又被其他进程占有了,导致进程运行暂停;第三,进程之间连环等待资源。如A等B,B等C,C等A。
避免死锁的办法:其一,按顺序加锁,如果有两个或多个锁曾在同一时间里被请求,那么以后其他函数请求它们时也必须按照前次加锁的顺序加锁。其二,防止发生饥饿,也就是代码要保证能结束。其三,不要重复请求同一个锁。其四,所以进程在开始运行前,申请需要占用的全部设备,防止中途因申请不到而暂停。其五,在进程申请不到继续运行所需要的设备时,必须释放已经占用的资源。
原子操作
内核提供两组原子操作的接口,一个针对整数,一个针对位进行操作。在编写代码时,能使用原子操作时,就尽量不要使用复杂的加锁机制。非原子位函数与原子位函数的操作完全相同,但是前者不保证原子性,且其名前缀多两个下划线。例如与test_bit函数对应的非原子形式__test_bit函数,如果你不需要原子性操作(比如你已经锁保护了自己的数据),那么这些非原子位函数将比原子位函数执行得更快些。
自旋锁
如果一个执行线程试图获得一个已经持有的自旋锁,那么该线程就会一直进行忙循环,旋转,等待锁重新可用,所以自旋锁的作用是在短期内进行轻量级加锁,持有自旋锁的世界最好小于两次上下文切换的耗时,否则还不如信号量呢。如果
写和读不能清楚分开,那么使用一般自旋锁就可以了,不要使用读写自旋锁。总之,如果加锁时间不长并且代码不会睡眠(比如中断处理程序),利用自旋锁是最佳的。如果加锁时间可能很长或者代码在持有锁时可能睡眠,那么最好用信号量完成加锁。
自旋锁可用使用在中断程序中,在中断程序中使用自旋锁前,一定要禁止本地中断,否则中断程序就会打断正持有锁的内核代码,有可能去试图争用这个已经被持有的锁,从而导致双重请求死锁。我们使用的最单纯的自旋锁是spin_lock和spin_unlock函数,而我们使用带处理本地中断锁的是spin_lock_irqsave和spin_unlock_irqrestore函数。
自旋锁和下半部的问题
由于下半部(或中断)可以抢占进程上下文中的代码,所以当下半部(或中断)和进程上下文共享数据时,必须对进程上下文中的共享数据进行保护,所以需要加锁的同时还有禁止下半部(或中断)的执行。同类的tasklet不可能同时运行,所以对于同类tasklet中的共享数据不需要保护,但是当数据被两个不同种类的tasklet共享时,就需要在访问下半部中的数据前先获得一个普通的自旋锁,这里不需要要禁止下半部分。对于软中断,无论是否同种类型,如果数据被软中断共享,那么它必须得到锁的保护,这里也不需要禁止下半部。
信号量
信号量适用于锁被长时间持有的情况,持有信号量的代码可以被抢占,可以在持有信号量时去睡眠,但是当占用信号量的时候不能同时占有自旋锁,因为在等待信号量时可能会睡眠,而在持有自旋锁时是不允许睡眠的。
信号量可以同时允许任意数量的锁持有者,通常只有一个持有者的信号量叫互斥信号量,我们常用dowm_inperruptible函数获取信号量,它将把调用进程设置为TASK_INTERRUPTIBLE状态进入睡眠,如果用dowm函数获取信号量,它则将把调用进程设置为TASK_UNINTERRUPTIBLE状态进入睡眠,但这样进程在等待信号量的时候就不再响应信号了,所以,常用dowm_inperruptible函数获取信号量,用up函数释放信号量。