В C, как я могу убедиться, что загрузка памяти выполняется только один раз?
Я программирую два процесса, которые обмениваются сообщениями друг с другом в сегменте разделяемой памяти. Хотя сообщения не обрабатываются атомарно, синхронизация достигается за счет защиты сообщений с общими атомными объектами, доступ к которым осуществляется с помощью хранилищ-релизов и загрузки нагрузки.
Моя проблема в безопасности. Процессы не доверяют друг другу. После получения сообщения процесс не делает предположения о том, что сообщение хорошо сформировано; он сначала копирует сообщение из разделяемой памяти в частную память, а затем выполняет некоторую проверку на этой частной копии и, если она действительна, продолжает обрабатывать эту же частную копию. Создание этой частной копии имеет решающее значение, поскольку она предотвращает атаку 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
). В реалистичной настройке сообщение будет структурой. Потребление сообщения не требует какой-либо конкретной атомарности или синхронизации, поскольку, как указано в протоколе, это происходит только тогда, когда потребляющий объект синхронизируется с другим и знает, что ему принадлежит слот для сообщений. Пока обе стороны следуют протоколу, все работает безупречно.
Теперь я не хочу, чтобы люди должны были доверять друг другу. Их реализация должна быть надежной против вредоносного объекта, который будет игнорировать протокол и записывать все сегменты разделяемой памяти в любое время. Если это произойдет, единственное, что может достичь злонамерная организация, - это нарушить общение. Подумайте о типичном сервере, который должен быть готов обрабатывать злоупотребительные запросы со стороны злонамеренного клиента, не допуская, чтобы такое неправильное поведение приводило к переполнению буфера или к отказам доступа.
Итак, хотя протокол использует синхронизацию для нормальной работы, сущности должны быть готовы к изменению содержимого разделяемой памяти в любое время. Все, что мне нужно - это убедиться, что после того, как сущность сделает частную копию сообщения, он проверяет и использует эту же копию и больше не обращается к оригиналу.
У меня есть реализация, которая копирует сообщение с помощью изменчивого чтения, тем самым давая понять компилятору, что в общей памяти нет обычной семантики памяти. Я считаю, что этого достаточно; Интересно, нужно ли это.
Ответы
Ответ 1
Вы должны сообщить компилятору, что разделяемая память может быть изменена в любой момент модификатором volatile
.
volatile int *pshared;
...
int private = *pshared; // pshared points to the message in shared memory
...
if (is_valid(private)) {
...
handle(private);
}
Поскольку *pshared
объявляется volatile, компилятор больше не может считать, что *pshared
и private
сохраняют одинаковое значение.
В вашем редактировании теперь ясно, что все мы знаем, что изменчивого модификатора в общей памяти достаточно, чтобы гарантировать, что компилятор будет соблюдать временные рамки всех обращений к этой общей памяти.
Во всяком случае, проект N1256 для C99 явно о нем говорит в 5.1.2.3 Выполнение программы (подчеркните мой)
2 Доступ к изменчивому объекту, изменение объекта, изменение файла или вызов функции что любая из этих операций - это все побочные эффекты, которые являются изменениями состояния среда исполнения. Оценка выражения может привести к побочным эффектам. В определенные определенные точки в последовательности выполнения, называемые точками последовательности, все побочные эффекты предыдущих оценок должны быть полными и никаких побочных эффектов последующих оценок должно быть.
5 Наименьшие требования к соответствующей реализации:
- В точках последовательности неустойчивые объекты стабильны в том смысле, что предыдущие обращения полный и последующий доступ еще не появился
- При завершении программы все данные, записанные в файлы, должны быть идентичны результату, который выполнение программы в соответствии с абстрактной семантикой.
Это позволяет предположить, что даже если pshared
не квалифицируется как volatile, значение private
должно быть загружено из *pshared
до оценки is_valid
, а поскольку абстрактная машина не имеет причин для ее изменения до оценка handle
, соответствующая реализация не должна ее изменять. В лучшем случае он может удалить вызов handle
, если он не содержит побочных эффектов, которые вряд ли произойдут
Во всяком случае, это только академическая дискуссия, потому что я не могу представить себе реальный случай использования, когда для общей памяти не нужен модификатор volatile
. Если вы его не используете, компилятор может поверить, что предыдущее значение все еще действует, поэтому при втором доступе вы все равно получите первое значение. Поэтому, даже если ответ на этот вопрос не нужен, вам все равно придется использовать volatile int *pshared;
.
Ответ 2
Трудно ответить на ваш вопрос, как опубликовано. Обратите внимание, что вы должны использовать объект синхронизации для предотвращения одновременного доступа, если только вы не читаете единицы, которые являются атомарными на платформе.
Я предполагаю, что вы намереваетесь спросить о (псевдокоде):
lock_shared_area();
int private = *pshared;
unlock_shared_area();
if (is_valid(private))
и что другой процесс также использует ту же блокировку. (Если нет, было бы полезно обновить ваш вопрос, чтобы быть более конкретным относительно вашей синхронизации).
Этот код гарантирует чтение *pshared
не более одного раза. Использование имени private
означает чтение переменной private
, а не объекта *pshared
. Компилятор "знает", что вызов разблокировки области действует как забор памяти, и он не будет переупорядочивать операции за ограждением.
Ответ 3
Поскольку C не имеет понятия об обмене между процессами, вы ничего не можете сделать, чтобы сообщить компилятору, что есть другой процесс, который может изменять память.
Таким образом, я считаю, что не существует способа предотвратить использование достаточно умной, злонамеренной, но соответствующей системы сборки из вызова правила "как если бы", чтобы позволить ему делать неправильную вещь.
Чтобы получить что-то, что "гарантировано" для работы, вам нужно работать независимо от того, какие гарантии предоставляются вашим конкретным компилятором и/или библиотекой разделяемой памяти, которую вы используете.