我是否需要在关键部分使用volatile关键字进行内存访问?

时间:2014-11-13 21:37:58

标签: c volatile producer-consumer critical-section memory-barriers

我正在使用gcc为单处理器32位微控制器编写代码。

我需要从链表中消耗带时间戳的对象。代码的另一部分可以是异步的(可能在ISR中)将它们添加到列表中。

通过关闭中断并使用barrier()功能来实现关键部分。

我很困惑gcc优化可以通过缓存指向列表项的指针(下一个要删除的最新项目,列表头或空闲列表)来破坏我的代码。我不希望while循环中的任何内容从前一个循环中缓存。内存屏障是否会保护我免受编译器决定在函数启动时加载一次指针而永远不会重新加载它?所有这些列表指针都可以在生产者代码的临界区中修改(未示出)。我试图了解pqueue_first是否应该是一个易失性指针,例如。

据推测,如果没有循环(这是添加到列表的情况),如果函数中的所有代码都在关键部分,我可以吗?

请不要只指向一些关于易失性或关键部分的通用文章,因为我已经阅读了很多这些部分,但是我很难看到如何将它应用于这个特定的代码。我知道volatile确保编译器每次引用时都会重新加载变量。但我不明白优化的可能范围及其与记忆障碍的相互作用。

typedef struct {
    EV_EventQueueEntry_t *pqueue_alloc; // allocation (never changes)
    EV_EventQueueEntry_t *pqueue_head; // head of active queue (ISR can change it)
    EV_EventQueueEntry_t *pqueue_free; // head of free list (ISR can change it)
    EV_EventQueueEntry_t *pqueue_first; // soonest item in queue (ISR can change it)
    EV_EventQueueEntry_t *pqueue_first_prev; // back pointer from soonest item (ISR can change it)
    EV_UInt_t max_event_count;
} EV_EventQueue_t;

void RunLoop(EV_EventQueue_t *pev)
{
    while(not timeout)
    {
        // Enter critical section
        disable_interrupts();
        barrier();

        // item with most recent timestamp
        // this can be changed by ISR add to queue operation
        EV_EventQueueEntry_t *pfirst = pev->pqueue_first;

        if(pfirst!=NULL && EV_PortIsFutureTime(pfirst->event.timestamp, EV_PortGetTime()))
        {
            // Copy out message
            EV_Event_t e = pfirst->event;

            // Remove event from queue
            if(pev->pqueue_first_prev != NULL)
                pev->pqueue_first_prev->pnext = pfirst->pnext;
            else
                pev->pqueue_head = pfirst->pnext;

            // Put event back on free list
            pfirst->pnext = pev->pqueue_free;
            pev->pqueue_free = pfirst;
            pfirst->event.message.type = EV_MESSAGE_NULL;

            // Find next soonest message to process after this one
            pev->pqueue_first = ...;
            pev->pqueue_first_prev = ...; // back pointer

            // Exit critical section
            barrier();
            enable_interrupts();

            // Dispatch message
            ...
        }
        else
        {
            // Exit critical section
            barrier();
            enable_interrupts();

            // waste some time
            ...
        }
    }
}

2 个答案:

答案 0 :(得分:5)

C ++ 11具有以下标准功能:std::atomic_signal_fence。 C11具有类似的功能,没有名称空间限定符。如果您的程序仅使用单个线程并且您只是试图阻止编译器跨越围栏移动加载/存储,那么它是合适的。在关键部分之前使用std::atomic_signal_fence(memory_order_acquire),在关键部分之后使用std:atomic_signal_fence(memory_order_release)

如果您不使用C ++ 11或C11,但只是使用gcc或了解gcc asms的编译器,则可以使用__asm__ __volatile__ ("": : :"memory")作为编译器障碍。 asm表示无法将其删除并威胁以神秘的方式修改内存,因此编译器无法在其上移动加载/存储。

答案 1 :(得分:1)

单词volatile指示编译器从内存而不是从缓存中检索值,并将值存储在内存中而不是缓存中。当多个内核可以在同一个内存上运行时,可以完成此操作,因此无需保证缓存是新鲜的。

在您的情况下,ISR将修改内存,因此它访问的任何变量都应标记为volatile。它不是真正的编译器优化会导致问题。问题是由ISR发生时发生的上下文切换引起的。处理器的状态(寄存器值)被存储并在ISR之后被恢复,这意味着寄存器中的值将是它们在ISR之前的值。