Linux内核之并发与竞态

                         linux内核并发和竞态
1.分析案例:要求一个硬件设备只被一个应用程序打开访问
方案1:从应用层面考虑,利用IPC进程间通信技术来实现,但是软件设计极其复杂。
方案2:从底层驱动层面考虑,不管有多少个进程访问设备,它们必须都要open设备,最终否会调用底层驱动的同一个open函数,只需在底层驱动的open函数中做代码限定即可!

首先,分析下面这条代码:
    --open_cnt;
这条语句C语言虽然是一条,但是通过反汇编看是三条:
ldr r3,[open_cnt的内存地址]
sub r3, r3, #1 
str r3,[open_cnt的内存地址]
汇编代码的执行也是一条一条的执行!

1)首先看正常情况
  先A进程open设备,A就会--open_cnt:
  先ldr...,r3=open_cnt=1
  然后:sub ..., r3=0
  最后:str ..., open_cnt=0(内存为0)
  结果:打开成功

  然后B进程open设备,B就会--open_cnt:
  先ldr..., r3=open_cnt=0
  然后:sub ..., r3=-1
  最后:str ..., open_cnt=-1(内存为-1)
  结果:打开失败

2)然后分析异常情况
先A进程open设备,A就会--open_cnt:
先ldr...,r3=open_cnt=1
就在此时此刻,由于linux内核支持进程之间的抢占,高优先级的进程抢夺低优先级进程的CPU资源,也就是此时B进程的优先级高于A进程,B进程毫无条件的抢夺A进程的CPU资源,B进程接着开始执行,A进程苦苦的等待着B进程执行完毕才能获取CPU资源接着执行.
切记:linux内核,进程之间的抢占基于软中断实现,遵循软中断异常处理的流程:向量表,然后保护现场,也就是B进程将A进程的ARM寄存器数据保存到栈中,也就是r3=1会保存到栈中,然后调用软中断异常处理函数,最后恢复现场,也就是从栈中将1恢复给r3,状态恢复,跳转返回,返回到A进程继续接着运行。

  接着B进程执行,B也会--open_cnt:
  先ldr...,r3=open_cnt=1
  然后:sub ..., r3=0
  最后:str ..., open_cnt=0(内存为0)
  结果:打开成功
  B执行完毕,B恢复现场,状态恢复,跳转返回到A继续运行

  A接着运行:
  然后:sub ..., r3=0
  最后:str ..., open_cnt=0(内存为0)
  结果:打开成功

 结论:形成此异常的根本原因就是进程之间的抢占引起。
再看下面的案例:
要求处理器通过一个GPIOB8引脚给LCD显示屏发送一个严格的10ms脉冲,点亮屏幕。
底层驱动参考代码:
void lcd_on(void)
{
    gpio_direction_output(PAD_GPIO_B+8, 1);
    mdelay(5);
    gpio_direction_output(PAD_GPIO_B+8, 0);
    mdelay(5);      
}   
结果:lcd显示屏异常,最终通过示波器抓取波形,发现GPIOB8引脚的波形脉冲超过10ms,问题在哪里?
分析:
假设调用此函数的任务是一个进程(进程调用ioctl最终访问底层驱动的lcd_on此函数),当进程刚执行完
gpio_direction_output(PAD_GPIO_B+8, 1);
这条语句,此时此刻如果来一个高优先级的进程或者任意中断,都会毫无条件的抢夺当前进程的CPU资源,那么当前进程的代码就会停止不前,CPU转去做其它事情,而做其它事情的期间,GPIOB8持续为高电平,当CPU在回到当前进程继续运行,开始运行mdelay(5),显然GPIOB8高电平的持续时间要大于5ms,显然严格的10ms脉冲不存在,造成LCD显示异常。        结论:造成LCD显示屏异常的根本原因是高优先级的任务抢夺CPU资源,造成CPU资源的切换,造成延时的不准确!
3.总结
1)案例1代码,是可以允许CPU资源切换,也就是如果linux内核中有高优先级的任务(进程或者中断)可以抢夺A进程的CPU资源,但是高优先级的任务不能访问全局变量open_cnt,否则异常!如果别的驱动文件(.c)的任务优先级高,会发生CPU资源的切换,但是由于open_cnt做了static限定,即使优先级高,也无法访问全局变量.如果本文件中有高优先级的任务,悠着点(想想AB进程)!
2)案例2代码,理论上也可以允许CPU资源的切换,但是高优先级的任务也不能访问同一个硬件资源GPIOB8(假设这边拉高,那边拉低),但是由于此代码中有严格的时间要求,更不能进行CPU资源的切换,所以案例2的代码不仅仅不能让访问全局的硬件资源GPIOB8,还不能让CPU资源发生切换!  
4.相关概念
并发:多个执行单元同时发生。
竞态:多个执行单元对共享资源的同时访问形成竞争的状态。
形成竞态的三个条件:
    1)要有多个执行单元
    2)要有共享资源
    3)还要同时访问
共享资源:软件上的全局变量(open_cnt)或者硬件资源(GPIOB8),要是寄存器都是共享资源(A &= ~(B << C),也会发生CPU资源切换)。
临界区:访问共享资源的代码区域
        例如:if(--open_cnt != 0)
                 ...
        例如:lcd_on此函数整个就是临界区
互斥访问:当一个执行单元对共享资源(临界区)进行访问时,其它执行单元禁止访问,直到前一个执行单元访问完毕。
执行路径具有原子性:当某个任务获取CPU资源访问临界区时,不允许发生CPU资源的切换。
例如:
1)当A进程获取到CPU资源,访问open_cnt,汇编三条语句的过程,不允许发生CPU资源切换。即使三条汇编语句执行期间发生CPU资
源切换,只要其他任务不访问open_cnt也是没问题!
    2)当任务访问lcd_on此函数,坚决不允许发生CPU资源切换

5.在linux内核中,形成竞态的四种情形
1)多核SMP(shared memeory proccessors:共享内存的多处理器)
多核它们是共享内存,闪存,GPIO,硬件中断资源等。
例如:CPU0核向内存地址0x48000000写入数据,而CPU1核从内存0x48000000读取数据,势必出问题
2)同一个CPU核上的进程与进程之前的抢占高优先级的进程抢夺唯一的CPU资源.
明确:进程抢占基于软中断实现
例如:
    A,B进程运行在CPU0核上
    C进程单独运行在CPU1核上
3)中断和进程
    硬件中断抢夺进程CPU资源
    软中断抢夺进程CPU资源
4)中断和中断
    硬件中断抢夺软中断CPU资源
    高优先级软中断抢夺低优先级软中断CPU资源
    例如:tasklet_schedule/tasklet_hi_schedule  

6.linux内核解决竞态的四种基本方法
中断屏蔽,自旋锁,衍生自旋锁,信号量,原子操作。

7.解决竞态问题方法之中断屏蔽
1)特点
i.中断屏蔽能够解决以下竞态问题:
进程抢占、中断和进程、中断和中断,解决不了多核引起的竞态问题。
ii.由于linux内核中很多机制跟中断相关(抢占,调度,软件定时器,硬件定时器),长时间的屏蔽中断势必对一些机制功能有影响,所以中断屏蔽保护的临界区代码执行速度要快,更不能进行休眠操作。
iii.中断屏蔽既可以用于中断也可以用于进程
2)利用中断屏蔽保护临界区的编程步骤:
i.实际开发,先根据用户需求完成驱动的基本功能和逻辑,先不要考虑竞态问题,此时代码裸奔状态。驱动写完以后,回过头认真仔细考虑驱动代码中是否存在竞态。如何知道代码中是否存在竞态,首先确定驱动代码中哪些是共享资源。然后确定驱动代码中哪些是临界区。然后确定临界区中是否有休眠操作。如果有休眠操作,势必不考虑中断屏蔽;如果没有休眠,还要考虑是否有多核竞态问题;如果有多核,势必不考虑;如果没有多核,最终可以考虑使用中断屏蔽。
ii.如果采用中断屏蔽,访问临界区之前先屏蔽(关闭)中断
    unsigned long flags;
    local_irq_save(flags); //关闭中断,并且让内核将中断标志保存到flags
iii.任务访问临界区完毕,记得要恢复(打开)中断。
    local_irq_restore(flags); //打开中断,从flagsh中恢复中断标志

8.解决竞态问题方法之自旋锁
1)特点
自旋锁=自旋+锁(重点),如果任务访问临界区,提前要获取自旋锁,如果获取失败,任务将会原地忙等待,直到自旋锁被释放。
2)自旋锁能够解决的竞态问题如下:
多核引起的竞态、进程之间的抢占,但是只要是中断引起的竞态无法解决。
3)linux内核描述自旋锁的数据类型:spinlock_t
4)利用自旋锁保护临界区的编程步骤
实际开发,先根据用户需求完成驱动的基本功能和逻辑,先不要考虑竞态问题。驱动写完以后,回过头认真仔细考虑驱动代码中是否存在竞态。然后确定驱动代码中哪些是临界区,然后确定临界区中是否有休眠操作。如果有休眠,势必不考虑使用自旋锁。如果没有休眠,然后再确定是否有中断参与竞态问题。这里要注意,这个中断有可能自己驱动中断,还有可能是linux内核其它的硬件中断(硬件定时器中断,每隔10ms触发一次),如果有中断参与,势必不考虑自旋锁。如果没有中断参与,可以考虑使用自旋锁。
如果采用自旋锁,首先定义初始化一个自旋锁对象
    spinlock_t lock; //定义对象
    spin_lock_init(&lock); //初始化自旋锁对象
任务(中断或者进程)访问临界区之前,先获取自旋锁
    spin_lock(&lock);
说明:任务获取自旋锁成功,立马返回继续运行,获取锁失败,在此函数中进入忙等待,直到自旋锁被释放.任务获取锁成功并且返回。
5)任务访问临界区完毕,记得要释放自旋锁
    spin_unlock(&lock);
注意:获取自旋锁和释放自旋锁必须逻辑上成对使用

9.解决竞态问题方法之衍生自旋锁
1特点
1)衍生自旋锁本质就是自旋锁:衍生自旋锁=中断屏蔽+自旋锁
2)衍生自旋锁必须附加在某个共享资源上
3)衍生自旋锁保护的临界区的代码执行速度要快,更不能进行休眠操作
4)如果任务访问临界区,提前要获取衍生自旋锁,如果获取失败,任务
将会原地忙等待,直到衍生自旋锁被释放
while(1) {
    if(衍生自旋锁空闲中)
       break;
    continue;
}  
5)衍生自旋锁能够解决所有的竞态问题
6)中断和进程都可以利用衍生自旋锁保护共享资源
2.linux内核描述衍生自旋锁的数据类型:spinlock_t
3.利用衍生自旋锁保护临界区的编程步骤
1)实际开发,先根据用户需求完成驱动的基本功能和逻辑,先不要考虑竞态问题,此时代码裸奔状态。
2)驱动写完以后,回过头认真仔细考虑驱动代码中是否存在竞态。
3)如何知道代码中是否存在竞态,首先确定驱动代码中哪些是共享资源。
4)然后确定驱动代码中哪些是临界区。
5)然后确定临界区中是否有休眠操作。
    如果有休眠,势必不考虑使用衍生自旋锁
    如果没有休眠,可以考虑使用衍生自旋锁
6)如果采用衍生自旋锁,首先定义初始化一个衍生自旋锁对象
    spinlock_t lock; //定义对象
    spin_lock_init(&lock); //初始化衍生自旋锁对象
7)任务(中断或者进程)访问临界区之前,先获取衍生自旋锁
    unsigned long flags;
    spin_lock_irqsave(&lock, flags); //先屏蔽中断后获取锁
说明:任务获取自旋锁成功,立马返回继续运行
      获取锁失败,在此函数中进入忙等待,直到自旋锁被释放
      任务获取锁成功并且返回
8)一旦获取衍生自旋锁成功,任务踏踏实实的访问临界区
心理念到:此时此刻不会有任何任务抢夺CPU资源
9)任务访问临界区完毕,记得要释放自旋锁
    spin_unlock_irqrestore(&lock, flags);
10)注意:获取衍生自旋锁和衍生释放自旋锁必须逻辑上成对使用

案例1:利用衍生自旋锁解决案例1的漏洞
参考代码:day08/3.0
结论:采用衍生自旋锁能够解决漏洞

案例2:利用衍生自旋锁解决案例2的漏洞
参考代码:
    void lcd_on(void)
    {
        unsigned long flags;
    //获取自旋锁
    spin_lock_irqsave(&lock, flags); 

        //临界区
        gpio_direction_output(PAD_GPIO_B+8, 1);
        mdelay(5);
        gpio_direction_output(PAD_GPIO_B+8, 0);
        mdelay(5);      

    //释放自旋锁
    spin_unlock_irqrestore(&lock, flags);
    } 

    结论:采用衍生自旋锁保护临界区最终成功

10.解决竞态问题方法之信号量
i.特点
内核信号量和应用信号量一模一样,信号量又称睡眠锁,本身基于自旋锁实现,信号量诞生的本质就是解决自旋锁保护的临界区不能休眠问题.有些场合,临界区如果有休眠要求,此时不能用自旋锁,必须采用信号量。信号量只能用于进程,中断不能用;如果进程获取信号量失败,进入将进入休眠等待状态,也就是进程会释放占用的CPU资源。
ii.linux内核描述信号量的数据结构:struct semaphore
iii.利用信号量来保护临界区的编程步骤
如果采用信号量,首先定义初始化一个信号量对象
    struct semaphore sema;
    sema_init(&sema, 1); //初始化信号量为互斥信号量为1
             //互斥信号量的值只有1或者0
             //1:表示信号量没有人占用
             //0:表示信号量有人使用
访问临界区之前,先获取信号量
    down(&sema);
本质:将信号量sema值减1为0 
说明:
    当进程调用此函数来获取信号量:
    如果获取信号量成功,进程立马从此函数中退出,接下来访问临界区           
    如果获取信号量失败,进程在此函数中进入不可中断的休眠状态,
    一直等待被唤醒,唤醒的方法只有一种:
    持有信号量的任务访问临界区之后释放信号量并且来唤醒休眠的进程!
“不可中断的休眠状态”:进程一旦进入此休眠状态,进程在休眠期间,如果接收到了kill去死信号,
这个进程不会立即被唤醒(信号已经在身边了,不会跑),直到持有信号量的任务释放信号量并且
唤醒休眠的这个进程,进程一旦被唤醒才会去处理之前接收到了kill信号一般结果进程会死去!
或者:
    down_interruptible(&sema);
本质:将信号量sema值减1为0 
说明:
    当进程调用此函数来获取信号量:
    如果获取信号量成功,进程立马从此函数中退出,接下来访问临界区           
    如果获取信号量失败,进程在此函数中进入可中断的休眠状态,
    一直等待被唤醒,唤醒的方法两种种:
    1)持有信号量的任务访问临界区之后释放信号量并且来唤醒休眠的进程!
    2)休眠期间接收到了kill信号,也会立即被唤醒,唤醒以后会立即处理kill信号,死去!
“可中断的休眠状态”:进程一旦进入此休眠状态,进程在休眠期间如果接收到了kill去死信号,这个进
程会立即被唤醒并且处理接收到了kill信号。
    如果返回真表示信号唤醒
    如果返回0表示驱动主动唤醒!
8)一旦进程获取信号量成功,进程就可以踏踏实实访问临界区
9)访问临界区之后,记得要释放信号量并且唤醒休眠的进程
    up(&sema);
本质:将信号量的值由0加1为1
功能:不仅仅释放信号量还要唤醒休眠的进程
注意:只要驱动程序调用某个函数来唤醒休眠的进程,此过程简称驱动主动唤醒或者内核唤醒!

10.原子操作
1)特点
不会发生CPU资源的切换,所有的竞态问题都可以解决,适用于中断和进程。
2)原子操作分两类:位原子操作和整型原子操作
3)位原子操作特点:
i.位原子操作=位操作+原子=位操作具有原子性
        =对共享资源进行位操作的过程不允许发生CPU资源的切换
ii.linux内核提供的位原子操作的相关函数
明确:如果将来对共享资源进行位操作,并且考虑到竞态问题,可以采用位原子操作的相关函数,这些函数在执行期间不会发生CPU资源的切换。
    void set_bit(int nr, void *addr)
    说明:将addr地址内数据的第nr(从0)位置1
    void clear_bit(int nr, void *addr)
    说明:将addr地址内数据的第nr位清0
    void change_bit(int nr, void *addr)
    说明:将addr地址内数据的第nr位反转
    int test_bit(int nr ,void *addr)
    说明:获取addr地址内数据的第nr位的值
    ... //一堆的组合函数
   3)利用位原子操作保护共享资源的编程步骤
   实际开发,先根据用户需求编写驱动的基本逻辑和功能,然后检查驱动代码中是否有竞态问题。首先确定驱动代码中哪些是共享资源,然后确定驱动代码中哪些是临界区,进而确定驱动代码中对共享资源是否有位操作。如果有位操作,可以考虑使用内核提供的位原子操作的相关函数。
   例如:
       int open_cnt = 1; //共享资源
       open_cnt &= ~(1 << 1); //临界区,目前裸奔中
   优化加以保护:
   方案1:采用中断屏蔽
          unsigned long flags;
          local_irq_save(flags);
          open_cnt &= ~(1 << 1); //临界区
          local_irq_restore(flags);
   方案2:采用衍生自旋锁
          unsigned long flags;
          spin_lock_irqsave(&lock, flags);
          open_cnt &= ~(1 << 1); //临界区
          spin_unlock_irqrestore(&lock, flags);
   方案3:采用信号量
          down(&sema);
          open_cnt &= ~(1 << 1); //临界区
          up(&sema);
   方案4:采用位原子操作
          clear_bit(1, &open_cnt);  
          切记:此函数的功能是不允许CPU资源切换
                它只是顺带着做了一个位清0操作
   案例:加载驱动的时候,将0x5555数据位反转
         不允许使用change_bit
4)整型原子操作特点
i.整型原子操作=整型数操作+原子=整型数操作具有原子性
      =对共享资源进行整型数操作(+/-)的过程
       不允许发生CPU资源的切换
ii.linux内核给整型原子操作提供了专有的数据类型:atomic_t
  本质是一个结构体(struct),但是类比成int类型
iii.linux内核给整型原子操作提供了对应的函数,这些函数调用的时候,不会发生CPU资源的切换:
    atomic_add/atomic_sub/atomic_inc/atomic_dec/atomic_read/
    atomic_set/atomic_return/atomic_test/...一堆的组合函数
  Vi.利用整型原子操作解决竞态问题的编程步骤:
  实际开发,先根据用户需求编写驱动的基本逻辑和功能,然后检查驱动代码中是否有竞态问题。首先确定驱动代码中哪些是共享资源,再确定驱动代码中哪些是临界区,最后确定临界区中对共享资源的操作是否是整型数操作(+/-/++/--),如果是整型操作,可以考虑使用整型原子操作方法解决竞态问题。
  (6)如果采用整型原子操作,把原先int/long/unsinged int/unsigned long
    char/short/unsigned char/unsigned short类型的共享资源替换成atomic_t类型的整型原子变量。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值