您的位置:首页 > 运维架构 > Linux

linux驱动学习之并发和竞争控制

2012-12-20 15:42 176 查看
linux驱动学习之信号量和互斥体

在驱动程序中,当多个线程同时访问相同的资源时(全局变量或硬件资源),可能会引发竞态因此我们必须对共享资源进行并发控制。linux内核中解决并发控制的最常用方法是自旋锁与信号量。

一 信号量

1965年,荷兰学者Dijkstra提出了利用信号量机制解决进程同步问题,信号量正式成为有效的进程同步工具,现在信号量机制被广泛的用于单处理机和多处理机系统以及计算机网络中。

  信号量S是一个整数,S大于等于零时代表可供并发进程使用的资源实体数,当S小于零时则表示正在等待使用临界区的进程数。

  Dijkstra同时提出了对信号量操作的PV原语。

  P原语操作的动作是:

   (1)S减1;

   (2)若S减1后仍大于或等于零,则进程继续执行;

   (3)若S减1后小于零,则该进程被阻塞后进入与该信号相对应的队列中,然后转进程调度。

  V原语操作的动作是:

   (1)S加1;

   (2)若相加结果大于零,则进程继续执行;

   (3)若相加结果小于或等于零,则从该信号的等待队列中唤醒一等待进程,然后再返回原进程继续执行或转进程调度。

   PV操作对于每一个进程来说,都只能进行一次,而且必须成对使用。在PV原语执行期间不允许有中断的发生。

PV原语通过操作信号量来处理进程间的同步与互斥的问题,其核心就是处理一段不可分割的程序。信号值0表示当前已没有空闲资源,而正数表示当前空闲资源的数量,负数的绝对值表示正在等待进入临界区的进程个数,信号量是由操作系统来维护的,用户进程只能通过初始化和两个标准原语(P、V原语)来访问。

1.1 定义

/*定义时初始化*/
DECLARE_MUTEX(name);                     //初始化信号值为1
DECLARE_MUTEX_LOCKED(name);      //初始化信号值为0

/*先定义后初始化*/
struct semaphore sem;                                              //定义
void init_MUTEX(struct semaphore *sem);               //初始化信号值为1
void init_MUTEX_LOCKED(struct semaphore *sem);//初始化信号值为0
void sema_init(struct semaphore *sem, int val);      //初始化信号值为val


1.2 操作函数

P函数为:
void down(struct semaphore *sem);                   /*当信号值小于等于0时导致睡眠,且不可杀死,当有线程释放资源后才被唤醒*/
int down_interruptible(struct semaphore *sem);/*和down相似,但操作被中断时,该函数会返回非零值,而调用者不会拥有该信号量。返回后需要检查返回值做出相应的响应。*/
int down_trylock(struct semaphore *sem);         /*永不休眠,若信号量在调用是不可获得,会返回非零值。*/

V函数为:
void up(struct semaphore *sem);                       /*任何拿到信号量的线程都必须调用一次(且只能一次)对up的调用而释放该信号量*/


1.3 读者/写者旗帜

有些情况下我们允许多个线程读只有一个线程写,因为写会使得资源状态改变影响读者,但读不会相互影响。例如三个人看同一本书,三个人可以同时一起看,A很快看完想翻页了,A必须等B和C看完才能改变资源的状态,如果A等的过程中又来一个人D想看,如果给D看那不又要等D看完这一页吗,A比较霸道不允许D看了,如果没人想翻页那么还有人想加入一起看是允许的。

初始化:
void init_rwsem(struct rw_semaphore *sem);

对被保护资源的只读存取接口:
void down_read(struct rw_semaphore *sem);                      //可能将调用进程置为不可中断的睡眠
int down_read_trylock(struct rw_semaphore *sem);            //如果读存取是不可用时不会等待; 如果被准予存取它返回非零, 否则是 0
void up_read(struct rw_semaphore *sem);                           //释放读存取

写入接口:
void down_write(struct rw_semaphore *sem);                     //可能将调用进程置为不可中断的睡眠,多个线程申请写时,只有一个允许写,其他线程等待他写完。且只有当全部读线程释放读存取时才会唤醒,在等待过程不允许先的读者加入。
int down_write_trylock(struct rw_semaphore *sem);           //如果写存取是不可用时不会等待; 如果被准予写存取它返回非零, 否则是 0
void up_write(struct rw_semaphore *sem);                         //释放写存取
void downgrade_write(struct rw_semaphore *sem);           //写者变成读者 相当于up_write释放写者后再变成读者down_read,最后需要用up_read来释放。


注意当有线程试图获取写存取时(还没开始写),也是不允许新的读者加入的也就是down_read会休眠.

二 互斥体

不能在中断上下文中使用,获取互斥体后只能再获取他的线程中释放它,the task may not exit without first unlocking the mutex. Also, kernel memory where the mutex resides mutex must not be freed with the mutex still locked.

定义时初始化
DEFINE_MUTEX(mutexname);
先定义后初始化
struct mutex mutexname;
mutex_init(&mutexname);

获取互斥体
void inline __sched mutex_lock(struct mutex *lock);                     //不可中断,加锁,如果加锁不成功,会阻塞当前进程
int __sched mutex_lock_interruptible(struct mutex *lock);            //加锁,如果加锁不成功,会阻塞当前进程
int __sched mutex_trylock(struct mutex *lock);                             //尝试加锁,会立即返回,不会阻塞进程  ,成功返回1 失败返回0

释放互斥体
void __sched mutex_unlock(struct mutex *lock);                           //解锁


三 完成量的用法

completion 是任务使用的一个轻量级机制: 允许一个线程告诉另一个线程工作已经完成. 为使用 completion, 你的代码必须包含 <linux/completion.h>

1. 定义
定义时初始化
DECLARE_COMPLETION(my_completion);
先定义后初始化
struct completion my_compeltion;
init_completion(&my_completion);
2. 等待完成量
void wait_for_completion(struct completion *c);
4. 唤醒完成量
void completion(struct completion *c);     //只唤醒一个等待的执行单元
void completion_all(struct completion *c);//唤醒全部等待的执行单元,需要重新初始化,否则wait_for_completion直接返回。
一个 completion 正常地是一个单发设备; 使用一次就放弃. 然而, 如果采取正确的措施重新使用 completion 结构是可能的. 如果没有使用 complete_all, 重新使用一个 completion 结构没有任何问题, 只要对于发出什么事件没有模糊. 如果你使用 complete_all, 然而, 你必须在重新使用前重新初始化 completion 结构. 宏定义:

INIT_COMPLETION(struct completion c);

典型应用如内核线程创建完子线程后,需要等待子线程某个动作执行完后主线程才能继续。

四 自旋锁

自旋锁可以确保在同时只有一个线程进入临界区,所要保护的临界资源访问时间比较短时,用自旋锁是非常方便的和高效的,自选锁有如下特点:

1. 内核代码持有一个自旋锁的任何时间, 抢占在相关处理器上被禁止.

2. 如果访问临界资源的时间较长,则选用信号量,否则选用自旋锁。

3. 保护临界区代码必须是原子的,自旋锁后进行调度、抢占以及在等待队列上睡眠都是非法的

4. 在中断处理函数中,只能使用自旋锁,可选择持有锁时禁止中断

与自旋锁相关的API主要有:

spinlock_t my_lock = SPIN_LOCK_UNLOCKED;/* 编译时初始化spinlock*/
void spin_lock_init(spinlock_t *lock);                /* 运行时初始化spinlock*/

获得自旋锁
spin_lock(lock)        该宏用于获得自旋锁lock,如果能够立即获得锁,它就马上返回,否则,它将自旋在那里,直到该自旋锁的保持者释放;
spin_trylock(lock)   该宏尝试获得自旋锁lock,如果能立即获得锁,它获得锁并返回真,否则立即返回假,实际上不再"在原地打转";
spin_unlock(lock)    释放自旋锁,它与spin_trylock或spin_lock配对使用;

/* 所有spinlock等待本质上是不可中断的,一旦调用spin_lock,在获得锁之前一直处于自旋状态*/
void spin_lock(spinlock_t *lock);     ;/* 获得spinlock*/
void spin_lock_irqsave(spinlock_t   ;/*lock, unsigned long flags);/* 获得spinlock,禁止本地cpu中断,保存中断标志于flags*/
void spin_lock_irq(spinlock_t *lock);/* 获得spinlock,禁止本地cpu中断*/
void spin_lock_bh(spinlock_t *lock);/* 获得spinlock,禁止软件中断,保持硬件中断打开*/

/* 以下是对应的锁释放函数*/
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);

/* 以下非阻塞自旋锁函数,成功获得,返回非零值;否则返回零*/
int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);


/*新内核的包含了更多函数*/

内核提供了一个自旋锁的读者/写者形式, 直接模仿我们在本章前面见到的读者/写者旗标. 这些锁允许任何数目的读者同时进入临界区, 但是写者必须是排他的存取.

读取者/写入者自旋锁:

rwlock_t my_rwlock = RW_LOCK_UNLOCKED;/* 编译时初始化*/
rwlock_t my_rwlock;
rwlock_init(&my_rwlock); /* 运行时初始化*/
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);

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

int write_trylock(rwlock_t *lock);


五 原子变量

完整的锁机制对一个简单的整数来讲显得浪费。内核提供了一种原子的整数类型,称为atomic_t,定义在。原子变量操作是非常快的, 因为它们在任何可能时编译成一条单个机器指令。

以下是其接口函数:

atomic_t v = ATOMIC_INIT(0);         /*编译时使用宏定义 ATOMIC_INIT 初始化原子值.*/
void atomic_set(atomic_t *v, int i); /*设置原子变量 v 为整数值 i.*/
int atomic_read(atomic_t *v);          /*返回 v 的当前值.*/
void atomic_add(int i, atomic_t *v);/*由 v 指向的原子变量加 i. 返回值是 void*/
void atomic_sub(int i, atomic_t *v); /*从 *v 减去 i.*/
void atomic_inc(atomic_t *v);
void atomic_dec(atomic_t *v);          /*递增或递减一个原子变量.*/
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, 那么返回值是真; 否则, 它是假. 注意没有 atomic_add_and_test.*/
int atomic_add_negative(int i, atomic_t *v);
/*加整数变量 i 到 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_add 和其类似函数, 除了它们返回原子变量的新值给调用者.atomic_t 数据项必须通过这些函数存取,需要多个 atomic_t 变量的操作仍然需要某种其他种类的加锁。

六 位变量

内核提供了一套函数来原子地修改或测试单个位。原子位操作非常快, 因为它们使用单个机器指令来进行操作。

void set_bit(nr, void *addr);       /*设置第 nr 位在 addr 指向的数据项中。*/
void clear_bit(nr, void *addr);    /*清除指定位在 addr 处的无符号长型数据.*/
void change_bit(nr, void *addr);/*翻转nr位.*/
int test_bit(nr, void *addr);         /*这个函数是唯一一个不需要是原子的位操作; 它简单地返回这个位的当前值.*/

/*以下原子操作如同前面列出的, 除了它们还返回这个位以前的值.*/
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: