当信号量用于互斥时(即避免多个进程同是在一个临界区运行),信号量的值应初始化为1。这种信号量在任何给定时刻只能由单个进程或线程拥有。在这种使用模式下,一个信号量有事也称为一个“互斥体(mutex)”,它是互斥(mutual exclusion)的简称。Linux内核中几乎所有的信号量均用于互斥。
使用信号量,内核代码必须包含。直接创建信号量:
void sema_init(struct semaphore *sem, int val); // val赋予信号量初始值
|
信号量通常被用于互斥模式,初始化方式:
1、用于声明和初始化 互斥体 的宏 DECLARE_MUTEX(name); DECLARE_MUTEX_LOCKED(name);
2、在运行时初始化互斥体
void init_MUTEX(struct semaphore *sem); void init_MUTEX_LOCKED(struct semaphore *sem); /*带有“_LOCKED”的是将信号量初始化为0,其初始状态是锁定的,允许任何线程访问之前显式解锁该互斥体。没带的初始化为1。*/
|
锁定和解锁信号量:
void down(struct semaphore *sem); int down_interruptible(struct semaphore *sem); int down_trylock(struct semaphore *sem);
void up(struct semaphore *sem);
|
down会将调用进程置于不可中断的休眠状态。但是是建立不可杀进程的好方法。通常不使用该方法;
down_interruptible可被信号中断。
需要格外小心,若操作被中断,该函数会返回非零值,而调用这不会拥有该信号量。对其
正确使用需要始终检查返回值,并做出相应的响应;
down_trylock不会休眠,且会在信号量不可用时立即返回;
up即解锁,
任何拿到信号量的线程都必须通过一次(只有一次)对up的调用而释放该信号量。在出错时,要特别小心;若在拥有一个信号量时发生错误,必须在将错误状态返回前释放信号量。
读取者/写入者信号量
一些任务只需要读取受保护的数据结构,而其他的则须做出修改。允许多个并发的读取者是可能的,只是它们之中没有哪个要做修改。这样做可大大提高性能,因为只读任务可并行完成它们的工作,而不需要等待其他读取者退出临界区。Linux内核为这种情形提供了一种特殊的信号量类型,称读取者/写入者信号量“rwsem”,使用时必须包括 。读取者/写入者信号量的数据类型是struct rw_semaphore。
初始化:
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); /* 释放rwsem */
|
写入接口:
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);
|
对于downgrade_write,
该函数用于把写者降级为读者,这有时是必要的。因为写者是排他性的,因此在写者保持读写信号量期间,任何读者或写者都将无法访问该读写信号量保护的共享资源,对于那些当前条件下不需要写访问的写者,降级为读者,使得等待访问的读者能够立刻访问,从而增加了并发性,提高了效率。
一个 rwsem 允许一个写者或无限多个读者来拥有该信号量. 写者有优先权; 当某个写者试图进入临界区,
就不会允许读者进入直到写者完成了它的工作. 如果有大量的写者竞争该信号量,则这个实现可能导致读者“饿死”,即可能会长期拒绝读者访问。因此,
rwsem 最好用在很少请求写的时候, 并且写者只占用短时间.
二、completion
completion是一种轻量级的机制,它允许一个线程告诉另一个线程某个工作已经完成。代码必须包含
。
创建completion:
DECLARE_COMPLETION(my_completion); |
或者动态创建和初始化completion:
struct completion my_completion;
static inline void init_completion(&my_completion); |
等待completion:
void wait_for_completion(struct completion *c); |
触发completion事件:
void complete(struct completion *c); /* 唤醒一个等待completion的线程 */ void complete_all(struct completion *c); /* 唤醒所有等待completion的线程 */
|
如果未使用completion_all,completion可重复使用;否则必须使用下面宏对其重新初始化:
INIT_COMPLETION(struct completion c);/ |
completion的典型使用是模块退出时的内核线程终止。在这种远行中,某些驱动程序的内部工作有一个内核线程在while(1)循环中
完成。当内核准备清楚该模块时,exit函数会告诉该线程退出并等待completion。为此内核包含了用于这种线程的一个特殊函数:
void complete_and_exit(struct completion *c, long retval);
|
三、自旋锁
自旋锁是一个互斥设备,它只能有两个值:“锁定”和“解锁”。它通常实现为某个整数之中的单个位。如果锁可用,则“锁定”位被设置,而代码继续进入临界区;相反,如果锁被其他人获得,刚代码进入忙循环,直到锁可用为止。
适用于自旋锁的核心规则:
(1)任何拥有自旋锁的代码都必须使原子的,除服务中断外(某些情况下也不能放弃CPU,如中断服务也要获得自旋锁。为了避免这种锁陷阱,需要在拥有自旋锁时禁止中断),不能放弃CPU(如休眠,休眠可发生在许多无法预期的地方)。否则CPU将有可能永远自旋下去(死机)。
(2)拥有自旋锁的时间越短越好。
自旋锁原语所需包含的文件是 ,以下是自旋锁的内核API:
初始化spinlock:
spinlock_t my_lock = SPIN_LOCK_UNLOCKED; /* 编译时初始化spinlock */ void spin_lock_init(spinlock_t *lock); /* 运行时初始化spinlock */ /* 所有spinlock等待本质上是不可中断的,一旦调用spin_lock,在获得锁之前一直处于自旋状态*/
获得spinlock:
void spin_lock(spinlock_t *lock); /* 获得spinlock */ void spin_lock_irqsave(spinlock_t *lock, unsigned long flags); /* 在获得自旋锁之前禁止中断,先前的中断状态保存在flags中 */ void spin_lock_irq(spinlock_t *lock); /* 能确保释放自旋锁时应该启用中断 */ 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);
/* 以上两个函数在成功获得自旋锁时,返回非零值;否则返回零*/
|
读取者/写入者自旋锁:
内核提供提供自旋锁的读取者/写入者形式,与rwsem十分相似。这种锁允许任意数量的读取者同时进入临界区,但写入者必须互斥访问。其数据类型rwlock_t,在中定义。
声明和初始化:
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); int write_trylock(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);
|
总结自旋锁和信号量:
自旋锁和信号量"类似而不类",类似说的是他们功能上的相似性,"不类"指代他们在本质和实现机理上完全不相同,不属于一类。
"不类":自旋锁不会引起调用者睡眠,假如自旋锁已被别的执行单元保持,调用者就一直循环查看是否该自旋锁的保持者已释放了锁,"自旋"就是"在原地打转"。而信号量则引起调用者睡眠,他把进程从运行队列上拖出去,除非获得锁。
"类似":无论是信号量,还是自旋锁,在任何时刻,最多只能有一个保持者,即在任何时刻最多只能有一个执行单元获得锁。
鉴于自旋锁和信号量的上述特点,一般而言,自旋锁适合于保持时间很短的情况,它能够在任何上下文使用;信号量适合于保持时间较长的情况,会只能在进程上下文使用。假如被保护的共享资源只在进程上下文访问,则能够以信号量来保护该共享资源,假如对共享资源的访问时间很短,自旋锁也是好的选择。但是,假如被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。
四、锁陷阱
锁定模式必须在一开始就安排好,否则其后的改进将会非常困难。
不明确规则:如果某个获得锁的函数要调用其他同样试图获取这个锁的函数,代码就会锁死。(不允许锁的拥有者第二次获得同个锁。)为了锁的正确工作,不得不编写一些函数,这些函数假定调用这已经获得了相关的锁。
锁的顺序规则:再必须获取多个锁时,应始终以相同顺序获取。
(1) 若必须获得一个局部锁和一个属于内核更中心位置的锁,应先获得局部锁。
(2) 若我们拥有信号量和自旋锁的组合,必须先获得信号量。
(3) 不得再拥有自旋锁时调用down。(可导致休眠)
(4) 尽量避免需要多个锁的情况。
细颗粒度和粗颗粒度的对比:应该在最初使用粗颗粒度的锁,除非有真正的原因相信竞争会导致问题。
五、锁之外的办法
1、免锁算法
经常用于免锁的生产者/消费者任务的数据结构之一是循环缓冲区。它在设备驱动程序中相当普遍,如以前移植的网卡驱动程序。内核里有一个通用的循环缓冲区的实现在 。
2、原子变量
完整的锁机制对一个简单的整数来讲显得浪费。内核提供了一种原子的整数类型,称为atomic_t,定义在。原子变量操作是非常快的, 因为它们在任何可能时编译成一条单个机器指令。以下是其接口函数:
void atomic_set(atomic_t *v, int i); /* 设置原子变量 v 为整数值 i */ atomic_t v = ATOMIC_INIT(0); /* 编译时利用ATOMIC_INIT 宏 初始化原子值 */
int atomic_read(atomic_t *v); /* 返回 v 的当前值 */
void atomic_add(int i, atomic_t *v); /* 将i累加到V指向的原子变量,返回值是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, 那么返回值为true; 否则返回false. 注意不存在atomic_add_and_test函数 */
int atomic_add_negative(int i, atomic_t *v); /* 加整数变量i到v。 如果结果是负值返回值为 true; 否则返回false */
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 和其类似函数, 除此之外,它们会将新值返回给调用者 */
|
记住:
(1)atomic_t 数据项必须通过上述函数访问。 如果将原子变量传递给一个需要整数参数的函数,则会遇到编译错误。
(2)只有原子变量的数目是原子的,atomic_t 变量才能工作。需要多个atomic_t变量的操作仍然需要某种其他种类的加锁。
3、位操作
内核提供了一套可原子地修改或测试单个位的函数。原子位操作非常快, 因为它们使用单个机器指令来进行操作,且不需要禁止中断。这些函数依赖于具体的架构,在 中声明。同时这些函数数据类型也是依赖于具体架构的。 nr参数(描述要操作哪个位)在ARM体系中定义为unsigned int:
void set_bit(nr, void *addr); /* 设置 addr指向的数据项的 第nr位 */ void clear_bit(nr, void *addr); /* 清除 addr指向的数据项的 第nr位 */
void change_bit(nr, void *addr); /* 翻转nr位 */ 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); /* 像前面列出的函数一样具有原子化行为,除此之外,它们还返回这个位以前的值 */
|
4、seqlock
2.6内核包含了两个新机制,可提供对共享资源的快速、免锁访问。当要保护的资源很小、很简单、会频繁被访问、写入访问很少发生且必须快速时,就可以使用seqlock。seqlock允许读取者对资源的自由访问,但需要读取者检查和写入者发生冲突。seqlock通常不能用于包含有指针的数据结构。seqlock数据类型为seqlock_t,定义在 中。
初始化两种方法:
1) seqlock_t lock1 = SEQLOCK_UNLOCKED; 2) seqlock_t lock2; seqlock_init(&lock2);
|
读取访问通过在进入临界区入口获取一个(无符号的)整数序列来工作。 在退出时, 那个序列值与当前值比较; 如果不匹配, 读读取访问必须重试。读者代码形式如下:
unsigned int seq; do { seq = read_seqbegin(&the_lock); /* 完成需要做的工作 */ } while read_seqretry(&the_lock, seq);
|
如果在一个中断处理中访问seqlock, 你应当使用IRQ安全的版本:
unsigned int read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags); int read_seqretry_irqrestore(seqlock_t *lock, unsigned int seq, unsigned long flags);
|
写入者必须进入由seqlock保护的临界时获取一个互斥锁,需要调用下面的函数:
void write_seqlock(seqlock_t *lock); void write_sequnlock(seqlock_t *lock);
|
因为自旋锁用来控制写入访问, 所有通常的变体都可用:
void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags); void write_seqlock_irq(seqlock_t *lock); void write_seqlock_bh(seqlock_t *lock);
void write_sequnlock_irqrestore(seqlock_t *lock, unsigned long flags); void write_sequnlock_irq(seqlock_t *lock); void write_sequnlock_bh(seqlock_t *lock);
|
还有一个write_tryseqlock在它能够获得锁时返回非零.
5、读取-复制-更新
读取-复制-更新(RCU) 也是一个高级的互斥方法, 在合适的情况下也可获得高性能。它在驱动中的使用很少。
RCU对它保护的数据做了一些限定。它针对经常发生读取而很少写入的情形做了优化。被保护的资源应该通过指针访问。而对这些资源的引用必须仅由原子代码拥有。在需要修改该数据结构时,写入线程首先提制,然后修改复本,之后用新的版本替代相关指针,这也是该算法名称的由来。
在读取端,代码使用受RCU保护的数据结构时,必须将引用数据结构的代码包括在rcu_read_lock和rcu_read_unlock调用之间。例如:
struct my_stuff *stuff;
rcu_read_lock(); stuff = find_the_stuff(args...); do_something_with(stuff); rcu_read_unlock();
|