Я программирую два процесса, которые обмениваются сообщениями друг с другом в сегменте разделяемой памяти. Хотя сообщения не обрабатываются атомарно, синхронизация достигается за счет защиты сообщений с общими атомными объектами, доступ к которым осуществляется с помощью хранилищ-релизов и загрузки нагрузки.
Моя проблема в безопасности. Процессы не доверяют друг другу. После получения сообщения процесс не делает предположения о том, что сообщение хорошо сформировано; он сначала копирует сообщение из разделяемой памяти в частную память, а затем выполняет некоторую проверку на этой частной копии и, если она действительна, продолжает обрабатывать эту же частную копию. Создание этой частной копии имеет решающее значение, поскольку она предотвращает атаку TOC/TOU, в которой другой процесс будет изменять сообщение между валидацией и использованием.
Мой вопрос заключается в следующем: гарантирует ли стандарт, что умный компилятор C никогда не решит, что он может прочитать оригинал вместо копии? Представьте себе следующий сценарий, в котором сообщение является простым целым числом:
int private = *pshared; // pshared points to the message in shared memory
...
if (is_valid(private)) {
...
handle(private);
}
Если у компилятора закончились регистры и временно необходимо пролить private
, может ли он решить вместо того, чтобы проливать его в стек, что он может просто отказаться от своего значения и перезагрузить его с *pshared
позже, при условии, что alias analysis гарантирует, что этот поток не изменился *pshared
?
Мое предположение заключается в том, что такая оптимизация компилятора не сохранит семантику исходной программы и поэтому будет незаконной: pshared
не указывает на объект, который предположительно доступен из этого потока (например, выделенного объекта на стек, адрес которого не просочился), поэтому компилятор не может исключить, что другой поток может одновременно изменять *pshared
. По контрасту компилятор может исключить избыточные нагрузки, поскольку одно из возможных вариантов поведения заключается в том, что ни один другой поток не работает между избыточными нагрузками, поэтому текущий поток должен быть готов к решению этого конкретного поведения.
Может ли кто-нибудь подтвердить или увести это предположение и, возможно, предоставить ссылки на соответствующие части стандарта?
(Кстати: я предполагаю, что тип сообщения не имеет ловушечных представлений, так что нагрузки всегда определяются.)
UPDATE
Несколько плакатов прокомментировали необходимость синхронизации, на которую я не собирался входить, поскольку я считаю, что у меня уже есть это. Но поскольку люди указывают на это, справедливо, что я предоставляю более подробную информацию.
Я реализую асинхронную систему связи низкого уровня между двумя объектами, которые не доверяют друг другу. Я запускаю тесты с процессами, но в конечном итоге перейду к виртуальным машинам поверх гипервизора. У меня есть два основных компонента в моем распоряжении: общая память и механизм уведомления (как правило, ввод IRQ в другую виртуальную машину).
Я реализовал общую структуру буферного буфера, с которой сообщающиеся объекты могут создавать сообщения, а затем отправлять вышеупомянутые уведомления, чтобы дать друг другу знать, когда есть что-то, что нужно потреблять. Каждый объект поддерживает свое собственное частное состояние, которое отслеживает, что оно произвело/потребляло, и в общей памяти имеется разделяемое состояние, состоящее из слотов сообщений и атомных целых чисел, отслеживающих границы регионов, в которых хранятся ожидающие сообщения. Протокол однозначно определяет, к каким слотам сообщений должен быть обращен исключительно доступ к объекту в любой момент. Когда ему нужно создать сообщение, сущность записывает сообщение (неатомно) в соответствующий слот, затем выполняет атомный выпуск-хранилище в соответствующее атомное целое, чтобы передать право собственности на слот другому объекту, а затем ожидает, пока память записи завершены, а затем отправляет уведомление для пробуждения другого объекта. Получив уведомление, ожидается, что другой объект выполнит атомную загрузку нагрузки в соответствующем атомном целое, определит, сколько там ожидающих сообщений, затем потребляйте их.
Загрузка *pshared
в моем фрагменте кода - это просто пример того, как выглядит тривиальное сообщение (int
). В реалистичной настройке сообщение будет структурой. Потребление сообщения не требует какой-либо конкретной атомарности или синхронизации, поскольку, как указано в протоколе, это происходит только тогда, когда потребляющий объект синхронизируется с другим и знает, что ему принадлежит слот для сообщений. Пока обе стороны следуют протоколу, все работает безупречно.
Теперь я не хочу, чтобы люди должны были доверять друг другу. Их реализация должна быть надежной против вредоносного объекта, который будет игнорировать протокол и записывать все сегменты разделяемой памяти в любое время. Если это произойдет, единственное, что может достичь злонамерная организация, - это нарушить общение. Подумайте о типичном сервере, который должен быть готов обрабатывать злоупотребительные запросы со стороны злонамеренного клиента, не допуская, чтобы такое неправильное поведение приводило к переполнению буфера или к отказам доступа.
Итак, хотя протокол использует синхронизацию для нормальной работы, сущности должны быть готовы к изменению содержимого разделяемой памяти в любое время. Все, что мне нужно - это убедиться, что после того, как сущность сделает частную копию сообщения, он проверяет и использует эту же копию и больше не обращается к оригиналу.
У меня есть реализация, которая копирует сообщение с помощью изменчивого чтения, тем самым давая понять компилятору, что в общей памяти нет обычной семантики памяти. Я считаю, что этого достаточно; Интересно, нужно ли это.