3. Отличия в интерфейсах

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

3.1. Совместимость с компиляторами

Компилятор lcc стремится к максимальной совместимости с gcc. Версия lcc-1.24 соответствует gcc-7.3.0.

3.1.1. Конструкции языка

3.1.1.1. Variable length array inside a struct

Не поддержана недокументированная функция gcc: Variable length arrays (VLA) в середине структуры.

3.1.1.2. Nested functions

Не поддержан элемент диалекта GNU C: nested functions.

3.1.2. gcc builtins

Ряд билтинов (builtin) gcc поддержан с ограничениями. Некоторые из них вызваны особенностями реализации компилятора, другие отсутствием практической необходимости. С выходом новых версий lcc расширяется состав поддержанных билтинов.

Ограничения перечислены в разделе документации на компилятор:

/opt/mcst/doc/builtin_gnu.html.

3.1.3. Прагмы

Информация о поддержанных прагмах в документации компилятора:

/opt/mcst/doc/pragma.html

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

Вместо

#pragma omp parallel for if(!singleThreaded) schedule(dynamic)

нужно написать

bool multiThreaded = !singleThreaded;

#pragma omp parallel for if(multiThreaded) schedule(dynamic)

3.1.4. OpenMP

3.1.4.1. Возможности

  • Поддержан стандарт OpenMP 3.1.

  • Доступны языки C, C++, Fortran.

3.1.4.2. Ограничения

  • Для e2k не поддержано в режиме -m128.

  • Nested параллелизм не поддержан. Если при исполнении уже распараллеленного цикла встречаются циклы, которые нужно распараллелить, то эти (вложенные) циклы будут исполняться последовательно.

  • Не поддержан clause collapse.

  • Для C/C++ после директивы #pragma omp всегда должен следовать statement языка. Проблемы могут возникнуть для #pragma omp barrier и #pragma omp flush, если за ними нет statement’а. Для обхода проблемы рекомендуется в следующей строке поставить пустой statement, например “0;” или “;”

  • Переменные, перечисленные в clause’ах private, lastprivate, firstprivate и threadprivate должны иметь скалярный базовый тип или массив скалярного базового типа. В противном случае результат программы неопределен.

  • Директива #pragma omp for не поддержана для итераторов C++.

  • Для C/C++ clause’ы if и num_threads своими параметрами могут иметь только константы и переменные целого типа, выражения не допускаются.

3.1.4.3. Справочный файл

В компиляторе информация об OpenMP хранится здесь:

/opt/mcst/doc/openmp.html

3.2. Системные интерфейсы

3.2.1. makecontext

Функция makecontext() для управления контекстом пользователя реализована на Эльбрусе с другой семантикой.

Вместо вызова makecontext() необходимо вызывать makecontext_e2k(). Функции дано другое название, чтобы отображать несовместимость с реализацией на других архитектурах.

makecontext_e2k() - в отличие от makecontext(), дополнительно выделяет аппаратные стеки, из-за чего является более тяжеловесным.

Можно вызвать makecontext_e2k() дважды на один и тот же контекст, не выполняя freecontext_e2k(). Для ядер >= 5.4 контекст будет переиспользован напрямую, что гораздо быстрее, чем освобождать его через freecontext_e2k() и выделять новый. Перед этим следует убедиться, что контекст не используется. При повторном использовании makecontext можно подменять стек, т.е. с точки зрения пользователя вызов «makecontext + makecontext» полностью эквивалентен вызову «makecontext + freecontext + makecontext», за исключением производительности.

makecontext_e2k() возвращает значение int, а не void. Значение вызова необходимо проверять на статус ошибки (< 0).

Возможные ошибки:

  • EFAULT - не удалось обратиться по указателю ucp или ucp->uc_stack;

  • ENOMEM - в системе недостаточно памяти;

  • EINVAL - подозрительное содержимое ucontext_t;

  • EBUSY - ucp->uc_stack уже отдан под другой контекст.

3.2.2. freecontext

При использовании makecontext_e2k() в конце области видимости необходимо вызвать freecontext_e2k().

freecontext_e2k() - освобождает связанные с контекстом аппаратные стеки. Если контекст в данный момент занят, то реальное освобождение произойдёт, только когда он перестанет использоваться. Попытка освободить текущий контекст (используемый текущим потоком) игнорируется.

3.2.3. swapcontext/setcontext

swapcontext()/setcontext() - при ошибке возвращают не только ENOMEM.

Возможные ошибки:

  • EFAULT - подан плохой указатель или в системе недостаточно памяти;

  • ENOMEM - в системе недостаточно памяти;

  • ESRCH - некорректный ucp;

  • EBUSY - контекст ucp уже используется.

3.2.3.1. Особенность переключения контекстов

Для архитектуры Эльбрус, помимо доступного пользовательскому приложению контекста (%r, %b[],содержимое стека данных), существует также и привилегированный контекст, доступный на запись лишь ядру ОС, например:

  • указатели на стеки (%pcsp, %psp, %usd, %sbr);

  • содержимое стека связующей инфорации (он же «стек возвратов»);

  • при исполнении в обработчике сигнала: указатель на стек из прерванных сигналами контекстов (%tir, trap cellar, %ctpr, %g16-%g31, …)

Это означает, что каждый сохранённый пользовательский контекст состоит из двух половин: ровно одной привилегированной, хранящейся в ядре ОС; и >= 1 непривилегированной, хранящейся в памяти приложения (struct ucontext).

Ввиду этого, во-первых, makecontext помимо struct ucontext дополнительно инициализирует привилегированную половинку нового контекста (в основном это два стека - связующей и процедурный), и во избежание утечки памяти при освобождении struct ucontext нужно дополнительно освободить привилегированную половинку вызовом freecontext_e2k(). Во-вторых, для связи этих двух половинок заводится хеш-таблица, и swapcontext должен найти два элемента этой таблицы.

В итоге: примерно равный вклад в замедление вносят: организация хэш-таблицы; необходимость полноценного системного вызова; большой размер контекста.

3.2.4. sys_get_backtrace/sys_set_backtrace

Функции sys_get_backtrace() и sys_set_backtrace() используются для чтения и модификации адресов возврата в стеке связующей информации. sys_get_backtrace() считывает адреса в буфер, sys_set_backtrace() - записывает.

Если при записи в буфер положить специальное значение -1UL, то адрес возврата будет подменен на пустую функцию, просто выполняющую return, это полезно для пропуска функций, у который нет обработки исключений.

long sys_get_backtrace(unsigned long *buf, size_t count,
size_t skip, unsigned long flags);

long sys_set_backtrace(const unsigned long *buf, size_t count,
size_t skip, unsigned long flags);

Здесь:

count - размер буфера;

skip - сколько пропустить фреймов в стеке связующей;

flags - должен быть равен 0.

При успехе возвращают число прочитанных/записанных адресов (даже если это число меньше чем count). При неудаче возвращают код ошибки.

В случае, если удалось записать не все адреса, код ошибки можно получить, повторив вызов sys_set_backtrace со skip увеличенным и count уменьшенным на число успешно записанных адресов.

sys_get_backtrace/sys_set_backtrace возвращают ошибку:

  • EFAULT В случае если буфер недоступен;

  • EINVAL В случае нулевого flags.

sys_set_backtrace возвращает ошибку:

  • ESRCH Если один из IP указывает на незамапированную область.

  • EPERM В случае если сработала проверка:
    1. Либо старый и новый IP имеют различные разрешения r/w/x;

    2. Либо старый и новый IP соответствуют различным исполняемым файлам.

Если требуется просто заменить ip возврата текущий функции, то sys_set_backtrace() может оказаться слишком тяжеловесным, тогда можно воспользоваться fast_sys_set_return() :

#if __ptr32__
# define FAST_SYS_ENTRY "5"
#elif __ptr64__
# define FAST_SYS_ENTRY "6"
#else
# error
#endif

#include <asm/unistd.h>

    unsigned long ip = <return ip>;
    int ret;

    asm ("{sdisp %%ctpr1, " FAST_SYS_ENTRY "}\n" \
            "{adds 0, %1, %%b[0]\n"\
            "addd 0, %2, %%db[1]\n"\
            "adds 0, 0, %%b[2]\n"\
            "call %%ctpr1, wbs=%#}\n"\
            "adds 0, %%b[0], %0\n"\
            : "=r" (ret)
            : "i" (__NR_fast_sys_set_return), "r" (ip)
            : "ctpr1", "ctpr2", "ctpr3", "memory",
              "b[0]", "b[1]", "b[2]", "b[3]", "b[4]", "b[5]", "b[6]", "b[7]",
              "g16", "g17", "g18", "g19", "g20", "g21", "g22", "g23",
              "g24", "g25", "g26", "g27", "g28", "g29", "g30", "g31");

3.2.5. sys_access_hw_stacks

Функция sys_access_hw_stacks() используется для чтения и записи аппаратных стеков текущего потока в памяти, предполагается такое использование:

  • получаем размер через E2K_GET_*_STACK_SIZE

  • выделяем буфер при необходимости

  • читаем в буфер сколько нужно/откуда нужно через E2K_READ_*_STACK_EX

  • правим в буфере

  • пишем обратно только изменённую часть буфера через E2K_WRITE_*_STACK_EX

Для подключения нужных определений следует использоваь:

#include <asm/e2k_syswork.h>

1) E2K_GET_CHAIN_STACK_SIZE и E2K_GET_PROCEDURE_STACK_SIZE используются для получения размера аппаратных стеков.

__u64 chain_size, procedure_size;

  sys_access_hw_stacks(E2K_GET_CHAIN_STACK_SIZE, NULL, NULL, 0,
&chain_size)
  sys_access_hw_stacks(E2K_GET_PROCEDURE_STACK_SIZE, NULL, NULL, 0,
&procedure_size)

2) Для чтения/записи аппаратных стеков по смещениям относительно их реальной базы (реальная база может быть меньше прописанного в регистре значения %pcsp.base). Предполагается выравнивание offset и size на 8 для процедурного стека и на 32 для связующей (в связующую пишем только фреймы целиком).

__u64 offset;
void *buffer;
unsigned long size;

sys_access_hw_stacks(mode, &offset, buffer, size, NULL)

Здесь:

mode - один из E2K_READ_CHAIN_STACK_EX, E2K_READ_PROCEDURE_STACK_EX, E2K_WRITE_PROCEDURE_STACK_EX, E2K_WRITE_CHAIN_STACK_EX.

offset - смещение в стеке, откуда читать или куда писать

buffer - буфер в пользователе, в который/из которого копировать

size - сколько копировать

При попытке считать/записать больше, чем есть в стеке, функция вернёт ошибку - EINVAL.