• 第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
    ", 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);
    无欲速,无见小利。欲速,则不达;见小利,则大事不成。
  • 相关阅读:
    生产者消费者模型
    varchar2存储汉字,英文字符,数字在oracle中的多少
    正则表达式以及邮箱
    爬虫
    创建git本地仓库和GitHub远程仓库并配置连接的从无到有
    CSSHTML实现高度宽度自适应
    实现一个元素在当前窗口垂直水平居中的几种方法
    angular4 rxjs 异步处理多个http请求数据
    angular 4 父子组件异步交互
    同步异步单线程多线程初级理解
  • 原文地址:https://www.cnblogs.com/ch122633/p/9204883.html
Copyright © 2020-2023  润新知