Linux 并发控制

[竞态]

[1] 概念

    竞争使用独占共享资源的状态

   

[2] 产生条件

    1. 并发

    2. "同时"访问"独占共享资源"

 

[避免竞态 -- 使代码可重入]

[1] 如何使file_operations可重入?

    1. 什么是可重入?

       多个任务执行同一段代码,彼此不影响

           

    2. 如何使代码可重入?

       单输入, 单输出--函数的单输入、单输出是指所有的输入都来自于函数的形参,所有的输出从形参或返回值输出

                     --不直接使用全局变量或静态局部变量

 

[解决竞态]

[2] 操作系统中的并发

    1. 多CPU之间的并发

    2. 单CPU上进程之间的并发

    3. 单CPU上进程和中断之间的并发

    4. 单CPU上中断之间的并发

   

[3] 抢占(一个进程强制剥夺另一个进程的CPU叫抢占)

    抢占发生在什么时候?

    int main(void)

    {

     什么程序(OS中的)可以插到中间运行?(只有中断程序可以做到)

     while (1);

     

     return 0;

    }

   

[4] 中断屏蔽

    1. 原理

       消除单CPU上的并发

       

    2. 用法

       #include

   

       /*

        * @brief          关闭中断

        */

       local_irq_disable()

       

       ......            // 访问独占共享资源

       

       /*

        * @brief          使能中断

        */

       local_irq_enable()

       

       /*

        * @brief         保存中断状态并关闭中断

        * @param[out]    flags             保存中断状态

        */

       local_irq_save(unsigned long flags)

       

       .......           // 访问独占共享资源

       

       /*

        * @brief         恢复中断状态

        * @param[in]     flags             中断状态

        */

       local_irq_restore(unsigned long flags)

       

    3. 适用

       (1) 解决单cpu上的竞态

       

       (2) 关闭中断时间不宜过长(操作系统利用时钟中断计时)

   

[5] 原子操作

    例:防止文件被多次打开

   

    int i = 1;

   

    int xxx_open(struct inode *inode, struct file *filp)

    {

     if (i - 1 < 0) {

      return -EBUSY;

     }

     i -= 1;

     

     分析如下:

     A进程                            B进程

     i - 1 < 0

     切换到B进程执行

                                      i - 1 < 0

                                      切换回A进程

     i -= 1;

     切换到B进程执行

                                     

     ...

     return 0;

    }

 

  int xxx_release(struct inode *inode, struct file *filp)

  {

   i++;

  }

 

    1. 原理

       解决整数(保证整数的访问是一个原子操作)的竞态访问

       

    2. 用法

       #include

       

       // v->counter = i

       void atomic_set(atomic_t *v, int i);

   

       // v->counter = 0

       atomic_t v = ATOMIC_INIT(0);

   

       // retun v->counter

       atomic_read(atomic_t *v);

   

       // v->counter += i;

       void atomic_add(int i, atomic_t *v);

   

       // v->counter -=i

       void atomic_sub(int i, atomic_t *v);

   

       // v->counter++

       void atomic_inc(atomic_t *v);

     

       // v->counter--

       void atomic_dec(atomic_t *v);

       

       // (++v->counter) == 0

       int atomic_inc_and_test(atomic_t *v);

       

       // (--v->counter) == 0

       int atomic_dec_and_test(atomic_t *v);

       

       // (v->counter - i) == 0

       int atomic_sub_and_test(int i, atomic_t *v);

   

       // return (v->counter + i)

       int atomic_add_return(int i, atomic_t *v);

       

       // return (v->counter - i)

       int atomic_sub_return(int i, atomic_t *v);

       

       // return ++v->counter;

       int atomic_inc_return(atomic_t *v);

       

       // return --v->counter;

       int atomic_dec_return(atomic_t *v);

       

       // addr[nr] = 1

       void set_bit(nr, void *addr);

   

       // addr[nr] = 0

       void clear_bit(nr, void *addr);

       

       // ~addr[nr]

       void change_bit(nr, void *addr);

     

       // 检测第nr位是否为1 (拿到内核代码最好确认一下)

       test_bit(nr, void *addr);

     

       // 检测第nr位是否为1,然后设置第nr位为1,如果来的nr位1,则返回真,否则假

       int test_and_set_bit(nr, void *addr);

   

       // 检测第nr位是否为1,然后清除第nr位为0,如果来的nr位1,则返回真,否则假

       int test_and_clear_bit(nr, void *addr);

   

       // 检测第nr位是否为1,然后改变第nr位为0,如果来的nr位1,则返回真,否则假

       int test_and_change_bit(nr, void *addr);

 

       例:防止文件被多次打开

          atomic_t i = ATOMIC_INIT(1);

   

          int xxx_open(struct inode *inode, struct file *filp)

          {

           if (!atomic_dec_and_test(i)) {

            atomic_inc(i);

            return -EBUSY;

           }

            ...

           return 0;

          }

         

          int xxx_release(struct inode *inode, struct file *filp)

          {

           atomic_inc(i);

          }

         

    3. 适用

       可以解决多CPU之间的整数的竞态访问

                 

[6] 自旋锁

    1. 原理

       见《4.自旋锁.bmp》

       

    2. 用法

        #include

       

        spinlock_t lock;                            // 定义

       

        /*

         * @brief          初始化

         * @param[out]     lock           锁

         */

        spin_lock_init(spinlock_t * lock)

       

        /*

         * @brief          获得锁,未获得不返回

         * @param[out]     lock           锁

         */

        void spin_lock(spinlock_t * lock)

       

        或:

       

        /*

         * @brief          尝试锁定,获得返回真,未获得返回假

         * @param[out]     lock           锁

         */

        int spin_trylock(spinlock_t * lock)

       

        ......             // 访问独占的共享资源

 

        /*

         * @brief          释放锁

         * @param[out]     lock           锁

         */

        void spin_unlock(spinlock_t * lock)

        其它用法见PPT

       

        例:防止文件被多次打开

   

            int i = 1;

           

            int xxx_open(struct inode *inode, struct file *filp)

            {

             spin_lock(&lock);

             if (i - 1 < 0) {

              spin_unlock(&lock);

              return -EBUSY;

             }

             i -= 1;

             spin_unlock(&lock);

             

             ...

             return 0;

            }

           

            int xxx_release(struct inode *inode, struct file *filp)

          {

           spin_lock(&lock);

           i++;

           spin_unlock(&lock);

          }

         

  3. 分析

     (1) 多CPU上的并发

           CPU0            CPU1

           ...             ...

           lock(加锁)            

                           lock(等待自旋锁)

           ...

           操作

           ...

           ulock(解锁)

                           lock(加锁)

                           ...

                           操作

                           ...

                           ulock(解锁)

                           

       (2) 单CPU上的进程之间的并发

           消除了(因为关闭了抢占)

           

       (3) 单CPU上的进程和中断之间的并发

           进程             中断

           lock(加锁)

           ...

           被中断

                            lock(等待, 永远加不上锁,也不会退出,死机)

           

           总结: 进程加锁时,一定同时关闭中断

                           

           进程             中断

                            lock(加锁)

                            ...

                            访问资源

                            ulock(解锁)

                            ...

                            返回

           lock(加锁)

           访问资源

           unlock(解锁)

           

           总结: 进程中加锁并关闭中断,中断中,只需要加锁

           

       (4) 单CPU上的中断和中断之间的并发

           中断             中断(高)

           lock(加锁)

           ...

           被中断

                             ...

                             lock(等待, 永远加不上锁,也不会退出,死机)

           总结:低优先级的中断,加锁同时一定要关闭中断

           

           中断             中断(高)

                            ...

                            lock(加锁)

                            ...

                            访问资源

                            ...

                            ulock(解锁)

                            返回

           lock(加锁)

           ...

           unlock(解锁)

           

           总结:加锁时,关闭中断

             

     4. 适用

        (1) 可以解决多CPU之间的并发引起的竞态(实现多CPU之间的互斥访问)

        (2) 消除单CPU上的并发

           方法:

           1. 单CPU上,进程之间的并发,直接加锁

           2. 单CPU上,进程与中断之间的并发和中断与中断之间的并发,加锁且关闭中断

       

        (3) 加锁时间不能太长

        (4) 获得自旋锁期间,不能有引起调度的函数,自己放弃cpu(休眠是典型的代表)

            例:

            假设下列AB进程运行于同一CPU:

            A进程                                    B进程

            加锁

            ......

            sleep后正好切换到B进程加同一锁          

                                                    加锁(一直自旋,且占有CPU,导致本CPU死锁)

            解锁

               

[7] 读写自旋锁

     1. 原理  

        AB两个线程同时做如下操作:

        A                    B               buf(完整单词才能理解)

                                             hello

                                           

        读                   读              可以理解读到的内容

        读                   写world         hello w不能理解

        写world              写! hello w!orld 内容不能理解

       

      总结: 两个任务都是读,不需要加锁,只要有一个任务是写的时候需要加锁

             读不锁定读,读会锁定写, 写锁定读和写

       

     2. 用法

        #include

       

        rwlock_t lock;                               // 定义

        rwlock_init(&lock);                          // 初始化

 

        // 读 获取锁

        read_lock(&lock);                            // 可以换成read_lock_...

        ...                                          // 读临界资源

        read_unlock(&lock);                          // 可以换成read_unlock_...

 

        // 写 获取锁

        write_lock_irqsave(&lock, flags);            // 可以换成write_lock_...

        ...                                          // 写临界资源

        write_unlock_irqrestore(&lock, flags);       // 可以换成write_unlock_...

        其它用法见PPT

       

        例: 见PPT

       

     3. 分析

        (1) 读            读

            CPU0          CPU1

            ...           ....

            加读锁

            ....

            读资源        加读锁(可以加到读锁)

            ...           ...

            ...           读资源

            解读锁        解读锁    

           

        (2) 读            读写

            CPU0          CPU1

            加读锁  

            读资源

                          加写锁(自旋, 等待)

                          ...

            解读锁

                          加写锁

                          读写资源

                          解写锁

                           

           

        (3)  读写          读

             CPU0          CPU1

             加写锁  

             读写资源

                           加读锁(自旋, 等待)

                           ...

             解写锁

                           加读锁

                           读

                           解读锁

             

         (3) 读写        读写

         (4) 写          读写

         (5) 读写        写

             跟普通锁完全相同

             

     4. 适用

        读的概率(时间)大于写的概率(时间)

        读写自旋锁适用频率低于自旋锁

         

[8] 顺序自旋锁

    1. 原理

       读写锁的改进,读不锁定写,写锁定读

       

    2. 用法

       #include

       

       seqlock_t lock;                              // 定义

       seqlock_init(&lock);                         // 初始化

       

       // 写 获取锁

       write_seqlock(&lock);                        // 可以换成write_seqlock_...

       ...                                          // 写临界资源

       write_sequnlock(&lock);                      // 可以换成write_sequnlock_...

       

       // 读 不会锁定写,但会被写锁定

       unsigned seqnum;

       

       do {

           seqnum = read_seqbegin(&lock);           // 可以换成read_seqbegin_irqsave(&lock, flags)

           ...                                      // 读临界资源

       } while (read_seqretry(&lock, seqnum));      // 可以换成read_seqretry_restore(&lock, flags)

       

       例:

       A线程                                       B线程

       do {

           seqnum = read_seqbegin(&lock);

           ......

                               write_seqlock(&lock);

                               p = NULL;

                               write_sequnlock(&lock);

           *p = 5;

       

       } while ((read_seqretry(&lock, seqnum));  

   

    3. 分析

       (1) 读           锁定     写

           CPU0                  CPU1

           加读锁                

                               

                                 加写锁(假设成功)

           读                    写(碰到了读,写不会出错)

                                 解写锁

                                 

           解读锁

           (检测出来,

            是否加过写锁

            写过,重读)

           总结: 读 可以不锁定 写

       

       (2) 写           锁定     读

           CPU0                  CPU1

           加写锁

           写

                                 加读锁(假设成功)

           写                    读(碰到了写,读出错)

                                 解读锁

           解写锁

           (检测出来,

            是否读过)

           总结:写 必须锁定 读

     

    4. 适用

       (1) 读独占共享资源的概率(时间),远大于写独占共享资源的概率(时间)

       

       (2) 共享资源不能指针

               

[9] 信号量

     1. 原理

        见《5-信号量》图

       

     2. 用法

         #include

   

         struct semaphore sem;                           // 定义

         sema_init(&sem, 1);                             // 初始化

   

         down(&sem);                                     // 获取信号量

         或:

         /*

          * 调用本驱动的进程收到消息,本函数会返回

          * 获取到信号量返回0,否则1

          */

         down_interruptible(&sem);

   

        /*

         * 尝试获取信号量,未获取信号量立即返回,不休眠

         * 获取到信号量返回0,否则1(注意跟自旋锁不同)

         */                      

         down_trylock(&sem)

         ...                                             // 临界资源访问

         up(&sem);                                       // 释放信号量  

         

    3. 分析

       (1) 多CPU之间的并发

           可以

   

       (2) 单CPU进程之间的并发

           可以

           

       (3) 单CPU进程和中断之间的并发

           不可以使用,中断中绝对不能调用可能会引起休眠的函数

           

       (4) 中断之间的并发

           中断中绝对不能调用可能会引起休眠的函数

     

     4. 适用

        (1) 多cpu之间的并发所引起的竞态

        (2) 单CPU上,多进程之间的并发引起的竞态

       

        (3) 不能用于中断上下文中(中断里面不能休眠,操作系统不支持)

        (4) 获得信号量的时间可以很长(等待信号量时,线程休眠不浪费系统资源(相对自旋锁而言),信号量的持有时间还是应该尽可能的短)

                 

[10] 读写信号量

     1. 原理

        读不锁定读,读锁定写,写锁定写

       

     2. 用法

        PPT

       

     3. 适用

        用于需要用到信号量的情况(读的概率(时间)大于写的概率(时间))

       

[11] 互斥体

     1. 原理

        采用信号量实现1个资源的互斥访问

       

     2. 用法

        见PPT

       

     3. 适用

        可以适用信号量互斥的地方,互斥体都可以用

       

[12] 实践

 

 

CPU0             CPU1

读               写

 

加读锁

...

....

                 加写锁

                 ...

                 ...

                 写资源

                 ...

                 解写锁

解读锁,返回

重读

 

 

写               读

 

加写锁

....

....

写资源

...              加读锁(锁定,一直等待,等待写解锁)

...

写解锁

                 加读锁  

                 读资源

                 ...

                 ...  

                 解读锁          

                 

                    

你可能感兴趣的:(Linux 并发控制)