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
原创粉丝点击