4.信号量
Linux中的信号量是一种睡眠锁。如果一个任务试图获得一个已经被占用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。这时处理器能重获自由,从而去执行其他代码。当持有信号量的进程将信号量释放后,处于等待队列中的那个任务将被唤醒,并获得该信号量。
信号量和自旋锁在使用上的差异:
1)由于争用信号量的过程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长时间持有的情况;相反,锁被短时间持有时,使用信号量就不太适宜了。因为睡眠、维护等待队列以及唤醒所花费的开销可能比锁被占用的全部时间还要长。
2)由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中才能获取信号量锁,因为中断上下文中是不能进行调度的。
3)你可以在持有信号量时去睡眠,因为当其他进程试图获得同一信号量时不会因此而死锁(因为该进程也只是去睡眠而已,最终会继续执行的)。
4)在你占用信号量的同时不能占用自旋锁。因为在你等待信号量时可能会睡眠,而在持有自旋锁时是不允许睡眠的。
5)信号量同时允许任意数量的锁持有者,而自旋锁在一个时刻最多允许一个任务持有它。
静态声明信号量
static DECLARE_SEMAPHORE_GENRIC( name, count );
name 是信号量变量名,count 是信号量的使用者数量,创建互斥信号量也可以使用:
static DECLARE_MUTEX( name );
动态声明信号量
sema_init( sem, count );
sem 为结构指针,count 是使用者数量,初始化一个互斥信号量也可以使用:
init_MUTEX( sem );
/* 定义并声明一个信号量,名字为 mr_sem,用于信号量计数 */ static DECLARE_MUTEX( mr_sem ); /* 试图获取信号量... */ if ( down_interruptible( &mr_sem ) ) { /* 信号被接收,信号量还未获取 */ } /* 临界区... */ /* 释放给定的信号量 */ up( &mr_sem );
5.读-写信号量
读-写信号量在内核中是由 rw_semaphore 结构表示的,定义在文件<linux/rwsem.h>中,通过以下语句创建读-写信号量。
静态:static DECLARE_RWSEM( name );
动态:init_rwsem( struct rw_semaphore *sem );
所有的读-写信号量都是互斥信号量,只要没有写者,并发持有读锁的读者锁不限。相反,只有惟一的写者(在没有读者时)可以获得写锁。所有读-写锁的睡眠都不会被信号打断,所以它只有一个版本的down操作。
static DECLARE_RWSEM( mr_rwsem ); /* 试图获取信号量用于读... */ down_read( &mr_rwsem ); /* 临界区(只读)... */ /* 释放信号量 */ up_read( &mr_rwsem ); /* 试图获取信号量用于写... */ down_write( &mr_sem ); /* 临界区(读和写)... */ /* 释放信号量 */ up_write( &mr_sem );
读-写信号量也提供了 down_read_trylock 和 down_write_trylock方法。如果成功获得了信号量锁,它们返回非0值;如果信号量锁被争用,则返回0。要小心——这与普通信号量的情形完全相反。
读-写信号量相比读-写自旋锁多一种特有的操作,downgrade_writer。这个函数可以动态地将获取的写锁转换为读锁。
6.完成变量
如果在内核中一个任务需要发出信号通知另一任务发生了某个特定事件,利用完成变量(completion variable)是使两个任务得以同步的简单方法。如果一个任务要执行一些工作时,另一个任务就会在完成变量上等待。当这个任务完成工作后,会使用完成变量去唤醒在等待的任务。
7.Seq锁
这种锁提供了一种很简单的机制,用于读写共享数据。实现这种锁主要依靠一个序列计数器。当有疑义的数据被写入时,会得到一个锁,并且序列值会增加。在读取数据之前和之后,序列号都被读取。如果读取的序列号值相同,说明在读操作进行的过程中没有被写操作打断过。此外,如果读取的值是偶数,那么就表明写操作没有发生(要明白因为锁的初值是0,所以写锁会使值成奇数,释放的时候变成偶数)。
/* 定义一个seq锁 */ seqlock_t mr_seq_lock = SEQLOCK_UNLOCKED; write_seqlock( &mr_seq_lock ); /* 写锁被获取... */ write_sequnlock( &mr_seq_lock ); /* 在读的情况,与自旋锁有很大不同 */ unsigned long seq; do { seq = read_seqbegin( &mr_seq_lock ); /* 这里读数据... */ } while ( read_seqretry( &mr_seq_lock, seq ) );
在多个读者和少数写者共享一把锁的时候,seq锁有助于提供一种非常轻量级和具有可扩展性的外观。但是 seq 锁对写者更有利,只要没有其他写者,写锁总是能够被成功获得。挂起的写者会不断地使得读操作循环(前一个例子),直到不再有任何写者持有锁为止。
8.禁止抢占
由于内核是抢占性的,内核中的进程在任何时刻都可能停下来以便另一个具有更高优先权的进程运行。这意味着一个任务与被抢占的任务可能会在同一个临界区内运行。为了避免这种情况,内核抢占代码使用自旋锁作为非抢占区域的标记。如果一个自旋锁被持有,内核便不能进行抢占。
实际中,某些情况并不需要自旋锁,但是仍然需要关闭内核抢占。为了解决这个问题,可以通过 preempt_disable 禁止内核抢占。这是一个可以嵌套调用的函数,可以调用任意次。每次调用都必须有一个相应的 preempt_enable 调用。当最后一次 preempt_enable 被调用后,内核抢占才重新占用。
preempt_disable(); /* 抢占被禁止... */ preempt_enable();
为了用更简洁的方法解决每个处理器上的数据访问问题,可以通过 get_cpu 获得处理器编号,这个函数在返回当前处理器号前首先会关闭内核抢占。
int cpu; /* 禁止内核抢占,并将 cpu 设置为当前处理器 */ cpu = get_cpu(); /* 对每个处理器的数据进行操作... */ /* 再给予内核抢占性 */ put_cpu();
9.顺序和屏障
编译器和处理器为了提高效率,可能对读和写重新排序,幸好,所有可能重新排序和写的提供了机器指令来确保顺序要求,同样也可以指示编译器不要对给定点周围的指令序列进行重新排序。这些确保顺序的指令称做屏障(barrier)。