Ищу статьи по проблемам блокировки общей памяти

Я рассматриваю некоторый код и чувствую подозрение на используемую технику.

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

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

Поиск в интернете не привел меня к чему-то определенному в этой области.

Я сильно подозреваю, что это небезопасно или разумно, и что мне действительно нравится, так это некоторые ссылки на статьи, в которых подробно описываются проблемы.

13 ответов

Решение

Это законно - так как в ОС это не остановит вас.

Но умно ли это? Нет, у вас должен быть какой-то тип синхронизации.

Там не было бы "искалеченных битов на проводе". Они будут либо в виде единиц, либо в виде нулей. Но нечего сказать, что все ваши биты будут записаны до того, как другой процесс попытается их прочитать. И нет НИКАКИХ гарантий относительно того, как быстро они будут написаны, и как быстро они будут прочитаны.

Вы должны всегда предполагать, что нет абсолютно НИКАКОЙ связи между действиями двух процессов (или потоков в этом отношении).

Аппаратная блокировка шины уровня не происходит, если вы не понимаете это правильно. Может быть сложнее, чем ожидалось, заставить ваш компилятор / библиотеку / os / cpu сделать это правильно. Примитивы синхронизации написаны, чтобы убедиться, что это происходит правильно.

Блокировка сделает его безопасным, и это не так сложно сделать. Так что просто сделай это.


@unknown - вопрос несколько изменился с момента публикации моего ответа. Однако описанное вами поведение явно зависит от платформы (аппаратная часть, ОС, библиотека и компилятор).

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

Между этими операциями может произойти прерывание. Если переключение контекста происходит в середине, некоторые биты записаны, некоторые нет. Взрыв, ты мертв.

Также давайте подумаем о 16-битном или 64-битном процессоре. Оба они по-прежнему популярны и не обязательно работают так, как вы думаете.

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

Теперь есть способы предварительно записать ваши записи, чтобы убедиться, что вы записали целое слово. Эти методы попадают в категорию синхронизации, и создание примитивов синхронизации - это то, что лучше всего оставить разработчикам библиотек, компиляторов, ОС и аппаратного обеспечения. Особенно, если вы заинтересованы в переносимости (которой вы должны быть, даже если вы никогда не переносите свой код)

Проблема на самом деле хуже, чем некоторые люди обсуждали. Zifre прав в том, что в современных процессорах x86 записи в память являются атомарными, но это быстро перестает иметь место - записи в память являются атомарными только для одного ядра - другие ядра могут не видеть записи в том же порядке.

Другими словами, если вы делаете

a = 1;
b = 2;

на CPU 2 вы можете увидеть местоположение b изменено до того, как местоположение 'a' Кроме того, если вы записываете значение, которое больше, чем собственный размер слова (32 бита на процессоре x32), записи не являются атомарными - поэтому старшие 32 бита 64-битной записи попадут на шину в другое время по сравнению с низким 32 бита записи. Это может очень усложнить ситуацию.

Используйте барьер памяти, и вы будете в порядке.

Тебе нужно где-нибудь запереться. Если не на уровне кода, то на аппаратном кеше памяти и на шине.

Вы, вероятно, в порядке на процессоре Intel после PentiumPro. Из того, что я только что прочитал, Intel сделала свои более поздние процессоры практически игнорирующими префикс LOCK для машинного кода. Вместо этого протоколы когерентности кэша обеспечивают согласованность данных между всеми процессорами. Поэтому, если код записывает данные, которые не пересекают границу строки кэша, он будет работать. Порядок памяти записывает, что перекрестные строки кэша не гарантированы, поэтому многословные записи рискованны.

Если вы используете что-то другое, чем x86 или x86_64, то вы не в порядке. Многие не-Intel процессоры (и, возможно, Intel Itanium) повышают производительность, используя явные машинные команды когерентности кэша, и если вы не используете их (через пользовательский код ASM, встроенные функции компилятора или библиотеки), то запись в память через кэш не гарантируется когда-либо стать видимым для другого процессора или происходить в каком-то определенном порядке.

То, что в вашей системе Core2 что-то работает, не означает, что ваш код правильный. Если вы хотите проверить переносимость, попробуйте свой код также на других архитектурах SMP, таких как PPC (более старый MacPro или блейд Cell), Itanium, IBM Power или ARM. Alpha был отличным процессором для выявления плохого SMP-кода, но я сомневаюсь, что вы найдете его.

Два процесса, два потока, два процессора, два ядра требуют особого внимания при обмене данными через память.

Эта статья IBM предоставляет отличный обзор ваших возможностей.

Анатомия методов синхронизации Linux. Атомика ядра, спин-блокировки и мьютексы. Автор M. Tim Jones (mtj@mtjones.com), инженер-консультант, Emulex

http://www.ibm.com/developerworks/linux/library/l-linux-synchronization.html

Я на самом деле считаю, что это должно быть абсолютно безопасно (но это зависит от точной реализации). Предполагая, что "главный" сегмент - это в основном массив, если shmid может быть записан атомарно (если он 32-битный, то, вероятно, все в порядке), а второй процесс просто читает, у вас все должно быть в порядке. Блокировка необходима только тогда, когда пишутся оба процесса, или записываемые значения не могут быть записаны атомарно. Вы никогда не получите испорченный (наполовину записанные значения). Конечно, могут быть некоторые странные архитектуры, которые не могут справиться с этим, но на x86/x64 все должно быть в порядке (и, вероятно, также ARM, PowerPC и другие распространенные архитектуры).

Порядок чтения памяти в современных микропроцессорах, часть I и часть II

Они дают представление о том, почему это теоретически небезопасно.

Вот потенциальная гонка:

  • Процесс A (на ядре процессора A) выполняет запись в новую область общей памяти
  • Процесс A помещает этот идентификатор совместно используемой памяти в общую 32-разрядную переменную (которая выровнена по 32-разрядной схеме - любой компилятор будет пытаться выполнить выравнивание таким образом, если вы позволите это).
  • Процесс B (на ядре процессора B) читает переменную. Предполагая 32-битный размер и 32-битное выравнивание, на практике не должно быть мусора.
  • Процесс B пытается читать из области общей памяти. Теперь нет гарантии, что он увидит записанные данные, потому что вы пропустили барьер памяти. (На практике, вероятно, в процессоре B в коде библиотеки, который отображает сегмент общей памяти, были барьеры памяти; проблема в том, что процесс A не использовал барьер памяти).

Кроме того, неясно, как вы можете безопасно освободить область общей памяти с этим дизайном.

С последним ядром и libc вы можете поместить мьютекс pthreads в область общей памяти. (Для этого нужна последняя версия с NPTL - я использую Debian 5.0 "lenny", и она отлично работает). Простая блокировка общей переменной означает, что вам не нужно беспокоиться о проблемах с загадочным барьером памяти.

Правовой? Я полагаю. Зависит от вашей "юрисдикции". Сейф и вменяемый? Почти наверняка нет.

Изменить: я буду обновлять это с дополнительной информацией.

Возможно, вы захотите взглянуть на эту страницу Википедии; в частности, раздел "Координация доступа к ресурсам". В частности, обсуждение в Википедии, по сути, описывает провал доверия; неблокированный доступ к общим ресурсам может, даже для атомарных ресурсов, привести к искажению или искажению уверенности в том, что действие было выполнено. По существу, в промежуток времени между проверкой, чтобы увидеть, МОЖЕТ ли он изменить ресурс, ресурс изменяется внешне, и, следовательно, доверие, присущее условной проверке, нарушается.

Я согласен, что это может сработать - так что это может быть безопасно, но не в здравом уме. Основной вопрос заключается в том, нужен ли этот низкоуровневый общий доступ - я не эксперт по Linux, но я бы подумал использовать, например, очередь FIFO для основного сегмента разделяемой памяти, чтобы операционная система работала за вас., Потребителю / производителю обычно все равно нужны очереди для синхронизации.

Я не могу поверить, что ты спрашиваешь это. НЕТ, это не обязательно обязательно. По крайней мере, это будет зависеть от того, будет ли компилятор генерировать код, который будет атомарно устанавливать расположение в общей памяти при установке shmid.

Я не знаю Linux, но подозреваю, что значение shmid составляет от 16 до 64 бит. Это означает, что, по крайней мере, возможно, что на всех платформах будет какая-то инструкция, которая могла бы написать это значение атомарно. Но вы не можете полагаться на то, что компилятор сделает это без какого-либо запроса.

Детали реализации памяти являются одними из самых специфичных для платформы вещей!

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

Я не верю, что кто-то здесь обсуждал, сколько конфликтов может иметь место из-за ударной блокировки по шине, особенно в шинах с ограниченными системами.

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

Более современные процессорные ABI предоставляют альтернативные операции, чем простая блокировка.

Джеффр из FreeBSD (автор многих внутренних компонентов ядра) обсуждает монитор и mwait, 2 инструкции, добавленные для SSE3, где в простом тестовом примере было выявлено улучшение на 20%. Он позже постулирует;

Итак, теперь это первая стадия в адаптивном алгоритме, мы немного вращаемся, затем спим в состоянии высокой мощности, а затем спим в состоянии низкой мощности в зависимости от нагрузки.

...

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

Интересно, каков будет эффект использования паузы вместо hlt?

От Intel's TBB;

        ALIGN 8
        PUBLIC __TBB_machine_pause
__TBB_machine_pause:
L1:
        dw 090f3H; pause
        add ecx,-1
        jne L1
        ret
end

Art of Assembly также использует синхронизацию без использования префикса блокировки или xchg. Я давно не читал эту книгу и не буду говорить непосредственно о ее применимости в контексте SMP в режиме защиты прав пользователя, но это стоит посмотреть.

Удачи!

Похоже, вам нужен замок для чтения-записи: http://en.wikipedia.org/wiki/Readers-writer_lock.

Если у шмида есть какой-то другой тип, кроме volatile sig_atomic_t тогда вы можете быть уверены, что отдельные потоки будут иметь проблемы даже на одном и том же процессоре. Если тип volatile sig_atomic_t тогда вы не можете быть в этом уверены, но вам все же может повезти, потому что многопоточность может выполнять больше чередования, чем сигналы.

Если shmid пересекает строки кэша (частично в одной строке кэша и частично в другой), то во время записи процессора записи вы наверняка обнаружите, что процессор чтения читает часть нового значения и часть старого значения.

Именно поэтому были изобретены такие инструкции, как "сравнить и поменять местами".

Ответ - абсолютно безопасно делать чтение и запись одновременно.

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

Из статьи " Общая память в Linux".

Последняя версия Linux shm просто использует copy_to_user а также copy_from_user звонки, которые синхронизируются с шиной памяти внутри.

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