Linux内核读写信号量实现
来源:互联网 发布:手机网络棋牌作弊器 编辑:程序博客网 时间:2024/06/05 18:24
以下分析基于Linux kernel 3.10
读写信号量的原理
读写信号量的特点是:
1. 同一时刻最多有一个写者(writer)获得锁;
2. 同一时刻可以有多个读者(reader)获得锁;
3. 同一时刻写者和读者不能同时获得锁;
由于读者可以同时获得锁,因此提高了系统的并发程度,进而提高了系统的性能。
下图用状态转换图表示了在调用不同API时锁的相应状态。需要注意的是,锁的状态其实可以直接从读者有锁跳到写者有锁,例如有写者正sleep在down_write,而最后一个读者调用了up_read(),这时写者直接获得锁。
读写信号量的定义和API
参见文件include/linux/rwsem.h, 下面是读写信号量的定义和常用API。
struct rw_semaphore { long count; raw_spinlock_t wait_lock; struct list_head wait_list;#ifdef CONFIG_DEBUG_LOCK_ALLOC struct lockdep_map dep_map;#endif};#define init_rwsem(sem) \do { \ static struct lock_class_key __key; \ \ __init_rwsem((sem), #sem, &__key); \} while (0)extern void down_read(struct rw_semaphore *sem);extern int down_read_trylock(struct rw_semaphore *sem);extern void down_write(struct rw_semaphore *sem);extern int down_write_trylock(struct rw_semaphore *sem);extern void downgrade_write(struct rw_semaphore *sem);extern void up_read(struct rw_semaphore *sem);extern void up_write(struct rw_semaphore *sem);
从定义可以看出读写锁包含一个链表wait_list,它包含了所有等待该锁的task。wait_lock用于在多CPU情况下保护该链表。
1. 高16bits = 取补码(当前锁是否被写者获取() + 当前锁的等待链表非空())。如果当前锁被写者获取且等待链表非空,则高16bits应当等于(1+1)的补码,即0xfffe。如果链表为空,则高32bits等于(1+0)的补码,即0xffff。
2. 低16bits = 所有获取锁的写者加上获取锁的读者。如果有三个读者获取了锁,则低32bits等于3。
下面是当锁处于不同状态时锁的内部状态。后面将描述Linux的实现是如何保证锁能够正确工作。
1. 锁空闲。reader=0, writer=0, count=0, wait_list=empty。
2. 读者获得锁且等待链表空。reader=N, writer=0, count=0x0000000N, wait_list=empty。
3. 读者获得锁且等待链表非空。reader=N, writer=0, count=0xffff000N, wait_list=not-empty。
4. 写者获得锁且等待链表空。reader=0, writer=1, count=0xffff0001, wait_list=empty。
5. 写者获得锁且等待链表非空。reader=0, writer=1, count=0xfffe0001, wait_list=not-empty。
读写信号量的具体实现
实际当中锁的内部状态(count的值)要比上面描述的复杂。这是因为Linux为了提高性能在锁的获取和释放过程中设计了fast path和slow path。以down_read()为例,该函数最终调用__down_read()。首先对count+1,再判断count是否为负数。根据前面的讨论可知,只有当锁空闲或者只有读者且等待链表为空时,count不小于零。如果count>=0,则函数直接返回,这是fast path。如果count<0,说明有写者获得锁或者等待链表非空,这时进入slow path。在slow path中会获得wait_lock,并对count-1,这是因为进程在这时其实并没有获得锁,要把之前对count增加的1减回来。这个例子说明当count的低16bits为N时,并不一定有N个读者/写者获得锁,可能是因为某些进程在__down_read()中提前增加的。由于count没有被锁保护,且它的值可以被预先增加,导致读写信号量的实现还是比较复杂的。
static inline void __down_read(struct rw_semaphore *sem){ asm volatile("# beginning down_read\n\t" LOCK_PREFIX _ASM_INC "(%1)\n\t" /* adds 0x00000001 */ " jns 1f\n" " call call_rwsem_down_read_failed\n" "1:\n\t" "# ending down_read\n\t" : "+m" (sem->count) : "a" (sem) : "memory", "cc");}
下面两张图从进程状态的角度分析获取和释放锁的流程。每一个状态的第一行代表状态名称,例如trying_down_read_lock。状态中的wait的值代表信号量中count变量的高16bits,由于是补码,因此该值为负。状态中的active的值代表信号量中count变量的低16bits。需要注意这两个值代表的是该进程对信号量中count的贡献。例如如果有两个进程都处于trying_down_read_lock状态,则对active的贡献为1+1=2。需要注意的是无论多少个进程处于wait_in_list状态,对count.wait的贡献最多是1。
状态的转换过程可能依赖于信号量中count的值。例如当count中的高16bits(wait)为0时,trying_down_read_lock状态转换到reader状态,非0时则转换到wait_in_list状态。
从图中可以看出信号量中的count的值与当前系统中所有进程的状态应符合如下关系。其中NON_EMPTY表示存在非空,NR表示数目。
count.wait = NON_EMPTY(wait_in_list) + NR(trying_down_write_lock) + NR(writer)
count.active = NR(trying_down_read_lock) + NR(reader) + NR(trying_down_write_lock) + NR(writer)
由于对count的操作属于原子操作,因此同时只能有一个进程改变count的值。另外通过使用xadd等汇编命令,进程还可以同时原子性的获得改变后的count的值。
从以上关系可以再次说明count的值的含义是比较复杂的。
下面详细描述在获取/释放锁时进程状态的变化。
进程获取和释放读锁的流程
进程获取和释放写锁的流程
获取读锁(down_read)
当进程调用down_read()时,进程进入trying_down_read_lock状态,count.active+1。如果count.wait==0,说明NR(writer)==0,因此没有写者获取锁,当前进程直接进入reader状态,获取读锁。如果count.wait != 0,则可能当前有进程处于trying_down_write_lock、writer或者wait_in_list状态。当前进程->__down_read->rwsem_down_read_failed()。在rwsem_down_read_failed()中,进程获取wait_lock并进入wait_in_list状态。在进入该状态时,进程通过原子操作更新count并获取当前值。当:
1. count.wait == -1 && count.active == 0时,说明当前没有进程处于reader或writer状态,调用__rwsem_do_wake唤醒wait_list上的等待进程。
2. count.wait == -1 && count.active > 0 && wait_list里只有当前进程,说明当前锁处于读者状态,并且等待链表里只有当前进程,调用__rwsem_do_wake唤醒wait_list上的等待进程。这个唤醒是为了尽快让让当前进程获得读锁。
综上,进程要么直接获得读锁,要么进入等待链表等待唤醒后直接获得读锁。
对__rwsem_do_wake将在后面描述。
释放读锁(up_read)
up_read最终调用__up_read()。进程进入trying_up_read_lock状态,count.active-1。当count.wait == 0时,说明没有进程处于等待链表,因此不需要唤醒任何进程,当前进程直接进入no_read_lock状态,代表成功释放读锁。
当count.wait != 0时,wait_list中可能有等待进程(也可能有进程处于trying_write_lock或者writer状态)。当前进程进入wakeup_others状态。进程先判断count.active是否等于零。
1. 如果非零说明有进程处于trying_down_read_lock、trying_down_write_lock、reader或者writer状态。对于这些进程,他们最终会唤醒wait_list中的进程,因此当前进程可以直接进入no_read_lock状态。
2. 如果为零则调用rwsem_wake()->__rwsem_do_wake唤醒wait_list中的进程。
之后进程将循环等待直到被__rwsem_do_wake唤醒。
获取写锁(down_write)
进程调用down_write()获取写锁。该函数最终调用__down_write_nested()。1. 首先进程将count.wait-1并将count.active+1,进程进入trying_down_write_lock状态。如果count.active==0,说明当前没有进程处于reader或writer状态,当前进程自动进入writer状态,锁被成功获取。如果count.active != 0,调用rwsem_down_write_failed()。
2. 在rwsem_down_write_failed()中进程获取wait_lock并进入wait_in_list状态,同时通过原子操作更新count并获取当前值。当count.wait == -1 && wait_list里含有其他进程时,说明当前没有进程处于writer状态,且有其他进程在等待,调用__rwsem_do_wake(sem, RWSEM_WAKE_READERS)唤醒等待获取读锁的进程。注意这里只唤醒读进程,这和down_read时唤醒读或写进程不同。这样的目的应该是让尽量的读进程能够获取锁,这是因为:当前很有可能是读者拥有锁,根据__rwsem_do_wake的实现,如果唤醒读或写进程,则只有最早一个等待写锁的进程之前的读进程能够被唤醒。只唤醒读进程则能够使得所有读进程超越写进程同时拿到锁。
3. 唤醒之后,写进程开始循环等待获取锁。进程会sleep并等待__rwsem_do_wake的唤醒。
释放写锁(up_write)
up_write()最终调用__up_write()。该函数的实现非常类似__up_read()。进程进入trying_up_write_lock状态,count.wait+1且count.active-1。如果count.wait==0,说明当前没有进程在wait_list中,锁释放成功。如果count.wait != 0,可能有进程处于等待链表,进程进入wait_others状态。这之后的操作都与up_read相同。
唤醒函数__rwsem_do_wake
该函数用于唤醒等待在wait_list上的进程。输入参数wake_type用于控制唤醒模式:
1. RWSEM_WAKE_ANY - 按顺序唤醒。越早等待在链表上的进程将先被唤醒。
2. RWSEM_WAKE_READERS - 只唤醒读进程。
下面是关于函数的详细分析。
如果模式是RWSEM_WAKE_ANY并且链表上的第一个进程就是写进程,直接唤醒它并退出。
waiter = list_entry(sem->wait_list.next, struct rwsem_waiter, list); if (waiter->type == RWSEM_WAITING_FOR_WRITE) { if (wake_type == RWSEM_WAKE_ANY) /* Wake writer at the front of the queue, but do not * grant it the lock yet as we want other writers * to be able to steal it. Readers, on the other hand, * will block as they will notice the queued writer. */ wake_up_process(waiter->task); goto out; }
下面是比较tricky的一段,检查是否锁已经被某个写进程获得。这种情况可能存在是因为trying_down_write_lock状态只检查count.active是否为零,若为零则写锁被获取。但是在wait_in_list状态进程对active的贡献为零。因此可能当某个进程处于wait_in_list状态并且调用__rwsem_do_wake时,另一个写进程获取了锁。
检查的方法就是通过原子操作将count.active+1并获取oldcount。如果oldcount.wait < -1,说明当前可能有进程处于writer或者trying_down_write_lock状态,再次将count.active-1以抵消之前的操作,并且退出函数。如果oldcount.wait > -1,说明没有进程处于writer状态,可以开始唤醒读进程。由于active被+1,阻止了其他写进程进入writer状态。
/* Writers might steal the lock before we grant it to the next reader. * We prefer to do the first reader grant before counting readers * so we can bail out early if a writer stole the lock. */ adjustment = 0; if (wake_type != RWSEM_WAKE_READ_OWNED) { adjustment = RWSEM_ACTIVE_READ_BIAS; try_reader_grant: oldcount = rwsem_atomic_update(adjustment, sem) - adjustment; if (unlikely(oldcount < RWSEM_WAITING_BIAS)) { /* A writer stole the lock. Undo our reader grant. */ if (rwsem_atomic_update(-adjustment, sem) & RWSEM_ACTIVE_MASK) goto out; /* Last active locker left. Retry waking readers. */ goto try_reader_grant; } }函数将统计在wait_list上并且连续的读进程读数目,将其加到count.active中,并且唤醒这些读进程。函数结束。
0 0
- Linux内核读写信号量实现
- 大话Linux内核中锁机制之信号量、读写信号量
- Linux内核中锁机制之信号量、读写信号量
- 大话Linux内核中锁机制之信号量、读写信号量
- linux信号量实现线程读写同步
- Linux OS内核 作业三:设备驱动与读写信号量
- LINUX内核信号量设计与实现
- Linux内核源码之信号量的实现
- 信号量实现读写锁
- [Linux同步]读写信号量
- LINUX信号量实现对公共数据的读写
- linux 内核信号量
- linux内核信号量
- Linux内核中的信号量
- Linux驱动之信号量、读写信号量
- 用信号量实现读写锁
- windows信号量实现读写同步
- 内核同步方法之读写信号量
- R文件丢失解决方案
- 正则表达式语法 http://www.cnblogs.com/light169/archive/2006/10/18/532347.html
- Geeksquiz | Structure & Union
- java项目打jar包
- WIP 投料报 Invalid Serial Number
- Linux内核读写信号量实现
- wiki 1917 深海机器人问题
- hdu1106排序
- java web的并发处理
- 40_显示来电归属地,监听来电状态的服务
- 字幕基础:字幕介绍、字幕种类及常见格式
- Ubuntu环境下的php-Redis 配置与php使用入门
- Merge Intervals 练习vector
- objective c常用函数介绍