scull的缺陷
if (!dptr->data[s_pos]) {
dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL);
if (!dptr->data[s_pos])
goto out;
}
sucll代码中当有两个进程A、B正在独立尝试向同一个scull设备的相同偏移量写入数据,两个进程在同一时刻到达上述代码的第一个if处。执行接下来的kmalloc操作。当A先执行完,B后执行完时,B的结果会刷新A的结果,最终将A的结果覆盖。导致A分配的内存丢失。
这种情况就是竞态。竞态会导致对共享数据的非控制访问。
并发及其管理
内核代码是可抢占的,所以驱动程序代码可能在任何时候丢失对处理器的独占,而拥有处理器的进程可能正在调用我们的驱动程序代码。
设备中断时异步事件,也会导致代码的并发执行。
内核也会提供很多可延迟机制,workqueue、tasklet、time等,这些机制会使代码可在任何时刻执行,而不管当前进程在做什么。
竞态通常作为对资源的共享访问结果而产生。
在自己设计驱动程序时,应避免对资源的共享,尽量避免使用全局变量。
但是,资源的共享不可避免。硬件资源本质上就是共享的,而软件资源经常需要对其他执行线程可用。
全局变量并不是唯一的共享数据途径,只要我们将一个指针传递给内核的其他部分,一个新的共享就建立了。
资源共享的硬件规则:
1.在单个执行线程之外共享硬件或软件资源的时候,因为另一个线程可能对该资源的不一致观察,因此必须显式地管理对该资源的访问。
访问管理的常见技术称为”锁定“或者”互斥“——确保一次只有一个执行线程可操作共享资源。
2.当内核代码创建了一个可能和其他内核部分共享的对象时,该对象必须在还有其他组件引用自己时保持存在(并正确工作)。
信号量和互斥体
对上面提到的scull内存泄漏示例而言,我们需要对代码建立临界区:确保在任意给定时刻,代码只能被一个线程执行。
一个信号量(semaphore: 旗语,信号灯)本质上是一个整数值,它和一对函数联合使用,这一对函数通常称为P和V。希望进入临界区的进程将在相关信号量上调用P;如果信号量的值大于零,则该值会减小一,而进程可以继续。相反,如果信号量的值为零(或更小),进程必须等待知道其他人释放该信号。对信号量的解锁通过调用V完成;该函数增加信号量的值,并在必要时唤醒等待的进程。
当信号量用于互斥时(即避免多个进程同时在一个临界区运行),信号量的值应初始化为1。这种信号量在任何给定时刻只能由单个进程或线程拥有。在这种使用模式下,一个信号量有时也称为一个“互斥体(mutex)”,它是互斥(mutual exclusion)的简称。
Linux内核中几乎所有的信号量均用于互斥。
Linux信号量的实现
使用信号量,内核代码必须包含<asm/semaphore.h> 。
信号量的创建和初始化:
1.创建信号量,通过sema_init完成:
void sema_init(struct semaphore *sem,int val);
val是赋给信号量一个初值。
2.信号量的声明和初始化:
DECLARE_MUTEX(name);
DECLARE_MUTEX_LOCKED(name);
宏的作用是一个称为name的信号量变量被初始化为1(使用DECLARE_MUTEX)或者0(DECLARE_MUTEX_LOCKED)。
初始化为0 时,在允许任何线程访问之前,必须显示解锁该互斥体。
void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem);
这两个宏表明在运行时被初始化(例如动态分配互斥体的状态下)。
P函数被称为down。
“down”减小信号量的值,也许会将调用者置于休眠状态,然后等待信号量变得可用,之后授予调用者对被保护资源的访问。
down的三个版本:
void down(struct semaphore*sem);/*不推荐使用,会建立不可杀进程*/
int down_interruptible(struct semaphore*sem);/*推荐使用,使用down_interruptible需要格外小心,若操作被中断,该函数会返回非零值,而调用这不会拥有该信号量。对down_interruptible的正确使用需要始终检查返回值,并做出相应的响应。*/
int down_trylock(struct semaphore*sem);/*带有“_trylock”的永不休眠,若信号量在调用是不可获得,会返回非零值。*/
V函数等价于up。
void up(struct semaphore*sem);/*任何拿到信号量的线程都必须通过一次(只有一次)对up的调用而释放该信号量。在出错时,要特别小心;若在拥有一个信号量时发生错误,必须在将错误状态返回前释放信号量。*/
在scull中使用信号量
正确使用锁的关键是明确指定需要保护的资源,并使用正确的锁来访问这些资源。
scull中scul.h确定了锁的范围:
struct scull_dev {
struct scull_qset *data; /* Pointer to first quantum set */
int quantum; /* the current quantum size */
int qset; /* the current array size */
unsigned long size; /* amount of data stored here */
unsigned int access_key; /* used by sculluid and scullpriv */
struct semaphore sem; /* mutual exclusion semaphore */
struct cdev cdev; /* Char device structure */
};
信号量在使用前必须初始化。sucll在装载时进行初始化:
/* Initialize each device. */
for (i = 0; i < scull_nr_devs; i++) {
scull_devices[i].quantum = scull_quantum;
scull_devices[i].qset = scull_qset;
init_MUTEX(&scull_devices[i].sem);
scull_setup_cdev(&scull_devices[i], i);
}
信号量必须在scull设备对系统其他部分可用前被初始化。
因此在scull_setup_cdev之前调用init_MUTEX。以相反的顺序执行上述操作会引起一个竞态,即在信号量准备好之前,有代码可能访问它们。
对信号量的操作最后都必须释放信号量。在不拥有信号量的时候坚决不会访问scull_dev结构体。
读取者/写入者信号量
读写信号量适用于允许多个读者并发的读,但是只允许一个线程执行写操作。
读写信号量必须包含<linux/rwsem.h>,读写信号量的数据类型是struct rw_semaphore。
一个rwsem对象必须在运行时显示地初始化:
void init_rwsem(struct rw_semaphore *sem);
对于只读访问:
void down_read(struct rw_semaphore *sem);
/*提供对保护资源的只读访问内,可与其他读者并发地访问。可能会将进程置于不可中断的休眠*/
int down_read_trylock(struct ew_semaphore *sem);
/*不会在读取访问不可获得时等待;在授予访问时返回非零,其他情况下返回零。与其他函数不同,其他函数在成功时返回零*/
void up_read(struct rw_semaphore *sem);
/*down_read获得的rwsem对象最终必须通过up_read被释放*/
对于写访问:
void down_write(structrw_semaphore *sem);
int down_write_trylock(struct rw_semphore *sem);
void up_write(struct rw_semaphore *sem);
void downgrade_write(struct rw_semaphore *sem);
一个resem可允许一个写入者或者无限多个读取者拥有该信号量。
写入者拥有更高的优先级;当某个给定写入者试图进入临界区时,在所有写入者完成其工作之前,不会允许读取者访问。如果大量写会导致读者“饿死”,即可能会长期拒绝读者的访问。
completion
completion是一种轻量级的机制,它允许一个线程告诉另一个线程某个工作已经完成。
代码必须包含<linux/completion.h>
创建completion例程:
DECLARE_COMPLETION(my_completion);/* 创建completion(声明+初始化) */
动态创建和初始化completion:
struct completion my_completion;/* 动态声明completion 结构体*/
static inline void init_completion(&my_completion);/*动态初始化completion*/
等待completion:
void wait_for_completion(struct completion*c);/* 等待completion */
wait_for_completion执行一个非中断的等待。
如果代码调用了wait_for_completion且没有人会完成该任务,则将会产生一个不可杀的进程。
下列函数可触发completion事件:
void complete(struct completion*c);/*唤醒一个等待completion的线程*/
void complete_all(struct completion*c);/*唤醒所有等待completion的线程*/
如果未使用completion_all,completion可重复使用;否则必须使用以下函数重新初始化completion:
INIT_COMPLETION(struct completion c);/*快速重新初始化completion*/
completion的典型应用是模块退出时的内核线程终止。在这种运行中,某些驱动程序的内部工作有一个内核线程在while(1)循环中完成。当内核准备清除该模块时,exit函数会告诉该线程退出并等待completion。为此内核包含了用于这种线程的一个特殊函数:
void complete_and_exit(struct completion*c,long retval);
自旋锁
自旋锁可在不能休眠的代码中使用。
自旋锁是一个互斥设备,他只能会两个值:“锁定”和“解锁”。它通常实现为某个整数之中的单个位。
“测试并设置”的操作必须以原子方式完成。
自旋锁API
自旋锁原语所需包含的文件是<linux/spinlock.h>
对自旋锁的初始化可在编译时通过下面代码完成:
spinlock_t my_lock= SPIN_LOCK_UNLOCKED;/* 编译时初始化spinlock*/
或者在运行时,调用spin_lock_init:
void spin_lock_init(spinlock_t*lock);/* 运行时初始化spinlock*/
在进入临界区前,获得锁:
void spin_lock(spinlock_t*lock);/* 获得spinlock*/
所有spinlock等待本质上是不可中断的,一旦调用spin_lock,在获得锁之前一直处于自旋状态。
释放锁:
void spin_unlock(spinlock_t*lock);
自旋锁和原子上下文
任何拥有自旋锁的代码都是原子的,不能休眠,禁止中断。
任何时候,只要内核代码拥有自旋锁,在相关CPU上的抢占就会被禁止。
拥有自旋锁的时间尽可能的短。
自旋锁函数
#<linux/spinlock.h>
锁定自旋锁:
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类型,在<linux/spinlock.h>中定义。
声明和初始化:
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);
/* 新内核已经有了read_trylock*/
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);
陷阱锁
不明确规则
锁定模式必须在一开始就安排好,否则其后的改进将会非常困难。
不论是信号量还是自旋锁,都不允许锁者第二次获得这个锁。
锁的顺序规则
在必须获取多个锁时,应始终以相同顺序获取。
若必须获得一个局部锁和一个属于内核更中心位置的锁,应先获得局部锁。
若我们拥有信号量和自旋锁的组合,必须先获得信号量。
不得在拥有自旋锁时调用down。(可导致休眠)
尽量避免需要多个锁的情况。
细颗粒度和粗颗粒度的对比
应该在最初使用粗颗粒度的锁,除非有真正的原因相信竞争会导致问题。
除了锁之外的办法
免锁算法
经常用于免锁的生产者/消费者任务的数据结构之一是循环缓冲区。
它在设备驱动程序中相当普遍,如以前移植的网卡驱动程序。内核里有一个通用的循环缓冲区的实现在 <linux/kfifo.h> 。
原子变量
内核提供了一种原子的整数类型,称为atomic_t,定义在<asm/atomic.h>。
原子变量操作是非常快的, 因为它们在任何可能时编译成一条单个机器指令。
一个atomic_t变量在所有内核支持的架构上保存一个int值。
支持函数:
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);/*由 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 atomic_add(amount,&first_atomic);
void atomic_sub(amount,&secondt_atomic);
amount已经在第一个原子值加上,到还没减小到第二个原子之间,会有一小段时间。在这两段操作之间的代码须使用锁。
位操作
内核提供了一套函数来原子地修改或测试单个位。原子位操作非常快, 因为它们使用单个机器指令来进行操作, 而在任何时候低层平台做的时候不用禁止中断。
函数是体系依赖的并且在 <asm/bitops.h> 中声明。
以下函数中的数据是体系依赖的.nr 参数(描述要操作哪个位)在ARM体系中定义为unsigned int:
void set_bit(nr,void*addr);/*设置第 nr 位在 addr 指向的数据项中。*/
void clear_bit(nr,void*addr);/*清除指定位在 addr 处的无符号长型数据.*/
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);
使用范例:
/* try to set lock */
while (test_and_set_bit(nr, addr)!= 0)
wait_for_a_while();
/* do your work */
/* release lock, and check. */
if (test_and_clear_bit(nr, addr)== 0)
something_went_wrong();/* already released: error */
seqlock
2.6内核包含了一对新机制打算来提供快速地, 无锁地存取一个共享资源。
seqlock要保护的资源小, 简单, 并且常常被存取, 并且很少写存取但是必须要快。
seqlock 通常不能用在保护包含指针的数据结构。
seqlock 定义在<linux/seqlock.h>
两种初始化方法:
seqlock_t lock1 = SEQLOCK_UNLOCKED;
seqlock_t lock2;
seqlock_init(&lock2);
这个类型的锁常常用在保护某种简单计算,读存取通过在进入临界区入口获取一个(无符号的)整数序列来工作.。在退出时,那个序列值与当前值比较;如果不匹配,读存取必须重试。
读者代码形式:
unsignedint seq;
do {
seq = read_seqbegin(&the_lock);
/* Do what you need to do */
} while read_seqretry(&the_lock, seq);
如果你的 seqlock 可能从一个中断处理里存取,你应当使用 IRQ 安全的版本来代替:
unsignedint read_seqbegin_irqsave(seqlock_t*lock,unsigned long flags);
int read_seqretry_irqrestore(seqlock_t*lock,unsigned int seq,unsignedlong 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 在它能够获得锁时返回非零。
读取-复制-更新
读取-拷贝-更新(RCU) 是一个高级的互斥方法, 在合适的情况下能够有高效率. 它在驱动中的使用很少。