- 论坛徽章:
- 8
|
并发和竞态
对并发的管理是操作系统编程中核心的问题之一。
所谓并发,可以这样来理解。 并发:一个人(CPU)喂2个孩子(程序),轮换着每人喂一口饭,表面上2个孩子都在吃饭。 而并行:就是2个人喂2个孩子,2个孩子也同时在吃饭。
并发产生竞态,竞态导致共享数据的非法访问。
一、并发及其管理
竞争情况来自对资源的共享存取的结果。当2个执行的线路有机会操作同一个数据结构(或硬件资源),混合的可能性就一直存在。
第一个经验法则是在你设计驱动时在任何可能的时候记住避免共享的资源。如果没有并发存取,就没有竞争情况。这个想法最明显的应用是使用全局变量。
但是,实际情况是,共享是常常需要的。硬件资源的特性是共享的。软件代码传递一个指针给内核的其他部分,潜在的也创造了一个共享。
资源共享的硬规则:任何时候,一个硬件或软件资源被超过一个单一执行线程共享,并且,可能一个线程看到那个共享资源的不一致(即:或者看到内存已经分配了,或者知道没有内存,或者将要被其他人分配),你必须明确地管理对那个资源的存取。存取管理的常用技术是加锁或者互斥----确保在任何时间只有一个执行线程可以操作一个共享资源。
资源共享的另一个重要规则是:当内核代码创建一个会被内核其他部分共享的对象时,这个对象必须一直存在(而且功能正常)直到对他的外部引用都不存在为止,也就是不存在任何对他的外部引用为止。
二、信号量和互斥体
当一个LINUX进程到了一个它无法做出进一步处理的地方时,即必须等待资源的时候,它去睡眠(也就是一般说的阻塞)。从而让出处理器给别的进程,直到以后某个时间它能够再做事情。这样也是为了不把CPU浪费在进程的等待时间上,提高系统的性能。
信号量(semaphore,旗语,信号灯)。
信号量(LDD3称之为旗标)是一个单个整型值,结合一对函数联合使用。一对函数是P和V。
一个想进入临界区的进程将在相关信号量上调用P;如果信号量的值大于零,这个值递减1并且进程继续。
相反,如果信号量的值是0(或者负数),进程必须等待直到别人释放信号量。解锁一个信号量通过调用V完成;这个函数递增信号量的值,并且,如果需要,唤醒等待的进程。
当信号量用作互斥----阻止多个进程同时在同一个临界区内运行;信号量的值将初始化为1.这样的信号量在任何给定时间只能由一个单个进程或者线程持有。
以这种模式使用的信号量有时称为一个互斥锁,就是互斥。
LINUX内核中几乎所有的信号量都是用于互斥。
使用信号量,内核代码必须包含头文件。
信号量初始化的方法:
/*直接创建一个信号量*/
void sema_init(struct semaphore *sem, int val);
由于信号量通常是被用于互斥模式,所以内核提供了一组辅助函数和宏:
/*方法一、声明+初始化宏*/
DECLARE_MUTEX(name); DECLARE_MUTEX_LOCKED(name);
/*结果是一个信号量变量(称为name),初始化为1(使用DECLARE_MUTEX)或者0(使用DECLARE_MUTEX_LOCKED)。在后一种情况,互斥锁开始于上锁的状态;在允许任何线程存取之前必须显式解锁它*/ /*方法二、初始化函数(这里是在运行时间初始化,即动态分配它的情况)*/
void init_MUTEX(struct semaphore *sem); void init_MUTEX_LOCKED(struct semphore *sem);
/*带有“_LOCKED”的是将信号量初始化为0,也就是进行锁定,线程访问时必须先解锁。没带“_LOCKED”的,初始化为1*/
P函数为:
void down(struct semaphore *sem); /*不推荐使用,此函数会建立不可杀的进程,因为它的操作是不可中断的*/
int down_interruptible(struct semaphore *sem); /*推荐使用,使用down_interruptible需要格外小心,若操作被中断,该函数会返回非零值,而调用者不会拥有该信号量。正确使用是:需要始终检查返回值,并做出相应的响应。*/
int down_trylock(struct semphore *sem);/*从不睡眠;如果信号量在调用时不可用,down_trylock立刻返回一个非零值。*/
一旦一个线程成功调用了down各个版本中的一个,我们就说它持有信号量(或者已经“取得”或者“获得”信号量)。这个线程现在有权利存取这个信号量保护的临界区。当这个需要互斥的操作完成时,信号量必须被返回。V的LINUX对应的是up,对应于P的down。
void up(struct semaphore *sem);/*一旦up被调用,调用者就不再拥有信号量*//*任何拿到信号量的线程都必须通过一次(只有一次)对up的调用而释放该信号量。在出错时,要特别的小心;若在拥有一个信号量时发生错误,必须在将错误状态返回前释放信号量*/
读者/写者信号量
信号量为所有调用者进行互斥,不管每个线程可能想做什么。
然而,很多任务分为2中类型:只需要读取被保护的数据结构的类型和必须做改变的类型。允许多个并发读者常常来说是可能的,只要这些读者中没有人试图对数据做出修改。只读的任务可以并行进行它们的工作而不必等待其他读者退出临界区。
LINUX内核提供了读者/写者信号量“rwsem”,必须包括头文件。
初始化:
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);/*该函数用于把写者降级为读者,这个操作有时是必要的。因为写者是排他性的,即不可并发的,因此在写者保持读写信号量期间,任何读者或写者都将无法访问该读写信号量保护的共享资源,对于那些当前条件下不需要写访问的写者,降级为读者,将使得等待访问的读者能够立即访问,从而增加了并发性,提高了效率。*/
一个rwsem允许一个写者或无限多个读者来看拥有该信号量。写者有优先权;当某个写者试图进入临界区,就不会允许读者进入直到写者完成了它的工作。如果有大量的写者竞争该信号量,则这个实现可能导致读者“饿死”,即可能会长期拒绝读者访问。因此,rwsem最好用在很少请求写的时候,并且写者只占用较短的时间。
completion
completion是一种轻量级的机制,它允许一个线程告诉另一个线程某个工作已经完成。代码中必须包含头文件。使用的代码如下:
DECLARE_COMPLETION(my_completion);/*创建completion(声明+初始化)*/ struct completion my_completion; /*动态声明completion结构体*/ static inline void init_completion(&my_completion);/*动态初始化completion*/ void wait_for_completion(struct completion *c);/*等待completion*/ void completion(struct completion *c); /*唤醒一个等待completion的线程*/ void completion_all(struct completion *c);/*唤醒所有等待completion的线程*//*如果未使用completion_all,completion可重复利用;否则必须使用以下的函数重新初始化completion*/ INIT_COMPLETION(struct completion c);/*快速重新初始化completion*/
completion的典型应用是模块退出时的内核线程终止。在这种运行中,某些驱动程序的内部工作有一个内核线程在while(1)循环中完成。当内核准备清除该模块时,exit函数会告诉该线程退出并等待completion。为此内核包含了用于这种线程的一个特殊函数:
void completion_and_exit(struct completion *c, long retval);
本文来自ChinaUnix博客,如果查看原文请点:http://blog.chinaunix.net/u1/54642/showart_1675117.html |
|