临界区访问

为什么从临界区开始分析呢?因为在RT-Thread的很多代码,比如线程调度、互斥锁、信号量等等,都会使用到临界区,所以要先掌握临界区的实现的原理,再回过头看线程调度、互斥锁这些模块才能更加得心应手。这里提到的临界区资源主要是多线程间共同访问的资源(不可以在中断中访问)。临界区主要提供下面两个函数,本文也是主要分析这两个函数的实现原理:

rt_base_t rt_enter_critical(void);
void rt_exit_critical(void);

1 单核场景下的临界区实现原理

1.1 rt_enter_critical分析

对于多线程访问临界区就是希望把并行访问,变成串行访问。当上个线程访问完毕后,下个线程才能访问。RT-Thread的方案也是比较简单,直接在访问临界区时,直接把线程调度关闭。关闭线程调度就需要操作线程锁(线程锁是个全局变量,访问它也是访问临界区)。

rt_enter_criticalrt_exit_critical函数支持嵌套的,并且必须要成对出现才行。在单核场景下rt_enter_critical的实现相对比较简单:

rt_base_t rt_enter_critical(void)
{
    rt_base_t level;
    rt_base_t critical_level;

    /* disable interrupt */
    level = rt_hw_interrupt_disable();

    /*
     * the maximal number of nest is RT_UINT16_MAX, which is big
     * enough and does not check here
     */
    rt_scheduler_lock_nest ++;
    critical_level = rt_scheduler_lock_nest;

    /* enable interrupt */
    rt_hw_interrupt_enable(level);

    return critical_level;
}
  1. level = rt_hw_interrupt_disable();关闭中断,不响应中断

  2. rt_scheduler_lock_nest ++;,关闭线程调度。临界区资源一般是多线程之间的资源

  3. rt_hw_interrupt_enable(level);打开中断

这里关闭中断的原因其实就是为了执行rt_scheduler_lock_nest ++;,因为线程的调度是基于sys tick中断触发的。关闭中断后,执行rt_scheduler_lock_nest ++;,这是内核将不在调度线程。线程的调度是当rt_scheduler_lock_nest 等于0时,才会执行调度的。 因此在单核硬件系统中,中断中不可以访问临界资源。因为只有一个核,所以无法在访问临界资源时,关闭中断,会影响系统的实时性。

void rt_schedule(void)
{
    rt_base_t level;
    struct rt_thread *to_thread;
    struct rt_thread *from_thread;
    /* using local variable to avoid unecessary function call */
    struct rt_thread *curr_thread = rt_thread_self();

    /* disable interrupt */
    level = rt_hw_interrupt_disable();

    /* check the scheduler is enabled or not */
    if (rt_scheduler_lock_nest == 0)
    {
       .... // 选取优先级最高的task进行调度。该部分代码在此省略,在分析调度时再做详细分析
    }

    /* enable interrupt */
    rt_hw_interrupt_enable(level);

__exit:
    return;
}

1.2 rt_exit_critical分析

rt_exit_critical的代码逻辑实际上和rt_enter_critical的逻辑是相反的,rt_enter_critical是为了能够安全访问临界区,而禁止线程调度。所以rt_exit_critical则是打开线程调度锁,判断是否能够进行任务调度。打开线程调度锁则是执行rt_scheduler_lock_nest--操作,当rt_scheduler_lock_nest等于0时,此时可以进行任务调度。

void rt_exit_critical(void)
{
    rt_base_t level;

    /* disable interrupt */
    level = rt_hw_interrupt_disable();

    rt_scheduler_lock_nest --;
    if (rt_scheduler_lock_nest <= 0)
    {
        rt_scheduler_lock_nest = 0;
        /* enable interrupt */
        rt_hw_interrupt_enable(level);

        if (rt_current_thread)
        {
            /* if scheduler is started, do a schedule */
            rt_schedule();
        }
    }
    else
    {
        /* enable interrupt */
        rt_hw_interrupt_enable(level);
    }
}

2 多核场景下的临界区实现原理

其实调度锁是个全局变量,某种程度上就是临界区资源。对于临界区的访问(在不使用锁的前提下)就是关闭线程调度,保证临界区访问的串行化。所以无论多核还是单核场景,关闭线程调度,都是需要关闭中断后才能操作调度锁。

多核场景下临界区的实现比单核场景下的稍微复杂些。单核场景下临界区不能在中断上下文中访问,而多核场景下临界区可能在进程(线程)上下文或者中断上下文中访问。那么可能存在下面几个问题:

  1. 在物理核0的线程A中正在访问临界区资源,然后线程A的时间片使用完,此时选取优先级最高的线程B执行,线程B也有访问临界区的资源,此时就可能导致有两个都进入了临界区。
  2. 在物理核0的线程A和物理核1的线程B都需要访问临界区,此时也可能出现两个线程同时进入临界区。
  3. 在物理核0的线程A正在访问临界区资源,此时系统接收到一个物理中断,系统保存线程A的上下文,并开始相应中断,中断处理函数也需要访问临界区,此时也出现了临界区的并发访问。

当同一个物理核的多个线程或者中断例程想访问临界区,可以按照串行访问的方式,如何串行访问呢?其实和单核场景类似:(1)关闭本核的中断,(2)关闭本核的任务调度。而多个物理核访问临界区则无法通过当前手段解决。 同样的,中断中不可以访问临界区 后续会介绍自旋锁的实现机制,在多核场景下也是通过关中断、禁止抢占来实现临界区的访问。

rt_base_t rt_enter_critical(void)
{
    rt_base_t critical_level;
    struct rt_thread *current_thread;

    rt_base_t level;
    struct rt_cpu *pcpu;

    /* disable interrupt */
    level = rt_hw_local_irq_disable();

    pcpu = rt_cpu_self();
    current_thread = pcpu->current_thread;

    if (!current_thread)
    {
        FREE_THREAD_SELF(level);
        /* scheduler unavailable */
        return -RT_EINVAL;
    }

    /* critical for local cpu */
    RT_SCHED_CTX(current_thread).critical_lock_nest++;
    critical_level = RT_SCHED_CTX(current_thread).critical_lock_nest;

    FREE_THREAD_SELF(level);

    return critical_level;
}
  1. rt_hw_local_irq_disable();禁用本核的中断

  2. critical_lock_nest++;禁止本核的任务调度

  3. FREE_THREAD_SELF(level);打开本核中断

rt_exit_critical的逻辑也是和rt_enter_critical恰恰相反:

void rt_exit_critical(void)
{
    struct rt_thread *current_thread;
    rt_bool_t need_resched;

    rt_base_t level;
    struct rt_cpu *pcpu;

    /* disable interrupt */
    level = rt_hw_local_irq_disable();
    pcpu = rt_cpu_self();
    current_thread = pcpu->current_thread;

    if (!current_thread)
    {
        FREE_THREAD_SELF(level);
        return;
    }

    /* the necessary memory barrier is done on irq_(dis|en)able */
    RT_SCHED_CTX(current_thread).critical_lock_nest--;

    /* may need a rescheduling */
    if (RT_SCHED_CTX(current_thread).critical_lock_nest == 0)
    {
        /* is there any scheduling request unfinished? */
        need_resched = IS_CRITICAL_SWITCH_PEND(pcpu, current_thread);
        CLR_CRITICAL_SWITCH_FLAG(pcpu, current_thread);

        FREE_THREAD_SELF(level);
        if (need_resched)
            rt_schedule();
    }
    else
    {
        /* each exit_critical is strictly corresponding to an enter_critical */
        RT_ASSERT(RT_SCHED_CTX(current_thread).critical_lock_nest > 0);
        FREE_THREAD_SELF(level);
    }
}
  1. RT_SCHED_CTX(current_thread).critical_lock_nest--;critical_lock_nest等于0才启用任务调度

  2. FREE_THREAD_SELF(level); 打开本核中断

posted @ 2024-07-15 15:23  cockpunctual  阅读(51)  评论(0)    收藏  举报