找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 3813|回复: 0
打印 上一主题 下一主题
收起左侧

Android 深度探索(卷1)-学习笔记2(读写、顺序锁、信号量)

[复制链接]
跳转到指定楼层
楼主
ID:82083 发表于 2015-6-6 02:50 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
一、读写锁
使用自旋锁时,无论什么时候都只有一个可执行单元进入临界区,无论该执行单元是读操作还是写操作,大多数情况下我们读操作会多于写操作,
多个执行单元进入临界区进行读操作是不会有问题的,如果仍采用自旋锁就会有效率低下的问题,而采用读写锁的话就会大大的提升效率。


读写锁,从自旋锁中衍生而出,分读自旋锁和写自旋锁。
1、多个执行单元可以同时获取到读锁并访问临界区的资源(或代码),但只能获取一个写自旋锁。
2、如果某些执行单元已经获取到读锁仍未释放该锁,这时B执行单元去获取写锁,B执行单元就会阻塞,直到所有读锁被释放。
3、如果有个A执行单元获取到写锁仍未释放该锁,这时B执行单元去获取写锁,B执行单元就会阻塞,直到A写锁被释放。
4、如果有个A执行单元获取到写锁仍未释放该锁,这时B执行单元去获取读锁,B执行单元就会阻塞,直到A写锁释放。


无论是读锁还是写锁,都会对临界区加锁:
读锁:读锁不会互斥读锁,但会互斥写锁,也就是说可以重复获取读锁(读锁上锁期间不应修改共享资源)
写锁:写锁会互斥写锁和读锁,也就是说在上了写锁期间,其他执行单元无法获取到写锁和读锁
(读锁上锁期间不允许修改共享资源,写锁上锁期间只允许一个执行单元修改共享资源。)



使用示例:
// 定义并初始化读写自旋锁
static DEFINE_RWLOCK(rwlock);


static ssize_t demo_read(struct file *file, char __user *buf, size_t count,
loff_t *ppos)
{
// ......

read_lock(&rwlock);// 加了读锁,为了互斥写锁
// ..
// 读临界区这里可以并发进入临界区不会阻塞,提高效率
// ..
read_unlock(&rwlock);

// ......
}


static ssize_t demo_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
// .......

write_lock(&rwlock); //加了写锁,为了互斥读锁和写锁
// ..
// 修改临界区 这里只允许有一个执行单元在所有读锁释放后进入
// ..
write_unlock(&rwlock);

// .......
}


优点:在没有写操作时可以并发读操作,不会被阻塞。
缺点:当执行写操作时,所有读操作都会被阻塞。


读写锁仅适合在读多写少并且临界区很短的情况下。


一些接口:
DEFINE_RWLOCK(lock)
void rwlock_init(rwlock_t *lock)


int read_trylock(rwlock_t *lock)
void read_lock(rwlock_t *lock)
void read_unlock(rwlock_t *lock)
void read_lock_irq(rwlock_t *lock)
void read_unlock_irq(rwlock_t *lock)
void read_lock_bh(rwlock_t *lock)
void read_unlock_bh(rwlock_t *lock)


int write_trylock(rwlock_t *lock)
void write_lock(rwlock_t *lock)
void write_unlock(rwlock_t *lock)
void write_lock_irq(rwlock_t *lock)
void write_unlock_irq(rwlock_t *lock)
void write_lock_bh(rwlock_t *lock)
void write_unlock_bh(rwlock_t *lock)




二 顺序锁
顺序锁则是自旋锁的另外一个升级版,和读写锁有些相似。顺序锁主要是围绕顺序锁和顺序号来设计的。


typedef struct{
unsigned sequence;// 顺序锁的顺序计数器-顺序号
spinlock_t lock;// 自旋锁
}seqlock_t;


顺序锁:顺序锁未被释放时,获取顺序锁的执行单元会阻塞(自旋)
在需要修改共享资源(临界区)的时候获取顺序锁,成功获取顺序号+1
当完成共享资源的操作后释放顺序锁,顺序号+1


顺序号:顺序锁未被释放时,获取顺序号的执行单元会阻塞(自旋)
在读取共享资源时需要先取得顺序号,只有在顺序锁释放的情况下才会得到顺序号,该顺序号一定是偶数。
完成共享资源的读取后再次取得顺序号并对比之前获取的顺序号,若不一致则需要重新读取共享资源。

成功获取顺序锁时顺序号一定是奇数,释放顺序锁时一定是偶数。之所以有这种规律跟顺序锁实现有关。


// 实现源码,获取顺序号的过程中并没有上锁操作
static _always_inline unsigned read_seqbegin(const seqlock_t *sl)
{
unsigned ret;
repeat:
ret = sl->sequence; // 读取顺序号
smp_rmb(); // 读内存屏障

// 如果顺序号是奇数则循环检测(自旋)否则返回顺序号
if(unlikely(ret & 1))
{
cpu_relax();
goto repeat;
}
return ret;
}



使用示例:
// 定义并初始化读写自旋锁
static DEFINE_SEQLOCK(seqlock);


static ssize_t demo_read(struct file *file, char __user *buf, size_t count,
loff_t *ppos)
{
// ......
// -------------------- 关键代码 ----------------------------
unsigned seq;
do
{// 获取顺序号,如果顺序锁未被释放 将会被阻塞(自旋)
seq = read_seqbegin(&seqlock); // 成功返回的顺序号一定是偶数
// ...
// 临界区
// ...
//如果当前顺序号和seq一致,则退出循环,否则重新读取共享资源
}while(read_seqretry(&seqlock, seq));
// -------------------- 关键代码 ----------------------------
// ......
}


static ssize_t demo_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
// .......
// 获取顺序锁  seqlock.sequence 会被+1  这时 seqlock.sequence 会是奇数
write_seqlock(&seqlock);
// ..
// 修改临界区 这里只允许有一个执行单元在所有读锁释放后进入
// ..
// 释放顺序锁 seqlock.sequence 会被+1 这时 seqlock.sequence 会是偶数
write_sequnlock(&seqlock);

// .......
}


顺序锁的读临界区操作并没有锁定临界区,只是简单读取临界区中的共享资源,所以在读取共享数据时
是可以修改共享数据的。


优点:读取到的数据一定是最新的数据,在没有写操作时读操作不会阻塞并且可以并发读。
缺点:如果在读共享数据的过程中发生了写操作,就会使得系统不断的地循环等待(自旋)和重新执行读临界区数据。


顺序锁仅适合在读多写少、临界区很短并要求数据实时更新情况下。


一些接口:
DEFINE_SEQLOCK(lock)
void seqlock_init(seqlock_t *lock)
// 以下接口在成功执行后顺序号会+1
int write_tryseqlock(seqlock_t *lock)
void write_seqlock(seqlock_t *lock)
void write_seqlock_irqsave(seqlock_t *lock)
void write_seqlock_irq(lock)
void write_seqlock_bh(lock)
void wrtie_sequnlock(seqlock_t *lock)
void write_sequnlock_irqrestore(lock, flags)
void write_sequnlock_irq(lock)
void write_sequnlock_bh(lock)
// 以下接口不会修改顺序号
unsigned read_seqbegin(const seqlock_t *lock)
void read_seqbegin_irqsave(lock, flags)
int read_seqretry(const seqlock_t *lock, unsigned iv)
void read_seqretry_irqrestore(lock, iv, flags)


三、信号量


信号量和锁机制最大的区别就是实现阻塞的方式不一样。
自旋锁、读写锁、顺序锁都是通过不断循环检测实现阻塞,当临界区很短时效率很高,
当临界区很长的时候性能就会急剧下降。而信号量则是通过休眠方式实现阻塞,适合
临界区比较长的情况,休眠不会占用CPU资源,所以不会影响系统性能更不会出现死机现象。


信号量比锁机制要灵活很多,它可以指定可以有几个执行单元进入临界区。


1、信号量数据结构

struct semaphore sem;


struct semaphore {
raw_spinlock_tlock;// 锁
// 资源数 决定可以有几个执行单元进入临界区
// >0,资源空闲. ==0,资源忙  
// <0 资源不可用,并至少有一个进程等待资源
unsigned intcount;
struct list_headwait_list;// 链表
};


2、初始化

sema_init(&sem, 1);// 初始化信号量并指定 sem.count 的初始值(即资源数)


#define __SEMAPHORE_INITIALIZER(name, n)\
{\
.lock= __RAW_SPIN_LOCK_UNLOCKED((name).lock),\
.count= n,\  // <--- 这里设置了可用资源数目
.wait_list= LIST_HEAD_INIT((name).wait_list),\
}


#define DEFINE_SEMAPHORE(name)\
struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)


static inline void sema_init(struct semaphore *sem, int val)
{
static struct lock_class_key __key;
*sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}


3、获取信号量

中断无法唤醒:
down(&sem); // 获取信号量,若计数器的值小于或等于0则进入休眠,若大于0则递减。
void down(struct semaphore *sem)
{
unsigned long flags;
// 上锁
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0))
sem->count--;// 计数器大于 0 表示资源可用则将资源数自减
else
__down(sem);// 计数器小于或等于0 表示资源忙则进入休眠状态
// 解锁
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
EXPORT_SYMBOL(down);

static noinline void __sched __down(struct semaphore *sem)
{
__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}

中断可唤醒:返回非0值表示是由中断唤醒,返回0值则表示成功获取到信号量
int down_interruptible(struct semaphore *sem)
{
unsigned long flags;
int result = 0;
// 上锁
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0))
sem->count--;
else
result = __down_interruptible(sem);// 这里不一样
// 解锁
raw_spin_unlock_irqrestore(&sem->lock, flags);


return result;
}
EXPORT_SYMBOL(down_interruptible);


static noinline int __sched __down_interruptible(struct semaphore *sem)
{// 只是传递的参数不一样 TASK_INTERRUPTIBLE
// MAX_SCHEDULE_TIMEOUT 表示永不超时
return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}


static inline int __sched __down_common(struct semaphore *sem, long state, long timeout)
{
struct task_struct *task = current;
struct semaphore_waiter waiter;
// 将当前进程加入到等待链表
list_add_tail(&waiter.list, &sem->wait_list);
waiter.task = task;
waiter.up = 0;// 将当前进程标记为休眠


for (;;) {
// 这里是为 down_interruptible() 准备
if (signal_pending_state(state, task))
goto interrupted;

// 这里是为 down_timeout() 准备
// 若超时被唤醒则跳出
if (timeout <= 0)
goto timed_out;

// 设置进程状态
__set_task_state(task, state);

// 解锁
raw_spin_unlock_irq(&sem->lock);
// 让出CPU,此时执行到这里停止往下执行
timeout = schedule_timeout(timeout);
// 进程被唤醒时 再上锁
raw_spin_lock_irq(&sem->lock);
// 判断是否由  __up() 唤醒,如果是则表示资源可用返回0。
if (waiter.up)
return 0;
}


// 超时唤醒 将该进程从等待链表中删除。返回非0值
timed_out:
list_del(&waiter.list);
return -ETIME;
// 中断唤醒 将该进程从等待链表中删除。返回非0值
interrupted:
list_del(&waiter.list);
return -EINTR;
}


4、释放信号量

up(&sem); // 释放信号量 若有进程在等待则唤醒,直到没有进程再等待才将计数器自增


static noinline void __sched __up(struct semaphore *sem)
{
// 获取等待链表中第一个进程
struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list, struct semaphore_waiter, list);
list_del(&waiter->list);    // 从等待链表中移除该进程
waiter->up = 1;    // 唤醒标志
wake_up_process(waiter->task);    // 唤醒该进程
}


void up(struct semaphore *sem)
{
unsigned long flags;


raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(list_empty(&sem->wait_list)))
sem->count++;        // 如果没有等待链表中没有进程在等待 才自增 表示资源空闲
else
__up(sem);// 如果有进程等待则唤醒
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
EXPORT_SYMBOL(up);


使用示例:
struct semaphore sem;
static ssize_t demo_read(struct file *file, char __user *buf, size_t count,
loff_t *ppos)
{
// ......
// 获取信号量
down(&sem);
// ...
// 临界区
// ...
// 释放信号量
up(&sem);
// ......
}


static ssize_t demo_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
// ......
// 获取信号量
down(&sem);
// ...
// 临界区
// ...
// 释放信号量
up(&sem);
// ......
}


static int __init demo_init(void)
{
// 将信号量值初始化为 1 表示只允许一个执行单元进入临界区
sema_init(&sem, 1);
}


接口:略


四、读写信号量


读写信号量与信号量之间的关系和读写自旋锁与自旋锁一样。
读写信号量可以有多个执行单元获得读信号量从而并发读操作,但只能有一个执行单元获取到写信号量。


使用示例:


struct rw_semaphore rw_sem;
static ssize_t demo_read(struct file *file, char __user *buf, size_t count,
loff_t *ppos)
{
// ......
// 获取读信号量 如果有写信号量未释放则进入休眠
down_read(&rw_sem);
// ...
// 临界区
// ...
// 释放读信号量
up_read(&rw_sem);
// ......
}


static ssize_t demo_write(struct file *file, const char __user *buf,
size_t count, loff_t *ppos)
{
// ......
// 获取写信号量 如果有写信号量或所有读信号量未全部释放则进入休眠
down_write(&rw_sem);
// ...
// 临界区
// ...
// 释放写信号量
up_write(&rw_sem);
// ......
}


static int __init demo_init(void)
{
// 将信号量值初始化为 1 表示只允许一个执行单元进入临界区
init_rwsem(&rw_sem, 1);
}

接口:略


锁机制适合在临界区执行时间短的情况下。
信号量适合在临界区执行时间较长的情况下。
锁机制是采用不断循环检测,所以实时性、速度快。
信号量是采用休眠唤醒的方式,休眠唤醒需要时间,实时性和速度较慢。

分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏 分享淘帖 顶 踩
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|51黑电子论坛 |51黑电子论坛6群 QQ 管理员QQ:125739409;技术交流QQ群281945664

Powered by 单片机教程网

快速回复 返回顶部 返回列表