Можно ли читать и проверять общую память без мьютексов? - PullRequest
6 голосов
/ 31 марта 2010

В Linux я использую shmget и shmat для настройки сегмента общей памяти, в который будет записывать один процесс и из которого будет считываться один или несколько процессов. Размещаемые данные имеют размер несколько мегабайт, а при обновлении они полностью переписываются; оно никогда частично не обновляется.

Мой сегмент общей памяти выложен следующим образом:

    -------------------------
    | t<sub>0</sub> | actual data | t<sub>1</sub> |
    -------------------------

, где t 0 и t 1 - копии времени, когда устройство записи начало свое обновление (с достаточной точностью, чтобы последующие обновления гарантированно имели разное время). Автор сначала записывает в t 1 , затем копирует данные, затем записывает в t 0 . С другой стороны, считыватель читает t 0 , затем данные, затем t 1 . Если считыватель получает одно и то же значение для t 0 и t 1 , то он считает данные непротиворечивыми и действительными, если нет, он пытается снова.

Гарантирует ли эта процедура, что, если читатель считает, что данные действительны, то на самом деле это так?

Нужно ли беспокоиться о выполнении вне очереди (OOE)? Если да, сможет ли считыватель, использующий memcpy, получить весь сегмент совместно используемой памяти, преодолеть проблемы OOE на стороне считывателя? (Предполагается, что memcpy выполняет копирование линейно и по возрастанию через адресное пространство. Это предположение верно?)

Ответы [ 2 ]

5 голосов
/ 01 апреля 2010

Современное оборудование на самом деле совсем не последовательно. Таким образом, это не гарантируется, если вы не выполните барьеры памяти в соответствующих местах. Необходимы барьеры, потому что архитектура реализует более слабую модель согласованности совместно используемой памяти, чем последовательная согласованность. Это как таковое не имеет никакого отношения к конвейерной обработке или OoO, но позволяет нескольким процессорам эффективно получать доступ к системе памяти параллельно. Смотрите, например Модели согласованности совместно используемой памяти: учебное пособие . В однопроцессорном режиме вам не нужны барьеры, потому что весь код выполняется последовательно на этом одном процессоре.

Кроме того, нет необходимости иметь два поля времени, вероятно, лучше выбрать счетчик последовательности, поскольку не нужно беспокоиться о том, что два обновления настолько близки, что они получают одинаковую временную метку, а обновление счетчика происходит намного быстрее. чем получить текущее время. Кроме того, нет никакой вероятности того, что часы перемещаются во времени назад, что может произойти, например, когда ntpd настраивается на смещение часов. Хотя эту последнюю проблему можно преодолеть в Linux с помощью clock_gettime (CLOCK_MONOTONIC, ...). Другое преимущество использования счетчиков последовательности вместо меток времени состоит в том, что вам нужен только один счетчик последовательности. Модуль записи увеличивает счетчик как до записи данных, так и после завершения записи. Затем считыватель считывает порядковый номер, проверяет его четность и, если да, считывает данные, и, наконец, снова считывает порядковый номер и сравнивает с первым порядковым номером. Если порядковый номер нечетный, это означает, что выполняется запись, и нет необходимости читать данные.

Ядро Linux использует блокирующий примитив с именем seqlocks , который выполняет что-то подобное вышеописанному. Если вы не боитесь «заражения GPL», вы можете зайти в Google; Таким образом, это тривиально, но хитрость заключается в том, чтобы правильно преодолеть барьеры.

2 голосов
/ 02 апреля 2010

Джо Даффи дает точно такой же алгоритм и называет его: "Масштабируемая схема чтения / записи с оптимистичным повторением" .

Это работает.
Вам нужно два поля порядкового номера.

Вам необходимо прочитать и записать их в обратном порядке.
Возможно, вам понадобится установить барьеры памяти, в зависимости от гарантий упорядочения памяти системы .

В частности, вам нужно прочитать и сохранить семантику выпуска для читателей и писателей, когда они получают доступ к t 0 или t 1 для чтения и записи соответственно.

Какие инструкции необходимы для достижения этой цели, зависит от архитектуры.Например, на x86 / x64, из-за относительно сильных гарантий в этом конкретном случае вообще не требуется никаких машинно-специфических барьеров *.

*, все же необходимо убедиться, что компилятор/ JIT не связывается с загрузками и хранением, например, с использованием volatile (что имеет другое значение в Java и C #, чем в ISO C / C ++. Однако компиляторы могут отличаться. Например, при использовании VC ++ 2005 или выше с volatile оно будетВыполняйте вышеперечисленные действия безопасно. См. раздел *1029* «Microsoft Specific» *1029* . Это можно сделать и с другими компиляторами на x86 / x64. Извлекаемый код сборки должен быть проверен, и необходимоубедитесь, что доступ к t 0 и t 1 не исключен и не перемещен компилятором.)

Какпримечание: если вам когда-нибудь понадобится MFENCE, lock or [TopOfStack],0 может быть лучшим вариантом, в зависимости от ваших потребностей .

...