话说小明驾车正从银行返回家里的途中,还在琢磨银行中取钱过程中银行的相关事宜。想想每次排队来办业务真麻烦。也就是自己这次取的钱比较多,才这么麻烦,否则,自己才懒得来呢。其实,现在银行做的已经不错了,为了减少业务员的工作量,同时,也是缩短银行业务的办理时间和办理途径,在全国各地,各大城市的各个地图都安置了n多取款机,像一些简单的业务,如查询各余额、打印个账单之类的,可以不用到银行,直接在取款机就办了,大大提高了办事效率。而对应到linux上(没办法,三句话不离linux,谁让自己是搞linux的嵌入式工程师的呢),也有类似的提高临界区访问的机制:读写锁。
读写锁现在看来,大致有如下三种:读写自旋锁;读写信号量;RCU(读写锁的一种变种)
读写自旋锁:其定义与自旋锁没有什么大差异,如下:
typedef struct {
volatile unsigned int read_counter : 31;
volatile unsigned int write_lock : 1;
} arch_rwlock_t;
主要差异来自于对读、写操作的动作,先看写的动作(与架构相关,让我们先看arm的):
static inline void arch_write_lock(arch_rwlock_t *rw)
{
unsigned long tmp;
__asm__ __volatile__(
"1: ldrex %0, [%1]\n"
" teq %0, #0\n"
#ifdef CONFIG_CPU_32v6K
" wfene\n"
#endif
" strexeq %0, %2, [%1]\n"
" teq %0, #0\n"
" bne 1b"
: "=&r" (tmp)
: "r" (&rw->lock), "r" (0x80000000)
: "cc");
smp_mb();
}
其他的内容与自旋锁类似,在前文已经介绍过,主要差异点是从蓝色的语句开始,蓝色的语句是用0x80000000来更新rw->lock,而绿色的语句同样是为了原子性的原因,测试上面更新是否成功,否则,自旋。既然写的时候,更新成0x80000000,试想一下,读的时候,一定会测试这个位。让我们来进一步看看读的操作:
static inline void arch_read_lock(arch_rwlock_t *rw)
{
unsigned long tmp, tmp2;
__asm__ __volatile__(
"1: ldrex %0, [%2]\n"
" adds %0, %0, #1\n"
" strexpl %1, %0, [%2]\n"
#ifdef CONFIG_CPU_32v6K
" wfemi\n"
#endif
" rsbpls %0, %1, #0\n"
" bmi 1b"
: "=&r" (tmp), "=&r" (tmp2)
: "r" (&rw->lock)
: "cc");
smp_mb();
}
红色语句是adds,也就是说这条语句在执行后,会更新flag,从而导致其中的N,具体为(result(红色语句计算出来的值)&(1<<31))?1:0,说白了,就是判断是否已经被上锁了,这个判断主要由蓝色语句执行,若是被上锁了,就自旋,否则,就执行strex。而绿色的语句是为了防止多个读取者对lock值更新引起混乱。
读写信号量:
struct rw_semaphore {
__s32 activity;
spinlock_t wait_lock;
struct list_head wait_list;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
可以看到比信号量多了红色的activity,而activity的可以有三类值:
值为0:表示该信号量上当前没有任何活动的读取者和写入者
值为-1:表示该信号量上有一个活动的写入者
值为正值n:表示该信号量上有n个活动的读取者
声明:
#define DECLARE_RWSEM(name) \
struct rw_semaphore name = __RWSEM_INITIALIZER(name)
初始化:
#define init_rwsem(sem) \
do { \
static struct lock_class_key __key; \
\
__init_rwsem((sem), #sem, &__key); \
} while (0)
void __init_rwsem(struct rw_semaphore *sem, const char *name,
struct lock_class_key *key)
{
sem->activity = 0;
spin_lock_init(&sem->wait_lock);
INIT_LIST_HEAD(&sem->wait_list);
}
读取操作:
void __sched __down_read(struct rw_semaphore *sem)
{
struct rwsem_waiter waiter;
struct task_struct *tsk;
unsigned long flags;
spin_lock_irqsave(&sem->wait_lock, flags);
if (sem->activity >= 0 && list_empty(&sem->wait_list)) { //判断是否有读者在读,而且可以读
/* granted */
sem->activity++;
spin_unlock_irqrestore(&sem->wait_lock, flags);
goto out;
}
tsk = current;
set_task_state(tsk, TASK_UNINTERRUPTIBLE);
/* set up my own style of waitqueue */
waiter.task = tsk;
waiter.flags = RWSEM_WAITING_FOR_READ;
get_task_struct(tsk);
list_add_tail(&waiter.list, &sem->wait_list);//获取不到锁,因为有人写,所以,将自己加入到等待队列
/* we don't need to touch the semaphore struct anymore */
spin_unlock_irqrestore(&sem->wait_lock, flags);
/* wait to be given the lock */
for (;;) {
if (!waiter.task) //循环等待可以获得锁,则break;
break;
schedule();//获得不到的情况下,让出cpu
set_task_state(tsk, TASK_UNINTERRUPTIBLE);
}
tsk->state = TASK_RUNNING;
out:
;
}
写的代码类似,在此,不累述。
RCU:Read-Copy-Update,是linux内核的一种免锁机制,实际上是读写锁的一种变形,其原理相对来说比较简单:将读者与写者要访问的共享数据放在一个指针p中,读者通过p来访问数据,而写者通过修改p来更新数据,对读者的开销较小,写者的开销较大。当初的设计原则:如何写入者的操作比例小于10%,则优先考虑RCU锁,否则,考虑其他的互斥方法。
读取RCU临界区:使用rcu_read_lock和rcu_read_unlock保护临界区,这个临界区的保护过程中会关抢占
,就是即使发生了中断也不会导致进程切换。同时,注意对指针的引用只能在临界区
使用,出了临界区就不要再使用。
读者使用样例:
rcu_read_lock
......
rcu_read_unlock
写入RCU操作:写入者要完成重新分配一个被保护的共享数据区并将老数据复制到新的数据区上,再根据
需要修改新数据区,最后用新数据区指针替换老数据区指针,这个替换操作为原子操作。
注意,写入者在替换共享区的指针后,还不能马上释放老的数据区,需要使用call_rcu注
册回调函数来释放,主要防止释放了别人正在用的老的数据区。而内核确定没有对老指针
引用的条件是:系统中所有处理器上都至少发生了一次进程切换。因为读取者关闭了抢占。
写者使用样例:
首先,实现回调函数用来释放老指针
call_back()
{
free(old_ptr);
}
其次,使用rcu写锁
new_ptr;
old_ptr;
用新更新老rcu_assign_pointer(ptr,new_ptr);
释放老指针
call_rcu(ptr->rcu,demo_del_oldptr);
注:因为rcu锁的实现比较复杂,后面的章节我们会有专门的篇幅介绍。
转眼间,小明就到了自己的家里,开始收拾一下,准备向二叔家出发。。。。。。