Я не думаю, что ваша процедура обновления безопасна. В отличие от x86, кеши инструкций ARM несовместимы с кешами данных, согласно этому сообщение в блоге с самомодифицирующимся кодом.
Первая инструкция без перехода все еще может быть кэширована, чтобы другой поток мог войти в блок. Когда выполнение достигает 2-й строки i-cache блока, возможно, эта строка перезагружается и видит частично измененное состояние.
Есть еще одна проблема: прерывание (или переключение контекста) может привести к выселению / перезагрузке строки кэша в потоке, который все еще находится в середине выполнения старой версии. Перезапись блока инструкций на месте требует, чтобы вы были уверены, что выполнение во всех других потоках вышло из блока после того, как вы изменили что-то, чтобы новые потоки не входили в него. Это проблема даже с согласованный I-кеш (например, x86), и даже если блок кода умещается в одной строке кеша.
Я не думаю, что есть способ сделать перезапись на месте одновременно безопасной и эффективной на ARM.
Без согласованных I-кешей вы также не можете гарантировать, что другие потоки быстро увидят изменения кода с этим дизайном, без смехотворно дорогих вещей, таких как очистка блоков из кеша L1I перед их запуском каждый раз.
С согласованным I-кешем (стиль x86) вы можете достаточно долго ждать любой возможной задержки в другом потоке, завершающем выполнение старой версии. Даже если блок не выполняет никаких операций ввода-вывода или системных вызовов, возможны промахи в кэше и переключение контекста. Если он работает с приоритетом в реальном времени, особенно с отключенными прерываниями, то худший кеш - это просто промахи кеша, то есть не очень длинные. В противном случае я бы не стал ставить на то, что что-то меньшее, чем один-два квартала (возможно, 10 мс) будет действительно безопасным.
а>.
Я процитирую еще один слайд (о виртуализации ARM) для этого краткого обзора, но я бы рекомендовал читать слайды ELC2016, а не слайды виртуализации.
Программное обеспечение должно знать о кешах в нескольких случаях: загрузка / генерация исполняемого кода.
- Требуется очистка D-кеша до точки объединения + аннулирование I-кеша
- Возможно из пользовательского пространства на ARMv8
- Требуется системный вызов на ARMv7
D-кеш может быть признан недействительным с обратной записью или без нее (поэтому убедитесь, что вы очищаете / сбрасываете, а не отбрасываете!). Вы можете и должны запускать это по виртуальному адресу (вместо того, чтобы очищать весь кеш сразу, и определенно не используйте для этого сброс с помощью set / way).
Если вы не очистили свой D-кеш перед аннулированием I-кеша, выборка кода могла бы производить выборку непосредственно из основной памяти в некогерентный I-кеш после отсутствия в L2. (Без выделения устаревшей строки в каких-либо унифицированных кешах, что может предотвратить MESI, поскольку L1D имеет строку в Модифицированном состоянии). В любом случае очистка L1D до PoU является архитектурно необходимой и в любом случае происходит в потоке записи, не критичной для производительности, поэтому, вероятно, лучше просто сделать это, а не пытаться обосновать, безопасно ли это. для конкретной микроархитектуры ARM. См. Комментарии к усилиям @ Notlikethat, чтобы прояснить мое замешательство по этому поводу.
Подробнее об очистке I-кеша из пользовательского пространства см. Как очистить и сделать недействительным кеш процессора ARM v7 из пользовательского режима в Linux 2.6.35. Функция GCC __clear_cache()
и Linux sys_cacheflush
работают только с областями памяти, которые mmap
были заполнены PROT_EXEC
.
Не изменять на месте: использовать новое местоположение
Если вы планировали иметь целые блоки инструментального кода, поместите один непрямой переход (или сохранение / восстановление lr
и вызов функции, если у вас все равно будет ветка). Каждый блок имеет свою собственную целевую переменную перехода, которая может обновляться атомарно. Ключевым моментом здесь является то, что адресатом косвенного перехода являются данные, поэтому он согласован с хранилищами из потока записи.
Поскольку вы обновляете указатель атомарно, потребительские потоки либо переходят к старому, либо к новому блоку кода.
Теперь ваша проблема заключается в том, чтобы убедиться, что ни одно ядро не имеет устаревшей копии нового местоположения в его i-cache. Учитывая возможности переключения контекста, которое включает текущее ядро, если переключение контекста не полностью очистить i-cache.
Если вы используете достаточно большой кольцевой буфер локаций для новых блоков, так что они остаются неиспользованными достаточно долго, чтобы их можно было выселить, на практике может оказаться невозможным когда-либо возникать проблема. Однако это звучит невероятно сложно доказать.
Если обновления происходят нечасто по сравнению с тем, как часто другие потоки запускают эти динамически изменяемые блоки, вероятно, достаточно дешево, чтобы поток публикации запускал очистку кеша в других потоках после записи нового блока, но перед обновлением указателя косвенного перехода, чтобы он указывал на него.
Принуждение других потоков к очистке кеша:
В Linux 4.3 и более поздних версиях есть membarrier()
системный вызов, который будет выполняться барьер памяти на всех других ядрах системы (обычно с межпроцессорным прерыванием) перед возвратом (таким образом блокируя все потоки всех процессов). См. Также это сообщение в блоге, описывающее некоторые варианты использования (например, RCU в пользовательском пространстве) и mprotect()
в качестве альтернативы.
Однако, похоже, он не поддерживает кеши инструкций по очистке. Если вы создаете собственное ядро, вы можете рассмотреть возможность добавления поддержки нового значения cmd
или flag
, которое означает очистку кешей инструкций вместо (или также) запуска барьера памяти. Возможно, значение flag
могло быть виртуальным адресом? Это будет работать только на архитектурах, где адрес соответствует int
, если вы не настроите API системного вызова, чтобы смотреть на полную ширину регистра flag
для вашего нового cmd, но только на значение int
для существующего MEMBARRIER_CMD_SHARED
.
Помимо взломаmbarrier (), вы можете отправлять сигналы потокам-потребителям, а их обработчики сигналов очищают соответствующую область i-cache. Это асинхронно, поэтому поток-производитель не знает, когда безопасно повторно использовать старый блок.
IDK, если munmap()
ing, он будет работать, но, вероятно, дороже, чем необходимо (потому что он должен изменить таблицы страниц и сделать недействительными соответствующие записи TLB).
Другие стратегии
Вы могли бы что-то сделать, опубликовав монотонно увеличивающийся порядковый номер в общей переменной (с семантикой выпуска, чтобы он упорядочен относительно записи инструкций). Затем потребительские потоки проверяют порядковый номер на соответствие локальному потоку, наиболее часто встречающемуся, и аннулируют i-cache, если есть новые данные. Это может быть поблочное или глобальное.
Это не решает напрямую проблему определения того, когда последний поток, выполняющий старый блок, покинул его, если только те счетчики с наибольшим количеством просмотров для каждого потока на самом деле не являются локальными для потока: все еще для каждого потока, но поток-производитель может смотреть на их. Он может сканировать их на предмет наименьшего порядкового номера в любом потоке, и если он выше, чем порядковый номер, когда на блок не было ссылки, теперь его можно использовать повторно. Остерегайтесь ложного обмена: не используйте для него глобальный массив unsigned long
, потому что вы хотите, чтобы частная переменная каждого потока находилась в отдельной строке кэша с другими локальными данными потока.
Другой возможный метод: если существует только один потребительский поток, производитель устанавливает целевой указатель перехода так, чтобы он указывал на блок, который не изменяется (поэтому нет необходимости очищать i-cache). Этот блок (который выполняется в потоке-потребителе) выполняет очистку кеша для соответствующей строки i-cache, а затем снова изменяет указатель цели перехода, на этот раз так, чтобы он указывал на блок, который должен запускаться каждый раз.
С несколькими потребительскими потоками это становится немного неуклюжим: может быть, у каждого потребителя есть свой собственный частный указатель на цель перехода, и производитель обновляет их все?
person
Peter Cordes
schedule
03.09.2016
__builtin___clear_cache
. - person ensc   schedule 02.09.2016