Linux 设备驱动学习笔记 - 并发和竞争情况篇 - Note.2[信号量、互斥提、自旋锁、原子操作]
- LINUX DEVICE DRIVERS,3RD EDITION
- 里面很多地方都摘取自 Linux 内核源码
五、Unit5(并发和竞争情况)
5.2 并发和它的管理
SMP 系统能够同时在不同处理器上执行你的代码. 内核代码是可抢占的; 你的驱动代码可能在任何时间失去处理器,代替它的进程可能也在你的驱动中运行. 设备中断是能够导致你的代码并发执行的异步事件. 内核也提供各种延迟代码执行的机制, 例如 workqueue, tasklet, 以及定时器, 这些能够使你的代码在任何时间以一种与当前进程在做的事情无关的方式运行. 在现代的, 热插拔的世界中, 你的设备可能在你使用它们的时候轻易地消失.
这是资源共享的硬规则: 任何时候一个硬件或软件资源被超出一个单个执行线程共享, 并且可能存在一个线程看到那个资源的不一致时, 你必须明确地管理对那个资源的存取. 在上面的 scull 例子, 这个情况在进程 B 看来是不一致的; 不知道进程 A 已经为( 共享的 ) 设备分配了内存, 它做它自己的分配并且覆盖了 A 的工作. 在这个例子里, 我们必须控制对 scull 数据结构的存取. 我们需要安排, 这样代码或者看到内存已经分配了, 或者知道没有内存已经或者将要被其他人分配. 存取管理的常用技术是加锁或者互斥 --确保在任何时间只有一个执行线程可以操作一个共享资源.
5.3 信号量和互斥体
旗标在计算机科学中是一个被很好理解的概念. 在它的核心, 一个旗标是一个单个整型值,结合有一对函数, 典型地称为 P 和 V. 一个想进入临界区的进程将在相关旗标上调用 P;如果旗标的值大于零, 这个值递减 1 并且进程继续. 相反, 如果旗标的值是 0 ( 或更小 ), 进程必须等待直到别人释放旗标. 解锁一个旗标通过调用 V 完成; 这个函数递增旗标的值, 并且, 如果需要, 唤醒等待的进程.
信号量:(重点掌握) --> 进程中使用信号量 |–>当一个资源获得信号量时,另外一个资源等待其释放信号量后才可运行,中间过程不需要一直等待, 不一致占用cpu消耗cpu,特点如下: (1)可以保护大量的资源 (2)延时操作也可以正常在信号量保护的机制中进行运行 (3)信号量时工作在进程上下文的
信号量的使用流程:
(1)定义一个信号量
struct semaphore sem;
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
(2)初始化信号量
void sema_init(struct semaphore *sem, int val);
|-->struct semaphore *sem 初始化信号量的结构体
|-->int val : 信号量的初始值
(3)信号量加锁
void down(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
|-->struct semaphore *sem : 初始化信号量的结构体
|--> int : 返回值: 成功返回0 失败返回1
(4)信号量解锁
void up(struct semaphore *sem);
互斥体:(掌握) --> 进程中可用 特点如下: (1)可以保护大量的资源 (2)延时操作也可以正常在互斥体保护的机制中进行运行 (3)互斥体时工作在进程上下文的
互斥体的使用流程:
(1)定义一个互斥体
struct mutex lock
struct mutex {
atomic_t count;
spinlock_t wait_lock;
struct list_head wait_list;
#if defined(CONFIG_DEBUG_MUTEXES) || defined(CONFIG_SMP)
struct task_struct *owner;
#endif
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
void *spin_mlock;
#endif
#ifdef CONFIG_DEBUG_MUTEXES
const char *name;
void *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
};
(2)初始化互斥体
mutex_init(&lock);
(3)互斥体加锁
mutex_lock(&lock);
int mutex_trylock(struct mutex *lock);
|-->int : 返回值: 成功返回1 失败返回0
(4)互斥体解锁
mutex_unlock(&lock);
5.3.1 Linux 信号量实现
为使用旗标, 内核代码必须包含 <asm/semaphore.h>. 相关的类型是 struct semaphore; 实际旗标可以用几种方法来声明和初始化. 一种是直接创建一个旗标, 接着使用 sema_init 来设定它:
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 semaphore *sem);
在 Linux 世界中, P 函数称为 down – 或者这个名子的某个变体. 这里, “down” 指的是这样的事实, 这个函数递减旗标的值, 并且, 也许在使调用者睡眠一会儿来等待旗标变可用之后, 给予对被保护资源的存取. 有 3 个版本的 down:
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
down 递减旗标值并且等待需要的时间.
down_interruptible 同样, 但是操作是可中断的.这个可中断的版本几乎一直是你要的那个; 它允许一个在等待一个旗标的用户空间进程被用户中断. 作为一个通用的规则, 你不想使用不可中断的操作, 除非实在是没有选择. 不可中断操作是一个创建不可杀死的进程( 在 ps 中见到的可怕的 “D 状态” )和惹恼你的用户的好方法, 使用 down_interruptible 需要一些格外的小心, 但是, 如果操作是可中断的, 函数返回一个非零值, 并且调用者不持有旗标. 正确的使用 down_interruptible 需要一直检查返回值并且针对性地响应.
( down_trylock ) 从不睡眠; 如果旗标在调用时不可用, down_trylock 立刻返回一个非零值.
一旦一个线程已经成功调用 down 各个版本中的一个, 就说它持有着旗标(或者已经"取得"或者"获得"旗标). 这个线程现在有权力存取这个旗标保护的临界区. 当这个需要互斥的操作完成时, 旗标必须被返回. V 的 Linux 对应物是 up:
void up(struct semaphore *sem);
一旦 up 被调用, 调用者就不再拥有旗标.
如你所愿, 要求获取一个旗标的任何线程, 使用一个(且只能一个)对 up 的调用释放它. 在错误路径中常常需要特别的小心; 如果在持有一个旗标时遇到一个错误, 旗标必须在返回错误状态给调用者之前释放旗标. 没有释放旗标是容易犯的一个错误; 这个结果( 进程挂在看来无关的地方 )可能是难于重现和跟踪的.
5.3.2 在驱动中使用旗标
struct scull_dev {
struct scull_qset *data;
int quantum;
int qset;
unsigned long size;
unsigned int access_key;
struct semaphore sem;
struct cdev cdev;
};
该机构体底部是一个旗标,不同设备使用单独的旗标允许并行进行对不同设备的操作, 这样提高了性能.
5.3.3. 读者/写者旗标
旗标为所有调用者进行互斥, 不管每个线程可能想做什么. 然而, 很多任务分为 2 种清楚的类型: 只需要读取被保护的数据结构的类型, 和必须做改变的类型. 允许多个并发读者常常是可能的, 只要没有人试图做任何改变. 这样做能够显著提高性能; 只读的任务可以并行进行它们的工作而不必等待其他读者退出临界区.
Linux 内核为这种情况提供一个特殊的旗标类型称为 rwsem (或者" reader/writer semaphore"). rwsem 在驱动中的使用相对较少, 但是有时它们有用. 使用 rwsem 的代码必须包含 <linux/rwsem.h>. 读者写者旗标 的相关数据类型是struct rw_semaphore; 一个 rwsem 必须在运行时显式初始化:
void init_rwsem(struct rw_semaphore *sem);
一个新初始化的 rwsem 对出现的下一个任务( 读者或者写者 )是可用的. 对需要只读存取的代码的接口是:
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);
down_read 的调用提供了对被保护资源的只读存取, 与其他读者可能地并发地存取.注意 down_read 可能将调用进程置为不可中断的睡眠. down_read_trylock 如果读存取是不可用时不会等待;如果被准予存取它返回非零, 否则是 0. 注意 down_read_trylock 的惯例不同于大部分的内核函数, 返回值 0 指示成功. 一个使用 down_read 获取的 rwsem 必须最终使用 up_read 释放.
读者的接口类似:
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);
down_write, down_write_trylock, 和 up_write 全部就像它们的读者对应部分, 除了,当然, 它们提供写存取. 如果你处于这样的情况, 需要一个写者锁来做一个快速改变, 接着一个长时间的只读存取, 你可以使用 downgrade_write 在一旦你已完成改变后允许其他读者进入. 一个 rwsem 允许一个读者或者不限数目的读者来持有旗标. 写者有优先权; 当一个写者试图进入临界区, 就不会允许读者进入直到所有的写者完成了它们的工作. 这个实现可能导致读者饥饿 – 读者被长时间拒绝存取 – 如果你有大量的写者来竞争旗标. 由于这个原因, /rwsem 最好用在很少请求写的时候, 并且写者只占用短时间/.
5.4 Completions 机制
允许一个线程告诉另一个线程工作已完成.
创建:
DECLARE_COMPLETION(my_completion);
或者 completion 必须动态创建和初始化:
struct completion my_completion;
init_completion(&my_completion);
等待:
void wait_for_completion(struct completion *c);
|-->这是个不可打断的等待, 调用后如果没有人完成这个任务, 结果会是一个不可杀死的进程.
响应:
void complete(struct completion *c);
void complete_all(struct completion *c);
|-->complete 只唤醒一个等待的线程
|-->complete_all 允许所有等待都继续
Demo1:{
任何试图从一个设备读的进程将等待(使用wait_for_completion)直到其他进程向这个设备写. 实现这个行为的代码是:
DECLARE_COMPLETION(comp);
ssize_t complete_read (struct file *filp, char __user *buf, size_t count, loff_t *pos)
{
printk(KERN_DEBUG "process %i (%s) going to sleep\n",current->pid, current->comm);
wait_for_completion(&comp);
printk(KERN_DEBUG "awoken %i (%s)\n", current->pid, current->comm);
return 0;
}
ssize_t complete_write (struct file *filp, const char __user *buf, size_t count, loff_t *pos)
{
printk(KERN_DEBUG "process %i (%s) awakening the readers...\n", current->pid, current->comm);
complete(&comp);
return count;
}
}
completion 机制的典型使用是在模块退出时与内核线程的终止一起. 一些驱动的内部工作是通过一个内核线程在一个 while(1) 循环中进行的. 当模块准备好被清理时, exit 函数告知线程退出并且等待结束. 为此目的, 内核包含一个特殊的函数给线程使用:
void complete_and_exit(struct completion *c, long retval);
5.5 自旋锁
自旋锁可用在不能睡眠的代码中, 例如中断处理. 当正确地使用了, 通常自旋锁提供了比旗标更高的性能.
如果这个锁已经被别人获得, 代码进入一个紧凑的循环中反复检查这个锁, 直到它变为可用. 这个循环就是自旋锁的"自旋"部分.
这个"测试并置位"操作必须以原子方式进行, 以便只有一个线程能够获得锁.
5.5.1 自旋锁 API 简介
API: 1.定义锁
spinlock_t lock;
spin_lock_init(&lock);
spinlock_t my_lock = SPIN_LOCK_UNLOCKED;
2.上锁
void spin_lock(spinlock_t *lock)
3.解锁
void spin_unlock(spinlock_t *lock)
5.5.2 自旋锁和原子上下文
如果某个别的线程想获得同一个锁, 它会, 在最好的情况下, 等待( 在处理器中自旋 )很长时间. 最坏的情况, 系统可能完全死锁.
应用到自旋锁的核心规则是任何代码必须, 在持有自旋锁时, 是原子性的. 它不能睡眠; 事实上, 它不能因为任何原因放弃处理器,除了服务中断(并且有时即便此时也不行).
内核抢占的情况由自旋锁代码自己处理. 内核代码持有一个自旋锁的任何时间, 抢占在相关处理器上被禁止.
关于自旋锁使用的最后一个重要规则是自旋锁必须一直是尽可能短时间的持有.
5.5.3 自旋锁函数{
实际上有 4 个函数可以加锁一个自旋锁:
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_loc_irqsave 禁止中断(只在本地处理器)在获得自旋锁之前; 之前的中断状态保存在 flags 里. 你确信你应当在你释放你的自旋锁时打开中断, 你可以使用 spin_lock_irq 代替, 并且不必保持跟踪 flags. 最后, spin_lock_bh 在获取锁之前禁止软件中断, 但是硬件中断留作打开的. 如果你有一个可能被在(硬件或软件)中断上下文运行的代码获得的自旋锁, 你必须使用一种 spin_lock 形式来禁止中断. 其他做法可能死锁系统, 迟早. 如果你不在硬件中断处理里存取你的锁, 但是你通过软件中断, 你可以使用 spin_lock_bh 来安全地避免死锁, 而仍然允许硬件中断被服务.
//也有 4 个方法来释放一个自旋锁; 你用的那个必须对应你用来获取锁的函数. 每个 spin_unlock 变体恢复由对应的 spin_lock 函数锁做的工作. 传递给 spin_unlock_irqrestore 的 flags 参数必须是传递给 spin_lock_irqsave 的同一个变量. 你必须也调用 spin_lock_irqsave 和 spin_unlock_irqrestore 在同一个函数里. 否则, 你的代码可能破坏某些体系.
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);
还有一套非阻塞的自旋锁操作: 这些函数成功时返回非零( 获得了锁 ), 否则 0. 没有"try"版本来禁止中断.
int spin_trylock(spinlock_t *lock);
int spin_trylock_bh(spinlock_t *lock);
5.5.4 读者/写者自旋锁{
一个自旋锁的读者/写者形式, 这些锁允许任何数目的读者同时进入临界区, 但是写者必须是排他的存取. 读者写者锁有一个类型 rwlock_t, 在 <linux/spinlokc.h> 中定义. 它们可以以 2 种方式被声明和被初始化:
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);
没有 read_trylock. 对于写存取的函数是类似的:
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);
读者/写者锁能够饿坏读者, 就像 rwsem 一样. 这个行为很少是一个问题; 然而, 如果有足够的锁竞争来引起饥饿, 性能无论如何都不行.
5.7 加锁的各种选择
5.7.1 不加锁算法
一个环形缓存在没有多个生产者或消费者时不需要加锁. 生产者是唯一允许修改写索引和它所指向的数组位置的线程. 只要写者在更新写索引之前存储一个新值到缓存中, 读者将一直看到一个一致的视图. 读者, 轮换地, 是唯一存取读索引和它指向的值的线程. 加一点小心到确保 2 个指针不相互覆盖, 生产者和消费者可以并发存取缓存而没有竞争情况.
5.7.2 原子变量, 原子操作
原子操作:(掌握) |–>就是将一个变量放入的内联汇编程序中,在改程序中的变量,必须在内存一次执行完成,不可打断,从而保护变量不被调度 (1)不可中断,必须一次性执行完成 (2)atomic的原理就是对比0值,通过加1或者减1和0进行对比,如果加1减1后等于0表示加锁成功
原子操作的使用流程
(1)定义一个原子操作变量
atomic_t atm;
typedef struct {
int counter;
} atomic_t;
(2)初始化原子操作变量
atm = ATOMIC_INIT(1);
atm = ATOMIC_INIT(-1);
(3)原子操作加锁
atomic_inc_and_test(v)
|-->#define atomic_inc_and_test(v) (atomic_add_return(1, v) == 0)
|-->内联汇编程序的格式:将如下程序在内存中一次性执行完成,不可打断,保证变量不可被竞争调度
__asm__ __volatile__ (
" .align 2 \n\t"
" mova 1f, r0 \n\t"
" mov r15, r1 \n\t"
" mov #-6, r15 \n\t"
" mov.l @%1, %0 \n\t"
" add %2, %0 \n\t"
" mov.l %0, @%1 \n\t"
"1: mov r1, r15 \n\t"
: "=&r" (tmp),
"+r" (v)
: "r" (i)
: "memory" , "r0", "r1");
atomic_dec_and_test(v)
|--#define atomic_dec_and_test(v) (atomic_sub_return(1, v) == 0)
(4)原子操作解锁
atomic_dec(v)
atomic_inc(v)
下面的操作为这个类型定义并且保证对于一个 SMP 计算机的所有处理器来说是原子的. 操作是非常快的, 因为它们在任何可能时编译成一条单个机器指令.
void atomic_set(atomic_t *v, int i);
atomic_t v = ATOMIC_INIT(0);
设置原子变量 v 为整数值 i. 你也可在编译时使用宏定义 ATOMIC_INIT 初始化原子值.
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 变量的操作仍然需要某种其他种类的加锁. 考虑一下下面的代码:
atomic_sub(amount, &first_atomic);
atomic_add(amount, &second_atomic);
从第一个原子值中减去 amount, 但是还没有加到第二个时, 存在一段时间. 如果事情的这个状态可能产生麻烦给可能在这 2 个操作之间运行的代码, 某种加锁必须采用.
5.7.3 原子位操作
{
static inline void set_bit(int nr, unsigned long *addr)
{
addr[nr / BITS_PER_LONG] |= 1UL << (nr % BITS_PER_LONG);
}
static inline void clear_bit(int nr, unsigned long *addr)
{
addr[nr / BITS_PER_LONG] &= ~(1UL << (nr % BITS_PER_LONG));
}
static __always_inline int test_bit(unsigned int nr, const unsigned long *addr)
{
return ((1UL << (nr % BITS_PER_LONG)) &
(((unsigned long *)addr)[nr / BITS_PER_LONG])) != 0;
}
}
原子位操作非常快, 因为它们使用单个机器指令来进行操作, 而在任何时候低层平台做的时候不用禁止中断. 函数是体系依赖的并且在 <asm/bitops.h> 中声明. 它们保证是原子的, 即便在 SMP 计算机上, 并且对于跨处理器保持一致是有用的. nr 参数(描述要操作哪个位)常常定义为 int, 但是在几个体系中是 unsigned long. 要修改的地址常常是一个 unsigned long 指针, 但是几个体系使用 void * 代替.
各种位操作是:
void set_bit(nr, void *addr);
void clear_bit(nr, void *addr);
void change_bit(nr, void *addr);
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);
while (test_and_set_bit(nr, addr) != 0) {
wait_for_a_while();
}
if (test_and_clear_bit(nr, addr) == 0) {
something_went_wrong();
}
5.7.4 seqlock 锁
seqlock 在这种情况下工作, 要保护的资源小, 简单, 并且常常被存取, 并且很少写存取但是必须要快. 它们通过允许读者释放对资源的存取, 但是要求这些读者来检查与写者的冲突而工作, 并且当发生这样的冲突时, 重试它们的存取. seqlock 通常不能用在保护包含指针的数据结构, 因为读者可能跟随着一个无效指针而写者在改变数据结构. seqlock 定义在 <linux/seqlock.h>. 有 2 个通常的方法来初始化一个 seqlock( 有 seqlock_t 类型 ):
seqlock_t lock1 = SEQLOCK_UNLOCKED;
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.7.5. 读取-拷贝-更新
{
static inline void rcu_read_lock(void)
{
__rcu_read_lock();
__acquire(RCU);
rcu_lock_acquire(&rcu_lock_map);
rcu_lockdep_assert(rcu_is_watching(),
"rcu_read_lock() used illegally while idle");
}
static inline void rcu_read_unlock(void)
{
rcu_lockdep_assert(rcu_is_watching(),
"rcu_read_unlock() used illegally while idle");
rcu_lock_release(&rcu_lock_map);
__release(RCU);
__rcu_read_unlock();
}
static inline void rcu_read_lock_bh(void)
{
local_bh_disable();
__acquire(RCU_BH);
rcu_lock_acquire(&rcu_bh_lock_map);
rcu_lockdep_assert(rcu_is_watching(),
"rcu_read_lock_bh() used illegally while idle");
}
static inline void rcu_read_unlock_bh(void)
{
rcu_lockdep_assert(rcu_is_watching(),
"rcu_read_unlock_bh() used illegally while idle");
rcu_lock_release(&rcu_bh_lock_map);
__release(RCU_BH);
local_bh_enable();
}
}
读取-拷贝-更新(RCU) 是一个高级的互斥方法, 能够有高效率在合适的情况下.RCU 算法的完整细节的人可以在由它的创建者出版的白皮书中找到 ( http:/www.rdrop.com/users/paulmck/rclock/intro/rclock_intro.html/).
RCU 对它所保护的数据结构设置了不少限制. 它对经常读而极少写的情况做了优化. 被保护的资源应当通过指针来存取, 并且所有对这些资源的引用必须由原子代码持有. 当数据结构需要改变, 写线程做一个拷贝, 改变这个拷贝, 接着使相关的指针对准新的版本 – 因此, 有了算法的名子. 当内核确认没有留下对旧版本的引用, 它可以被释放.
作为在真实世界中使用 RCU 的例子, 考虑一下网络路由表. 每个外出的报文需要请求检查路由表来决定应当使用哪个接口. 这个检查是快速的, 并且, 一旦内核发现了目标接口, 它不再需要路由表入口项. RCU 允许路由查找在没有锁的情况下进行, 具有相当多的性能好处. 内核中的 Startmode 无线 IP 驱动也使用 RCU 来跟踪它的设备列表.
使用 RCU 的代码应当包含 <linux/rcupdate.h>. 在读这一边, 使用一个 RCU-保护的数据结构的代码应当用 rcu_read_lock 和 rcu_read_unlock 调用将它的引用包含起来. 结果就是, RCU 代码往往是象这样:
struct my_stuff *stuff;
rcu_read_lock();
stuff = find_the_stuff(args...);
do_something_with(stuff);
rcu_read_unlock();
rcu_read_lock 调用是快的; 它禁止内核抢占但是没有等待任何东西. 在读"锁"被持有时执行的代码必须是原子的. 在对 rcu_read_unlock 调用后, 没有使用对被保护的资源的引用.
需要改变被保护的结构的代码必须进行几个步骤. 第一步是容易的; 它分配一个新结构, 如果需要就从旧的拷贝数据, 接着替换读代码所看到的指针. 在此, 对于读一边的目的, 改变结束了. 任何进入临界区的代码看到数据的新版本.
剩下的是释放旧版本. 当然, 问题是在其他处理器上运行的代码可能仍然有对旧数据的一个引用, 因此它不能立刻释放. 相反, 写代码必须等待直到它知道没有这样的引用存在了. 因为所有持有对这个数据结构引用的代码必须(规则规定)是原子的, 一旦系统中的每个处理器已经被调度了至少一次, 所有的引用必须消失.
改变一个 RCU-保护的数据结构的代码必须通过分配一个 struct rcu_head 来获得它的清理回调, 尽管不需要以任何方式初始化这个结构. 那个结构被简单地嵌入在 RCU 所保护的大的资源里面. 在改变资源完成后, 应当调用:
void call_rcu(struct rcu_head *head, void (*func)(void *arg), void *arg);
|