面试题:单片机裸机和RTOS开发过程中,如何保证全局变量在中断和主循环中读写的正确性

    这个面试题时考察的关键字volatile, 临界区,原子操作和锁的概念, 因此首先需要搞清楚这几个知识点以及使用方法。

1、关键字volatile

         关键字volatile时告诉编译器,被关键字volatile修饰的变量可能会被意外的改变,防止编译器对代码进行优化。举个具体的例子说明这个问题:

         比如在某个任务中设置了一个变量g_counter用于计数。

 g_counter = 0x01;
 g_counter = 0x02;
 g_counter = 0x03;

        执行上述3条语句,如果变量g_counter没有用关键字volatile修饰,那么编译器有可能对它进行优化,只编译最后一条语句g_counter=0x03;忽略前面两条语句,只产生一条汇编代码。

        如果在变量声明的时候使用volatile关键字的话,编译器就会将三条语句逐条编译产生3条相应的机器代码。也就是说,编译器在编译这个变量的语句时必须每次都重新读取这个变量的值,而不是

使用保存在寄存器中的备份。一般在下面几个情况在声明的时候都需要用关键字volatile进行修饰:

  1. 并行设备的硬件寄存器(如:状态寄存器)
  2. 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
  3. 多线程应用中被几个任务共享的变量

2、临界区

      指的时一个访问共用资源(例如:共用设备或这共用的缓存区)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。临界区的代码保护一般有下面代码中的方式:

#define    _DINT()    disable_interrupt()        // 关闭中断
#define    _EINT()    enable_interrupt()         // 打开中断

void EnterCritical(unsigned int *SR_Val)
{
    *SR_Val = _get_SR_register();     // 保存中断状态
     _DINT();                                // 禁止中断,进入临界区。这里要考虑:进入临界区之前是什么状态,如果本来就是禁止中断的呢?
}

void ExitCritical(unsigned int * SR_Val)
{
    if(*SR_Val & GIE)                   // 判断进入临界区前的状态,如果是使能中断的状态,则开启中断。
          _EINT();
}

void Function_A(void)
{
    ....
     _DINT();     // 进入临界代码区,关闭中断;
     ......
     ......                                 // 临界区代码
     _EINT();       // 退出临界区,开启中断;
     .....
}

void Function_B(void)
{
    unsigned int GIE_Val;
    ....
     EnterCritical(&GIE_Val);     // 进入临界代码区,保存当前中断状态在GIE_Val变量中;
     ......
     ......                                 // 临界区代码
     ExitCritical(&GIE_Val);       // 退出临界区,并根据GIE_Val变量决定是否开中断;
     .....
}

第一种方法,开关中断保护法。优点:简单,执行速度快(只有一条指令),在临界保护操作频繁的场合优势突出。
缺点:存在一个隐患:如果在A函数的临界代码区调用了另一个函数B,B函数内也有临界代码区,从B函数返回时,中断被打开了。这将造成A函数后续代码失去临界保护。

第二种方法,关中断前将总中断允许控制位状态保存到一个变量里,然后再关中断保护临界代码,之后根据保存的控制字决定是否恢复中断。
这样做,同样可以实现退出临界区时恢复进入前的中断允许状态。
缺点:每一段临界代码都要额外耗费两个字节的存储空间。

3、原子操作

     原子操作可以理解为不被打断的操作,可以是一个步骤的操作,也可以是多个步骤的操作,总之确保操作不能被打断。

     在多进程(线程)的操作系统中不能被其它进程(线程)打断的操作就叫原子操作,文件的原子操作是指操作文件时的不能被打断的操作。原子操作是不可分割的,在执行过程中不会被任何其它任务或事件中断。

      linux内核提供了一系列函数来实现内核中的原子操作,这些函数又分为两类,分别针对位和整型变量进行原子操作。它们的共同点是在任何情况下操作都是原子的,内核代码可以安全地调用它们而不被打断。位和整型变量原子操作都依赖底层CPU的原子操作实现,因此所有这些函数都与CPU的架构密切相关。

4、锁
     在linux中,实现文件上锁的函数有lock和fcntl,其中lock用于对文件施加建议性锁,而fcntl不仅可以施加建议性锁,还可以施加强制锁。同时,fcntl还能对文件的莫一记录进行上锁,也即是记录锁。记录锁分为读取锁(共享锁)和写入锁(排斥锁)。
  自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,不需要自旋锁)。
  自旋锁最多只能被一个内核任务持有,如果一个内核任务试图请求一个已被争用(已经被持有)的自旋锁,那么这个任务就会一直进行忙循环——旋转——等待锁重新可用。要是锁未被争用,请求它的内核任务便能立刻得到它并且继续进行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。
  事实上,自旋锁的初衷就是:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话, 最好使用信号量。
  自旋锁的基本形式如下:
  spin_lock(&mr_lock);
  //临界区
  spin_unlock(&mr_lock);
  因为自旋锁在同一时刻只能被最多一个内核任务持有,所以一个时刻只有一个线程允许存在于临界区中。这点很好地满足了对称多处理机器需要的锁定服务。在单处理器上,自旋锁仅仅当作一个设置内核抢占的开关。如果内核抢占也不存在,那么自旋锁会在编译时被完全剔除出内核。
  简单的说,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。另外自旋锁不允许任务睡眠(持有自旋锁的任务睡眠会造成自死锁——因为睡眠有可能造成持有锁的内核任务被重新调度,而再次申请自己已持有的锁),它能够在中断上下文中使用。
  死锁:假设有一个或多个内核任务和一个或多个资源,每个内核都在等待其中的一个资源,但所有的资源都已经被占用了。这便会发生所有内核任务都在相互等待,但它们永远不会释放已经占有的资源,于是任何内核任务都无法获得所需要的资源,无法继续运行,这便意味着死锁发生了。自死琐是说自己占有了某个资源,然后自己又申请自己已占有的资源,显然不可能再获得该资源,因此就自缚手脚了

5、全局变量的使用及保护

     单片机裸机编程,使用全局变量时,一般是一个或多个*.c文件(模块)中会使用到某个全局变量,假设这个全局变量g_var,另外中断中也会使用到这个变量。这时在使用这个变量时,要考虑这个变量的安全性。单片机裸机编程时前后台系统。

首先要明白大循环(后台)一般时while(1)中对这个变量的访问是依次访问的,不管全局变量g_var是在哪个模块或者*.c文件中,每个时刻只有一个地方对变量g_var进行访问。然后中断和中断嵌套程序中也会对这个全局变量访问。这样会存在隐患,当大循环(后台)在访问全局变量g_var时(比如说访问到一半时),突然中断(前台)程序修改了这个全局变量g_var,再返回大循环(后台)程序对全局变量g_var访问,就会导致访问到的g_var已经不是我们所需要的数据了。从而导致程序运行不正常。

因此,只要在大循环(后台)访问g_var时,不让中断(前台)打断其访问即可。确保对全局变量g_var的访问是原子操作。于是可以通过关中断->访问全局变量g_var->开中断的方式。但是,有的时候,如果访问全局变量g_var的过程比较复杂,可以对全局变量做个拷贝g_var_copy,用拷贝的g_var_copy用作模块处理的数据。于是就可以用:关中断->访问全局变量g_var->副本拷贝g_var_copy->开中断->操作副本g_var_copy。

针对这种复杂的情况也可以加入锁的机制:

大循环(后台): 关中断->上锁->开中断->访问全局变量g_var->关中断->解锁->开中断

中断(前台):如果是解锁,操作全局变量g_var;如果是上锁,就不操作。

当然,如果访问全局变量g_var本身就是一个原子操作(比如一条指令就可以访问完成),这样也就不需要开关中断了。

while(1)
{
    _DINT();
    
    lock = 1;  //上锁

    //访问全局变量g_var
    ...
    ...
    
    _EINT();

    
    _DINT();
    
    lock = 0; //解锁
    
    _EINT();
    
}


interrupt func()
{
    if (lock == 0)
    {
        //操作全局变量g_var
    }
    else
    {
        /* do other thing*/
    }
}

总结:中断里的全局变量尽量使用volatile关键字修饰,中断全局变量要原子操作访问,要时刻明白中断全局变量是临界区资源,共享访问时需要加保护。

猜你喜欢

转载自blog.csdn.net/king110108/article/details/116235300