为什么需要同步?
并发访问共享数据可能会造成数据的不一致、相互覆盖等问题,是导致系统不稳定的隐患,但是这种问题又很难在调试中发现,所以对于共享资源要小心谨慎,需要同步。
并发执行是造成竞争条件的基础,中断、软中断、tasklet、内核抢占、多处理器等都会形成并发执行。
死锁:一个或多个线程和一个或多个资源,每个线程都在等待其中一个资源,但所有的资源都已被占用,造成所有的线程都在等待,无法运行。
同步方法
原子操作
原子操作可以保证指令以原子的方式执行(执行过程不会被打断)。
原子整数操作
atomic_t 类型的数据 是Linux内核里面声明为原子类型的整数变量,同时定义了一系列内联函数,用于操作atomic_t数据,加减等操作。
对于64位系统,定义了atomic64_t类型。
自旋锁 spinlock
自旋锁最多只能被一个可执行线程持有,如果一个执行线程试图获得一个已经被占用的自旋锁,那么该线程就会一直忙循环,直到锁可用。忙循环是无意义的浪费计算资源,所以持有自旋锁的时间应该尽可能的短。
自旋锁不可递归!
中断处理中也可以使用自旋锁,但是在获得自旋锁之前一定要禁止本地中断,否则,中断程序可能被中断,如果获取同一个自旋锁,就会死锁。
DEFINE_SPINLOCK(my_spinlock);
unsigned long flags;
spin_lock_irqsave(&my_spinlock, flags);
...
spin_unlock_irqrestore(&my_spinlock, flags);
内核配置选项CONFIG_DEBUG_SPINLOCK为使用自旋锁的代码加入许多调试检测手段。用以检查自旋锁是否正常。
读写自旋锁
对于某些数据结构,可能有多个线程要对其进行读写。读写锁是为了并发读,排斥写而生的。
多个线程可以并发的持有读锁,但是对于写操作,只能有一个线程,且此时不能有线程持有读锁。如果在请求写锁时,有读锁被持有,则自旋等待,直到读锁都被释放。
信号量
Linux中的信号量是一个睡眠锁。当进程想要获得某个信号量时,这个信号量是不可用的,那么这个进程就进入等待队列,让出CPU资源。直到该信号量可用,唤醒等待队列中的进程。
相较于自旋锁,信号量适合于锁会被长时间持有的情况。持有信号量的进程会被抢占,而持有自旋锁的进程不会被抢占。
互斥信号量:计数等于1,只允许一个进程持有该锁。down(), up()操作获得释放锁。
互斥体
mutex与互斥信号量很类似,只是更加简洁高效。它也会把未获得锁的进程睡眠。
RCU
Read-Copy Update,适用于频繁读数据,而修改数据并不多的场景。
宽限期:一个线程要修改数据,要等到所有读线程都读取完成后,才能更新数据,这个过程叫宽限期。
自旋锁是互斥的,同一时刻只能有一个线程进入临界区,读写自旋锁性能要好一些,允许多个读并发,但是updater不能同时执行。RCU允许一个updater(需要spin lock保证)和多个reader并发执行。