Chinaunix首页 | 论坛 | 博客
  • 博客访问: 3547511
  • 博文数量: 1805
  • 博客积分: 135
  • 博客等级: 入伍新兵
  • 技术积分: 3345
  • 用 户 组: 普通用户
  • 注册时间: 2010-03-19 20:01
文章分类

全部博文(1805)

文章存档

2017年(19)

2016年(80)

2015年(341)

2014年(438)

2013年(349)

2012年(332)

2011年(248)

分类: LINUX

2015-03-24 21:10:18

原文地址:Linux Kernel 同步机制 作者:ville_lee

1. 为什么驱动程序需要管理同步
首先,Linux驱动程序有两种实现方式,可以在用户态实现,/*{{{*/
例如Android中的好多驱动程序就是这么实现的,但是更为一般的是在
内核态实现,一下的讨论都是基于内核态的驱动程序。

为什么驱动程序需要实现并发管理?
其实,对任何一个计算机程序而言,并发问题的考虑都是必须的!在用户态,由于
有操作系统对其提供了一个稳定安全的运行环境,所以,除非是多线程程序,一般的
程序都不用自行实现并发管理,在加上现在的好多中间件都提供了应用程序框架,所以,
一般的非多线程程序都不用考虑这方面的问题。但是内核态的情况不同,对于操作系统而言,
它的下一层,可能是BSP,BIOS,或者直接就是硬件!对于BSP和BIOS而言,设计之初,
就几乎没有考虑这个问题,只是实现和计算机硬件管理,加载OS相关的事情,所以,对kernel
而言,没有人提供运行环境,所以一切都要自己来。

对于Linux这种操作系统来说,它的一些特性使得内核态编程需要进行并发管理:
1. 多任务,多进程(包括LWP)
2. 可抢占
3. 完善的设备资源管理,支持中断优先级和中断嵌套
4. 异常
5. SMP
这些特性总的来说,一句话,你不能断定,你的程序在内核态运行的时候,系统处于一个什么样的
上下文之中!!!不知道,在哪个CPU上运行,何时进行任务切换,是否有资源竞争,申请的资源
是否能够得到满足,调用的内核函数是否会将自身阻塞,用户态是否会发送信号将自己kill,...
正是由于这种运行环境使得内核态运行的程序必须进行并发管理!!!/*}}}*/

2. 并发及其管理

驱动程序始终需要考虑的就是效率。任何同步机制都是需要代价和开销的,所以最好的方式/*{{{*/
就是避免并发的出现!

原则:
2.1 只要有可能就应该避免资源共享:例如:避免使用全局变量,在只读的情况下避免传递指针
2.2 在单个线程之外访问共享资源,因为不能确定其他线程是否也要访问,所以必须要显示的进行并发管理
确保,一次之有一个线程能够访问共享资源。
2.3 当存在对共享资源的访问时,必须保证共享资源存在,也就是说,资源需要统一的管理策略
资源尚未准备好的时候,不能允许访问;存在引用的时候,不能释放资源(引用计数)

竞态:如果执行的最终结果取决于对资源的访问顺序,那么就会产生对资源访问的竟态。

临界区:在任意时刻,代码只能被一个线程访问,而且,在这个线程退出临界区之前,不能有其他线程对代码的访问。
临界区是指一个代码段!/*}}}*/

3. 内核提供的管理同步的设施
内核同步源语:/*{{{*/
每CPU变量
原子操作(automic_t)
内存屏障
自旋锁
信号量
顺序锁
禁止本地中断
禁止本地软件中断
RCU
completion/*}}}*/
3.1 原子操作:
内核文档 /*{{{*/
在atomic_t类型的变量上进行特定的函数调用可以实现原子操作!
本质上atomic_t是一个struct,但是对应于一个signed int,尽管如此,
任何试图将atomic_t转换成int类型的操作都会引起编译失败!
只有在atomic_t上,执行特定的函数,才能够实现原子操作!它们都是以atomic_开头的函数!
arch/x86/asm/atomic.h

API:
声明和定义:
void atomic_set(atomic_t *v, int i);
atomic_t v = ATOMIC_INIT(0);

读写操作:
int atomic_read(atomic_t *v);
void atomic_add(int i, atomic_t *v);
void atomic_sub(int i, atomic_t *v);

加一减一:
void atomic_inc(atomic_t *v);
void atomic_dec(atomic_t *v);
执行操作并且测试结果:执行操作之后,如果v是0,那么返回1,否则返回0
int atomic_inc_and_test(atomic_t *v);
int atomic_dec_and_test(atomic_t *v);
int atomic_sub_and_test(int i, atomic_t *v);
int atomic_add_negative(int i, atomic_t *v);
int atomic_add_return(int i, atomic_t *v);
int atomic_sub_return(int i, atomic_t *v);
int atomic_inc_return(atomic_t *v);
int atomic_dec_return(atomic_t *v);
atomic_sub(amount, &first_atomic);
atomic_add(amount, &second_atomic);/*}}}*/

3.2 内存屏障
内核文档
       (5) LOCK operations.
       (6) UNLOCK operations.

3.3 信号量:
信号量本质上是一个整数和一个等待队列,和一对函数一起使用!也就是通常说的P,V操作。/*{{{*/
信号量的最大特点就是拥有信号量的内核路径可以阻塞!!!而拥有自旋锁的路径不能够休眠!

信号量API:
struct semaphore sem;
声明初始化:
void sema_init(struct semaphore *sem, int val);
DECLARE_MUTEX(sem); 初始化一个信号量,使之能够向互斥锁一样使用。
DECLARE_MUTEX_LOCKED(sem); 初始化一个信号量,类似于一个已经上锁的互斥量。
void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem);

P操作:
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem); 返回非零值,表明在等待锁的过程中被中断了!
int down_trylock(struct semaphore *sem); 返回非零值,表明没有获得锁!

V操作:
void up(struct semaphore *sem);/*}}}*/

3.4 读写信号量:
一个读写信号量允许一个写者或者多个读者拥有信号量!写者拥有更高的优先级别,/*{{{*/
读写信号量适合于:写入的情况很少,而且很快就会写完!

API:
声明和定义:
struct rw_semaphore rwsem;
void init_rwsem(struct rw_semaphore *rwsem); 只能动态初始化,不能静态

读访问:
void down_read(struct rw_semaphore *rwsem);
int down_read_trylock(struct rw_semaphore *rwsem);
void up_read(struct rw_semaphore *rwsem);

写访问:
void down_write(struct rw_semaphore *rwsem);
int down_write_trylock(struct rw_semaphore *rwsem);
void up_write(struct rw_semaphore *rwsem);
void downgrade_write(struct rw_semaphore *rwsem); 将锁的级别由写变成读!/*}}}*/

3.5 自旋锁:
关键字:不能阻塞,不可中断, 多CPU架构/*{{{*/

大部分的锁,底层都是通过自旋锁实现的。自旋锁只能在不能休眠的代码中使用!
自旋锁的性能要比信号量更高!一个自旋锁只有两个值:“锁定”和“解锁”!
在自旋锁的实现中,一般都是通过某个整数值中的单个位,所以最重要的是 test_and_set 的原子执行!

自旋锁可以应用在多处理器架构上,这也是它的设计初衷。非抢占内核运行在单处理器上的时候,
Linux 自旋锁实际上什么都不作。
自旋锁使用原则:
任何拥有自旋锁的代码,都不能休眠,不能因为任何原因放弃CPU。除了服务中断!

自旋锁会禁止当前CPU上的抢占,即使在单处理器架构上。

拥有锁的时间越短越好!

在中断中使用自旋锁,那么这个自旋锁必须是通过禁止中断形式上锁的。

在软件中断中使用自旋锁,那么这个自旋锁必须是通过关闭软件中断形式上锁的。

API:
声明和定义:
spinlock_t my_lock = SPIN_LOCK_UNLOCKED; 静态

void spin_lock_init(spinlock_t *lock); 动态

上锁:
void spin_lock(spinlock_t *lock);
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags); /* 保存中断状态,关闭中断 */
void spin_lock_irq(spinlock_t *lock); /* 关闭中断,但是不保存中断状态,解锁的时候,直接开中断 */
void spin_lock_bh(spinlock_t *lock); /* 关闭软件中断 */

解锁:
void spin_unlock(spinlock_t *lock);
void spin_unlock_irqrestored(spinlock_t *lock, unsigned long flags);
void spin_unlock_irq(spinlock_t *lock);
void spin_unlock_bh(spinlock_t *lock);
非阻塞形:成功获得自旋锁的时候,返回非零值!
int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);/*}}}*/

3.6 读写自旋锁:
允许任意数量的读者,但是写者必须互斥!/*{{{*/
声明和定义:
rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* Static way */
rwlock_t my_rwlock;
rwlock_init(&my_rwlock);  /* Dynamic way */
读者:
上锁:
void read_lock(rwlock_t *lock);
void read_lock_irqsave(rwlock_t *lock, unsigned long flags);
void read_lock_irq(rwlock_t *lock);
void read_lock_bh(rwlock_t *lock);
解锁:
void read_unlock(rwlock_t *lock);
void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void read_unlock_irq(rwlock_t *lock);
void read_unlock_bh(rwlock_t *lock);

写者:
上锁:
void write_lock(rwlock_t *lock);
void write_lock_irqsave(rwlock_t *lock, unsigned long flags);
void write_lock_irq(rwlock_t *lock);
void write_lock_bh(rwlock_t *lock);
int write_trylock(rwlock_t *lock);

解锁:
void write_unlock(rwlock_t *lock);
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);
void write_unlock_irq(rwlock_t *lock);
void write_unlock_bh(rwlock_t *lock);/*}}}*/

3.7 顺序锁
适用情况:/*{{{*/
1. 当要保护的资源很小,很简单
2. 会被频繁读取访问并且很少发生写入访问
3. 访问会很快
这种情况下就可以使用 seqlock

使用限制:
通常,seqlock不能保护包含指针的数据结构。
因为,seqlock会允许读者自由的访问资源,但是需要读者自己检查
是否和写者发生了冲突,如果发生了,就需要对资源重新访问。所以,
如果数据结构中包含指针,那么读者有可能会访问一个无效的指针。
当读取访问的时候,会首先获得一个整数值,然后进入临界区,当退出
临界区的时候会将这个值和当前值进行比较,如果不相等,那么需要重新
读取。

API:
声明和定义:
seqlock_t lock = SEQLOCK_UNLOCKED;

seqlock_t lock;
seqlock_init(&lock);
读者:
unsigned int read_seqbegin(seqlock_t *lock);
int read_seqretry(seqlock_t *lock, unsigned int seq);

unsigned int read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags);
int read_seqretry_irqrestore(seqlock_t *lock, unsigned int seq, unsigned long flags);

写者:
写顺序锁通过自旋锁实现,所以自旋锁的限制也适用于顺序锁
void write_seqlock(seqlock_t *lock);
void write_sequnlock(seqlock_t *lock);
void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags);
void write_seqlock_irq(seqlock_t *lock);
void write_seqlock_bh(seqlock_t *lock);

void write_sequnlock_irqrestore(seqlock_t *lock, unsigned long flags);
void write_sequnlock_irq(seqlock_t *lock);
void write_sequnlock_bh(seqlock_t *lock);

示例:
unsigned int seq;
do {
    seq = read_seqbegin(&the_lock);
    /* Do what you need to do */
} while read_seqretry(&the_lock, seq);

/*}}}*/

3.8 禁止本地中断


3.9 禁止本地软件中断

3.10 每CPU变量

3.11 RCU
很少在驱动程序中使用,被保护地数据必须通过指针访问,而这些资源的引用必须通过原子方式
访问。在需要修改数据的时候,写入线程首先复制,然后修改副本,之后用新版本代替相关的指针。

API:
<驱动程序很少用到,以后在总结>

3.12 completion


4. API列举

5. 和同步机制相关的主题
5.1 阻塞与非阻塞
5.2 tasklet
5.3 work_queue

阅读(538) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~