Подтвердить что ты не робот

WRITE_ONCE в списках ядра Linux

Я читаю linux kernel implementation из дважды связанного списка. Я не понимаю использование макроса WRITE_ONCE(x, val). Он определяется следующим образом в compiler.h:

#define WRITE_ONCE(x, val) x=(val)

Он используется семь раз в файле, например

static inline void __list_add(struct list_head *new,
                  struct list_head *prev,
                  struct list_head *next)
{
    next->prev = new;
    new->next = next;
    new->prev = prev;
    WRITE_ONCE(prev->next, new);
}

Я читал, что он используется для предотвращения условий гонки.

У меня есть два вопроса:
1/Я думал, что макрос заменяется кодом во время компиляции. Итак, как этот код отличается от следующего? Как этот макрос может избежать условий гонки?

static inline void __list_add(struct list_head *new,
                  struct list_head *prev,
                  struct list_head *next)
{
    next->prev = new;
    new->next = next;
    new->prev = prev;
    prev->next = new;
}

2/Как узнать, когда мы должны его использовать? Например, он используется для __lst_add(), но не для __lst_splice():

static inline void __list_splice(const struct list_head *list,
                 struct list_head *prev,
                 struct list_head *next)
{
    struct list_head *first = list->next;
    struct list_head *last = list->prev;

    first->prev = prev;
    prev->next = first;

    last->next = next;
    next->prev = last;
}

Редактирование:
Вот сообщение фиксации относительно этого файла и WRITE_ONCE, но это не помогает мне ничего понять...

: используйте WRITE_ONCE() при инициализации структур list_head
Кодекс, который делает бесконтактное тестирование пустоты не-RCU-списков, полагается на INIT_LIST_HEAD(), чтобы написать заголовок списка → следующий указатель атомарно, особенно когда INIT_LIST_HEAD() вызывается из list_del_init(). Поэтому это commit добавляет WRITE_ONCE() к этому хранилища указателей функций, которые могут повлиять на головку → следующий указатель.

4b9b3361

Ответ 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;
        /* ... */
    }
  1. В отсутствие "тегирования" доступа к общей памяти мы не можем автоматически обнаруживать непреднамеренные обращения такого рода. Автоматические инструменты, которые пытаются найти такие ошибки, не могут отличить их от намеренно доступных доступов.

Решение

Начнем с того, что ядро ​​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, но если бы вы хотели, чтобы я мог потратить время на это.