第五章:并发与竞态

原创 2012年03月30日 10:44:52

竞态通常作为对资源的共享访问结果而产生。

linux信号量的实现

内核代码首先需要包括<asm/semaphore.h>头文件。相关的类型是struct semaphore;

直接创建并初始化信号的函数:

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

val是赋予信号量的初始值。

其他的宏有:

DECLARE_MUTEX(name); //一个成为name的信号量变量被初始化为1

DECLARE_MUTEX_LOCKED(name); //被初始化为0。这种情况,在允许任何线程访问之前,必须显式地解锁该互斥体

动态分配互斥体的情况下,信号量需要在运行时被初始化:

void init_MUTEX(struct semaphore *sem);

void init_MUTEX_LOCKED(struct semaphore *sem);

P函数成为down,有三个版本:

void down(struct semaphore *sem); //阻塞版本

int down_interruptible(struct semaphore *sem); //可中断版本

int down_trylock(struct semaphore *sem); //不会休眠版本

当上述函数调用成功以后,该线程就获得了该信号量,拥有处理共享资源的权利。

对应的up是:

void up(struct semaphore *sem);

读取这/写入者信号量

头文件:<linux/rwsem.h>

相关函数:

void init_rwsem(struct rw_semaphore *sem);

void down_read(struct rw_semaphore *sem);

int  down_read_trylock(struct rw_semaphore *sem);

void up_read(struct rw_semaphore *sem);

void down_write(struct rw_semaphore *sem);

int down_write_trylock(struct rw_semaphore *sem);

void up_write(struct rw_semaphore *sem);

void downgrade_write(struct rw_semaphore *sem);

completion机制

内核编程中有如下一种模式:在当前线程之外初始化某个活动,然后等待该活动的结束。此时,可以使用competition机制,头文件:<linux/completion.h>.

DECLARE_COMPLETION(my_completion); //创建completion

动态的创建并初始化completion

struct completion my_completion;

init_completion(&my_completion);

等待completion完成:

void wait_for_completion(struct completion *c);

非中断的等待,若调用了该函数,但没有进程理会该任务,则产生一个不可杀的进程。任务的完成调用下函数:

void complete(struct completion *c);

void complete_all(struct completion *c);

区别:若有多个等待线程,complete只会唤醒一个等待线程(具体哪一个不可知),complete_all会唤醒所有的等待线程。

completion一般是一个单次设备,即使用一次会被丢弃。若要重复使用建议重新初始化,尤其在使用complete_all的情况下:

INIT_COMPLETION(struct completion *c);

completion机制的典型应用:模块退出时的内核线程终止。某些驱动程序内部由一个while(1)循环完成,当内核准备清楚模块时,exit函数会告诉该线程退出,并等待completion(wait_for_completion)。被通知函数执行下函数:

void complete_and_exit(struct completion *c, long retval);

自旋锁

自旋锁的核心思想:进入某段代码前要检查锁,若锁可用,则代码继续,若不可用,则代码进入忙循环并重复检查这个锁。这个循环就是自旋锁的“自旋”部分。自旋期间,执行忙循环的处理器做不了任何有用的工作。

自旋锁API

自旋锁头文件:<linux/spinlock.h>。锁类型:spinlock_t

静态创建锁:

spinlock_t my_lock = SPIN_LOCK_UNLOCKED;

动态创建锁:

void spin_lock_init(spinlock_t *lock);

代码进入临界区之前,获得锁的函数:

void spin_lock(spinlock_t *lock);

释放锁:

void spin_unlock(spinlock_t *lock);

使用自旋锁应注意的问题:

若某段代码拥有了锁,此刻被中断,或者CPU被抢占,而中断代码或者新拥有CPU的代码中又有对该锁的请求,则在可以预见的时间里,锁将会一直处于自旋状态,因此,使用自旋锁的的核心规则:任何自旋锁的代码必须是原子的,不能休眠,不能放弃处理器,中断服务除外(某些情况下,此时也不能放弃处理器)。

² 内核抢占的情况自旋锁代码自身处理,即只要内核拥有了自旋锁,对应处理器上的抢占会被禁止。

² 自旋锁代码中不可以休眠,在调用内核函数时,需要多注意函数是否会休眠。

² 需要时,自旋锁需要禁止中断(仅在本地CPU上)。

² 最后一个重要原则是:自旋锁必须在尽可能短的时间里拥有。

自旋锁函数

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

spin_lock_irqsave会在获得自旋锁之前禁止本地CPU上的中断,先前的中断状态保存在flags中。如果我们能够确保没有任何其他代码禁止本地处理器的中断(即,我们能够确保在释放自旋锁时应该启用中断),则可以使用spin_lock_irqspin_lock_bh在获得锁之前禁止软件中断,但是会让硬件中断保持打开。

释放自旋锁的方法:

void spin_unlock(spinlock_t *lock);

void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);

void spin_unlock_irq(spinlock_t *lock);

void spin_unlock_bh(spinlock_t *lock);

spin_lock_irqsavespin_lock_irq和对应的解锁函数必须在同一个函数中调用,否则代码可能在某些架构上出现问题。

非阻塞的自旋锁操作:

int spin_trylock(spinlock_t *lock);

int spin_trylock_bh(spinlock_t *lock);

成功返回非零值,不成功返回零值。

读取者/写入者自旋锁

锁类型:rwlock_t类型。其他初始化以及应用API见头文件:<linux/spinlock.h>

锁陷阱

使用锁需要注意可能导致错误的东西:

² 如果某个获得锁的代码又调用其他同样试图获取这个锁的函数,代码就会死锁。因此,决不允许锁拥有者第二次或者这个锁。

² 尽量避免某个操作需要多个锁的情况,若不可避免,则规定获得锁的顺序。有益的规则:1.若有多个锁,先获得局部锁,再获得全局锁。2.若同时要获得信号量和自旋锁,须先获得信号量。因为down锁可导致休眠,而休眠在自旋锁中是不允许存在的。

² 使用lockmeter可以度量花在锁上的时间。

除了锁之外的方法

1. 循环缓冲区方法。适合一个生产者,多个消费者的情况。内核有一个通用的循环缓冲区的实现,头文件是<linux/kfifo.h>

2. 原子变量。头文件<asm/atomic.h>。类型为:atomic_t

3. 位操作。以原子形式来操作单个位,头文件<asm/bitops.h>

4. seqlock机制。适用于被保护的资源很小,会被频繁的访问而且写入访问很少发生且快速时的情况。seqlock通常不能用于保护包含数据指针的数据结构。头文件<linux/seqlock.h>。使用方法:读取访问获得一个(无符号)整数值而进入临界区,退出时,检查该顺序值与当前值,若不相等,则说明此值已被修改,则必须重试读取访问。写访问需获得一个互斥锁,具体函数见头文件,该函数使用自旋锁实现,因此自旋锁的常见限制也适用于此。

读取-复制-更新(RCU)。工作方法:需要修改某个共享的数据结构时,写入线程首先复制,然后修改副本,之后用新的版本替代相关指针。详见头文件<linux/rcupdate.h>

并发编程五:竞态条件与临界区

并发编程:竞态条件与临界区介绍当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。导致竟态条件发生的代码称作临界区。备注:以下这段代码就存在竞态条件,其中return ++count...
  • nicewuranran
  • nicewuranran
  • 2016年08月14日 14:59
  • 460

Linux----并发与竞态

并发是指多个执行单元同时、并发被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问也很容易导致竞态。 即:同一时间多个“设备”同时访问同一个“区域”是会出错的。 但是在C...
  • smallfish_love
  • smallfish_love
  • 2016年02月27日 11:42
  • 491

linux内核的并发与竞态

并发与竞态是指多个任务单元同时访问同一个资源,就会出现并发,竞态的现象。 其中多个任务单元包括中断,进程/线程,甚至多个多处理器;同一资源既包含硬件资源,也包括软件数据,比如:gpio,硬件设备.....
  • hpu11
  • hpu11
  • 2016年08月30日 19:35
  • 439

第五章:并发与竞态

两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序,就成为竞争条件(race condition)。 竟态通常作为对资源的共享访问结果而产生的。当两个或多个进程需要访问相同的数据结...
  • u013684730
  • u013684730
  • 2017年07月05日 09:43
  • 103

第五章--并发和竞态

本文作为第五章--并发和竞态,主要讲述两个重要概念: 1、临界区。 2、原子。...
  • apple_guet
  • apple_guet
  • 2014年03月11日 11:52
  • 717

并发与竞态

Linux设备驱动中必须解决的一个问题是多个进程对共享资源的并发访问,并发访问会导致竞态。 并发(concurrency)指的是多个执行单元同时、并行被执行,而并行的执行单元对共享资...
  • maxwell_fog
  • maxwell_fog
  • 2013年11月07日 19:03
  • 373

竞态与并发

竞态与并发的情况广泛存在,中断屏蔽、原子操作、自旋锁、信号量可以解决并发问题。中断屏蔽很少用,而原子操作只能针对整数使用,所以一般是自旋锁和信号量使用的比较多; 自旋锁使用不当会导致死锁,在锁定期间不...
  • boyka_
  • boyka_
  • 2016年12月06日 17:08
  • 95

竞态条件 数据竞争区别

数据竞争:出现在一般数据的访问,一个读进程和写进程,如果没有进行同步,那么就会出现数据访问错误。竞态条件:不是所有竞态条件都会出现数据竞争,竞态条件有时取决于运气,竞态条件指的是类中没有加锁的对象。如...
  • Ordain_3050
  • Ordain_3050
  • 2017年08月09日 22:29
  • 282

学习Ldd3--并发与竞态(第五章)

作者:张伟AreS /******************************************************************************/ 代码:LDD3...
  • AzRael_AreS
  • AzRael_AreS
  • 2012年10月14日 10:41
  • 248

什么叫竞态条件?

竞态条件是由和事件时间相关的意料之外的依赖所导致的反常行为。换句话说,一个程序员不正确的假设一个特殊的事件总是在另一个事件之前发生。  一些通常的导致竞态条件的原因是信号,存取检查和打开文件操作。由于...
  • tanliyoung
  • tanliyoung
  • 2007年01月12日 14:38
  • 1438
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:第五章:并发与竞态
举报原因:
原因补充:

(最多只允许输入30个字)