现在的位置: 首页 > 综合 > 正文

<深入浅出>读写锁 读写信号量 spinlock 顺序锁 etc

2013年10月21日 ⁄ 综合 ⁄ 共 3342字 ⁄ 字号 评论关闭

 经典的读写锁,读者优先,写者次之,核心之思想是rwlock_t结构体,有一个字段,写者尝试加锁的时候,就减去
0x1000000,读者加锁的时候,就尝试减去1, 只要非负就加锁成功。
如果想把读写锁改成写者优先,就是说,当读者发现有写者被pending时,读者不去获取锁,而是等待写者获取到锁之后把pend位清空。

 

#ifdef CONFIG_RWSEM_PI
struct rt_rw_semaphore {
 struct rt_mutex  lock;
 int   read_depth;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
 struct lockdep_map dep_map;
#endif
};

#define up_read(rwsem) \
 PICK_FUNC_1ARG(struct rw_semaphore, struct rt_rw_semaphore, \
  anon_up_read, rt_up_read, rwsem)

如果是不配置CONFIG_RWSEM_PI,则
__down_read的实现为:
if (sem->activity >= 0 && list_empty(&sem->wait_list)) {
  /* granted */
  sem->activity++;
             goto out;

即原始的read是判断activity字段是否大于等于0,那么什么时候小于0呢
在__down_write中,
if (sem->activity == 0 && list_empty(&sem->wait_list)) {
  /* granted */
  sem->activity = -1;
              goto out;

关于wait_list字段还需要说明一下,为什么判断时要加一句wait_list的判断?

1)考虑以下情况,如果已经有很多reader在读,突然来了一个writer,那么writer会被挂起,
从而wait_list不为空。如果这个时候再来reader,那么由于sem->activity仍然大于0,则需要判断wait_list是否为空,
从而指示当前是否有写者在等待,最后挂起读者。
这是关于reader端判断wait_list的必要性。

2)那么在写者端的wait_list是否空的判断是什么情况下生效? 即某时候sem->activity为0,但sem->wait_list不为空。也就是说,写加锁成功仅仅在没有读者,并且也没有其他写者的情况下才生效。

从以上分析可以看出,wait_list中,可能有写者,也可能有读者。加一个这个判断,是为了不至于发生写者饥饿。即一个写者被挂在队列里,如果不停的来

读者,如果不判断队列是否为空,则读者会一直加锁成功,使写者饿死在队列里。

 

再来看up_read,释放读锁,即减少sem->activity计数,如果计数为0,则说明临界区所有读者都释放锁了,如果此时wait_list还不为空,说明有写者在等,则唤醒写者,在唤醒写者的流程里,有一句
sem->activity = -1;
即强行把activity计数改为-1 然后再唤醒写者,指示着写者获取到了此锁。
为什么说up_read里唤醒的一定是写者?
首先,等待队列是先入先出的,第一个被读者阻塞的,一定是写者。
既等待队列可能类似:
(W)RRRRRWRWWWR...
也就是说,读者尝试唤醒时的队列,一定是写者打头的。

而对于up_write,释放写锁时,则可能既唤醒写者,也唤醒读者。
则写者唤醒的策略为,连续尽可能多的唤醒读者直到下一个写者为止。
可以看出linux内核设计的是多么巧妙。

 

 关于spinlock debug的解释

具体实现在spinlock_debug.c

中心函数在:

void _raw_spin_lock(spinlock_t *lock)
{
 debug_spin_lock_before(lock);
 if (unlikely(!__raw_spin_trylock(&lock->raw_lock)))
  __spin_lock_debug(lock);
 debug_spin_lock_after(lock);
}
如果超过一段时间(1s) 没有获取到lock,则打印BUG

static void __spin_lock_debug(spinlock_t *lock)
{
 u64 i;
 u64 loops = loops_per_jiffy * HZ;
 int print_once = 1;

 for (;;) {
  for (i = 0; i < loops; i++) {
   if (__raw_spin_trylock(&lock->raw_lock))
    return;
   __delay(1);
  }
  /* lockup suspected: */
  if (print_once) {
   print_once = 0;
   printk(KERN_EMERG "BUG: spinlock lockup on CPU#%d, "
     "%s/%d, %p\n",
    raw_smp_processor_id(), current->comm,
    task_pid_nr(current), lock);
   dump_stack();
#ifdef CONFIG_SMP
   trigger_all_cpu_backtrace();
#endif
  }
 }
}

其中debug_spin_lock_before(lock); 可以检测当前cpu,或者是当前进程发生了获取锁的重入

static inline void
debug_spin_lock_before(spinlock_t *lock)
{
 SPIN_BUG_ON(lock->magic != SPINLOCK_MAGIC, lock, "bad magic");
 SPIN_BUG_ON(lock->owner == current, lock, "recursion");
 SPIN_BUG_ON(lock->owner_cpu == raw_smp_processor_id(),
       lock, "cpu recursion");
}
debug_spin_lock_after则执行获取到锁之后的owner和cpu赋值等

static inline void debug_spin_lock_after(spinlock_t *lock)
{
 lock->owner_cpu = raw_smp_processor_id();
 lock->owner = current;

}

 

顺序锁,提出的背景是,写者优先,即需要如下场景:
当有写者时,读者必须等待写者执行完再进入读取;或者读者正在进行读取,此时发生了强行写,则读者需要等待写者完成,并且重新读取。

这里隐含的意思有,写者之前并不能同步,也就是说,顺序锁只保证单个写者对读者的优先权。

写加锁:

write_seqcount_begin(&seq_lock)
CRITICAL
write_seqcount_end(&seq_lock)

加锁,解锁都是直接对seq_lock->seq++  ,并且需要spinlock保护这个seq字段,其实也隐含了写者操作时是关抢占的。
那么这里还有2个限制,如果加锁解锁的次数比较多,可能造成溢出;同时由于写者是关抢占的,需要写者操作CRITICAL SECTION时不能执行睡眠操作。

同步的关键在于读者:

一般读者的代码这么设计:

 do {
  seq = read_seqbegin(&seq_lock);
  CRITICAL
 } while (read_seqretry(&seq_lock, seq));

read_seqbegin:要达到的功能是,如果有写者,则等待写者完成:
repeat:
   ret = lock->seq;
   if(ret&1)   //如果有写者,则死循环
     goto repeat:
   return ret;

read_seqretry要实现的功能是,如果读者在读的过程中,有写者进入并对值进行了修改,则返回1,要求重新读取,读者对seq字段不会修改。

if(lock->seq!=ret)
   return 1;
else
   return 0;

 

抱歉!评论已关闭.