Разделяемая память: подводные камни и практические рекомендации
· Го Комура · Разделяемая память, IPC, Конкурентность, C++, C#, Разработка Windows
Кадры изображений, результаты проверок, временные ряды логов, данные биржевого стакана, огромные буферы. Когда нужно обмениваться большими объёмами данных с низкой задержкой в пределах одной машины, разделяемая память выглядит весьма привлекательно.
Однако здесь есть небольшая опасность: разделяемая память подходит к вам под маской «быстрого IPC». На деле разделяемая память - это «IPC, который сокращает количество копирований, но взамен перекладывает на приложение ответственность за целостность данных».
- Быстро
- Гибко
- Но протокол приходится создавать самим
- А если что-то ломается, симптомы получаются эффектными
В целом это набор из этих четырёх пунктов.
В этой статье, держа в уме file mapping в Windows и shm_open / mmap в POSIX, мы разберём типичные проблемы практического использования разделяемой памяти и проектирование, снижающее частоту сбоев.
Пишете вы на C/C++ или используете MemoryMappedFile в C# - суть остаётся практически той же. 1
1. Сначала вывод (одной фразой)
Если сформулировать довольно грубо, но полезно для практики, получится так.
- Разделяемая память - это механизм, который показывает одну и ту же последовательность байтов нескольким процессам, а не саму синхронизацию 23
- Она быстра именно при обмене большими данными в пределах одной машины. Для мелких управляющих сообщений часто гораздо удобнее pipe / socket / named pipe / очередь
- В разделяемой памяти видимость и безопасность чтения - разные вопросы
volatileне стоит делать фундаментом проектирования. Атомарность, порядок и ожидание нужно рассматривать отдельно 45- Если разместить сырые указатели,
HANDLE, файловые дескрипторы,std::string,std::vector,std::mutexнапрямую, это почти наверняка аукнется позже - Данные, размещаемые в разделяемой памяти, безопаснее приводить к целым числам фиксированной ширины + явной раскладке + версионированному заголовку
- Простое размещение magic / version / size / state / generation / heartbeat в заголовке сильно меняет лёгкость расследования инцидентов
- Сложность разделяемой памяти - не в скорости, а в инициализации, времени жизни, восстановлении, правах доступа и ABI
- В Windows основа - это
CreateFileMapping/OpenFileMapping/MapViewOfFile, в POSIX -shm_open/ftruncate/mmap63 - Наименее подверженная сбоям отправная точка - SPSC-кольцевой буфер (single-producer single-consumer) или двойной буфер
Иными словами: разделяемая память быстра, но при небрежном использовании легко подхватить «болезнь ощущения, будто всё синхронизируется само собой». Избежать этого - первая и главная задача.
2. Что разделяемая память разделяет, а что - нет
Говоря упрощённо, разделяемая память - это механизм, отображающий одни и те же физические страницы в виртуальные адресные пространства нескольких процессов.
В Windows используются объект file mapping и view, в POSIX объект разделяемой памяти отображается через mmap. 273
Здесь важны два момента.
- Разделяется содержимое байтов, а не сами виртуальные адреса
- Согласованность (coherent) и синхронизированность - разные вещи
В документации Windows тоже указано, что view, созданные из одного и того же объекта file mapping, согласованы (coherent) в один и тот же момент времени. Однако это не означает, что читатель всегда может прочитать целостную, полностью обновлённую запись. 8
Например, даже если writer собирается записать
length- затем
payload - затем
ready flag
именно в этом порядке, reader, читающий без какой-либо синхронизации, может увидеть комбинацию из нового length и старого payload.
Разделяемая память автоматически это не исправляет.
Итак, разделяемая память разделяет байты. Она не разделяет смысл, порядок, уведомление о завершении и политику восстановления. Всё это приходится проектировать самостоятельно.
3. Где разделяемая память подходит, а где нет
| Ситуация | Подходит / не подходит | Причина |
|---|---|---|
| Передача крупных кадров или буферов в пределах одной машины | Подходит | Легко сократить число копирований |
| Высокочастотные показания датчиков, изображения, аудио, данные биржевого стакана и т. п. | Подходит | Легко добиться низкой задержки и высокой пропускной способности |
| Обмен только мелкими командами и ответами | Не очень подходит | Затраты на синхронизацию управления сравнительно велики |
| Обмен с другими машинами | Не подходит | Разделяемая память в основе своей рассчитана на один хост |
| Долгое сосуществование разных языков и версий | Сложно | Требует проектирования ABI и версионирования |
| Требуется также персистентность | Зависит от цели | File-backed mapping - рабочий вариант, но ответственность за персистентность и за IPC легко перемешать |
На практике очень устойчиво разделение управление через сообщения, данные - через разделяемую память. Например:
- процесс UI уведомляет процесс worker «используй следующий кадр» через event / pipe / socket;
- сам кадр данных лежит в разделяемой памяти.
Такая конфигурация обычно спокойная.
4. Четыре вещи, которые нужно решить в первую очередь
При проектировании разделяемой памяти в первую очередь нужно решить следующие четыре вещи.
4.1 Разделить control plane и data plane
Заранее решить, что именно размещается в разделяемой памяти.
- data plane: изображения, аудио, последовательности записей, объёмные данные
- control plane: запуск, остановка, ошибки, переподключение, повторная инициализация, уведомления
Одно только это разделение заметно упрощает проектирование стороны разделяемой памяти.
4.2 Сузить модель конкурентности
- SPSC: 1 producer / 1 consumer
- MPSC: несколько writer-ов / 1 consumer
- SPMC: 1 writer / несколько reader-ов
- MPMC: несколько writer-ов / несколько reader-ов
Сложность возрастает примерно в этом порядке. Сразу браться за MPMC - довольно смелое решение. Обычно позже вылезают призраки порядка памяти.
4.3 Определить владельца и время жизни
- Кто создаёт
- Кто инициализирует
- Кто удаляет
- Кто восстанавливает, если участник аварийно завершился на середине работы
Если это остаётся неопределённым, при каждом изменении порядка запуска или перезапуске обстановка мутнеет.
4.4 Определить ABI и версионирование
- Раскладка
- Размеры типов
- Выравнивание
- Зарезервированные области
- Версия / флаги функций
- Наличие обратной совместимости
Разделяемая память - это разговор не об API, а об ABI (бинарном интерфейсе). Небрежность здесь оборачивается неприятным сценарием: исходники совместимы, а всё ломается только во время выполнения.
5. Частые подводные камни
5.1 Отсутствие синхронизации
Это самая частая проблема.
«Раз мы смотрим на одну и ту же память, то, что записано, наверняка можно прочитать».
Иногда так и есть. Но это не значит, что прочитать можно в правильный момент, правильной единицей и в правильном порядке.
И в Windows, и в POSIX доступ к разделяемой памяти предполагает сочетание с отдельным средством синхронизации. В документации Windows тоже написано, что доступ к общим view нужно координировать через mutex / semaphore / event. 2 В материалах по POSIX тоже указано, что доступ к разделяемой памяти требует синхронизации. 9
5.2 Попытка решить всё через volatile
volatile - не волшебное средство, спасающее проектирование разделяемой памяти.
Как минимум atomicity и mutual exclusion - разные вопросы. 45
Например, проектирование, при котором размещают volatile bool ready; и следят за ним в busy loop:
- впустую расходует CPU;
- оставляет неопределённой гарантию порядка между payload и ready;
- не является переносимым;
- легко ловит промежуточные состояния.
В целом ничего хорошего из этого не выходит.
Кроме того, WaitOnAddress в Windows предназначен для thread-ов внутри одного процесса.
Его безопаснее не рассматривать как межпроцессный механизм ожидания. 10
5.3 Чтение промежуточных состояний
Когда разделяемая память даёт сбой, симптомы выглядят вполне обыденно.
- Обновился только заголовок
- Устарел только payload
- Обновилась только длина
- Пара из двух полей оказалась несогласованной
Если атомарно обновляется единственный скаляр, всё относительно просто, но если публикуется запись из нескольких полей, нужна процедура commit-а.
Обычно это одно из следующего:
- защитить всё целиком через mutex;
- использовать двойной буфер и в конце переключать «номер текущего действующего буфера»;
- использовать кольцевой буфер с state / sequence для каждого слота;
- при 1 writer / нескольких reader-ах брать снимки через счётчик последовательности (sequence counter).
Даже «выставить ready flag последним» - недостаточно зрелое решение, пока не определено, в каком порядке памяти этот флаг записывается и читается. В разделяемой памяти сам момент публикации и есть протокол.
5.4 Размещение указателей или сложных объектов «как есть»
Ещё один частый паттерн.
- Сырые указатели
HANDLE- Файловые дескрипторы
std::stringstd::vectorstd::unordered_mapstd::mutexCRITICAL_SECTION
Их размещают в разделяемой памяти как есть и пытаются использовать из другого процесса. Обычно начинается небольшой ад.
Причина проста: виртуальные адреса и process-local ресурсы имеют смысл только в контексте своего процесса. То же и с view в Windows: даже если тот же mapping отобразить в другом процессе, виртуальные адреса не обязательно совпадут. 711
Поэтому, если нужна ссылка, базовый подход - хранить её как offset от базового адреса.
typedef struct ShmRef {
uint64_t offset; // относительная позиция от начала сегмента
uint32_t length;
uint32_t kind;
} ShmRef;
Тогда каждый процесс может привести это к своему адресу как base + offset.
5.5 Поломка ABI
Разделяемая память - это не исходный код, а бинарный договор. То есть значение имеет каждое из следующих различий.
- Размер
int/long - Представление
bool - Базовый тип (underlying type) для
enum - Размер
wchar_t - Различия между 32-бит и 64-бит
#pragma pack- Различия компилятора / языка
- Выравнивание / padding
- Порядок байтов little-endian / big-endian
В пределах одного хоста endianness обычно совпадает, но стоит добавить поддержку ARM64 или смешанный toolchain, и рассогласование становится вполне обыденным.
Поэтому для структур, размещаемых в разделяемой памяти, настоятельно рекомендуется:
- целые числа фиксированной ширины, такие как
uint32_t/uint64_t; - явные padding / reserved-поля;
- заголовок с
version,header_size,record_size,total_size; - при необходимости
static_assert(sizeof(...)); - отсутствие нетривиальных объектов.
5.6 Гонки при инициализации
Разделяемая память легко ломается из-за предположения «раз кто-то создал, значит он же и инициализировал».
В Windows, если CreateFileMapping натыкается на уже существующее имя, он возвращает существующий объект, а GetLastError() сообщает ERROR_ALREADY_EXISTS.
Начальные страницы mapping-а, опирающегося на pagefile, изначально заполнены нулями. 8
В POSIX новый объект разделяемой памяти изначально имеет длину 0, а размер задаётся через ftruncate. Вновь выделенные байты инициализируются нулями. Создание через O_CREAT | O_EXCL атомарно. 3
Если, не зная этих различий,
- использовать сразу после open;
- не иметь флага завершения инициализации;
- позволить участникам инициализировать одновременно;
- не проверять несовпадение версий,
то результат зависит от порядка запуска и может сломаться.
Как минимум, в заголовке стоит разместить такие состояния:
INITIALIZINGREADYBROKEN
И пусть инициализирует только creator, а joiner дожидается READY.
Одна только эта дисциплина заметно успокаивает обстановку.
5.7 Отсутствие плана восстановления после аварийного завершения
Что делать, если writer падает во время обновления общих данных? Если оставить это неопределённым и выпустить в продакшн, картина при инциденте резко становится серьёзной.
Mutex в Windows становится abandoned, если владеющий thread завершается без release, и ожидающая сторона получает WAIT_ABANDONED. Это означает, что общий ресурс может находиться в неопределённом состоянии. 12
У robust mutex в POSIX тоже: когда владелец умирает, возвращается EOWNERDEAD, а после восстановления состояния вызывается pthread_mutex_consistent(). 1314
Важно здесь - не «продолжать как ни в чём не бывало». Для восстановления нужен как минимум один из следующих механизмов:
- номер поколения (generation);
- последняя зафиксированная последовательность (sequence);
- heartbeat;
- флаг dirty / clean;
- журнальный двухфазный commit;
- процедура полной повторной инициализации при повреждении.
5.8 False sharing и конкуренция за cache line
Про разделяемую память часто говорят, что она быстрая. Но если «горячие» счётчики упакованы в одну и ту же cache line, линия начинает бегать туда-сюда между процессорами, и производительность бодро падает.
Классический пример:
- producer обновляет
write_index; - consumer обновляет
read_index; - оба поля лежат в одной и той же cache line.
В этом случае помогают:
- разнесение «горячих» полей по разным cache line;
- разделение часто и редко обновляемых полей;
- ориентир «1 writer - 1 cache line».
Это заметно меняет картину само по себе. Часто упоминают выравнивание по 64 байтам - относитесь к этому как к «64 байта - распространённое, но не абсолютное значение для многих CPU».
5.9 Недооценка имён, прав доступа и безопасности
Именованная разделяемая память удобна, но небрежность с именами и правами доступа приводит к сбоям.
В Windows есть такие особенности:
- существуют пространства имён
Global\иLocal\; - для создания file mapping в
Global\за пределами session 0 требуетсяSeCreateGlobalPrivilege; - имена объектов используют общее пространство имён с event / semaphore / mutex / waitable timer / job.
Иными словами:
- кажется, что если назвать объект
"Global\\MyApp", его смогут разделять служба и desktop-приложение; - но возникает сбой из-за прав доступа;
- и вдобавок, если mutex с тем же именем уже был создан раньше, получаем
ERROR_INVALID_HANDLE.
Возникает весьма характерная для Windows путаница.
На стороне POSIX небрежность с mode или umask у shm_open тоже приводит либо к излишней открытости, либо, наоборот, к невозможности открыть объект. 3
Разделяемая память - это не «просто память, поэтому безопасно». Из любого процесса с правом на чтение она видна вполне прямолинейно. Если размещаете конфиденциальную информацию, нужно учитывать paging / swap / dump / права доступа - так же, как и для обычной памяти.
5.10 Небрежное изменение размера и обновление
Желание «немного расширить» разделяемую память впоследствии - довольно опасный запрос.
- У объекта mapping в Windows размер фиксируется при создании 8
- В POSIX тоже: если не следить за согласованностью
ftruncateиmmap, длина отображения у участников перестаёт совпадать 316
На практике безопаснее сделать размер неизменным в пределах одного поколения. Если расширение необходимо:
- создать сегмент с новой версией / именем / поколением (generation);
- переключить на него участников;
- закрыть старый сегмент.
Так частота сбоев снижается.
5.11 Попытка втиснуть в разделяемую память даже уведомления
Частый паттерн:
- записать в разделяемую память
ready = 1; - на другой стороне выполнять
while (!ready) Sleep(1);.
Поначалу это работает. Но позже возвращается в виде:
- бесполезного расхода CPU;
- дрожания задержки из-за
Sleep(1); - пропусков, которые трудно заметить;
- сложности с аккуратным написанием тайм-аутов и уведомлений о завершении.
Разделяемую память лучше держать на стороне данных, а уведомления выносить на примитивы, которые можно ждать.
- Windows: event / semaphore / mutex / named pipe и т. п. 217
- POSIX: semaphore / process-shared mutex + condvar и т. п. 1819
5.12 Мысль «так можно разделять данные и с другими машинами»
Бывает момент, когда хочется подумать: если использовать file-backed mapping и отобразить сетевой общий файл, не получится ли обмен наподобие разделяемой памяти и с другими машинами.
Это опасно.
В документации по CreateFileMapping в Windows тоже указано, что для удалённых файлов согласованность (coherence) не гарантируется.
Если две машины отображают одну и ту же страницу как writable, каждая видит только свою собственную запись, и при обновлении диска слияния (merge) не происходит. 8
Разделяемая память в основе своей - механизм в пределах одного хоста. Если нужно выходить за пределы машины, честнее сразу выбрать socket / RPC / брокер сообщений - так проще сохранить рассудок.
6. Практические рекомендации
6.1 Разделить control plane и data plane
В разделяемой памяти размещаем только объёмные данные, а уведомления и переходы состояний выносим на отдельный канал.
- Разделяемая память: frame, sample, batch, snapshot
- Event / semaphore / pipe / socket: ready, consumed, stop, error, reconnect
Это разделение улучшает прозрачность проектирования ещё до того, как повлияет на производительность.
6.2 Разместить фиксированный заголовок в начале
Как минимум настоятельно рекомендуем такой заголовок в начале.
typedef struct SharedHeader {
uint32_t magic;
uint16_t abi_version;
uint16_t header_size;
uint32_t state; // 0=initializing, 1=ready, 2=broken
uint32_t flags;
uint64_t total_size;
uint64_t generation;
uint64_t heartbeat_ns;
uint64_t payload_offset;
uint64_t payload_size;
uint64_t write_seq;
uint64_t read_seq;
uint8_t reserved[64];
} SharedHeader;
Ключевые моменты:
magicотсеивает чужие или неинициализированные сегменты;abi_versionиheader_sizeотсеивают различия раскладки;stateотсеивает состояние незавершённой инициализации;generationпозволяет обнаружить повторное создание;heartbeatпозволяет следить за жизнеспособностью;reservedоставляет запас для будущего расширения.
Сложность разделяемой памяти в том, что «трудно увидеть, что происходит». Именно поэтому с самого начала стоит закладывать метаданные для наблюдаемости.
6.3 Использовать ссылки на основе offset
Ссылки храним не как pointer, а как offset.
- Разрешать через
base + offset - Добавить проверку диапазона
offset + length - Определить sentinel-значение для некорректных данных
Уже это заметно снижает число инцидентов, связанных с несовпадением адресов.
6.4 Сузить модель конкурентности
Разделяемая память резко усложняется по мере роста числа writer-ов. Поэтому в качестве отправной точки сильны варианты:
- SPSC-кольцевой буфер
- snapshot по схеме 1 writer / несколько reader-ов
Если нужно несколько writer-ов, обычно лучше работает подход, при котором сокращается число точек ответственности за целостность, например:
- lock-free / atomic остаётся только сама операция enqueue;
- фактическое обновление данных стягивается к единственному consumer-у.
6.5 Явно задать протокол commit-а
Проектирование, в котором нельзя словами объяснить, «с какого момента можно читать», опасно.
Например, для двойного буфера определяют ритуал публикации:
- записать в непубликуемый буфер;
- зафиксировать контрольную сумму и длину;
- переключить индекс активного буфера с семантикой release;
- reader читает активный индекс с семантикой acquire;
- после чтения проверить, что индекс не изменился.
6.6 Фиксировать размер по поколениям
Вместо resize in place удобнее в сопровождении разделение на поколения, например:
name = MyShm.v3abi_version = 3generation = 42
Разделяемая память не выполняет «проверку типов при вызове», как это делает API. Именно поэтому так важно не ломать однажды зафиксированный ABI.
6.7 Заложить наблюдаемость
Как минимум помогают следующие показатели.
- Время последнего обновления
- Последняя успешная последовательность (sequence)
- Число drop / overwrite
- Число несовпадений версий
- Число attach / detach
- Последний код ошибки
- Heartbeat
Когда разделяемая память ломается, логов обычно немного. Собственные счётчики заметно облегчают реагирование на инциденты.
6.8 Сначала писать тесты аварийных сценариев
Одного лишь happy path недостаточно. Как минимум стоит проверить следующее.
- Принудительное завершение writer-а во время обновления
- Переполнение кольца из-за задержки reader-а
- Подключение при несовпадении версий
- Смешение 32-бит / 64-бит
- Open между разными сессиями
- Недостаточные права доступа
- Перезапуск процесса-предшественника с устаревшим поколением
- Промахи кэша / влияние NUMA при непрерывной передаче огромных данных
Для разделяемой памяти тесты на «поломку» ценнее, чем тесты happy path.
7. Что смотреть в Windows и в POSIX
| Аспект | Windows | POSIX |
|---|---|---|
| Создание / open | CreateFileMapping / OpenFileMapping / MapViewOfFile 6 |
shm_open / ftruncate / mmap 3 |
| Обмен без привязки к диску | pagefile-backed mapping с INVALID_HANDLE_VALUE 68 |
Объект разделяемой памяти POSIX + mmap 3 |
| Начальные значения | Страницы pagefile-backed mapping инициализируются нулями 8 | Новый объект начинается с длины 0. Вновь выделенные байты инициализируются нулями 3 |
| Синхронизация | mutex / semaphore / event / interlocked и т. п. 25 | process-shared mutex / condvar / semaphore 2018 |
| Что нельзя использовать межпроцессно | CRITICAL_SECTION, WaitOnAddress 2110 |
mutex / condvar, оставленные как PTHREAD_PROCESS_PRIVATE 2019 |
| Смерть владельца | WAIT_ABANDONED 12 |
robust mutex + EOWNERDEAD / pthread_mutex_consistent() 1314 |
| Удаление имени | Исчезает при освобождении последнего handle / view 28 | shm_unlink удаляет имя. Объект сохраняется, пока остаются ссылки 2223 |
| Пространство имён / права доступа | Global\ / Local\, ACL, SeCreateGlobalPrivilege 1524 |
mode, umask, пространство имён, O_CREAT|O_EXCL 3 |
MemoryMappedFile в C# по сути тоже является обёрткой над file mapping в Windows.
Поэтому основные правила не меняются:
- open по тому же имени;
- отдельное использование mutex / event;
- чтение view с явной раскладкой;
- не размещать ссылки на объекты как есть.
8. Чек-лист для первой проверки
- Действительно ли нужна разделяемая память? Речь о больших данных в пределах одного хоста?
- Разделены ли control plane и data plane?
- Можно ли свести модель конкурентности к SPSC / 1 writer, несколько reader-ов?
- Есть ли в заголовке magic / version / size / state / generation / heartbeat?
- Не размещаете ли pointer /
HANDLE/ fd / объект STL /std::mutex? - Есть ли протокол commit-а, гарантирующий, что reader никогда не увидит промежуточное состояние?
- Определён ли ровно один инициализатор?
- Есть ли процедура восстановления при аварийном завершении?
- Явно ли заданы имена и права доступа?
- Действительно ли необходим
Global\? - Не предполагается ли resize in place?
- Проверены ли kill writer-а / зависание reader-а / несовпадение версий / нехватка прав доступа?
9. Итог
При грамотном использовании разделяемая память действительно очень сильна. Особенно для больших данных в пределах одной машины, таких как:
- изображения;
- аудио;
- потоки данных с датчиков;
- крупные батчи;
- высокочастотные snapshot,
она действительно окупается.
Однако суть разделяемой памяти - не столько «скорость», сколько перекладывание ответственности. В обмен на меньшее число копирований и меньше сообщений через ядро вы берёте на себя:
- синхронизацию;
- видимость;
- инициализацию;
- ABI;
- восстановление;
- права доступа;
- наблюдаемость.
Поэтому для первой реализации безопасна такая форма:
- SPSC-кольцевой буфер или двойной буфер
- фиксированный заголовок в начале
- ссылки через offset
- уведомление через отдельный канал
- наличие version / generation / heartbeat
- наличие тестов аварийных сценариев
Начав именно с такой формы, разделяемая память становится вполне послушным инструментом. Если же с первого дня относиться к ней как к «быстрой общей памяти, куда можно класть что угодно», со временем это превращается уже не в приложение, а в археологию.
10. Справочные материалы
- Windows: основы file mapping и именованной разделяемой памяти 682
- Windows: пространство имён / безопасность / синхронизация 1524512
- POSIX:
shm_open,shm_unlink,mmap, process-shared / robust-синхронизация 322162013 - .NET: обзор
MemoryMappedFile1
-
Microsoft Learn, “Memory-Mapped Files” / Microsoft Learn, “MemoryMappedFile Class” ↩ ↩2 ↩3
-
Microsoft Learn, “/volatile (volatile Keyword Interpretation)” / Microsoft Learn, “volatile (C++)” ↩ ↩2
-
Microsoft Learn, “Interlocked Variable Access” / Microsoft Learn, “MemoryBarrier function” ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, “Creating Named Shared Memory” ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, “Scope of Allocated Memory” ↩ ↩2
-
Microsoft Learn, “CreateFileMappingA function” ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
man7.org, “POSIX Shared Memory” training slides ↩
-
Microsoft Learn, “WaitOnAddress function” ↩ ↩2
-
Microsoft Learn, “MapViewOfFileEx function” / Microsoft Learn, “MapViewOfFile function” ↩
-
Microsoft Learn, “Mutex Objects” ↩ ↩2 ↩3
-
man7.org, “pthread_mutex_lock(3p)” / man7.org, “pthread_mutexattr_setrobust(3)” ↩ ↩2 ↩3
-
man7.org, “pthread_mutex_consistent(3)” / man7.org, “pthread_mutex_consistent(3p)” ↩ ↩2
-
Microsoft Learn, “Kernel object namespaces” ↩ ↩2 ↩3
-
Microsoft Learn, “Using Mutex Objects” ↩
-
man7.org, “sem_init(3)” / man7.org, “sem_init(3p)” ↩ ↩2
-
Microsoft Learn, “Critical Section Objects” ↩
-
man7.org, “shm_unlink(3p)” ↩ ↩2
-
man7.org, “shm_open(3)” (семантика shm_unlink) ↩
-
Microsoft Learn, “File Mapping Security and Access Rights” ↩ ↩2
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Чек-лист безопасной работы с дочерними процессами в Windows-приложении
Для безопасной работы с дочерними процессами в Windows-приложении важнее не API запуска, а то, кто владеет деревом процессов, и как спрое...
Как вызвать C# Native AOT DLL из C/C++
Разбираем, как публиковать библиотеку классов C# в виде нативной DLL через Native AOT и вызывать точки входа UnmanagedCallersOnly из C/C+...
Обратная совместимость интерфейсов DLL и COM — таблица решений: какие изменения ломают вызывающую сторону
Какие изменения DLL или COM-компонента на самом деле ломают вызывающую сторону? Разбираем три слоя совместимости — бинарную, исходную и п...
Версионирование схемы БД бизнес-приложения — практика миграций, предотвращающая «у каждого клиента своя база»
Практическое руководство по версионированию схемы БД бизнес-приложения, установленного у множества клиентов. Разбираем PRAGMA user_versio...
CI/CD для приложений WinForms / WPF на практике — автоматизация от сборки до подписи и распространения через GitHub Actions
Практическое руководство по настройке CI/CD для приложений WinForms / WPF через GitHub Actions. Минимальный YAML для сборки и тестов на w...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Обмен большими объёмами данных и проектирование изоляции процессов с использованием разделяемой памяти, file mapping и MemoryMappedFile - тема, напрямую связанная с разработкой Windows-приложений.
Технические консультации и ревью дизайна
Проработка проектных решений, снижающих частоту сбоев, - выбор способа синхронизации, проектирование ABI, стратегия восстановления, разделение control plane и data plane - хорошо сочетается с технической консультацией и ревью архитектуры.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Можно ли сразу и корректно прочитать из другого процесса значение, записанное в разделяемую память?
- Видимость и безопасность чтения - это разные вопросы. Разделяемая память - это механизм, который показывает одну и ту же последовательность байтов нескольким процессам, а не сама синхронизация. Даже если writer собирается записать length, payload и ready flag именно в этом порядке, reader без какой-либо синхронизации может увидеть комбинацию из нового length и старого payload. И в Windows, и в POSIX доступ к разделяемой памяти предполагается сочетать со средствами синхронизации - mutex, semaphore, event.
- Можно ли размещать в разделяемой памяти указатели, std::string или HANDLE?
- Лучше не размещать. Виртуальные адреса и process-local ресурсы имеют смысл только в контексте своего процесса, и даже если тот же mapping отобразить (map) в другом процессе, виртуальные адреса не обязательно совпадут. То же касается std::vector, std::mutex, CRITICAL_SECTION и подобного. Если нужна ссылка, её лучше хранить как offset от базового адреса, а данные в разделяемой памяти - приводить к целым числам фиксированной ширины, явной раскладке и версионированному заголовку.
- Избавляет ли использование volatile от необходимости синхронизации в разделяемой памяти?
- Нет. volatile - не волшебное средство, спасающее проектирование разделяемой памяти: как минимум atomicity и mutual exclusion - разные вопросы. Проектирование, при котором volatile bool отслеживается в busy loop, впустую расходует CPU, оставляет неопределённой гарантию порядка между payload и ready flag и легко ловит промежуточные состояния. Кроме того, WaitOnAddress в Windows предназначен для thread-ов внутри одного процесса, и его безопаснее не рассматривать как межпроцессный механизм ожидания. Уведомления стоит выносить на примитивы, которые можно ждать, - event или semaphore.
- Что нужно решить в первую очередь при проектировании разделяемой памяти?
- Четыре вещи. Разделение control plane и data plane: управление (запуск, остановка, уведомления) идёт через сообщения, а сами данные - через разделяемую память. Сужение модели конкурентности (проще всего начать с SPSC-кольцевого буфера или двойного буфера). Владелец и время жизни: кто создаёт, инициализирует, удаляет и восстанавливает при аварийном завершении участника. И проектирование ABI, включая раскладку и версию. Уже само наличие в заголовке magic, version, size, state, generation и heartbeat заметно облегчает расследование сбоев.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки