美文网首页
[读书笔记]并发和竞态(第五章)

[读书笔记]并发和竞态(第五章)

作者: c枫_撸码的日子 | 来源:发表于2018-09-04 21:28 被阅读0次

    综述

    并发问题是编程中经常遇到的难题,我们需要学会针对并发产生的竞态进行编程

    一、信号量和互斥体

    Linux上信号量的实现

    1.信号量:本质上是一个整数值,它和一对函数联合使用,这对函数通常称为P和V,希望进入临界区的进程将在相关信号量上调用P,如果信号量大于0,该值减1,进程可以进入临界区代码运行;相反,如果信号量的值<=0,进程必须等待其他人释放该信号量。解锁信号量调用V函数,该函数增加信号量的值,并在必要时唤醒等待的进程。

    当信号量用于互斥时(即避免多个进程同时在一个临界区中运行)
    信号量应初始化为1
    这种信号量在任何时刻只能由单个进程或者线程拥有,这种模式下,
    信号量也成为互斥体
    2.声明和初始化
    void sema_init(strcut semaphore *sem,int val);
    sem代表信号量,val代表初始值,一般初始化为1.
    DECLARE_MUTEX(name):一个名称为name的信号量被初始化为1
    DECLARE_MUTEX_LOCKED(name):一个名称为name的信号量被初始化为0

    void init_MUTEX(stuct semaphore *sem):
    void init_MUTEX_LOCKED(stuct semaphore *sem):
    LINUX2.6版本后已经被遗弃 无法使用

    Linux中的P函数是down(),该函数会减少信号量的值,必要时会一直等待。
    void down(struct semaphore *sem)
    减少信号量的值 并在必要时一直等待,操作不可中断
    void down_interruptible(struct semaphore *sem)
    完成和down相同的工作,但是操作是可中断,通常推荐使用,如果操作被中断,该函数返回非零值,而调用者不会拥有该信号,使用时注意检查返回值,并且做出相应的操作。
    int down_trylock(struct semaphore *sem)
    永远不会休眠,如果信号量在调用时不可获得,该函数会立即返回一个非零值。

    当一个函数成功调用上述的down函数,就称为该线程拥有(获得、拿到)了该信号量,这样该线程就被赋予访问由该信号量保护的临界区的权利,当互斥操作完成后,必须返回该信号量,即调用V函数

    void up(strcut semaphore *sem);
    调用后,调用者不在拥有该信号量。

    使用信号量实例

    步骤1:定义信号量
    在自己的定义的结构体加入semaphore *sem;//互斥信号量
    步骤2:初始化信号量
    sema_init(sem,1);
    默认初始化信号量sem的值为1
    步骤3:
    在要保护的资源调用dowm_interruptible();
    if(dowm_interruptible(&sem))
    retrun -ERESTARTSYS;
    //这里是要保护资源的代码
    out:
    up(&sem);
    在函数调用up最后释放信号量

    strcut hello_dev{
      int val;
      semaphore *sem;//互斥信号量
    }
    static hello_init()
    {
    //初始化信号量
    sema_init(dev->sem,1);
    }
    /*读取寄存器设备 val的值*/
    static ssize_t hello_read(struct file *filp,char __user *buf,
    size_t count,loff_t *f_ops) {
        ssize_t err = 0;
        struct hello_dev *dev = filp->private_data;
    
        /*同步访问*/
        if(down_interruptible(&(dev->sem)));
            return -ERESTARTSYS;
        
        if(count < sizeof(dev->val)){
            goto out;
        }
        /*将寄存器val的值拷贝到用户提供的缓存区*/
        if(copy_to_user(buf,&(dev->val),sizeof(dev->val))){
            err = -EFAULT;
            goto out;
        }
    
        out:
        up(&(dev->sem));
        return err;
    }
    

    读取者/写入者信号量

    许多任务可分为:
    1.只需要读取受保护的数据(多个进程和线程可以同时并发访问)
    2.写入受保护的数据
    为此,Linux提供了特殊的信号量"rwsem"(或者reader/writer semaphore),rwsem使用很少,偶尔有用

    相关定义包含在<linux/rwsem.h>头文件中
    struct rw_semaphore *sem;
    初始化:
    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);
    downgrade_write允许其他读取者访问

    完成量

    completion是一种轻量级的机制,它允许一个线程告诉另一个线程某个工作已经完成了,包含在<linux/completion.h>中
    1.创建和初始化completion
    DECLARE_COMPLETION(my_com);
    或者动态创建和初始化
    strcut completion my_com;
    inti_completion(&my_com);
    2.等待completion
    void wait_for_completion(strcut completion *c);
    注意,该函数执行一个非中断等待,如果代码调用了该函数且没人能完成该信号量,则会产生一个不可杀进程!
    3.唤醒completion
    void completion(struct completion *c);
    void complete_all(struct completion *c);
    快速重新初始化某个复用的completion
    INIT_COMPLETION(struct completion c);

    自旋锁

    信号量在互斥中是非常有用的工具,内核还提供另一种工具--自旋锁。
    和信号量不同,自旋锁可以在休眠的代码中使用,如中断处理例程,正确使用的情况下,自旋锁性能比信号量好!

    自旋锁API

    初始化
    自旋锁相关定义包含在头文件<linux/spinlock.h>中
    编译时初始化
    strcuvt spinlock_t my_lock;
    my_lock = SPIN_LOCK_UNLOCKED;
    或者
    运行时初始化
    void spin_lock_init(spinlock_t *lock);
    进入临界区之前,必须调用以下函数获取锁
    void spin_lock(spinlock_t *lock);
    注意:所以自旋锁本质上都是不可中断的,一旦调用了spin_lock,在获取锁之前一直处于自旋状态

    释放锁函数
    void spin_unlock(spinlock_t *lock);

    注意:为了避免
    在中断例程自旋时,非中断代码将没有机会释放这种个自旋锁,导致处理器将永远自旋下去的情况
    我们需要在拥有自旋锁时禁止中断(仅本地CPU上),下面的函数可以实现用于禁止中断的自旋锁函数。
    另一个重要原则:自旋转必须的在尽可能短的时间内拥有!

    自旋锁函数
    void spin_lock(spinlock_t *lock):允许中断的自旋锁
    void spin_lock_irqsave(spinlock_t *lock,unsigned long flags)
    在获得自旋锁之前禁止中断(只在本地cup上),先前中断报错在flags中
    void spin_lock_irq(spinlock_t *lock)
    如果我们能确保没有任何其他代码禁止本地处理器的中断,则可以使用spin_lock_irq,而无需跟踪标志!
    void spin_lock_bh(spinlock_t *lock)
    在获得锁之前禁止软件中断,允许硬件中断打开
    该函数可以安全的避免死锁问题,还能服务硬件中断
    释放锁函数

    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);
    两个函数在成功获取自旋锁时,返回非零值,失败时返回零,对应禁止中断的情况没有对应的try版本
    读取者/写入者自旋锁
    和rwsem信号量很相似,但是可能会造成读取者饥饿,导致性能变低!

    注意防止死锁情况
    死锁1:当某个获得锁的函数要调用其他同样试图获取这个锁的函数,代码就会死锁,无论是信号量还是自旋锁,都不允许锁拥有者第二次获得这个锁

    死锁2:线程1拥有锁1,线程2拥有锁2,这时候,当这两个线程都试图获取另外线程的的锁时,这两个线程将处于死锁状态
    最好的办法是避免同时需要多个锁的情况

    锁之外的办法

    1.免锁算法

    常用于免锁算法的生产者/消费者任务的数据结构就是循环缓冲区!

    2.原子变量

    共享资源是一个简单的整数型时,内核提供了一种原子的整数类型
    称为atomic_t 定义在<asm/atomic.h>中
    初始化
    void atomic_set(atomic *v,int t);
    或者
    atomic_t v = ATOMIC_INIT(0);初始化为0
    还有读写函数,运算操作函数,位操作函数就不一一列举了

    3.seqlock

    当要保护的资源很小、很简单、会被频繁读取访问且写入访问很少发生且必须快速时,就可以使用内核提供的seqlock
    允许读取者自由访问,但是需要读取者检测是否和写入者冲突
    seqlock通常不能包含在含有指针的数据结构中,因为在写入者修改数据结构的同时,读取者可能会追随一个无效的指针。

    seqlock定义在<linux/seqlock.h>
    初始化方法2种
    1.seqlock_t lock1 = SEQLOCK_UNLOCKED;
    2.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_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()可以获得自旋锁,它也会返回非0值。

    读取-复制-更新(read-copy-update,RCU)

    RCU是一种高级的互斥机制,正确使用下,也可获得很高的性能,但很少在驱动程序中使用。
    RCU针对经常发生读取而很少写入的情形做了优化,被保护的资源应通过指针访问,而对这些资源的引用必须由原子代码拥有!

    #include <linux/rcupdate.h>
    使用读取-复制-更新(RCU)机制是需要包含的头文件
    void rcu_read_lock();
    void rcu_read_unlock();
    获取对受RCU保护资源的读取访问的宏
    void call_rcu(srcut rcu_head head,void (func)(void *arg),void *arg);
    准备用于安全示范受RCU保护的资源的回调函数,该函数将在所有的处理器被调度后运行!

    相关文章

      网友评论

          本文标题:[读书笔记]并发和竞态(第五章)

          本文链接:https://www.haomeiwen.com/subject/ipjbwftx.html