Ответ 1
Первое определение, которое вы называете, является частью механизма проверки блокировки ядра , ака "lockdep". WRITE_ONCE
(и другие) не нуждаются в специальном лечении, но причина, по которой возникает вопрос другого вопроса.
Соответствующее определение будет здесь, и в очень кратком комментарии говорится, что их целью является:
Предотвратить компилятор от слияния или повторного чтения чтения или записи.
...
Обеспечение того, чтобы компилятор не складывал, шпиндель или иным образом искажал доступа, которые либо не требуют упорядочения, либо взаимодействуют с явным барьером памяти или атомарной инструкцией, которая обеспечивает требуемый порядок.
Но что означают эти слова?
Проблема
Проблема на самом деле множественна:
-
Чтение/запись "разрыва": замена одного доступа к памяти на многие более мелкие. GCC может (и делает!) В определенных ситуациях заменять что-то вроде
p = 0x01020304;
двумя 16-разрядными операциями немедленного хранения - вместо предположения размещения константы в регистре, а затем доступа к памяти и т.д.WRITE_ONCE
позволяет нам говорить GCC, "не делайте этого", например:WRITE_ONCE(p, 0x01020304);
-
Компиляторы C перестали гарантировать, что доступ к словам является атомарным. Любая программа, которая не является гоночной, может быть скомпрометирована с впечатляющими результатами. Не только это, но компилятор может решить не сохранять определенные значения в регистре внутри цикла, что приводит к нескольким ссылкам, которые могут испортить код следующим образом:
for(;;) { owner = lock->owner; if (owner && !mutex_spin_on_owner(lock, owner)) break; /* ... */ }
- В отсутствие "тегирования" доступа к общей памяти мы не можем автоматически обнаруживать непреднамеренные обращения такого рода. Автоматические инструменты, которые пытаются найти такие ошибки, не могут отличить их от намеренно доступных доступов.
Решение
Начнем с того, что ядро Linux требует создания с помощью GCC. Таким образом, только один компилятор нам нужно позаботиться с решением, и мы можем использовать его документацию в качестве единственного руководства.
Для общего решения нам необходимо обрабатывать обращения к памяти всех размеров. У нас есть все различные типы специфической ширины и все остальное. Мы также отмечаем, что нам не нужно специально отмечать обращения к памяти, которые уже находятся в критических разделах (почему бы и нет?).
Для размеров 1, 2, 4 и 8 байтов существуют соответствующие типы, а volatile
специально запрещает GCC применять описанную в (1) оптимизацию, а также заботиться о другие случаи (последний маркер в разделе "COMPILER BARRIERS" ). Он также запрещает GCC ошибочно комбинировать цикл в (2), поскольку он перемещает доступ к volatile
через точку последовательности, и это запрещено стандартом C. Linux использует то, что мы называем "изменчивым доступом" (см. Ниже), вместо того, чтобы пометить объект как изменчивый. Мы могли бы решить нашу проблему, отметив конкретный объект как volatile
, но это (почти?) Никогда не было хорошим выбором. Есть many причины, это может быть вредно.
Таким образом, в ядре используется volatile (write) доступ для 8-разрядного широкоформатного типа:
*(volatile __u8_alias_t *) p = *(__u8_alias_t *) res;
Предположим, что мы точно не знаем, что делает volatile
и обнаруживаем не просто! (проверьте # 5) - еще один способ выполнить это было бы помещать барьеры памяти: это именно то, что делает Linux, если размер составляет ничего, кроме 1,2,4 или 8, прибегая к memcpy
и помещая барьеры памяти до и после вызова. Пункты памяти легко решают проблему (2), но несут большие штрафные санкции.
Надеюсь, я рассмотрел обзор, не вникая в интерпретации стандарта C, но если бы вы хотели, чтобы я мог потратить время на это.