TL; DR: Это зависит от архитектуры и ОС. В x86 этот тип опасности чтения после записи в основном не является проблемой, которую необходимо учитывать на уровне программного обеспечения, за исключением хранилищ слабого порядка W C, которые требуют выполнения ограничения хранилища в программном обеспечении на том же логическое ядро до переноса потока.
Обычно операция переноса потока включает в себя как минимум одно хранилище памяти. Рассмотрим архитектуру со следующим свойством:
- Модель памяти такова, что хранилища памяти могут не стать глобально наблюдаемыми в программном порядке. В этой статье Википедии есть не точная, но достаточно хорошая таблица, в которой приведены примеры архитектур, обладающих этим свойством (см. Строку «Хранилища могут быть переупорядочены после хранилищ»).
Упомянутая вами опасность упорядочения может быть возможной в такой архитектуре, поскольку даже если операция переноса потока завершается, это не обязательно означает, что все хранилища, выполненные потоком, являются глобально наблюдаемыми. На архитектурах со строгим последовательным упорядочением хранилищ эта опасность не может возникать.
На полностью гипотетической архитектуре, где возможно перенести поток без единого хранилища памяти (например, путем прямой передачи контекста потока в другое ядро) опасность может возникнуть, даже если все хранилища являются последовательными в архитектуре со следующим свойством:
- Существует «окно уязвимости» между временем, когда хранилище закрывается, и когда оно становится глобально наблюдаемым. Это может произойти, например, из-за наличия буферов хранилища и / или MSHR. У большинства современных процессоров есть это свойство.
Таким образом, даже при последовательном упорядочении хранилища может быть возможно, что поток, работающий на новом ядре, может не увидеть последние N хранилищ.
Примечание что на машине с удалением по порядку окно уязвимости является необходимым, но недостаточным условием для модели памяти, которая поддерживает хранилища, которые могут быть не последовательными.
Обычно поток перепланируется для запуска на другом ядре используя один из следующих двух методов:
- Произошло аппаратное прерывание, такое как прерывание по таймеру, которое в конечном итоге приводит к перепланированию потока на другом логическом ядре.
- Поток сам выполняет системный вызов, такой как
sched_setaffinity
, который в конечном итоге заставляет его работать на другом ядре.
Вопрос в том, в какой момент система гарантирует, что удаленные магазины станут глобально наблюдаемыми? На процессорах Intel и AMD x86 аппаратные прерывания полностью сериализуют события, поэтому все хранилища в пользовательском режиме (включая кешируемые и не кэшируемые) гарантированно будут глобально наблюдаемыми до выполнения обработчика прерываний, в котором поток может быть перепланирован для запуска другого логическое ядро.
На процессорах Intel и AMD x86 существует несколько способов выполнения системных вызовов (т. е. изменение уровня привилегий), включая INT
, SYSCALL
, SYSENTER
и дальний CALL
, Ни один из них не гарантирует, что все предыдущие магазины станут глобально наблюдаемыми. Следовательно, ОС должна делать это явно при планировании потока на другом ядре путем выполнения операции ограничения хранилища. Это делается как часть сохранения контекста потока (архитектурные регистры пользовательского режима) в памяти и добавления потока в очередь, связанную с другим ядром. Эти операции включают, по крайней мере, один магазин, на который распространяется гарантия последовательного заказа. Когда планировщик запускается на целевом ядре, он будет видеть полный регистр и архитектурное состояние памяти (в точке последней удаленной инструкции) потока будет доступно на этом ядре.
На x86, если Поток использует хранилища типа W C, которые не гарантируют последовательное упорядочение, ОС в этом случае может не гарантировать, что это сделает эти хранилища глобально наблюдаемыми. В x86 spe c прямо говорится, что для того, чтобы сделать хранилища W C глобально наблюдаемыми, необходимо использовать ограничитель хранилища (либо в потоке на том же ядре, либо, что намного проще, в ОС). ОС обычно должна делать это, как указано в ответе @ JohnDMcCalpin. В противном случае, если ОС не предоставляет гарантию заказа программы программным потокам, программисту в пользовательском режиме может потребоваться принять это во внимание. Один из способов будет следующим:
- Сохраните копию текущей маски ЦП и закрепите нить на текущем ядре (или на любом одном ядре).
- Выполните слабо упорядоченные store.
- Выполнение ограничения хранилища.
- Восстановление маски ЦП.
Это временно отключает миграцию, чтобы гарантировать, что ограничение хранилища выполняется на том же ядре, что и слабо заказанные магазины. После выполнения ограничения хранилища поток может безопасно мигрировать без возможного нарушения порядка программы.
Обратите внимание, что инструкции режима ожидания пользовательского режима, такие как UMWAIT
, не могут привести к перепланированию потока на другом ядре, поскольку В этом случае ОС не получает контроль.
Миграция потока в Linux Ядро
Фрагмент кода из ответа @ JohnDMcCalpin находится на пути к отправьте межпроцессорное прерывание, которое достигается с помощью инструкции WRMSR
в регистр API C. IPI может быть отправлен по многим причинам. Например, чтобы выполнить операцию сброса TLB. В этом случае важно убедиться, что обновленные структуры пейджинга являются глобально наблюдаемыми, прежде чем отключить записи TLB на других ядрах. Вот почему может потребоваться x2apic_wrmsr_fence
, который вызывается непосредственно перед отправкой IPI.
Тем не менее, я не думаю, что миграция потоков требует отправки IPI. По сути, поток переносится путем удаления его из некоторой структуры данных, связанной с одним ядром, и добавления ее к той, которая связана с целевым ядром. Поток может быть перенесен по многим причинам, например, когда изменяется сродство или когда планировщик решает перебалансировать нагрузку. Как упомянуто в Linux исходном коде , все пути миграции потоков в исходном коде заканчиваются выполнением следующего:
stop_one_cpu(cpu_of(rq), migration_cpu_stop, &arg)
, где arg
содержит задачу для переноса и идентификатор ядра назначения. migration_cpu_stop
- это функция, которая выполняет фактическую миграцию. Однако задача, которая должна быть перенесена, может в данный момент выполняться или ожидать в какой-либо очереди выполнения для запуска на исходном ядре (т. Е. На том ядре, на котором в данный момент запланирована задача). Требуется остановить задачу перед ее миграцией. Это достигается добавлением вызова функции migration_cpu_stop
в очередь задачи-ограничителя, связанной с исходным ядром. stop_one_cpu
затем устанавливает задание стопора как готовое к выполнению. Задание стопора имеет наивысший приоритет. Поэтому при следующем прерывании таймера на исходном ядре (которое может совпадать с текущим ядром) будет выбрана для запуска одна из задач с наивысшим приоритетом. В конце концов, задача-ограничитель запустится и выполнит migration_cpu_stop
, что, в свою очередь, выполнит миграцию. Поскольку этот процесс включает аппаратное прерывание, все хранилища целевой задачи гарантированно будут глобально наблюдаемыми.
Похоже, что ошибка в x2apic_wrmsr_fence
Цель x2apic_wrmsr_fence
- сделать все предыдущие хранилища глобально наблюдаемыми перед отправкой IPI. Как обсуждалось в этой ветке , SFENCE
здесь недостаточно. Чтобы понять почему, рассмотрим следующую последовательность:
store
sfence
wrmsr
Ограничение хранилища здесь может заказать предыдущую операцию хранилища, но не запись MSR. Инструкция WRMSR не имеет свойств сериализации при записи в регистр API C в режиме x2API C. Это упомянуто в разделе 3.12.3 модуля 3 SDM Intel:
Для обеспечения эффективного доступа к регистрам API C в режиме x2API C семантика сериализации WRMSR ослаблена, когда запись в регистры API C.
Проблема здесь в том, что MFENCE
также не гарантирует заказ более позднего WRMSR
относительно предыдущих магазинов. На процессорах Intel задокументировано только упорядочение операций с памятью. Только на процессорах AMD гарантируется полная сериализация. Таким образом, чтобы он работал на процессорах Intel, должен быть LFENCE
после того, как MFENCE
(SFENCE
не заказан с LFENCE
, поэтому необходимо использовать MFENCE
, даже если нам не нужно заказывать нагрузки). На самом деле Раздел 10.12.3 упоминает это.