Как используется барьер памяти в ядре Linux

В исходном коде ядра есть документация /memory-barriers.txt, например:

    CPU 1                   CPU 2
    ======================= =======================
            { B = 7; X = 9; Y = 8; C = &Y }
    STORE A = 1
    STORE B = 2
    <write barrier>
    STORE C = &B            LOAD X
    STORE D = 4             LOAD C (gets &B)
                            LOAD *C (reads B)

Без вмешательства ЦП 2 может воспринимать события на ЦП 1 в некотором фактически случайном порядке, несмотря на барьер записи, создаваемый ЦП 1:

    +-------+       :      :                :       :
    |       |       +------+                +-------+  | Sequence of update
    |       |------>| B=2  |-----       --->| Y->8  |  | of perception on
    |       |  :    +------+     \          +-------+  | CPU 2
    | CPU 1 |  :    | A=1  |      \     --->| C->&Y |  V
    |       |       +------+       |        +-------+
    |       |   wwwwwwwwwwwwwwww   |        :       :
    |       |       +------+       |        :       :
    |       |  :    | C=&B |---    |        :       :       +-------+
    |       |  :    +------+   \   |        +-------+       |       |
    |       |------>| D=4  |    ----------->| C->&B |------>|       |
    |       |       +------+       |        +-------+       |       |
    +-------+       :      :       |        :       :       |       |
                                   |        :       :       |       |
                                   |        :       :       | CPU 2 |
                                   |        +-------+       |       |
        Apparently incorrect --->  |        | B->7  |------>|       |
        perception of B (!)        |        +-------+       |       |
                                   |        :       :       |       |
                                   |        +-------+       |       |
        The load of X holds --->    \       | X->9  |------>|       |
        up the maintenance           \      +-------+       |       |
        of coherence of B             ----->| B->2  |       +-------+
                                            +-------+
                                            :       :

Я не понимаю, так как у нас есть барьер записи, поэтому любое хранилище должно вступать в силу при выполнении C = &B, что означает, что B будет равно 2. Для CPU 2 B должно быть равно 2, когда он получает значение C, который является & B, почему он воспринимает B как 7. Я действительно запутался.

2 ответа

Решение

Ключевой отсутствующей точкой является ошибочное предположение, что для последовательности:

LOAD C (gets &B)
LOAD *C (reads B)

первая нагрузка должна предшествовать второй загрузке. В слабо упорядоченных архитектурах может действовать "как будто" следующее:

LOAD B (reads B)  
LOAD C (reads &B)
if( C!=&B ) 
    LOAD *C
else
    Congratulate self on having already loaded *C

Спекулятивное "LOAD B" может произойти, например, потому что B находился в той же строке кэша, что и некоторая другая переменная, интересующая ранее, или аппаратная предварительная выборка захватила его.

Из раздела документа под названием "ЧТО НЕ МОЖЕТ БЫТЬ ПРЕДПОЛАГАЕТСЯ О БАРЬЕРАХ ПАМЯТИ?":

Нет никакой гарантии, что какой-либо из обращений к памяти, указанных до того, как барьер памяти будет завершен завершением инструкции барьера памяти; можно считать, что барьер рисует линию в очереди доступа этого ЦП, которая не может пересекаться при доступе соответствующего типа.

а также

Нет гарантии, что ЦП увидит правильный порядок эффектов при обращении ко второму ЦП, даже если второй ЦП использует барьер памяти, если только первый ЦП не использует соответствующий барьер памяти (см. Подраздел "Сопряжение SMP-барьеров").).

Что делает барьеры памяти (очень упрощенным способом, конечно), так это удостоверяется, что ни компилятор, ни аппаратное обеспечение в ЦП не выполняют каких-либо хитрых попыток переупорядочить операции загрузки (или сохранения) через барьер, и что ЦП правильно воспринимает изменения в память, сделанная другими частями системы. Это необходимо, когда нагрузки (или хранилища) имеют дополнительное значение, например, блокировка блокировки перед тем, как получить доступ к тому, что мы блокируем. В этом случае, позволяя компилятору / ЦП сделать доступ более эффективным, переупорядочив их, опасно для правильной работы нашей программы.

При чтении этого документа нам нужно помнить две вещи:

  1. Под нагрузкой подразумевается передача значения из памяти (или кэша) в регистр ЦП.
  2. Что если процессоры не разделяют кеш (или вообще не имеют кеша), то их кеш-системы могут на мгновение быть нашей синхронизацией.

Факт № 2 является одной из причин, по которой один процессор может воспринимать данные иначе, чем другой. Хотя системы кэширования предназначены для обеспечения хорошей производительности и согласованности в общем случае, но в некоторых случаях может потребоваться некоторая помощь, как показано в документе.

В целом, как следует из документа, барьеры в системах, включающих более одного ЦП, должны быть спарены, чтобы заставить систему синхронизировать восприятие обоих (или всех участвующих) ЦП. Представьте ситуацию, в которой один ЦП завершает загрузку или сохранение и обновляет основную память, но новые данные еще не были переданы в кэш второго ЦП, что приводит к недостаточной согласованности между обоими ЦП.

Надеюсь, это поможет. Я бы посоветовал прочесть memory-barriers.txt еще раз, помня об этом, и в частности раздел "Эффекты кэша процессора".

Другие вопросы по тегам