在现代操作系统里,同一时间可能有多个内核执行流在执行,因此内核其实象多进程多线程编程一样也需要一些同步机制来同步各执行单元对共享数据的访问。尤其是在多处理器系统上,更需要一些同步机制来同步不同处理器上的执行单元对共享的数据的访问。
在主流的Linux内核中包含了几乎所有现代的操作系统具有的同步机制,这些同步机制包括:原子操作、信号量(semaphore)、读写信号量(rw_semaphore)、spinlock、BKL(Big Kernel Lock)、rwlock、brlock(只包含在2.4内核中)、RCU(只包含在2.6内核中)和seqlock(只包含在2.6内核中)。
比较经典的有原子操作、spin_lock(忙等待的锁)、mutex(互斥锁)、semaphore(信号量)等。并且它们几乎都有对应的rw_XXX(读写锁),以便在能够区分读与写的情况下,让读操作相互不互斥(读写、写写依然互斥)。而seqlock和rcu应该可以不算在经典之列,它们是两种比较有意思的同步机制。
原子操作
原子操作就是指某一个操作在执行过程中不可以被打断,要么全部执行,要不就一点也不执行。原子操作需要硬件的支持,与体系结构相关,使用汇编语言实现。原子操作主要用于实现资源计数,很多引用计数就是通过原子操作实现。Linux中提供了两种原子操作接口,分别是原子整数操作和原子位操作。
原子整数操作只对atomic_t类型的数据进行操作,不能对C语言的int进行操作,使用atomic_t只能将其作为24位数据处理,主要是在SPARC体系结构中int的低8为中设置了一个锁,避免对原子类型数据的并发访问。
原子位操作是针对由指针变量指定的任意一块内存区域的位序列的某一位进行操作。它只是针对普通指针的操作,不需要定义一个与该操作相对应的数据类型。
原子类型定义如下:
typedefstruct { volatile int counter; }atomic_t;
volatile修饰字段告诉gcc不要对该类型的数据做优化处理,对它的访问都是对内存的访问,而不是对寄存器的访问。
原子操作API包括:
atomic_read(atomic_t* v);
该函数对原子类型的变量进行原子读操作,它返回原子类型的变量v的值。
atomic_set(atomic_t* v, int i);
该函数设置原子类型的变量v的值为i。
voidatomic_add(int i, atomic_t *v);
该函数给原子类型的变量v增加值i。
atomic_sub(inti, atomic_t *v);
该函数从原子类型的变量v中减去i。
intatomic_sub_and_test(int i, atomic_t *v);
该函数从原子类型的变量v中减去i,并判断结果是否为0,如果为0,返回真,否则返回假。
voidatomic_inc(atomic_t *v);
该函数对原子类型变量v原子地增加1。
voidatomic_dec(atomic_t *v);
该函数对原子类型的变量v原子地减1。
intatomic_dec_and_test(atomic_t *v);
该函数对原子类型的变量v原子地减1,并判断结果是否为0,如果为0,返回真,否则返回假。
intatomic_inc_and_test(atomic_t *v);
该函数对原子类型的变量v原子地增加1,并判断结果是否为0,如果为0,返回真,否则返回假。
intatomic_add_negative(int i, atomic_t*v);
该函数对原子类型的变量v原子地增加I,并判断结果是否为负数,如果是,返回真,否则返回假。
intatomic_add_return(int i, atomic_t *v);
该函数对原子类型的变量v原子地增加i,并且返回指向v的指针。
intatomic_sub_return(int i, atomic_t *v);
该函数从原子类型的变量v中减去i,并且返回指向v的指针。
intatomic_inc_return(atomic_t * v);
该函数对原子类型的变量v原子地增加1并且返回指向v的指针。
intatomic_dec_return(atomic_t * v);
该函数对原子类型的变量v原子地减1并且返回指向v的指针。
原子操作通常用于实现资源的引用计数,在TCP/IP协议栈的IP碎片处理中,就使用了引用计数,碎片队列结构structipq描述了一个IP碎片,字段refcnt就是引用计数器,它的类型为atomic_t,当创建IP碎片时(在函数ip_frag_create中),使用atomic_set函数把它设置为1,当引用该IP碎片时,就使用函数atomic_inc把引用计数加1,当不需要引用该IP碎片时,就使用函数ipq_put来释放该IP碎片,ipq_put使用函数atomic_dec_and_test把引用计数减1并判断引用计数是否为0,如果是就释放Ip碎片。函数ipq_kill把IP碎片从ipq队列中删除,并把该删除的IP碎片的引用计数减1(通过使用函数atomic_dec实现)。
自旋锁
Linux自旋锁保证了任意时刻只能有一个执行线程进入临界区,其他试图进入临界区的线程将一直进行尝试(即自旋),直到获得该锁。自旋锁主要应用在加锁时间不长并且不会睡眠的情况。
自旋锁的本质是对内存区域的一个整数的操作,任何线程进入临界区之前都必须检查该整数,可用则进入,都则一直忙循环等待。
自旋锁机制让试图获得该锁的线程一直进行忙循环(占用CPU),因此自旋锁适合于断时间内进行轻量级加锁。而且自旋锁绝对不可以递归使用,否则会被自己锁死。
Linux自旋锁主要应用与多核处理器中,单CPU中不会进行自旋锁操作。
linux上的自旋锁有三种实现:
a. 在单cpu,不可抢占内核中,自旋锁为空操作。
b. 在单cpu,可抢占内核中,自旋锁实现为“禁止内核抢占”,并不实现“自旋”。
c. 在多cpu,可抢占内核中,自旋锁实现为“禁止内核抢占” + “自旋”。其中,禁止内核抢占只是关闭“可抢占标志”,而不是禁止进程切换。显式使用schedule或进程阻塞(此也会导致调用schedule)时,还是会发生进程调度的。
使用自旋锁需要注意有可能造成的死锁情况:
static DEFINE_SPINLOCK(xxx_lock);
unsigned long flags;
spin_lock_irqsave(&xxx_lock, flags);
。。。 critical section here 。。
spin_unlock_irqrestore(&xxx_lock, flags);
代码中spin_lock_irqsave会禁止本地cpu中断的抢占。以上代码在任何情况下都是安全的。但问题是关中断的代价太大。
如果把spin_lock_irqsave/spin_unlock_irqrestore换成spin_lock/spin_unlock会有什么问题吗?
答案是,如果中断中调用了spin_lock,可能会引起死锁!
例如:
spin_lock(&lock);
。。。
《- interrupt comes in:
spin_lock(&lock);
值得注意的是,如果产生中断的cpu和进程中调用spin_lock的cpu不是同一个,则不会有问题。这也是irq版本的spin_lock函数实现时只需要禁止本地cpu中断的原因。
结论:要想在进程中用spin_lock代替spin_lock_irqsave,条件是中断中不会使用相应的spin_lock
何时使用自旋锁?
不允许睡眠的上下文且临界区操作较短时使用自旋锁。
读/写自旋锁
Linux中规定,读/写自旋锁允许多个线程同时以只读的方式访问临界资源,只有当一个线程想更新数据时,才会互斥访问资源。
读写自旋锁包括一个24位读者计数和一个解锁标记来实现的。
注意:读写锁需要比spinlocks更多的访问原子内存操作,如果读临界区不是很大,最好别使用读写锁。
读写锁代码:
点击(此处)折叠或打开rwlock_t xxx_lock = __RW_LOCK_UNLOCKED(xxx_lock);
unsigned long flags;
read_lock_irqsave(&xxx_lock, flags);
。。 critical section that only reads the info 。。。
read_unlock_irqrestore(&xxx_lock, flags);
write_lock_irqsave(&xxx_lock, flags);
。。 read and write exclusive access to the info 。。。
write_unlock_irqrestore(&xxx_lock, flags);
读写锁比较适合链表等数据结构,特别是查找远多于修改的情况。
另外,可以灵活的使用read-write和irq版本的自旋锁。例如,如果中断中只是用了读锁,进程中就可以使用non-irq版本的读锁和irq版本的写锁。
注意:RCU比读写锁更适合遍历list,但需要更关注细节。目前kernel社区正在努力用RCU代替读写锁。
评论
查看更多