20150518 Linux设备驱动中的并发控制

20150518 Linux设备驱动中的并发控制

2015-05-18 Lover雪儿

总结一下并发控制的相关知识:

本文参考:华清远见《Linux 设备驱动开发详解》—第7章 Linux 设备驱动中的并发控制,更多详细内容请看原书

一、并发与竞态

并发(concurrency)指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态(race conditions)。

在 Linux 内核中,主要的竞态发生于如下几种情况:

对称多处理器(SMP)的多个 CPU

SMP 是一种紧耦合、共享存储的系统模型,如下图所示,它的特点是多个 CPU 使用共同的系统总线,因此可访问共同的外设和储存器。


CPU
内进程与抢占它的进程

Linux
2.6 内核支持抢占调度,一个进程在内核执行的时候可能被另一高优先级进程打断,进程与抢占它的进程访问共享资源的情况类似于
SMP
的多个
CPU。

中断(硬中断、软中断、Tasklet、底半部)与进程之间

中断可以打断正在执行的进程,如果中断处理程序访问进程正在访问的资源,则竞态也会发生。此外,中断也有可能被新的更高优先级的中断打断,因此,多个中断之间本身也可能引起并发而导致竞态

二、解决竞态的方法

解决竞态问题的途径是保证对共享资源的互斥访问,所谓互斥访问是指一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问。

访问共享资源的代码区域称为临界区(critical
sections),临界区需要以某种互斥机制加以保护。中断屏蔽、原子操作、自旋锁和信号量等是
Linux
设备驱动中可采用的互斥途径,以下开始逐一讲解:

中断屏蔽

在单
CPU
范围内避免竞态的一种简单方法是在进入临界区之前屏蔽系统的中断。

CPU
一般都具备屏蔽中断和打开中断的功能,这项功能可以保证正在执行的内核执行路径不被中断处理程序所抢占,防止某些竞态条件的发生。中断屏蔽将使得中断与进程之间的并发不再发生,从而避免了内核中的竞争了.


中断屏蔽的使用方法为:

local_irq_disable()
//屏蔽中断

...

critical
section //临界区

...

local_irq_enable()
//开中断

由于
Linux
系统的异步
I/O、进程调度等很多重要操作都依赖于中断,中断对于内核的运行非常重要,在屏蔽中断期间所有的中断都无法得到处理,因此长时间屏蔽中断是很危险的,有可能造成数据丢失甚至系统崩溃。这就要求在屏蔽了中断之后,当前的内核执行路径应当尽快地执行完临界区的代码。

local_irq_disable()和
local_irq_enable()都只能禁止和使能本
CPU
内的中断,因此,并不能解决
SMP

CPU
引发的竞态。因此,单独使用中断屏蔽通常不是一种值得推荐的避免竞态的方法,它适宜与自旋锁联合使用。


local_irq_disable()不同的是,local_irq_save(flags)除了进行禁止中断的操作以外,还保存目前
CPU
的中断位信息,local_irq_restore(flags)进行的是local_irq_save(flags)相反的操作。

原子操作

原子操作指的是在执行过程中不会被别的代码路径所中断的操作。


1.设置原子变量的值

void
atomic_set(atomic_t *v, int i); //设置原子变量的值为
i

atomic_t
v = ATOMIC_INIT(0); //定义原子变量
v
并初始化为
0

//--------------------------------------------------------------------------

2.获取原子变量的值

atomic_read(atomic_t
*v); //返回原子变量的值

//--------------------------------------------------------------------------

3.原子变量加/减

void
atomic_add(int i, atomic_t *v); //原子变量增加
i

void
atomic_sub(int i, atomic_t *v); //原子变量减少
I

//--------------------------------------------------------------------------

4.原子变量自增/自减

void
atomic_inc(atomic_t *v); //原子变量增加
1

void
atomic_dec(atomic_t *v); //原子变量减少
1

//--------------------------------------------------------------------------

5.操作并测试

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

上述操作对原子变量执行自增、自减和减操作后(注意没有加)测试其是否为
0,为
0
则返回
true,否则返回
false。

//--------------------------------------------------------------------------

6.操作并返回

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

上述操作对原子变量进行加/减和自增/自减操作,并返回新的值。

位原子操作


1.设置位

void
set_bit(nr, void *addr);

上述操作设置
addr
地址的第
nr
位,所谓设置位即将位写为
1。

//--------------------------------------------------------------------------

2.清除位

void
clear_bit(nr, void *addr);

上述操作清除
addr
地址的第
nr
位,所谓清除位即将位写为
0。

//--------------------------------------------------------------------------

3.改变位

void
change_bit(nr, void *addr);

上述操作对
addr
地址的第
nr
位进行反置。

---------------------------------------------------------------------------

4.测试位

test_bit(nr,
void *addr);

上述操作返回
addr
地址的第
nr
位。

//--------------------------------------------------------------------------

5.测试并操作位

int
test_and_set_bit(nr, void *addr);

int
test_and_clear_bit(nr, void *addr);

int
test_and_change_bit(nr, void *addr);

上述
test_and_xxx_bit
( nr, void *addr) 操作等同于执行
test_bit
(nr, void *addr) 后再执行
xxx_bit(nr,
void *addr) 。

例.使用原子变量使设备只能被一个进程打开.

 1 static atomic_t xxx_available = ATOMIC_INIT(1); /*定义原子变量*/
 2 static int xxx_open(struct inode *inode, struct file *filp){
 3     ...
 4     if (!atomic_dec_and_test(&xxx_available)){
 5         atomic_inc(&xxx_available);
 6         return - EBUSY; /*已经打开*/
 7     }
 8     ...
 9     return 0; /* 成功 */
10 }
11 static int xxx_release(struct inode *inode, struct file *filp){
12     atomic_inc(&xxx_available); /* 释放设备 */
13     return 0;
14 }

自旋锁

自旋锁(spin
lock)是一种对临界资源进行互斥手访问的典型手段.

为了获得一个自旋锁,在某
CPU
上运行的代码需先执行一个原子操作,该操作测试并设置(test-and-set)某个内存变量,由于它是原子操作,所以在该操作完成之前其他执行单元不可能访问这个内存变量。

如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行;如果测试结果表明锁仍被占用,程序将在一个小的循环内重复这个“测试并设置”操作,即进行所谓的“自旋”,通俗地说就是“在原地打转”。当自旋锁的持有者通过重置该变量释放这个自旋锁后,某个等待的“测试并设置”操作向其调用者报告锁已释放。


1.定义自旋锁

spinlock_t
spin;

//--------------------------------------------------------------------------

2.初始化自旋锁

spin_lock_init(lock)

该宏用于动态初始化自旋锁
lock

//--------------------------------------------------------------------------

3.获得自旋锁

spin_lock(lock)

该宏用于获得自旋锁
lock,如果能够立即获得锁,它就马上返回,否则,它将自

旋在那里,直到该自旋锁的保持者释放;

spin_trylock(lock)

该宏尝试获得自旋锁
lock,如果能立即获得锁,它获得锁并返回真,否则立即返回假,实际上不再“在原地打转”;

//--------------------------------------------------------------------------

4.释放自旋锁

spin_unlock(lock)

该宏释放自旋锁一般这样被使用,如下所示:

//定义一个自旋锁

spinlock_t
lock;

spin_lock_init(&lock);

spin_lock
(&lock) ; //获取自旋锁,保护临界区

...//临界区

spin_unlock
(&lock) ; //解锁自旋锁
lock,它与
spin_trylock

spin_lock
配对使用。

//--------------------------------------------------------------------------

自旋锁一般这样被使用,如下所示:

//定义一个自旋锁

spinlock_t
lock;

spin_lock_init(&lock);

spin_lock
(&lock) ; //获取自旋锁,保护临界区

...//临界区

spin_unlock
(&lock) ; //解锁

自旋锁主要针对
SMP
或单
CPU
但内核可抢占的情况,对于单
CPU
和内核不支持抢占的系统,自旋锁退化为空操作.

尽管用了自旋锁可以保证临界区不受别的
CPU
和本
CPU
内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候还可能受到中断和底半部(BH)的影响。为了防止这种影响,就需要用到自旋锁的衍生。spin_lock()/spin_unlock()是自旋锁机制的基础,它们和关中断
local_irq_
disable()/开中断
local_irq_enable()、

关底半部local_bh_disable()/开底半部
local_bh_enable()、

关中断并保存状态字local_irq_save()/开中断并恢复状态
local_irq_restore()

结合就形成了整套自旋锁机制,关系如下所示:


spin_lock_irq()
= spin_lock() + local_irq_disable()

spin_unlock_irq()
= spin_unlock() + local_irq_enable()

spin_lock_irqsave()
= spin_unlock() + local_irq_save()

spin_unlock_irqrestore()
= spin_unlock() + local_irq_restore()

spin_lock_bh()
= spin_lock() + local_bh_disable()

spin_unlock_bh()
= spin_unlock() + local_bh_enable()

驱动工程师应谨慎使用自旋锁,而且在使用中还要特别注意
自旋锁实际上是忙等锁,
自旋锁可能导致系统死锁
这两个问题.

int xxx_count = 0;/*定义文件打开次数计数*/
static int xxx_open(struct inode *inode, struct file *filp){
    ...
    spinlock(&xxx_lock);
    if (xxx_count)/*已经打开*/
    {
        spin_unlock(&xxx_lock);
        return - EBUSY;
    }
    xxx_count++;/*增加使用计数*/
    spin_unlock(&xxx_lock);
    ...
    return 0; /* 成功 */
}
static int xxx_release(struct inode *inode, struct file *filp){
    ...
    spinlock(&xxx_lock);
    xxx_count--; /*减少使用计数*/
    spin_unlock(&xxx_lock);
    return 0;
}

读写自旋锁

自旋锁不关心锁定的临界区究竟进行怎样的操作,不管是读还是写,它都一视同仁。即便多个执行单元同时读取临界资源也会被锁住。实际上,对共享资源并发访问时,多个执行单元同时读取它是不会有问题的,自旋锁的衍生锁读写自旋锁(rwlock)可允许读的并发。


1.定义和初始化读写自旋锁

rwlock_t
my_rwlock = RW_LOCK_UNLOCKED; /* 静态初始化
*/

rwlock_t
my_rwlock;

rwlock_init(&my_rwlock);
/* 动态初始化
*/

//--------------------------------------------------------------------------

2.读锁定

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

//--------------------------------------------------------------------------

3.读解锁

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

在对共享资源进行读取之前,应该先调用读锁定函数,完成之后应调用读解锁函数。

read_lock_irqsave()、read_lock_irq()和
read_lock_bh()分别是
read_lock()分别与local_irq_save()、local_irq_disable()和local_bh_disable()的组合
,读解锁函数read_unlock_irqrestore()、read_unlock_
irq()、read_unlock_bh()的情况与此类似。

//--------------------------------------------------------------------------

4.写锁定

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

//--------------------------------------------------------------------------

5.写解锁

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

write_lock_irqsave()、write_lock_irq()、write_lock_bh()分别是
write_lock()
与local_irq_save()、local_irq_disable()和local_bh_disable()的组合,写解锁函数

write_unlock_irqrestore()、write_unlock_irq()、write_unlock_bh()的情况与此类似

在对共享资源进行读取之前,应该先调用写锁定函数,完成之后应调用写解锁函数。和
spin_trylock()一样,
write_trylock()也只是尝试获取读写自旋锁,不管成功失败,都会立即返回。


读写自旋锁一般这样被使用,如下所示:

rwlock_t
lock; //定义
rwlock

rwlock_init(&lock);
//初始化
rwlock

//读时获取锁

read_lock(&lock);

...
//临界资源

read_unlock(&lock);

//写时获取锁

write_lock_irqsave(&lock,
flags);

...
//临界资源

write_unlock_irqrestore(&lock,
flags);

顺序锁

顺序锁(seqlock)是对读写锁的一种优化,若使用顺序锁,读执行单元绝不会被写执行单元阻塞,也就是说,读执行单元可以在写执行单元对被顺序锁保护的共享资源进行写操作时仍然可以继续读,而不必等待写执行单元完成写操作,写执行单元也不需要等待所有读执行单元完成读操作才去进行写操作。

但是,写执行单元与写执行单元之间仍然是互斥的,即如果有写执行单元在进行写操作,其他写执行单元必须自旋在那里,直到写执行单元释放了顺序锁。

如果读执行单元在读操作期间,写执行单元已经发生了写操作,那么,读执行单元必须重新读取数据,以便确保得到的数据是完整的。这种锁在读写同时进行的概率比较小时,性能是非常好的,而且它允许读写同时进行,因而更大地提高了并发性。

顺序锁有一个限制,它必须要求被保护的共享资源不含有指针,因为写执行单元可能使得指针失效,但读执行单元如果正要访问该指针,将导致
Oops。

写操作


1.获得顺序锁

void
write_seqlock(seqlock_t *sl);

int
write_tryseqlock(seqlock_t *sl);

write_seqlock_irqsave(lock,
flags)

write_seqlock_irq(lock)

write_seqlock_bh(lock)

其中:

write_seqlock_irqsave()
= loal_irq_save() + write_seqlock()

write_seqlock_irq()
= local_irq_disable() + write_seqlock()

write_seqlock_bh()
= local_bh_disable() + write_seqlock()

2.释放顺序锁

void
write_sequnlock(seqlock_t *sl);

write_sequnlock_irqrestore(lock,
flags)

write_sequnlock_irq(lock)

write_sequnlock_bh(lock)

其中:

write_sequnlock_irqrestore()
= write_sequnlock() + local_irq_restore()

write_sequnlock_irq()
= write_sequnlock() + local_irq_enable()

write_sequnlock_bh()
= write_sequnlock() + local_bh_enable()

写执行单元使用顺序锁的模式如下:

write_seqlock(&seqlock_a);

...//写操作代码块

write_sequnlock(&seqlock_a);

因此,对写执行单元而言,它的使用与
spinlock
相同。

读操作


1.读开始

unsigned
read_seqbegin(const seqlock_t *sl);

read_seqbegin_irqsave(lock,
flags)

读执行单元在对被顺序锁
s1
保护的共享资源进行访问前需要调用该函数,
该函数

仅返回顺序锁
s1
的当前顺序号。其中:

read_seqbegin_irqsave()
= local_irq_save() + read_seqbegin()

2.重读

int
read_seqretry(const seqlock_t *sl, unsigned iv);

read_seqretry_irqrestore(lock,
iv, flags)

读执行单元在访问完被顺序锁
s1
保护的共享资源后需要调用该函数来检查,
在读

访问期间是否有写操作。如果有写操作,读执行单元就需要重新进行读操作。其中:

read_seqretry_irqrestore()
= read_seqretry() + local_irq_restore()

读执行单元使用顺序锁的模式如下:

do
{

seqnum
= read_seqbegin(&seqlock_a);

//读操作代码块

...

}
while (read_seqretry(&seqlock_a, seqnum));

-拷贝-更新

RCU(Read-Copy
Update,读-拷贝-更新),它是基于其原理命名的。

对于被
RCU
保护的共享数据结构,读执行单元不需要获得任何锁就可以访问它,不使用原子指令,而且在除
Alpha
的所有架构上也不需要内存栅(Memory
Barrier),因此不会导致锁竞争、内存延迟以及流水线停滞。不需要锁也使得使用更容易,因为死锁问题就不需要考虑了。

使用
RCU
的写执行单元在访问它前需首先复制一个副本,然后对副本进行修改,最后使用一个回调机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据,这个时机就是所有引用该数据的
CPU
都退出对共享数据的操作的时候。读执行单元没有任何同步开销,而写执行单元的同步开销则取决于使用的写执行单元间的同步机制。

但是,RCU
不能替代读写锁,因为如果写比较多时,对读执行单元的性能提高不能弥补写执行单元导致的损失。因为使用
RCU
时,写执行单元之间的同步开销会比较大,它需要延迟数据结构的释放,复制被修改的数据结构,它也必须使用某种锁机制同步并行的其他写执行单元的修改操作。


1.读锁定

rcu_read_lock()

rcu_read_lock_bh()

2.读解锁

rcu_read_unlock()

rcu_read_unlock_bh()

使用
RCU
进行读的模式如下:

rcu_read_lock()

...//读临界区

rcu_read_unlock()

其中
rcu_read_lock()和
rcu_read_unlock()实质只是禁止和使能内核的抢占调度,
如下所示:

#define
rcu_read_lock() preempt_disable()

#define
rcu_read_unlock() preempt_enable()

其变种
rcu_read_lock_bh()、rcu_read_unlock_bh()则定义为:

#define
rcu_read_lock_bh() local_bh_disable()

#define
rcu_read_unlock_bh() local_bh_enable()

3.同步
RCU

synchronize_rcu()

该函数由
RCU
写执行单元调用,它将阻塞写执行单元,直到所有的读执行单元已经完成读执行单元临界区,写执行单元才可以继续下一步操作。如果有多个
RCU写执行单元调用该函数,它们将在一个
grace
period (即所有的读执行单元已经完成对临界区的访问)之后全部被唤醒。synchronize_rcu()保证所有
CPU
都处理完正在运行的读执行单元临界区。

synchronize_kernel()

内核代码使用该函数来等待所有
CPU
处于可抢占状态,目前功能等同于synchronize_rcu(),但现在已经不建议使用,而是使用
synchronize_sched(),该函数用于等待所有
CPU
都处在可抢占状态,它能保证正在运行的中断处理函数处理完毕,但不能保证正在运行的软中断处理完毕。

4.挂接回调

void
fastcall call_rcu(struct rcu_head *head,void (*func)(struct
rcu_head *rcu));

函数
call_rcu()也由
RCU
写执行单元调用,它不会使写执行单元阻塞,因而可以在中断上下文或软中断中使用。该函数将把函数
func
挂接到
RCU
回调函数链上,然后立即返回。函数
synchronize_rcu()的实现实际上使用了
call_rcu()函数。

void
fastcall call_rcu_bh(struct rcu_head *head,void (*func)(struct
rcu_head *rcu));

call_ruc_bh()函数的功能几乎与
call_rcu()完全相同,唯一差别就是它把软中断的完成也当做经历一个
quiescent
state(静默状态),因此如果写执行单元使用了该函数,在进程上下文的读执行单元必须使用
rcu_read_lock_bh()。

每个
CPU
维护两个数据结构
rcu_data

rcu_bh_data,它们用于保存回调函数,函数
call_rcu()把回调函数注册到
rcu_data,而
call_rcu_bh()则把回调函数注册到rcu_bh_data,在每一个数据结构上,回调函数被组成一个链表,先注册的排在前头,后注册的排在末尾。

使用
RCU
时,读执行单元必须提供一个信号给写执行单元以便写执行单元能够确定数据可以被安全地释放或修改的时机。有一个专门的垃圾收集器来探测读执行单元的信号,一旦所有的读执行单元都已经发送信号告知它们都不在使用被
RCU
保护的数据结构,垃圾收集器就调用回调函数完成最后的数据释放或修改操作。

RCU
还增加了链表操作函数的
RCU
版本


static
inline void list_add_rcu(struct list_head *new, struct list_head

*head);

该函数把链表元素
new
插入到
RCU
保护的链表
head
的开头,内存栅保证了在引用这个新插入的链表元素之前,新链表元素的链接指针的修改对所有读执行单元是可见的。

static
inline void list_add_tail_rcu(struct list_head *new,

struct
list_head *head);

该函数类似于
list_add_rcu(),它将把新的链表元素
new
添加到被
RCU
保护的链表的末尾。

static
inline void list_del_rcu(struct list_head *entry);

该函数从
RCU
保护的链表中删除指定的链表元素
entry。

static
inline void list_replace_rcu(struct list_head *old, struct

list_head
*new);

该函数是
RCU
新添加的函数,并不存在非
RCU
版本。它使用新的链表元素
new取代旧的链表元素
old,内存栅保证在引用新的链表元素之前,它对链接指针的修正对所有读行单元是可见的。

list_for_each_rcu(pos,
head)

该宏用于遍历由
RCU
保护的链表
head,只要在读执行单元临界区使用该函数,它就可以安全地和其他_rcu
链表操作函数并发运行如
list_add_rcu()。

list_for_each_safe_rcu(pos,
n, head)

该宏类似于
list_for_each_rcu,不同之处在于它允许安全地删除当前链表元素
pos。

list_for_each_entry_rcu(pos,
head, member)

该宏类似于
list_for_each_rcu,不同之处在于它用于遍历指定类型的数据结构链表,当前链表元素
pos
为一个包含
struct
list_head 结构的特定的数据结构。

static
inline void hlist_del_rcu(struct hlist_node *n)

它从由
RCU
保护的哈希链表中移走链表元素
n。

static
inline void hlist_add_head_rcu(struct hlist_node *n,

struct
hlist_head *h);

该函数用于把链表元素
n
插入到被
RCU
保护的哈希链表的开头,但同时允许读执行单元对该哈希链表的遍历。内存栅确保在引用新链表元素之前,
它对指针的修改对所有读执行单元可见。

hlist_for_each_rcu(pos,
head)

该宏用于遍历由
RCU
保护的哈希链表
head,只要在读端临界区使用该函数,它就可以安全地和其他_rcu
哈希链表操作函数(如
hlist_add_rcu)并发运行。

hlist_for_each_entry_rcu(tpos,
pos, head, member)

类似于
hlist_for_each_rcu(),不同之处在于它用于遍历指定类型的数据结构哈希链表,当前链表元素
pos
为一个包含
struct
list_head 结构的特定的数据结构。

目前,RCU
的使用在内核中已经非常普遍,内核中大量原先使用读写锁的代码被RCU
替换,下面的表单左右两列平行地分别给出使用读写锁和
RCU
实现链表元素读、删除、添加和修改的代码。

信号量的使用

信号量(semaphore)是用于保护临界区的一种常用方法,它的使用方式和自旋锁类似。与自旋锁相同,只有得到信号量的进程才能执行临界区代码。但是,与自旋锁不同的是,当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。


1.定义信号量

下列代码定义名称为
sem 的信号量。

struct
semaphore sem;

2.初始化信号量

void
sema_init (struct semaphore *sem, int val);

该函数初始化信号量,并设置信号量
sem 的值为
val。尽管信号量可以被初始化为大于
1
的值从而成为一个计数信号量,但是它通常不被这样使用。

void
init_MUTEX(struct semaphore *sem);

该函数用于初始化一个用于互斥的信号量,它把信号量
sem 的值设置为
1,等同于

sema_init
(struct semaphore *sem, 1)。

void
init_MUTEX_LOCKED (struct semaphore *sem);

该函数也用于初始化一个信号量,但它把信号量
sem 的值设置为
0,等同于

sema_init
(struct semaphore *sem, 0)。

此外,下面两个宏是定义并初始化信号量的“快捷方式”。

DECLARE_MUTEX(name)

DECLARE_MUTEX_LOCKED(name)

前者定义一个名为
name 的信号量并初始化为
1,后者定义一个名为
name 的信号量并初始化为
0。

3.获得信号量

void
down(struct semaphore * sem);

该函数用于获得信号量
sem,它会导致睡眠,因此不能在中断上下文使用。

int
down_interruptible(struct semaphore * sem);

该函数功能与
down()类似,不同之处为,因为
down()而进入睡眠状态的进程不能被信号打断,而因为
down_interruptible()而进入睡眠状态的进程能被信号打断,信号也会导致该函数返回,这时候函数的返回值非
0。

int
down_trylock(struct semaphore * sem);

该函数尝试获得信号量
sem,如果能够立刻获得,它就获得该信号量并返回
0,否则,返回非
0
值。它不会导致调用者睡眠,可以在中断上下文使用。在使用
down_interruptible()获取信号量时,对返回值一般会进行检查,如果非
0,通常立即返回-ERESTARTSYS,如:

if
(down_interruptible(&sem)){

return
- ERESTARTSYS;

}

4.释放信号量

void
up(struct semaphore * sem);

该函数释放信号量
sem,唤醒等待者。

信号量一般这样被使用,如下所示:

//定义信号量

DECLARE_MUTEX(mount_sem);

down(&mount_sem);//获取信号量,保护临界区

...

critical
section //临界区

...

up(&mount_sem);//释放信号量

完成量用于同步

Linux
系统提供了一种比信号量更好的同步机制,即完成量:它用于一个执行单元等待另一个执行单元执行完某事.


1.定义完成量

下列代码定义名为
my_completion 的完成量。

struct
completion my_completion;

2.初始化
completion

下列代码初始化
my_completion 这个完成量。

init_completion(&my_completion);


my_completion
的定义和初始化可以通过如下快捷方式实现。

DECLARE_COMPLETION(my_completion);

3.等待完成量

下列函数用于等待一个
completion 被唤醒。

void
wait_for_completion(struct completion *c);

4.唤醒完成量

下面两个函数用于唤醒完成量。

void
complete(struct completion *c);

void
complete_all(struct completion *c);

前者只唤醒一个等待的执行单元,后者释放所有等待同一完成量的执行单元。

信号量vs自旋锁

信号量是进程级的,用于多个进程之间对资源的互斥,虽然也是在内核中,但是该内核执行路径是以进程的身份,代表进程来争夺资源的。如果竞争失败,会发生进程上下文切换,当前进程进入睡眠状态,CPU
将运行其他进程。鉴于进程上下文切换的开销也很大,因此,只有当进程占用资源时间较长时,用信号量才是较好的选择。

当所要保护的临界区访问时间比较短时,用自旋锁是非常方便的,因为它节省上下文切换的时间。但是
CPU
得不到自旋锁会在那里空转直到其他执行单元解锁为止,所以要求锁不能在临界区里长时间停留,否则会降低系统的效率。


使用的三项原则:

1.当锁不能被获取时,使用信号量的开销是进程上下文切换时间
Tsw,使用自旋锁的开销是等待获取自旋锁
(由临界区执行时间决定)
Tcs, 若
Tcs
比较小,应使用自旋锁,若
Tcs
很大,应使用信号量。

2.信号量所保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护包含这样代码的临界区。因为阻塞意味着要进行进程的切换,如果进程被切换出去后,另一个进程企图获取本自旋锁,死锁就会发生。

3.信号量存在于进程上下文,因此,如果被保护的共享资源需要在中断或软中断情况下使用,则在信号量和自旋锁之间只能选择自旋锁。当然,如果一定要使用信号量,则只能通过
down_trylock()方式进行,不能获取就立即返回以避免阻塞。

读写信号量

读写信号量可能引起进程阻塞,但它可允许
N
个读执行单元同时访问共享资源,而最多只能有一个写执行单元。因此,读写信号量是一种相对放宽条件的粒度稍大于信号量的互斥机制。


1.定义和初始化读写信号量

struct
rw_semaphore my_rws; /*定义读写信号量*/

void
init_rwsem(struct rw_semaphore *sem); /*初始化读写信号量*/

2.读信号量获取

void
down_read(struct rw_semaphore *sem);

int
down_read_trylock(struct rw_semaphore *sem);

3.读信号量释放

void
up_read(struct rw_semaphore *sem);

4.写信号量获取

void
down_write(struct rw_semaphore *sem);

int
down_write_trylock(struct rw_semaphore *sem);

5.写信号量释放

void
up_write(struct rw_semaphore *sem);

读写信号量一般这样被使用,如下所示:

rw_semaphore
rw_sem; //定义读写信号量

init_rwsem(&rw_sem);
//初始化读写信号量

//读时获取信号量

down_read(&rw_sem);

...
//临界资源

up_read(&rw_sem);

//写时获取信号量

down_write(&rw_sem);

...
//临界资源

up_write(&rw_sem);

互斥体

尽管信号量已经可以实现互斥的功能,而且包含
DECLARE_MUTEX()
、init_MUTEX
()等定义信号量的宏或函数,
从名字上看就体现出了互斥体的概念,
但是mutex

Linux
内核中还是真实地存在的。


struct
mutex my_mutex;

mutex_init(&my_mutex);

下面的两个函数用于获取互斥体。

void
fastcall mutex_lock(struct mutex *lock);
引起的睡眠不能被信号打断

int
fastcall mutex_lock_interruptible(struct mutex *lock);
引起的睡眠能被信号打断

int
fastcall mutex_trylock(struct mutex *lock); 尝试获得
mutex,获取不到
mutex 时不会引起进程睡眠

下列函数用于释放互斥体。

void
fastcall mutex_unlock(struct mutex *lock);

使用方法:

struct
mutex my_mutex; //定义 mutex

mutex_init(&my_mutex);
//初始化 mutex

mutex_lock(&my_mutex);
//获取 mutex

...//临界资源

mutex_unlock(&my_mutex);
//释放 mutex

附驱动程序示例:

  1 #include <linux/module.h>
  2 #include <linux/types.h>
  3 #include <linux/fs.h>
  4 #include <linux/kernel.h>
  5 #include <linux/device.h>
  6 #include <linux/errno.h>
  7 #include <linux/mm.h>
  8 #include <linux/sched.h>
  9 #include <linux/init.h>
 10 #include <linux/cdev.h>
 11 #include <asm/io.h>
 12 #include <asm/system.h>
 13 #include <asm/uaccess.h>
 14 #include <linux/sem.h>
 15
 16 #define GLOBALMEM_SIZE    0x1000    /*全局内存大小:4KB*/
 17 #define MEM_CLEAR         0x1     /*清零全局内存*/
 18 #define GLOBALMEM_MAJOR 0        /*预设的 globalmem 的主设备号*/
 19
 20
 21 static int globalmem_major = GLOBALMEM_MAJOR;
 22 /*globalmem 设备结构体*/
 23 struct globalmem_dev
 24 {
 25     struct cdev cdev;                     /*cdev 结构体*/
 26     unsigned char mem[GLOBALMEM_SIZE];     /*全局内存*/
 27     struct semaphore sem;        /* 1. 并发控制用的信号量 */
 28 };
 29
 30 struct globalmem_dev *globalmem_devp; /*设备结构体实例*/
 31
 32 //自动添加设备节点
 33 static struct class *cls = NULL;
 34
 35
 36 /*文件打开函数*/
 37 static int globalmem_open(struct inode *inode, struct file *filp){
 38     /*将设备结构体指针赋值给文件私有数据指针*/
 39     filp->private_data = globalmem_devp;
 40     return 0;
 41 }
 42 //释放函数
 43 static int globalmem_release(struct inode *inode, struct file *filp){
 44     return 0;
 45 }
 46 //文件读函数
 47 static ssize_t globalmem_read(struct file *filp, char __user *buf,size_t count,loff_t *ppos)
 48 {
 49     unsigned long p = *ppos;
 50     int ret = 0;
 51     struct globalmem_dev *dev = filp->private_data; /*获得设备结构体指针*/
 52     /*分析和获取有效的读长度*/
 53     if (p >= GLOBALMEM_SIZE) //要读的偏移位置越界
 54         return count ? - ENXIO: 0;
 55     if (count > GLOBALMEM_SIZE - p)//要读的字节数太大
 56         count = GLOBALMEM_SIZE - p;
 57
 58     if(down_interruptible(&dev->sem))        //3.获取信号量
 59     {
 60         return -ERESTARTSYS;
 61     }
 62
 63     /*内核空间→用户空间*/
 64     if (copy_to_user(buf, (void*)(dev->mem + p), count))
 65     {
 66         ret = - EFAULT;
 67     }else{
 68         *ppos += count;
 69         ret = count;
 70         printk(KERN_INFO "read %d bytes(s) from %ld\n", count, p);
 71     }
 72
 73     up(&dev->sem);        //4.释放信号量
 74
 75     return ret;
 76 }
 77 //文件写函数
 78 static ssize_t globalmem_write(struct file *filp, const char __user *buf,size_t count, loff_t *ppos)
 79 {
 80     unsigned long p = *ppos;
 81     int ret = 0;
 82     struct globalmem_dev *dev = filp->private_data; /*获得设备结构体指针*/
 83     /*分析和获取有效的写长度*/
 84     if (p >= GLOBALMEM_SIZE)     //要写的偏移位置越界
 85         return count ? - ENXIO: 0;
 86     if (count > GLOBALMEM_SIZE - p) //要写的字节数太多
 87         count = GLOBALMEM_SIZE - p;
 88
 89     if(down_interruptible(&dev->sem))        //4.获取信号量
 90     {
 91         return -ERESTARTSYS;
 92     }
 93
 94     /*用户空间→内核空间*/
 95     if (copy_from_user(dev->mem + p, buf, count)){
 96         ret = - EFAULT;
 97     }else{
 98         *ppos += count;
 99         ret = count;
100         printk(KERN_INFO "written %d bytes(s) from %ld\n", count, p);
101     }
102     up(&dev->sem); //释放信号量
103     return ret;
104 }
105 //文件定位函数
106 static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
107 {
108     loff_t ret;
109     switch (orig){
110         case 0:   /*从文件开头开始偏移*/
111             if (offset < 0){
112                 ret = - EINVAL;
113                 break;
114             }
115             if ((unsigned int)offset > GLOBALMEM_SIZE) //偏移越界
116             {
117                 ret = - EINVAL;
118                 break;
119             }
120             filp->f_pos = (unsigned int)offset;
121             ret = filp->f_pos;
122             break;
123
124         case 1:  /*从当前位置开始偏移*/
125             if ((filp->f_pos + offset) > GLOBALMEM_SIZE) //偏移越界
126             {
127                 ret = - EINVAL;
128                 break;
129             }
130             if ((filp->f_pos + offset) < 0)
131             {
132                 ret = - EINVAL;
133                 break;
134             }
135             filp->f_pos += offset;
136             ret = filp->f_pos;
137             break;
138         default:  ret = - EINVAL; break;
139     }
140     return ret;
141 }
142 //
143 static int globalmem_ioctl(struct inode *inodep, struct file *filp,unsigned int cmd, unsigned long arg)
144 {
145     struct globalmem_dev *dev = filp->private_data;
146     switch (cmd){
147         case MEM_CLEAR:        //清除全局内存
148
149             if(down_interruptible(&dev->sem))        //3.获取信号量
150             {
151                 return -ERESTARTSYS;
152             }
153             memset(dev->mem, 0, GLOBALMEM_SIZE);
154             up(&dev->sem);        //释放信号量
155             printk(KERN_INFO "globalmem is set to zero\n");
156             break;
157         default:
158             return - EINVAL; //其他不支持的命令
159             break;
160     }
161     return 0;
162 }
163 //定义file_operatetiont
164 static const struct file_operations globalmem_fops =
165 {
166     .owner = THIS_MODULE,
167     .llseek = globalmem_llseek,
168     .read = globalmem_read,
169     .write = globalmem_write,
170     .ioctl = globalmem_ioctl,
171     .open = globalmem_open,
172     .release = globalmem_release,
173 };
174
175 /*globalmem 设备驱动模块加载函数*/
176 static int globalmem_init(void){
177     int result,err;
178     dev_t devno = MKDEV(globalmem_major, 0);
179         /* 申请字符设备驱动区域*/
180     if (globalmem_major){
181         result = register_chrdev_region(devno, 1, "globalmem");
182     }else{
183         /* 动态获得主设备号 */
184         result = alloc_chrdev_region(&devno, 0, 1, "globalmem");
185         globalmem_major = MAJOR(devno);
186     }
187     if (result < 0)
188         return result;
189     printk("request major %d,minor %d\n",globalmem_major,MINOR(devno));
190
191     /*动态申请设备结构体的内存*/
192     globalmem_devp = kmalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
193     if(!globalmem_devp){    /* 申请失败 */
194         result = -ENOMEM;
195         goto fail_malloc;
196     }
197     memset(globalmem_devp, 0, sizeof(struct globalmem_dev));
198
199     /*初始化设备结构体*/
200     cdev_init(&globalmem_devp->cdev, &globalmem_fops);
201     globalmem_devp->cdev.owner = THIS_MODULE;
202     globalmem_devp->cdev.ops = &globalmem_fops;
203     /* 注册cdev */
204     err = cdev_add(&globalmem_devp->cdev, devno, 1);
205     if (err)
206         printk(KERN_NOTICE "Error %d adding globalmem", err);
207
208     //自动创建设备节点
209     cls = class_create(THIS_MODULE,"globalmem");
210     device_create(cls, NULL, devno,NULL,"globalmem");
211
212     init_MUTEX(&globalmem_devp->sem);    //2.初始化信号量
213
214     return 0;
215 fail_malloc: unregister_chrdev_region(devno,1);
216     return result;
217 }
218 /*globalmem 设备驱动模块卸载函数*/
219 static void globalmem_exit(void)
220 {
221     dev_t devno = MKDEV(globalmem_major, 0);
222
223     device_destroy(cls,devno);
224     class_destroy(cls);
225     cdev_del(&globalmem_devp->cdev); /*删除 cdev 结构*/
226     kfree(globalmem_devp);    /* 释放设备结构体内存 */
227     unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);/*注销设备*/
228 }
229
230 module_init(globalmem_init);
231 module_exit(globalmem_exit);
232 MODULE_LICENSE("GPL");

mutex_globalmem.c

测试实例:


[email protected]
/mnt/nfs/module/48_mutex_globalmem# echo "hello,LoverXueEr"
> /dev/globalmem

written
17 bytes(s) from 0

[email protected]
/mnt/nfs/module/48_mutex_globalmem# cat /dev/globalmem

hello,LoverXueEr

时间: 2024-10-23 19:00:59

20150518 Linux设备驱动中的并发控制的相关文章

linux设备驱动中的并发控制

并发指的是多个执行单元同时.并行被执行,而并发的执行单元对共享资源的访问则很容易导致竞态 linux内核中主要竞态1.多对称处理器的多个CPU  2.单CPU内进程与抢占它的进程 3.中断(硬中断.软中断.Tasklet.下半部)与进程之间访问共享内存资源的代码区称为“临界区”,临界区需要被以某种互斥机制加以保护,中断屏蔽.原子操作.自旋锁和信号量等是linux设备驱动中可采用的互斥途径. 这几个互斥的介绍: 1.中断屏蔽,这个主要用于单CPU,中断屏蔽将使得中断和进程之间的并发不再发生.使用方

深入浅出~Linux设备驱动中的并发控制

并发和竞争发生在两类体系中: 对称多处理器(SMP)的多个CPU 内核可抢占的单CPU系统 访问共享资源的代码区域称为临界区(critical sections),临界区需要以某种互斥机制加以保护.在驱动程序中,当多个线程同时访问相同的资源(critical sections)时(驱动程序中的全局变量是一种典型的共享资源),可能会引发"竞态",因此我们必须对共享资源进行并发控制.Linux内核中解决并发控制的方法又中断屏蔽.原子操作.自旋锁.信号量.(后面为主要方式) 中断屏蔽: 使用

Linux设备驱动中的阻塞与非阻塞IO与并发控制

Linux设备驱动中的阻塞与非阻塞IO: 1.Linux设备驱动中的阻塞与非阻塞总结:http://m.blog.csdn.net/blog/dongteen/17264501 2.Linux设备驱动中的阻塞与非阻塞IO:http://m.blog.csdn.net/blog/dongteen/17264501 3.Linux设备驱动中的阻塞与非阻塞I/O实例:http://blog.csdn.net/wenhui_/article/details/6817659 linux内核中等待队列: 1

Linux设备驱动中的阻塞和非阻塞I/O

[基本概念] 1.阻塞 阻塞操作是指在执行设备操作时,托不能获得资源,则挂起进程直到满足操作所需的条件后再进行操作.被挂起的进程进入休眠状态(不占用cpu资源),从调度器的运行队列转移到等待队列,直到条件满足. 2.非阻塞 非阻塞操作是指在进行设备操作是,若操作条件不满足并不会挂起,而是直接返回或重新查询(一直占用CPU资源)直到操作条件满足为止. 当用户空间的应用程序调用read(),write()等方法时,若设备的资源不能被获取,而用户又希望以阻塞的方式来访问设备,驱动程序应当在设备驱动层的

深入浅出~Linux设备驱动中的阻塞和非阻塞I/O

今天意外收到一个消息,真是惊呆我了,博客轩给我发了信息,说是俺的博客文章有特色可以出本书,,这简直让我受宠若惊,俺只是个大三的技术宅,写的博客也是自己所学的一些见解和在网上看到我一些博文以及帖子里综合起来写的,,总之这又给了额外的动力,让自己继续前进,,希望和大家能够分享一些自己的经验,,在最需要奋斗的年级以及在技术的领域踽踽独行的过程中有共同的伙伴继续前进~ 今天写的是Linux设备驱动中的阻塞和非阻塞I/0,何谓阻塞与非阻塞I/O?简单来说就是对I/O操作的两种不同的方式,驱动程序可以灵活的

Hasen的linux设备驱动开发学习之旅--linux设备驱动中的并发与竞态

/** * Author:hasen * 参考 :<linux设备驱动开发详解> * 简介:android小菜鸟的linux * 设备驱动开发学习之旅 * 主题:linux设备驱动中的并发与竞态 * Date:2014-11-04 */ 1.并发与竞态 并发(concurrency)指的是多个执行单元同时.并行被执行,而并发的执行单元对共享资源(软件上的全 局变量,静态变量等)的访问则很容易导致竞态(race conditions). 主要的竞态发生在以下几种情况: (1)对称多处理(SMP)

《Linux4.0设备驱动开发详解》笔记--第九章:Linux设备驱动中的异步通知与同步I/O

在设备驱动中使用异步通知可以使得对设备的访问可进行时,由驱动主动通知应用程序进行访问.因此,使用无阻塞I/O的应用程序无需轮询设备是否可访问,而阻塞访问也可以被类似"中断"的异步通知所取代.异步通知类似于硬件上的"中断"概念,比较准确的称谓是"信号驱动的异步I/O". 9.1 异步通知的概念和作用 异步通知:一旦设备就绪,则主动通知应用程序,该应用程序无需查询设备状态 几种通知方式比较: 阻塞I/O :一直等待设备可访问后开始访问 非阻塞I/O:

Linux设备驱动中的IO模型---阻塞和非阻塞IO【转】

在前面学习网络编程时,曾经学过I/O模型 Linux 系统应用编程——网络编程(I/O模型),下面学习一下I/O模型在设备驱动中的应用. 回顾一下在Unix/Linux下共有五种I/O模型,分别是: a -- 阻塞I/Ob -- 非阻塞I/Oc -- I/O复用(select和poll)d -- 信号驱动I/O(SIGIO)e -- 异步I/O(Posix.1的aio_系列函数) 下面我们先学习阻塞I/O.非阻塞I/O .I/O复用(select和poll),先学习一下基础概念 a -- 阻塞 

Linux 设备驱动中的file_operations

Linxu驱动中的设备文件注册的操作方法结构体,也是向用户层提供操作接口的方法体,我的版本为3.1.10 撰写不易,转载需注明出处:http://blog.csdn.net/jscese/article/details/43408625 原型在 内核源码 /include/linux/fs.h中定义: struct file_operations { struct module *owner; //第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指