第5章 并发和竞争情况

并发问题,并发相关的错误是一些最易出现又最难发现的问题。

设备启动程序员现在必须从一开始就将并发作为他们设计的要素。

一、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\n", current->pid, current->comm);
    wait_for_completion(&comp);
    printk(KERN_DEBUG "awoken %i (%s)\n", 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...\n", 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);

 

posted @ 2018-06-20 17:22  习惯就好233  阅读(268)  评论(0编辑  收藏  举报