Skip to content

Repository files navigation

simplerpc (libsrpc)

  • Статус — в разработке.
  • Текущая версия 0.3.0
  • Language: 🇷🇺 Русский · 🇬🇧 English · 🇨🇳 中文
  • Coverity Scan Build Status
  • rpcgen — ветка, где X-макросы заменены кодогенерацией.

simplerpc — библиотека удалённого вызова процедур (RPC) между процессами одной машины под Linux, написанная на C. Функция, объявленная в общем списке, вызывается как обычная C-функция, а выполняется в том процессе, который её определил.

Аргументы и результаты передаются через разделяемую память, которую все процессы отображают по одному и тому же виртуальному адресу. Поэтому указатель на блок в общем пуле одинаково валиден в любом процессе, и данные за ним не копируются. UNIX-сокет используется только для служебных задач: регистрации, раздачи дескриптора памяти и обнаружения смерти процесса. Памятью пула управляет транзакционный аллокатор TLSF, который переживает падение процесса посреди операции.

Демонстрация

Три терминала, три программы из example/:

./build/example/log    # терминал 1: исполнитель log_write()
./build/example/clc    # терминал 2: исполнитель calc_add()
./build/example/app    # терминал 3: вызывающий

app вызывает log_write(...) и calc_add(1, 2) как обычные функции. Строка из app появляется в первом терминале: её напечатал процесс log. Сумму посчитал процесс clc. Сериализация аргументов генерируется X-макросами, вручную ничего писать не нужно. Остановить исполнителей можно командой ./build/example/all_exit.

Порядок подключения к своему приложению:

  1. Добавить прототип функции в X-макрос RPC_LIST в src/libsrpc_rpc_functions.h. Этот файл — часть исходников самой библиотеки, а не отдельный заголовок приложения.
  2. Пересобрать libsrpc.so.
  3. В процессе-исполнителе определить функцию, в процессе-вызывающем — просто вызвать её.
  4. Подключить #include "libsrpc.h" и слинковать приложение с libsrpc.so с флагом -Wl,--no-as-needed.
  5. После пересборки библиотеки перезапустить все процессы, которые ею пользуются.

Демон-координатор запускать вручную не нужно: его поднимает первый же процесс, загрузивший libsrpc.so. Когда отключается последний клиент, демон завершается сам.

📋 Основные возможности

  • RPC как обычный вызов. Роль процесса выбирает линковщик: кто определил функцию, тот её исполнитель, кто не определил — вызывающий. IDL, генератора кода и регистрации в рантайме нет.
  • Общая память без копирования данных. Пул отображается по одному виртуальному адресу во всех процессах. В RPC-запрос копируются только сами аргументы, а данные за указателями остаются на месте.
  • Транзакционный аллокатор TLSF. Выделение за O(1). Межпроцессный robust-мьютекс и журнал отката BUL (Binary Undo Log): если процесс умер посреди malloc или free, следующий захвативший мьютекс откатывает незавершённую операцию.
  • Модель владения памятью. Каждый блок помечен UID процессов-владельцев, до 12 на блок. libsrpc_shmem_link() добавляет владельца, libsrpc_shmem_free() снимает только своего. Так блок можно передать другому процессу без копирования.
  • Уборка за умершими процессами. Демон узнаёт о смерти клиента сразу, по обрыву сокета, и возвращает в пул его блоки. Вызывающий может временно удержать блоки исполнителя через libsrpc_shmem_proc_lock(): тогда они переживут его смерть до снятия удержания.
  • Сборщик мусора. Поток в демоне освобождает служебные блоки, которые ещё могли читать другие процессы. Занятость проверяется через Hazard Pointer.
  • Рассылка. Вызов может выполнить каждый зарегистрированный исполнитель (RPC_SEND_ALL), первый (RPC_SEND_FIRST), последний (RPC_SEND_LAST) или один по кругу (RPC_SEND_RR). Ответы всех исполнителей доступны через libsrpc_lastreq_num() / libsrpc_lastreq_get().
  • Очереди и синхронизация. У каждого процесса своя lock-free MPMC-очередь входящих запросов. Потоки-исполнители засыпают на POSIX-семафорах в разделяемой памяти.
  • Демон без файла на диске. Исполняемый файл демона вшит в libsrpc.so и запускается из memfd через fexecve().
  • Подмена аллокатора (опционально). Перехват malloc/calloc/realloc/free с переключением потока на общий пул. По умолчанию выключена, см. DISABLE_ALLOC.

🛠 Требования

  • ОС и архитектура: Linux на x86_64. Ядро должно поддерживать MAP_FIXED_NOREPLACE (4.17+). Используются memfd_create, fexecve, абстрактные UNIX-сокеты, MADV_DONTFORK. В коде есть x86-инструкции (pause) и флаг -mrdrnd.
  • Язык: C17.
  • Сборка: CMake 3.10+, Make, gcc (встроенный загрузчик собирается именно gcc, независимо от выбранного компилятора), утилита xxd.
  • Библиотеки: только системные pthread и dl. Внешних зависимостей и git-подмодулей нет, аллокатор tlsf_txn лежит в дереве проекта.

📂 Структура проекта

simplerpc/
├── CMakeLists.txt              # Основная конфигурация CMake
├── Makefile                    # Обёртка над CMake: make all / make clean
├── doc/
│   └── ARCHITECTURE*.md        # Внутреннее устройство (ru / en / zh)
├── src/                        # Исходники libsrpc
│   ├── libsrpc.h               # Публичный API
│   ├── libsrpc_rpc_functions.h # X-макрос RPC_LIST — список RPC-функций
│   ├── libsrpc_errno.h         # Коды ошибок libsrpc
│   ├── libsrpc.c               # Обёртки, диспетчер, реестр функций, конструктор
│   ├── libsrpc_daemon.c        # Запуск демона и его главный цикл
│   ├── libsrpc_loader.c        # Исходник встроенного загрузчика демона
│   ├── libsrpc_unix_socket.c   # UNIX-сокеты: подключение, регистрация, передача FD
│   ├── libsrpc_shmem.c         # Пул разделяемой памяти и обёртки аллокатора
│   ├── libsrpc_shm_gc.c        # Сборщик мусора
│   ├── libsrpc_proc.c          # Дескрипторы процессов, уборка за умершими
│   ├── lf_mpmc_queue.c         # Lock-free MPMC-очередь запросов
│   ├── libsrpc_list_spin.c     # Односвязный список (spinlock на запись)
│   ├── libsrpc_fixblockalloc.c # Пул блоков фиксированного размера (локальная память демона)
│   ├── libsrpc_pthread.c       # Создание потоков, привязка к CPU
│   ├── libsrpc_wrapper.h       # Семафоры, паузы, мелкие обёртки
│   ├── libsrpc_local.h         # Внутренние структуры: контекст, запрос, ответ
│   ├── libsrpc_private.h       # Идентификаторы RPC и реестр функций
│   └── macro.h, argfunc.h      # Препроцессорная кодогенерация
├── libs/
│   └── tlsf_txn/               # Транзакционный аллокатор TLSF v4.1 (+ тесты, README)
├── example/                    # Примеры
│   ├── app.c                   # Вызывающий
│   ├── log.c                   # Исполнитель log_write()
│   ├── clc.c                   # Исполнитель calc_add(), сам вызывает log_write()
│   ├── all_exit.c              # Рассылка all_exit(): остановка исполнителей
│   └── list/                   # Разделяемый список: передача владения и удержание
│       ├── dal_list.h          # Двусвязный список с robust-мьютексом
│       ├── list_db.c           # Владелец списка, исполнитель list_api()
│       └── list_cli.c          # Клиент: --add / --del / --print
└── bench/                      # Замеры производительности
    ├── bench_ipc_baseline.c    # Базовые линии: pipe, socketpair
    ├── bench_srpc_server.c     # Исполнитель для замеров
    ├── bench_srpc_client.c     # Задержка и пропускная способность
    └── run_bench.sh            # Сценарий запуска замеров

🚀 Сборка

git clone https://github.com/dsn76/simplerpc.git
cd simplerpc
make all

make all создаёт каталог build, конфигурирует проект CMake и собирает статическую библиотеку аллокатора libtlsf.a, libsrpc.so, примеры и бенчмарки. make clean удаляет build.

Сборка вручную:

mkdir -p build && cd build
cmake ..
make

Режимы и параметры

Режим задаётся переменной BUILD_TYPE:

  • release (по умолчанию) — -O3;
  • debug — -O0 -ggdb3 -DDEBUG, включает подробный вывод DBG_PRINT.
Параметр По умолчанию Назначение
BUILD_TYPE release Режим сборки: debug / release
ENABLE_SANITIZER OFF AddressSanitizer + UndefinedBehaviorSanitizer (только в debug)
DISABLE_ALLOC ON Не собирать подмену malloc/calloc/realloc/free
DBG_LVL 2 Уровень сообщений: 0 — выкл., 1 — ERR, 2 — ERR+WRN, 3 — ERR+WRN+INF
MPMCQ_DEGREE 10 Ёмкость очереди запросов процесса как степень двойки (2^10 = 1024)
SHMEM_SIZE_KB 4096 Размер пула разделяемой памяти, КиБ
SHMEM_BASE_VADR 0x200000000000 Виртуальный адрес, по которому пул отображается во всех процессах
cmake -DBUILD_TYPE=debug -DDBG_LVL=3 -DSHMEM_SIZE_KB=16384 ..

Важно: при каждой конфигурации CMake (в том числе при каждом make all) в библиотеку записывается новая метка времени BUILD_TS. Из неё строятся имя демона, имя сокета и сигнатура пула. Процессы, загрузившие разные сборки libsrpc.so, друг друга не видят: это защита от расхождения RPC_LIST. После пересборки все процессы нужно перезапустить.

Сообщения ERR и WRN печатаются в stderr процесса. Демон наследует stdout/stderr процесса, который его поднял, поэтому его сообщения видны в терминале этого процесса.

💡 Использование

Объявление RPC-функции

Все удалённо вызываемые функции перечислены в одном X-макросе src/libsrpc_rpc_functions.h. Сейчас там функции примеров:

#define RPC_LIST    \
    XF(RPC_SEND_ALL, int,    testlocal) \
    XF(RPC_SEND_ALL, pid_t,  log_write, const char*) \
    XF(RPC_SEND_ALL, int,    calc_add, int, int) \
    XF(RPC_SEND_ALL, void,   all_exit) \
    XF(RPC_SEND_ALL, void *, list_api, int, void *) \

Формат записи: XF(политика рассылки, тип возврата, имя, типы параметров...).

  • Политики. RPC_SEND_ALL — всем исполнителям. RPC_SEND_FIRST — первому доступному в реестре. RPC_SEND_LAST — последнему в реестре. RPC_SEND_RR — одному, по кругу.
  • Типы. Аргументы и результат копируются побайтово, поэтому допустимы скалярные типы и указатели. Указатель имеет смысл, только если указывает в разделяемый пул. Функции с переменным числом аргументов не поддерживаются.
  • Видимость типов. Типы должны быть объявлены до RPC_LIST: libsrpc.h подключает <stdint.h> и <sys/types.h>, свои типы нужно подключить в libsrpc_rpc_functions.h.
  • Идентификаторы. Для каждой функции генерируется libsrpc_funid_<имя>, он нужен для libsrpc_timeout_func_set() и libsrpc_fnreg_num_get().

Кто исполнитель, а кто вызывающий

Отдельного серверного API нет. Процесс, который определил функцию из RPC_LIST, становится её исполнителем: сильный символ приложения перекрывает слабый символ-заглушку из libsrpc.so. Вызов такой функции внутри самого процесса — обычный локальный вызов. Процесс, который функцию не определил, при вызове попадает в заглушку, и та отправляет RPC-запрос. Роль выбирается для каждой функции отдельно: один процесс может исполнять одни функции и вызывать другие.

/* Исполнитель: определяет функцию. */
pid_t log_write(const char *msg) {
    fprintf(stderr, "LOG: '%s'\n", msg);
    return getpid();
}
/* Вызывающий: просто вызывает. */
char *str = libsrpc_shmem_malloc(1024);          /* строка должна лежать в пуле */
snprintf(str, 1024, "Hello from pid=%d", getpid());

pid_t pid = log_write(str);                      /* результат первого исполнителя */
if (libsrpc_errno_get() != 0) { /* вызов не отправлен */ }

/* Результаты всех исполнителей (RPC_SEND_ALL): */
for (int i = 0, num = libsrpc_lastreq_num(); i < num; i++) {
    int rc = libsrpc_lastreq_get(i, &pid, sizeof(pid));
    if (rc == 0) printf("pid[%d]=%d\n", i, pid);
    else         printf("slot %d: %s\n", i, libsrpc_strerror(-rc));
}
libsrpc_shmem_free(str);

Потоки-исполнители сами делать RPC-вызовы не могут: запрос не отправляется, libsrpc_errno_get() возвращает ненулевой код (ERECURSIVE). Остальные потоки процесса-исполнителя, включая главный, вызывают RPC как обычно.

Результат вызова и ошибки

Сгенерированная обёртка возвращает результат первого слота ответа. Если ответа нет, возвращается нулевое значение типа (0, NULL). Нулевой результат сам по себе не отличает ошибку от успеха, поэтому состояние вызова проверяется двумя способами:

  • libsrpc_errno_get() — причина, по которой вызов не был отправлен: ENOREGFUN (нет исполнителей), ERPCDISABLE (нет связи с демоном), ERECURSIVE, ENOTAVAILABLE (ни один исполнитель не принял запрос), ENOMEM, ESHMNOINIT. Код положительный, обёртка сбрасывает его в начале каждого вызова.
  • libsrpc_lastreq_get(i, ...) — судьба отправленного запроса в слоте i: 0 — ответ получен, -ERPCWAITIMEDOUT — исполнитель не ответил вовремя, -ECANCELLED — исполнитель отключился, не взяв запрос, -ENOMEM — очередь исполнителя переполнена. Таймаут и отмена в libsrpc_errno_get() не попадают.

Вызов успешен, если libsrpc_errno_get() == 0 и нужный слот вернул 0. Проверять нужно оба условия. Если вызов не был отправлен, слот может хранить ответ предыдущего вызова из этого же потока. У функций с возвращаемым значением причина неотправки иногда подменяется на ETIMEDOUT, но ненулевой код остаётся надёжным признаком неудачи.

Коды больше 4095 — собственные коды libsrpc (список в src/libsrpc_errno.h), остальные — системные errno. Строку для кода возвращает libsrpc_strerror(code).

Память для аргументов

Всё, что передаётся по указателю, должно лежать в пуле: libsrpc_shmem_malloc(), libsrpc_shmem_calloc(), libsrpc_shmem_realloc(). Блок принадлежит процессу, который его выделил. libsrpc_shmem_free() снимает владельцем только текущий процесс, а блок возвращается в пул, когда владельцев не осталось. Если процесс завершился, не освободив блоки, их освобождает демон.

Чтобы блок пережил своего создателя, получатель делает libsrpc_shmem_link(ptr) и становится вторым владельцем, после чего создатель может вызвать free. Так владение переходит к другому процессу без копирования данных: этот приём показан в примере list.

Размер пула фиксируется при сборке (SHMEM_SIZE_KB, по умолчанию 4 МиБ) и на ходу не растёт.

Таймауты

Ожидание ответа ограничено таймаутом в микросекундах:

  • libsrpc_timeout_oneshot_set(us) — для следующего вызова в текущем потоке;
  • libsrpc_timeout_func_set(libsrpc_funid_<имя>, us) — для функции, в текущем потоке;
  • libsrpc_timeout_global_set(us) — для всех вызовов всех потоков процесса.

Приоритет — в порядке списка. По умолчанию таймаут 1 с, меньше 100 мкс задать нельзя. Интервал отсчитывается заново после каждого пришедшего ответа, поэтому при рассылке нескольким исполнителям общее ожидание может превысить заданное значение.

Линковка

Процесс, который только исполняет функции и сам ничего из libsrpc.so не вызывает, должен линковаться с -Wl,--no-as-needed. Иначе линковщик выбросит зависимость от библиотеки, её конструктор не выполнится, и процесс не подключится к демону. Примеры и бенчмарки линкуются с этим флагом всегда.

Конструктор библиотеки ждёт подключения к демону до входа в main(). К моменту старта main() процесс уже зарегистрирован и принимает запросы.

📖 Публичный API

Все объявления находятся в src/libsrpc.h. Результаты последнего вызова, коды ошибок, одноразовый таймаут, таймауты функций и удержания относятся к текущему потоку.

Память пула

  • void *libsrpc_shmem_malloc(size_t size), void *libsrpc_shmem_calloc(size_t num, size_t size) — выделить блок, владелец — текущий процесс. При ошибке NULL.
  • void *libsrpc_shmem_realloc(void *ptr, size_t newsize) — изменить размер блока. Работает, только если текущий процесс — единственный владелец блока, иначе NULL.
  • void libsrpc_shmem_free(void *ptr) — снять владение текущего процесса. Блок уходит в пул вместе с последним владельцем.
  • int libsrpc_shmem_link(void *ptr) — добавить текущий процесс во владельцы (не больше 12). Возвращает 0 или отрицательный код.
  • int libsrpc_shmem_get_size(void *ptr, size_t *psz) — записать размер полезной нагрузки блока в *psz. Возвращает 0 при успехе.

Функции памяти сообщают об ошибках кодами аллокатора TLSF_ERR_* (1…15, libs/tlsf_txn/tlsf_txn.h), и те же коды попадают в libsrpc_errno_get(). Номера этих кодов пересекаются с системными errno, поэтому расшифровывать их нужно через tlsf_strerror(), а не libsrpc_strerror().

Результаты последнего вызова

  • int libsrpc_lastreq_num(void) — число слотов ответа последнего RPC-вызова текущего потока (0, если вызовов не было).
  • int libsrpc_lastreq_get(int idx, void *retval, size_t sz) — скопировать результат слота idx в retval. sz должен точно совпадать с размером возвращаемого типа, для void — 0, иначе вернётся -EBADMSG. Возвращает 0 или отрицательный код слота.

Удержание памяти исполнителя

  • int libsrpc_shmem_proc_lock(int idx, void *ptr) — удержать UID исполнителя из слота idx последнего вызова, если блок ptr принадлежит этому исполнителю. Пока удержание действует, смерть исполнителя не возвращает его пользовательские блоки в пул. Возвращает номер слота удержания (0…3) или отрицательный код: -ENOTFOUND (блок не его), -EPROCDESTROYED (исполнитель уже отключён), -ENOMEM (все четыре слота заняты), -EINVALREQUEST / -EINVALRESPONSE / -ENOTAVAILABLE (нет подходящего вызова или слота).
  • int libsrpc_shmem_proc_unlock(int slot) — снять одно удержание. int libsrpc_shmem_proc_all_unlock(void) — снять все удержания потока.

Удержание снимает только тот поток, который его взял. Следующие RPC-вызовы этого потока удержание не сбрасывают. При завершении потока или процесса-держателя удержание пропадает само.

Реестр и таймауты

  • int libsrpc_fnreg_num_get(libsrpc_funid_t funid) — число зарегистрированных исполнителей функции; -EINVAL при неверном funid.
  • libsrpc_timeout_oneshot_set(), libsrpc_timeout_func_set(), libsrpc_timeout_global_set() — см. раздел «Таймауты».

Ошибки

  • int libsrpc_errno_get(void) — код последней ошибки текущего потока.
  • const char *libsrpc_strerror(int code) — строка для положительного кода: системного или собственного кода libsrpc.

Подмена аллокатора (только при сборке с -DDISABLE_ALLOC=OFF)

  • libsrpc_alloc_sw_shm() / libsrpc_alloc_sw_std() — переключить malloc/calloc/realloc/free текущего потока на пул и обратно.

▶️ Примеры

Все примеры собираются в build/example/. Исполнители работают, пока не получат all_exit().

Программа Роль
log Исполнитель log_write() и all_exit()
clc Исполнитель calc_add() и all_exit(); сам вызывает log_write()
app Вызывающий: log_write() и calc_add(). Свою testlocal() вызывает локально
all_exit Рассылает all_exit(): останавливает log, clc и list_db
list_db Хранит разделяемый список, исполнитель list_api() и all_exit()
list_cli Клиент списка: --add, --del, --print
./build/example/log         # терминал 1
./build/example/clc         # терминал 2
./build/example/app         # терминал 3
./build/example/all_exit    # остановка

app выделяет строку в пуле и вызывает log_write(): строка печатается в терминале log. Затем calc_add(1, 2) считается в процессе clc. Если clc не запущен, вызов вернёт 0, а libsrpc_errno_get() — ENOREGFUN.

Пример list: разделяемый список и передача владения

Пример из example/list/ показывает модель владения памятью на двусвязном списке строк, который живёт в пуле и доступен нескольким процессам.

Роли. list_db определяет list_api(int op, void *node) и становится её исполнителем. list_cli эту функцию не определяет, поэтому её вызов уходит в list_db по RPC. Операции:

  • DAL_LIST_OP_GET_HEAD — вернуть указатель на голову списка;
  • DAL_LIST_OP_INSERT — вставить узел;
  • DAL_LIST_OP_REMOVE — удалить узел.

Старт list_db. Через libsrpc_fnreg_num_get(libsrpc_funid_list_api) проверяется, нет ли уже другого исполнителя list_api(). Если есть, второй экземпляр завершается: список в примере один. Затем голова списка выделяется в пуле и в ней инициализируется robust-мьютекс с PTHREAD_PROCESS_SHARED. Через этот мьютекс процессы договариваются, кто сейчас меняет или читает ссылки списка. Голова принадлежит list_db, потому что выделил её он.

Добавление (--add) — передача владения.

list_cli_node_t *node = libsrpc_shmem_malloc(sizeof(*node) + len + 1);
/* заполнить size и data */
void *ptr = list_api(DAL_LIST_OP_INSERT, node);
libsrpc_shmem_free(node);

list_cli выделяет узел и передаёт указатель в list_api(). В list_db вставка начинается с libsrpc_shmem_link(node): у узла становится два владельца. Затем узел под мьютексом вставляется в список. Вернувшись из вызова, list_cli делает libsrpc_shmem_free(node). Это снимает только его UID, и узел остаётся в списке, принадлежа теперь одному list_db. Завершение list_cli на список больше не влияет.

Печать (--print) — удержание памяти исполнителя. list_cli получает голову вызовом list_api(DAL_LIST_OP_GET_HEAD, NULL) и сразу вызывает libsrpc_shmem_proc_lock(0, list_head). Библиотека проверяет, что голова принадлежит исполнителю из слота 0, и удерживает его UID. Пока удержание действует, внезапная смерть list_db не вернёт в пул ни голову, ни переданные ему узлы. Дальше list_cli берёт мьютекс, обходит список, печатает строки, отпускает мьютекс и снимает удержание libsrpc_shmem_proc_unlock().

Удаление (--del). Строка ищется тем же порядком: голова, удержание, мьютекс, обход. Найденный узел передаётся в list_api(DAL_LIST_OP_REMOVE, node). В list_db узел вынимается из списка и освобождается libsrpc_shmem_free(). list_db к этому моменту единственный владелец, поэтому блок возвращается в пул.

./build/example/list_db                                   # терминал 1
./build/example/list_cli --add hello --add world --print  # терминал 2
size: 30, data: world
size: 30, data: hello
./build/example/list_cli --del hello --print
size: 30, data: world
./build/example/all_exit

Узлы вставляются в начало списка, поэтому печатаются в обратном порядке. size — размер всего узла вместе с заголовком. Пример учебный: восстановление мьютекса списка после EOWNERDEAD в нём не реализовано.

📈 Замеры производительности

make all
./bench/run_bench.sh build 4000 4    # каталог сборки, число итераций, число потоков

Сценарий поднимает исполнитель, прогоняет базовые линии (pipe, socketpair), затем однопоточный и многопоточный round-trip RPC. Печатает распределение задержек (min/p50/p90/p99/max) и пропускную способность.

⚠️ Ограничения

  • Только Linux x86_64 и только процессы одной машины.
  • Все процессы должны загрузить одну и ту же сборку libsrpc.so. RPC_LIST вшит в библиотеку, поэтому изменение списка функций требует её пересборки и перезапуска всех процессов.
  • Демон — единая точка отказа. Если он упадёт, RPC в уже работающих процессах отключится и сам не восстановится. Новые процессы поднимут новый демон с новым пулом и старых не увидят.
  • Права клиентов не проверяются. Любой процесс, подключившийся к абстрактному сокету демона, получает доступ ко всему пулу. Для взаимодействия процессов с разным уровнем доверия библиотека не предназначена.
  • Каждый процесс, загрузивший библиотеку, даже только вызывающий, запускает потоки-исполнители по числу CPU. После каждого запроса они недолго активно ждут, прежде чем уснуть.
  • Таймаут отсчитывается от последнего пришедшего ответа, а не от начала вызова.
  • Размер пула фиксирован при сборке.

📚 Документация

  • doc/ARCHITECTURE.md — внутреннее устройство, жизненный цикл вызова, отказы и восстановление.
  • libs/tlsf_txn/README.md — транзакционный аллокатор TLSF.

📜 Лицензия

Код libsrpc распространяется под лицензией Apache-2.0. Подробности — в файле LICENSE.