并发问题,并发相关的错误是一些最易出现又最难发现的问题。
设备启动程序员现在必须从一开始就将并发作为他们设计的要素。
一、scull中的缺陷
if(!dptr->data[s_pos]) { dptr->data[s_pos] = kmalloc(quantum, GFP_KERNEL); if(!dptr->data[s_pos]) goto out; }
假设有2个进程("A"和“B”)独立试图写入同一个schll设备的相同便宜,每个进程同时到达上面片段的第一行的if测试。
因为两个进程都在赋值给同一个位置,显然只有一个能成功。
当然发生的是第2个完成赋值的进程将胜出,进程A先赋值,它的赋值会被进程B覆盖。scull将会完全忘记A分配的内存;只有指向B内存的指针,A所分配的指针,因此,将被对调并且不再返回给系统。
类似这种竞争情况是对共享数据的无控制存取的结果。 然后会产生不希望的东西,如内存泄漏。并且常常导致系统崩溃和数据损坏。
二、并发和它的管理
资源共享的硬规则:任何时候一个硬件或软件资源被超出一个单个执行线程共享,并且可能存在一个线程看到那个资源的不一致时,你必须明确的管理对那个资源的存取。
另一个重要规则:当内核代码创建一个会被内核其他部分共享的对象时,这个对象必须一直存在(并且功能正常)到它知道没有对它的外部引用存在为之。
三、旗标和互斥体
对于上面情况的处理,我们必须建立临界区:在任何给定时间只有一个线程可以执行。
旗标是一个单个整型值,结合有一对函数,典型地称为P和V。想进入临界区的进程将在旗标上调用P;如果旗标的值大于零,这个值递减1,并且进程继续。
如果旗标的值为0,进程必须等待知道别人释放旗标,解锁旗标通过V完成。
头文件<asm/semaphore.h>相关类型是struct semaphore
void sema_init(struct semaphore *sem, int val); val:是安排给旗标的初始值
旗标通产使用互斥锁的模式使用,声明和初始化:
DECLARE_MUTEX(name);
DECLARE_MUTEX_LOCKED(name);
初始化name为1或者0(DECLARE_MUTEX_LOCKED)
互斥锁必须运行时间初始化:
void init_MUTEX(struct semaphore *sem); void init_MUTEX_LOCKED(struct semaphore *sem);
在linux中P函数称为down:
void down(struct semaphore *sem); int down_interruptible(struct semaphroe *sem); int down_trylock(struct semaphore *sem); void up(struct semaphore *sem); 一旦up调用,调用者就不再拥有标志。
3.2 在scull中使用旗标
scull_dev结构:
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 */ };
旗标在使用前必须初始化,scull在加载时进行这个初始化,在这个循环中:
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_dev数据结构的存取。
if(down_interruptible(&dev->sem)) return -ERESTARTSYS;
返回-ERESTARTSYS,内核会在次调用要么返回错误给用户。所以要先恢复已经的改变,不然还是用-EINTR吧
在scull_write必须释放旗标,返回前:
out: up(&dev->sem); return retval;
3.3 读者/写者旗标
很多任务分为2中清楚类型:
- 只需要读取被保护的数据结构类型,和必须做改变的类型。
- 允许多个并发读者常常是可能的,只要没人试图做任何改变。
分开处理往往能够提高性能。linux内核为这种情况提供一个特殊旗标“rwsem”(reader/writer semaphore)
必须包含头文件<linux/rwsem.h>,相关结构体是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); 读者的机构类似: 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);
3.4 Completions机制
使用一个旗标来同步2个任务,使用这样的代码:
struct semaphore sem; init_MUTEX_LOCKED(&sem); start_external_task(&sem); down(&sem);
外部任务可以接着调用up(&sem),在它工作完成时。
2.4.7增加了completion接口,使用一个轻量级机制:允许宪哥线程告诉另一个线程工作已经完成。
头文件<linux/completion.h>,初始化:
DECLARE_COMPLETION(my_completion); 或者动态创建: struct completion my_completion; init_completion(&my_completion);
等待completion是一个简单事来调用:
void wait_for_completion(struct completion *c); 如果代码调用wait_for_completion并且没有人完成这个任务,结果就会不可杀死 void complete(struct completion *c); void complete_all(struct completion *c); 触发completion事件函数
当然这个可以多次重复使用,需要重新初始化:
INIT_COMPLETION(struct completion c);
使用例子:
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 ", current->pid, current->comm); wait_for_completion(&comp); printk(KERN_DEBUG "awoken %i (%s) ", current->pid, current->comm); return 0; /* EOF */ } 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... ", current->pid, current->comm); complete(&comp); return count; /* succeed, to avoid retrial */ }
结束函数:
void complete_and_exit(struct completion *c, long retval);
四、自旋锁
如果锁是可用的,这个“上锁”被置位并且diamante继续进入临界区。相反,如果这个锁已经被别人获得,代码进入一个紧凑的循环中反复检查这个锁,知道它变为可用。
4.1 自旋锁API简介
头文件<linux/sinlock.h>,一个实际的锁有类型spinlock_t,初始化
spinlock_t my_lock = SPIN_LOCK_UNLOCKED; 或者使用: void spin_lock_init(spinlock_t *lock); 进入一个临界区前,代码必须获得需要的lock,用: void spin_lock(spinlock_t *lock);一旦调用将自旋到可用 释放一个你已获得的锁: void spin_unlock(spinlock_t *lock);
4.2 自旋锁和原子上下文
应用到自旋锁的核心规则是任何代码必须,在持有自旋锁时,是原子性的。它不能睡眠。
编写会在自旋锁下执行的代码需要注意你调用的每个函数。
4.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 *lock);
也有4个函数来释放自旋锁,必须对应获取锁的函数:
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);
4.4 读者/写者自旋锁
头文件<linux/spinlock.h>,读写者锁类型rwlock_t,初始化:
rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* Static way */ rwlock_t my_rwlock; rwlock_init(&my_rwlock); /* Dynamic way */
对于读者,下列函数是可用的:
void read_lock(rwlock_t *lock); void read_lock_irqsave(rwlock_t *lock, unsinged 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_reylock,对于写存取的函数是类似的:
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);
4.5 锁陷阱
使用锁有很多出错的方式:
模糊规则
编写的代码中的函数需要一个锁,并且接着调用另一个函数也试图请求这个锁,容易死锁。
加锁顺序规则
使用多个锁是很危险的,当多个锁必须获得时,他们应当一直以同样顺序获得,只要遵照这个惯例,就能简单避免死锁。
细-粗-粒度加锁
4.6 加锁的各种选择
不加锁算法
环形缓冲(头文件<linux/kfifo.h>)
原子变量
内核提供了一个原子整数类型称为atomic_t,定义在<asm/atomic.h>
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);
位操作,头文件<asm/bitops.h>
void set_bit(nr, void *addr); 设置第nr位在addr指向的数据项中 void clear_bit(nr, void *addr); 清楚指定位在addr处的无符号长型数据,它的语义与set_bit相反 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); 原子的动作如同前面里出的,还返回这个位以前的值
例子:
/* 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锁
seqlock定义在<linux/seqlock.h>,有2个方法初始化一个seqlock(seqlock_t类型)
seqlock_t lock1 = SEQLOCK_UNLOCKED;
seqlock_t lock2;
seqlock_init(&lock2);
如果你的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 falgs); 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);