MEMBARRIER(2)

ИМЯ
membarrier - задаёт барьеры памяти в наборе нитей

ОБЗОР
#include <linux/membarrier.h>

int membarrier(int cmd, int flags);

ОПИСАНИЕ
Системный вызов membarrier() помогает сократить накладные расходы инструкций барьера памяти, которые требуются при доступе к памяти в многоядерных системах. Однако данный системный вызов затратнее чем барьер памяти, поэтому чтобы использовать его эффективно недостаточно просто заменить им барьеры памяти, требуется понимание описанного далее.

При использовании барьеров памяти нужно принимать во внимание, что барьер памяти всегда должен или иметь противоположную сторону барьера, или что модель памяти архитектуры этого не требует.

При работе бывает, что одна сторона барьера (которую будем называть «быстрой стороной») применяется чаще чем другая (которую будем называть «медленной стороной»). Это основная цель использования membarrier(). Основная идея в замене этих барьеров: быстрые стороны барьеров — простыми барьерами компилятора:

    asm volatile ("" : : : "memory")

а медленные стороны барьеров — вызовами membarrier().

Это добавит накладных расходов к медленной стороне и удалит расходы у быстрой стороны, что в результате повысит общую производительность в зависимости от того, как часто используются вызовы membarrier() у медленной стороны, из-за которой возникают издержки, и не перевесит ли это увеличение производительности быстрой стороны.

Аргумент cmd может принимать одно из следующих значений:

MEMBARRIER_CMD_QUERY
    Запросить набор поддерживаемых команд. Возвращаемое вызовом значение представляет собой битовую маску поддерживаемых команд. Сама команда MEMBARRIER_CMD_QUERY имеет значение 0 и не включается в эту маску. Данная команда поддерживается всегда (в ядрах с поддержкой membarrier()).

MEMBARRIER_CMD_SHARED
    Проверить, что все потоки всех процессов в системе прошли через состояние, где все доступы к памяти по адресам пространства пользователя соответствуют программному порядку между входом и возвратом из системного вызова membarrier(). Все потоки в системе являются целью этой команды.

MEMBARRIER_CMD_PRIVATE_EXPEDITED (начиная с Linux 4.14)
    Установить барьер памяти на каждую выполняющуюся нить, принадлежащую тому же процессу что и текущая нить. После возвращения из системного вызова вызывающей нити гарантируется, что все одноуровневые выполняющиеся нити перешли в состояние, где весь доступ к памяти по адресам пользовательского пространства соответствует порядку программы между входом и выходим из системного вызова (невыполняющиеся нити уже в таком состоянии). Это относится только к нитям того же процесса что и вызывающая.

    «Курируемые» (expedited) команды выполняются быстрее чем не курируемые; они никогда не блокируются, но на них приходятся дополнительные издержки. Процессу нужно регистрировать своё намерение использовать частную курируемую команду до её использования.

MEMBARRIER_CMD_REGISTER_PRIVATE_EXPEDITED (начиная с Linux 4.14)
    Зарегистрировать намерение процесса использовать MEMBARRIER_CMD_PRIVATE_EXPEDITED.

В настоящее время аргумент flags не используется и должен равняться 0.

Все доступы к памяти, выполняемые в программном порядке из каждого целевого потока, гарантированно упорядочены в отношении membarrier().

Если использовать семантику barrier() для представления барьера компилятора, обеспечивающего доступа к памяти, выполняемого в программном порядке для пересечения барьера, и smp_mb() — для представления явных барьеров памяти, обеспечивающих полный упорядоченный доступ к памяти через барьер, то получится следующая упорядочивающая таблица для каждой пары barrier(), membarrier() и smp_mb(). Наличие порядка в паре показано знаком (O: упорядочено, X: не упорядочено):

                       barrier() smp_mb() membarrier()
       barrier() X X O
       smp_mb() X O O
       membarrier() O O O

ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ
При успешном выполнении операции MEMBARRIER_CMD_QUERY возвращается битовая маска поддерживаемых команд, а MEMBARRIER_CMD_SHARED , MEMBARRIER_CMD_PRIVATE_EXPEDITED и MEMBARRIER_CMD_REGISTER_PRIVATE_EXPEDITED , возвращают ноль. При ошибке возвращается -1, а в errno устанавливается в соответствующее значение.

Для данной команды, если флаг flags равен 0, то системный вызов всегда гарантирует возврат одного и того же значения до перезагрузки. Последующие вызовы с теми же аргументами всегда возвращают тот же результат. Поэтому, если flags равен 0, то обработка ошибок требуется только при первом вызове membarrier().

ОШИБКИ

EINVAL
    Неверное значение cmd, значение flags не равно нулю или отключена команда MEMBARRIER_CMD_SHARED, так как указан параметр ЦП nohz_full.

ENOSYS
    Системный вызов membarrier() не реализован в данном ядре.

EPERM
    Текущий процесс не зарегистрирован до использования частных курируемых команд.

ВЕРСИИ
Системный вызов membarrier() впервые появился в Linux версии 4.3.

СООТВЕТСТВИЕ СТАНДАРТАМ
Вызов membarrier() есть только в Linux.

ЗАМЕЧАНИЯ
Инструкция барьера памяти является частью системы команд архитектуры со слабо упорядоченными моделями памяти. Она упорядочивает доступ к памяти до барьера и после барьера в соответствии совпадающими барьерами на других ядрах. Например, загрузка барьера (fence) может упорядочить загрузку до и после этого барьера в соответствии порядком хранилища, установленного барьерами хранилища.

Программный порядок — это порядок, в котором инструкции упорядочены в ассемблерном коде программы.

Вызов membarrier() может быть полезным для реализации библиотек чтения-копирования-обновления или сборщиков мусора.

ПРИМЕР
Предполагая, что в многонитевой программе «fast_path()» выполняется очень часто, а «slow_path()» — редко, следующий код (x86) можно преобразовать используя membarrier():

    #include <stdlib.h>
    static volatile int a, b;
    static void
    fast_path(int *read_b)
    {
        a = 1;
        asm volatile ("mfence" : : : "memory");
        *read_b = b;
    }
    static void
    slow_path(int *read_a)
    {
        b = 1;
        asm volatile ("mfence" : : : "memory");
        *read_a = a;
    }
    int
    main(int argc, char **argv)
    {
        int read_a, read_b;
        /*
         * В реальных приложениях вызовы fast_path() и slow_path()
         * были бы в разных нитях. Их вызов из main() сделан только
         * для укорачивания данного примера.
         */
        slow_path(&read_a);
        fast_path(&read_b);
        /*
         * read_b == 0 подразумевает read_a == 1 и
         * read_a == 0 подразумевает read_b == 1.
         */
        if (read_b == 0 && read_a == 0)
            abort();
        exit(EXIT_SUCCESS);
    }

Этот же код, переписанный с использованием membarrier():

    #define _GNU_SOURCE
    #include <stdlib.h>
    #include <stdio.h>
    #include <unistd.h>
    #include <sys/syscall.h>
    #include <linux/membarrier.h>
    static volatile int a, b;
    static int
    membarrier(int cmd, int flags)
    {
        return syscall(__NR_membarrier, cmd, flags);
    }
    static int
    init_membarrier(void)
    {
        int ret;
        /* Проверка поддержки в функции membarrier() */
        ret = membarrier(MEMBARRIER_CMD_QUERY, 0);
        if (ret < 0) {
            perror("membarrier");
            return -1;
        }
        if (!(ret & MEMBARRIER_CMD_SHARED)) {
            fprintf(stderr,
                "membarrier не поддерживает MEMBARRIER_CMD_SHARED\n");
            return -1;
        }
        return 0;
    }
    static void
    fast_path(int *read_b)
    {
        a = 1;
        asm volatile ("" : : : "memory");
        *read_b = b;
    }
    static void
    slow_path(int *read_a)
    {
        b = 1;
        membarrier(MEMBARRIER_CMD_SHARED, 0);
        *read_a = a;
    }
    int
    main(int argc, char **argv)
    {
        int read_a, read_b;
        if (init_membarrier())
            exit(EXIT_FAILURE);
        /*
         * В реальных приложениях вызовы fast_path() и slow_path()
         * были бы в разных нитях. Их вызов из main() сделан только
         * для укорачивания данного примера.
         */
        slow_path(&read_a);
        fast_path(&read_b);
        /*
         * read_b == 0 подразумевает read_a == 1 и
         * read_a == 0 подразумевает read_b == 1.
         */
        if (read_b == 0 && read_a == 0)
            abort();
        exit(EXIT_SUCCESS);
    }