struct completion
linux completion理解
http://blog.chinaunix.net/uid-22145625-id-1789484.html
Linux设备驱动中的并发控制总结
转载自:http://www.cnblogs.com/yangzd/archive/2010/10/16/1852975.html,仅供个人学习记录之用。
并发(concurrency)指的是多个执行单元同时、并行被执行。而并发的执行单元对共享资源(硬件资源和软件上的全局、静态变量)的访问则容易导致竞态(race conditions)。
SMP是一种紧耦合、共享存储的系统模型,它的特点是多个CPU使用共同的系统总线,因此可访问共同的外设和存储器。
进程与抢占它的进程访问共享资源的情况类似于SMP的多个CPU.
中断可打断正在执行的进程,若中断处理程序访问进程正在访问的资源,则竞态也会发生。中断也可能被新的更高优先级的中断打断,因此,多个中断之间也可能引起并发而导致竞态。上述并发的发生情况除了SMP是真正的并行以外,其他的都是“宏观并行、微观串行”的,但其引发的实质问题和SMP相似。解决竞态问题的途径是保证对共享资源的互斥访问,即一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问。
访问共享资源的代码区域成为临界区(critical sections),临界区需要以某种互斥机制加以保护。中断屏蔽、原子操作、自旋锁和信号量等是Linux设备驱动中可采用的互斥途径。
中断屏蔽的使用方法为:
local_irq_disable()//屏蔽中断 ... criticalsection // 临界区 ... local_irq_enable()// 开中断
在屏蔽了中断后,当前的内核执行路径应当尽快执行完临界区代码。上述两个函数都只能禁止和使能本CPU内的中断,不能解决SMP多CPU引发的竞态。
local_irq_save(flags) 除禁止中断的操作外,还保存目前CPU的中断位信息;
local_irq_restore(flags) 进行的是local_irq_save(flags)相反的操作;
若只想禁止中断的底半部,应使用local_bh_disable(), 使能被local_bh_disable()禁止的底半部应调用local_bh_enable()。
原子操作指的是在执行过程中不会被别的代码路径所中断的操作。
整型原子操作:
// 设置原子变量的值 voidatomic_set(atomic_t*v,inti); // 设置原子变量的值为i atomic_tv =ATOMIC_INIT(0);// 定义原子变量v,并初始化为0 // 获取原子变量的值 atomic_read(atomic_t*v);// 返回原子变量的值 // 原子变量加/减 voidatomic_add(inti, atomic_t*v);// 原子变量加i voidatomic_sub(inti, atomic_t*v);// 原子变量减i // 原子变量自增/自减 voidatomic_inc(atomic_t*v);// 原子变量增加1 voidatomic_dec(atomic_t*v);// 原子变量减少1 // 操作并测试:对原子变量进行自增、自减和减操作后(没有加)测试其是否为0,为0则返回true,否则返回false intatomic_inc_and_test(atomic_t*v); intatomic_dec_and_test(atomic_t*v); intatomic_sub_and_test(inti, atomic_t*v); // 操作并返回: 对原子变量进行加/减和自增/自减操作,并返回新的值 intatomic_add_return(inti, atomic_t*v); intatomic_sub_return(inti, atomic_t*v); intatomic_inc_return(atomic_t*v); intatomic_dec_return(atomic_t*v);
位原子操作:
// 设置位 voidset_bit(nr,void*addr);// 设置addr地址的第nr位,即将位写1 // 清除位 voidclear_bit(nr,void*addr);// 清除addr地址的第nr位,即将位写0 // 改变位 voidchange_bit(nr,void*addr);// 对addr地址的第nr位取反 // 测试位 test_bit(nr,void*addr);// 返回addr地址的第nr位 // 测试并操作:等同于执行test_bit(nr, void *addr)后再执行xxx_bit(nr, void *addr) inttest_and_set_bit(nr,void*addr); inttest_and_clear_bit(nr,void*addr); inttest_and_change_bit(nr,void*addr);
原子变量使用实例,使设备只能被一个进程打开:
staticatomic_t xxx_available = ATOMIC_INIT(1);// 定义原子变量 staticintxxx_open(structinode *inode,structfile *filp) { ... if(!atomic_dec_and_test(&xxx_available)) { atomic_inc(&xxx_availble); return- EBUSY;// 已经打开 } ... return0;// 成功 } staticintxxx_release(structinode *inode,structfile *filp) { atomic_inc(&xxx_available);// 释放设备 return0; }
自旋锁(spin lock)——“在原地打转”。若一个进程要访问临界资源,测试锁空闲,则进程获得这个锁并继续执行;若测试结果表明锁扔被占用,进程将在一个小的循环内重复“测试并设置”操作,进行所谓的“自旋”,等待自旋锁持有者释放这个锁。
自旋锁的相关操作:
//定义自旋锁 spinlock_tspin; // 初始化自旋锁 spin_lock_init(lock); //获得自旋锁:若能立即获得锁,它获得锁并返回,否则,自旋,直到该锁持有者释放 spin_lock(lock); //尝试获得自旋锁:若能立即获得锁,它获得并返回真,否则立即返回假,不再自旋 spin_trylock(lock); // 释放自旋锁: 与spin_lock(lock)和spin_trylock(lock)配对使用 spin_unlock(lock);
自旋锁的使用:
// 定义一个自旋锁 spinlock_tlock; spin_lock_init(&lock); spin_lock(&lock);// 获取自旋锁,保护临界区 ...// 临界区 spin_unlock();// 解锁
自旋锁持有期间内核的抢占将被禁止。
自旋锁可以保证临界区不受别的CPU和本CPU内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候还可能受到中断和底半部(BH)的影响。
为防止这种影响,需要用到自旋锁的衍生:
spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()
spin_lock_irqsave() = spin_lock() + 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()
注意:自旋锁实际上是忙等待,只有在占用锁的时间极短的情况下,使用自旋锁才是合理的
自旋锁可能导致死锁:递归使用一个自旋锁或进程获得自旋锁后阻塞。
自旋锁使用实例,使设备只能被最多一个进程打开:
intxxx_count = 0;// 定义文件打开次数计数 staticintxxx_open(structinode *inode,structfile *filp) { ... spinlock(&xxx_lock); if(xxx_count);// 已经打开 { spin_unlock(&xxx_lock); return- EBUSY; } xxx_count++;// 增加使用计数 spin_unlock(&xxx_lock); ... return0;// 成功 } staticintxxx_release(structinode *inode,structfile *filp) { ... spinlock(&xxx_lock); xxx_count--;// 减少使用计数 spin_unlock(&xxx_lock); return0; }
读写自旋锁(rwlock)允许读的并发。在写操作方面,只能最多有一个写进程,在读操作方面,同时可以有多个读执行单元。当然,读和写也不能同时进行。
// 定义和初始化读写自旋锁 rwlock_tmy_rwlock = RW_LOCK_UNLOCKED;// 静态初始化 rwlock_tmy_rwlock; rwlock)init(&my_rwlock);// 动态初始化 // 读锁定:在对共享资源进行读取之前,应先调用读锁定函数,完成之后调用读解锁函数 voidread_lock(rwlock_t*lock); voidread_lock_irqsave(rwlock_t*lock,unsignedlongflags); voidread_lock_irq(rwlock_t*lock); voidread_lock_bh(rwlock_t*lock); // 读解锁 voidread_unlock(rwlock_t*lock); voidread_unlock_irqrestore(rwlock_t*lock,unsignedlongflags); voidread_unlock_irq(rwlock_t*lock); voidread_unlock_bh(rwlock_t*lock); // 写锁定:在对共享资源进行写之前,应先调用写锁定函数,完成之后调用写解锁函数 voidwrite_lock(rwlock_t*lock); voidwrite_lock_irqsave(rwlock_t*lock,unsignedlongflags); voidwrite_lock_irq(rwlock_t*lock); voidwrite_lock_bh(rwlock_t*lock); intwrite_trylock(rwlock_t*lock); // 写解锁 voidwrite_unlock(rwlock_t*lock); voidwrite_unlock_irqsave(rwlock_t*lock,unsignedlongflags); voidwrite_unlock_irq(rwlock_t*lock); voidwrite_unlock_bh(rwlock_t*lock);
读写自旋锁一般用法:
rwlock_tlock; // 定义rwlock rwlock_init(&lock);// 初始化rwlock // 读时获取锁 read_lock(&lock); ...// 临界资源 read_unlock(&lock); // 写时获取锁 write_lock_irqsave(&lock,flags); ...// 临界资源 write_unlock_irqrestore(&lock,flags);
顺序锁(seqlock)是对读写锁的优化。
使用顺序锁,读执行单元不会被写执行单元阻塞,即读执行单元可以在写执行单元对被顺序锁保护的共享资源进行写操作时仍然可以继续读,而不必等待写执行单元完成写操作,写执行单元也不需要等待所有读执行单元完成读操作才去进行写操作。
写执行单元之间仍是互斥的。
若读操作期间,发生了写操作,必须重新读取数据。
顺序锁必须要求被保护的共享资源不含有指针。
写执行单元操作:
// 获得顺序锁 voidwrite_seqlock(seqlock_t*sl); intwrite_tryseqlock(seqlock_t*sl); write_seqlock_irqsave(lock,flags) write_seqlock_irq(lock) write_seqlock_bh() // 释放顺序锁 voidwrite_sequnlock(seqlock_t*sl); write_sequnlock_irqrestore(lock,flags) write_sequnlock_irq(lock) write_sequnlock_bh() // 写执行单元使用顺序锁的模式如下: write_seqlock(&seqlock_a); ...// 写操作代码块 write_sequnlock(&seqlock_a);
读执行单元操作:
// 读开始:返回顺序锁sl当前顺序号 unsignedread_seqbegin(constseqlock_t *sl); read_seqbegin_irqsave(lock,flags) // 重读:读执行单元在访问完被顺序锁sl保护的共享资源后需要调用该函数来检查,在读访问期间是否有写操作。若有写操作,重读 intread_seqretry(constseqlock_t *sl,unsignediv); read_seqretry_irqrestore(lock,iv, flags) // 读执行单元使用顺序锁的模式如下: do{ seqnum= read_seqbegin(&seqlock_a); // 读操作代码块 ... }while(read_seqretry(&seqlock_a,seqnum));
RCU(Read-Copy Update 读-拷贝-更新)
RCU可看作读写锁的高性能版本,既允许多个读执行单元同时访问被保护的数据,又允许多个读执行单元和多个写执行单元同时访问被保护的数据。
但是RCU不能替代读写锁。因为如果写操作比较多时,对读执行单元的性能提高不能弥补写执行单元导致的损失。因为使用RCU时,写执行单元之间的同步开销会比较大,它需要延迟数据结构的释放,复制被修改的数据结构,它也必须使用某种锁机制同步并行的其他写执行单元的修改操作。
RCU操作:
// 读锁定 rcu_read_lock() rcu_read_lock_bh() // 读解锁 rcu_read_unlock() rcu_read_unlock_bh() // 使用RCU进行读的模式如下: rcu_read_lock() ...// 读临界区 rcu_read_unlock()
rcu_read_lock() 和rcu_read_unlock()实质是禁止和使能内核的抢占调度:
#definercu_read_lock()preempt_disable() #definercu_read_unlock()preempt_enable()
rcu_read_lock_bh()、rcu_read_unlock_bh()定义为:
#definercu_read_lock_bh()local_bh_disable() #definercu_read_unlock_bh()local_bh_enable()
同步RCU
synchronize_rcu()
由RCU写执行单元调用,保证所有CPU都处理完正在运行的读执行单元临界区。
信号量的使用
信号量(semaphore)与自旋锁相同,只有得到信号量才能执行临界区代码,但,当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。
信号量的操作:
//定义信号量 structsemaphore sem; //初始化信号量: //初始化信号量,并设置sem的值为val voidsema_init(structsemaphore *sem,intval); //初始化一个用于互斥的信号量,sem的值设置为1。等同于sema_init(structsemaphore *sem, 1) voidinit_MUTEX(structsemaphore *sem); // 等同于sema_init(structsemaphore *sem, 0) voidinit_MUTEX_LOCKED(structsemaphore *sem); // 下面两个宏是定义并初始化信号量的“快捷方式”: DECLEAR_MUTEX(name) DECLEAR_MUTEX_LOCKED(name) //获得信号量: //用于获得信号量,它会导致睡眠,不能在中断上下文使用 voiddown(structsemaphore *sem); //类似down(),因为down()而进入休眠的进程不能被信号打断,而因为down_interruptible()而进入休眠的进程能被信号打断,
// 信号也会导致该函数返回,此时返回值非0voiddown_interruptible(structsemaphore *sem); // 尝试获得信号量sem,若立即获得,它就获得该信号量并返回0,否则,返回非0.它不会导致调用者睡眠,可在中断上下文使用 intdown_trylock(structsemaphore *sem); //使用down_interruptible()获取信号量时,对返回值一般会进行检查,若非0,通常立即返回-ERESTARTSYS,如: if(down_interruptible(&sem)) { return- ERESTARTSYS; } // 释放信号量 // 释放信号量sem, 唤醒等待者 voidup(structsemaphore *sem); // 信号量一般这样被使用: DECLARE_MUTEX(mount_sem); down(&mount_sem);// 获取信号量,保护临界区 ... criticalsection // 临界区 ... up(&mount_sem);// 释放信号量
Linux自旋锁和信号量锁采用的“获取锁-访问临界区-释放锁”的方式存在于几乎所有的多任务操作系统之中。
用信号量实现设备只能被一个进程打开的例子:
staticDECLEAR_MUTEX(xxx_lock)// 定义互斥锁 staticintxxx_open(structinode *inode,structfile *filp) { ... if(down_trylock(&xxx_lock))// 获得打开锁 return- EBUSY;// 设备忙 ... return0;// 成功 } staticintxxx_release(structinode *inode,structfile *filp) { up(&xxx_lock);// 释放打开锁 return0; }
信号量用于同步
若信号量被初始化为0,则它可以用于同步,同步意味着一个执行单元的继续执行需等待另一执行单元完成某事,保证执行的先后顺序。
完成量用于同步
完成量(completion)提供了一种比信号量更好的同步机制,它用于一个执行单元等待另一个执行单元执行完某事。
completion相关操作:
// 定义完成量 structcompletion my_completion; // 初始化completion init_completion(&my_completion); // 定义和初始化快捷方式: DECLEAR_COMPLETION(my_completion); // 等待一个completion被唤醒 voidwait_for_completion(structcompletion *c); // 唤醒完成量 voidcmplete(structcompletion *c); voidcmplete_all(structcompletion *c);
static struct completion als_thread_completion;
static int als_polling_function(void* arg)
{
uint32_t delay;
init_completion(&als_thread_completion);
while (1)
{
STK_LOCK(1);
pStkPsData->als_reading = get_als_reading();
if(pStkPsData->als_reading < 0)
{
ERR("STK PS %s: get_als_reading failed, ret=%d", __func__, pStkPsData->als_reading);
STK_LOCK(0);
continue;
}
update_and_check_report_als(pStkPsData->als_reading);
if (pStkPsData->bALSThreadRunning == 0) //很重要,如果不检查这个标志,那么线程会一直运行。
break;
STK_LOCK(0);
};
STK_LOCK(0);
complete(&als_thread_completion);// 唤醒完成量
return 0;
}
static int32_t enable_als(uint8_t enable)
{
int32_t ret;
if (enable)
{
if (pStkPsData->bALSThreadRunning == 0)
{
pStkPsData->bALSThreadRunning = 1;
als_polling_tsk = kthread_run(als_polling_function,NULL,"als_polling");//创建并运行一个线程
}
else
{
WARNING("STK ALS : thread has running\n");
}
}
else
{
if (pStkPsData->bALSThreadRunning)
{
pStkPsData->bALSThreadRunning = 0;//设置为0,als_polling线程会检查这个标志。
STK_LOCK(0);
wait_for_completion(&als_thread_completion);//等待完成量唤醒,这个完成量会在als_polling_function()中,当bALSTreadRunning标志为0时,被唤醒。
STK_LOCK(1);
als_polling_tsk = NULL;
}
}
return 0;
}
自旋锁和信号量的选择
当锁不能被获取时,使用信号量的开销是进程上下文切换时间Tsw,使用自旋锁的开销是等待获取自旋锁(由临界区执行时间决定)Tcs,若Tcs较小,应使用自旋锁,若Tcs较大,应使用信号量。
信号量保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护包含这样代码的临界区。因为阻塞意味着要进行进程切换,若进程被切换出去后,另一个进程企图获取本自旋锁,死锁就会发生。
信号量存在于进程上下文,因此,若被保护的共享资源需要在中断或软中断情况下使用,则在信号量和自旋锁之间只能选择自旋锁。若一定要使用信号量,则只能通过down_trylock()方式进行,不能获取就立即返回避免阻塞。
读写信号量
读写信号量与信号量的关系与读写自旋锁和自旋锁的关系类似,读写信号量可能引起进程阻塞,但它可允许N个读执行单元同事访问共享资源,而最多只能有一个写执行单元。
读写自旋锁的操作:
// 定义和初始化读写信号量 structrw_semaphoremy_res; // 定义 voidinit_rwsem(structrw_semaphore*sem);// 初始化 // 读信号量获取 voiddown_read(structrw_semaphore*sem); voiddown_read_trylock(structrw_semaphore*sem); // 读信号量释放 voidup_read(structrw_semaphore*sem); // 写信号量获取 voiddown_write(structrw_semaphore*sem); intdown_write_trylock(structrw_semaphore*sem); // 写信号量释放 voidup_write(structrw_semaphore*sem); // 读写信号量的使用: rw_semaphorerw_sem; // 定义 init_rwsem(&rw_sem);// 初始化 // 读时获取信号量 down_read(&rw_sem); ...// 临街资源 up_read(&rw_sem); // 写时获取信号量 down_write(&rw_sem); ...// 临界资源 up_writer(&rw_sem);
Linux设备驱动中的阻塞与非阻塞总结
阻塞与非阻塞访问是I/O操作的两种不同模式,前者在I/O操作暂时不可进行时会让进程睡眠。
在设备驱动中阻塞I/O一般基于等待队列来实现,等待队列可用于同步驱动中事件发生的先后顺序。
使用非阻塞I/O的应用程序也可借助轮询函数来查询设备是否能立即被访问。
阻塞操作是指在设备操作时若不能获得资源则挂起进程,直到满足可操作的条件后再进行操作。被挂起的进程进入休眠状态,被从调度器的运行队列移走,直到等待的条件满足。
非阻塞操作的进程在不能进行设备操作时并不挂起,它或者放弃,或者不停地查询,直到可进行操作为止。
等待队列
等待队列的操作:
// 定义“等待队列头” wait_queue_head_tmy_queue; // 初始化“等待队列头” init_wait_queue_head(&my_queue); // 定义并初始化等待队列头的快捷方式(宏) DECLARE_WAIT_QUEUE_HEAD(name) // 定义等待队列 DECLARE_WAITQUEUE(name,tsk) // 该宏用于定义并初始化一个名为name的等待队列 //添加/移除等待队列 voidfastcall add_wait_queue(wait_queue_head_t*q,wait_queue_t*wait); // 将等待队列wait添加到等待队列头q指向的等待队列链表中 voidfastcall remove_wait_queue(wait_queue_head_t*q,wait_queue_t*wait); // 将等待队列wait从附属的等待队列头q指向的等待队列链表中移除 // 等待事件 wait_event(queue,condition) // queue作为等待队列头的等待队列被唤醒,而且condition必须满足,否则阻塞。不能被信号打断 wait_event_interruptible(queue,condition) // 可以被信号打断 wait_event_timeout(queue,condition) // 加上_timeout后意味着阻塞等待的超时时间,以jiffy为单位。在timeout到达时不论condition是否满足,均返回 wait_event_interruptible_timeout(queue,condition) // 唤醒队列 // 唤醒以queue作为等待队列头的所有等待队列中所有属于该等待队列头的等待队列对应的进程 void wake_up(wait_queue_head_t *queue); void wake_up_interruptible(wait_queue_head_t *queue); // wake_up()应与wait_event()或wait_event_timeout()成对使用 // 而wake_up_intterruptible()则应与wait_event_interruptible()或wait_envent_interruptible_timeout()成对使用 // wake_up()可唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE的进程,而wake_up_interruptible()只能唤醒处于TASK_INTERRUPTIBLE的进程 // 在等待队列上睡眠 sleep_on(wait_queue_head_t *q); // 将目前进程的状态设置成TASK_UNINTERRUPTIBLE,并定义一个等待队列,之后把它附属到等待队列头q, // 直到资源可获得,q引导的等待队列被唤醒。 interruptible_seep_on(wait_queue_head_t *q); //将目前进程的状态设置成TASK_INTERRUPTIBLE,并定义一个等待队列,之后把它附属到等待队列头q, // 直到资源可获得,q引导的等待队列被唤醒或者进程收到信号。
轮询操作
使用非阻塞I/O的应用程序通常使用select()和poll()系统调用查询是否可对设备进行无阻塞的访问。select()和poll()系统调最终会引发设备中的poll()函数执行(xxx_poll()).
应用程序中的轮询编程
select()系统调用:
intselect(intnumfds, fd_set *readfds,fd_set *writefds,fd_set *exceptfds,structtimeval *timeout); 其中readfds、writefds、exceptfds分别是被select()监视的读、写和异常处理的文件描述符集合,numfds的值是需要检查的号码最高的文件描述符加1. timeout参数是一个指向struct timeval类型的指针,它可以使select()在等待timeout时间后若没有文件描述符准备好则返回。
structtimeval { inttv_sec; // 秒 inttv_usec; // 微秒 }
设置、清除、判断文件描述符集合:
FD_ZERO(fd_set*set)// 清除一个文件描述符集 FD_SET(intfd, fd_set *set)// 将一个文件描述符加入文件描述符集中 FD_CLR(intfd, fd_set *set)// 将一个文件描述符从文件描述符集中清除 FD_ISSET(intfd, fd_set *set)// 判断文件描述符是否被置位
设备驱动中的轮询编程
poll()函数:
unsignedint(*poll)(structfile *flip,structpoll_table *wait);
第一个参数为file结构指针,第二个参数为轮训表指针。这个函数进行一下两项工作:
● 对可能引起设备文件状态变化的等待队列调用poll_wait()函数,将对应的等待队列头添加到poll_table.
● 返回表示是否能对设备进行无阻塞读、写访问的掩码。
关键的用于向poll_table注册等待队列的poll_wait()函数的原型如下:
voidpoll_wait(structfile *flip,wait_queue_head_t*queue,poll_table *wait);
这个函数不会引起阻塞。poll_wait()函数所做的工作是把当前进程添加到wait参数指定的等待列表(poll_table)中。
poll()函数应该返回设备资源的可获取状态,即POLLIN、POLLOUT、POLLPRI、POLLERR、POLLNVAL等宏的位“或”结果。
poll()模板:
staticunsignedintxxx_poll(structfile *filp,poll_table *wait) { unsignedintmask = 0; structxxx_dev *dev= filp->private_data;// 获得设备结构指针 ... poll_wait(filp,&dev->r_wait,wait); // 加读等待队列头 poll_wait(filp,&dev->w_wait,wait); // 加写等待队列头 if(...)// 可读 { mask |=POLLIN | POLLRDNORM;// 标示数据可获得 } if(...)// 可写 { mask |=POLLOUT | POLLWRNORM;// 标示数据可写入 } ... returnmask; }
用户空间调用select()和poll()接口,设备驱动提供poll()函数。设备驱动的poll()本身不会阻塞,但是poll()和select()系统调用则会阻塞地等待文件描述符集合中的至少一个可访问或超时。