#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;