Skip to content

Repository files navigation

simplerpc (libsrpc)

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

simplerpc — это легковесный и высокопроизводительный фреймворк для реализации удалённого вызова процедур (RPC) между процессами в операционной системе Linux, написанный на языке C.

Проект предоставляет механизмы для прозрачного межпроцессного взаимодействия (IPC), используя разделяемую память (Shared Memory) и UNIX-сокеты, а также включает в себя собственные lock-free примитивы (MPMC-очереди, реестр функций) и транзакционный аллокатор разделяемой памяти.

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

Терминал один: ./log (можно запустить несколько экземпляров). Терминал два: ./clc (-//-), Терминал три: ./app . В выводе первого появляется LOG("Hello, world, from app! my pid=..."). Вызовы log_write("Hello") и calc_add(1, 2) в app — обычные С-функции, но выполняются они в процессах log и clc . Вся сериализация параметров функций скрыта в X-макросах, вручную это делать не нужно. Для ипользования в своих приложениях: подключить в свои исходники #include "libsrpc.h", описать прототипы экспортируемых функций в ./src/libsrpc_rpc_functions.h (это часть исходников самой библиотеки, а не отдельный публичный API-заголовок), пересобрать libsrpc.so и слинковать её в своё приложение с флагом -Wl,--no-as-needed . Подробнее — в ./example.

Краткое описание

Проект simplerpc (библиотека libsrpc) представляет собой высокопроизводительный фреймворк для организации удалённого вызова процедур (RPC) между различными процессами в среде Linux. Архитектура базируется на использовании разделяемой памяти (Shared Memory) для передачи данных и UNIX-сокетов для сигнального взаимодействия и раздачи дескриптора памяти. Для управления памятью в разделяемых сегментах используется транзакционный аллокатор TLSF (tlsf_txn v4.1), исходники которого включены в дерево проекта (libs/tlsf_txn/). Синхронизация между потоками и процессами построена на POSIX-семафорах, размещённых в разделяемой памяти (pshared), и на lock-free структурах: MPMC-очередях (Multi-Producer Multi-Consumer) и реестре функций с CAS-публикацией.

Особенность проекта — отсутствие IDL и генератора кода: роль процесса (вызывающий или исполнитель) определяется на этапе линковки через __attribute__((weak, alias)), а вся сериализация аргументов генерируется препроцессором из одного X-макроса RPC_LIST. Вторая особенность — встроенный статический загрузчик (embedded loader), который компилируется и интегрируется в динамическую библиотеку libsrpc.so в виде C-массива с помощью утилиты xxd, что позволяет библиотеке самостоятельно запускать фоновый процесс-координатор (демон) прямо из памяти, без файла на диске.

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

  • Межпроцессный RPC: Прозрачный вызов функций, реализованных в одном процессе, из адресного пространства другого процесса.
  • Разделяемая память (Shared Memory): Использование общего участка памяти, отображаемого всеми процессами по одному виртуальному адресу, для обмена данными без лишних копирований.
  • Транзакционный аллокатор TLSF: Аллокатор tlsf_txn с детерминированным O(1) выделением, robust-мьютексом и журналом отката BUL (Binary Undo Log): падение процесса посреди операции не разрушает пул.
  • Модель владения памятью: Каждый блок в пуле помечен UID процесса-владельца (до 12 владельцев на блок). При отключении процесса демон освобождает все его блоки автоматически.
  • Сборщик мусора: Фоновый поток GC в демоне забирает служебные блоки, помеченные на отложенную очистку, проверяя их занятость через Hazard Pointer'ы.
  • Динамическая линковка RPC-функций: Процесс сообщает демону битовую карту функций, которые он умеет исполнять; демон регистрирует его в lock-free реестре в разделяемой памяти.
  • Многоадресная рассылка: Один вызов может быть исполнен всеми зарегистрированными процессами (RPC_SEND_ALL), только первым (RPC_SEND_FIRST), только последним (RPC_SEND_LAST) или по кругу (RPC_SEND_RR); результаты собираются через libsrpc_lastreq_num() / libsrpc_lastreq_get().
  • Синхронизация и очереди: Lock-free MPMC-очередь на процесс и POSIX-семафоры в разделяемой памяти для усыпления/пробуждения потоков-исполнителей.
  • Встроенный загрузчик (Embedded Loader): Статически скомпилированный загрузчик внутри библиотеки, запускающий демона из memfd через fexecve() — без файла на диске.
  • Опциональная подмена аллокатора: Перехват malloc/calloc/realloc/free с переключением на пул разделяемой памяти «на лету» (по умолчанию отключён, см. DISABLE_ALLOC).

🛠 Технологический стек

  • Язык программирования: C17
  • Система сборки: CMake (версия 3.10+), Make
  • ОС: Linux (x86_64; используются memfd_create, fexecve, абстрактные UNIX-сокеты, MAP_FIXED_NOREPLACE, MADV_DONTFORK)
  • Зависимости:
    • Стандартные библиотеки: pthread (потоки, семафоры), dl (динамическая загрузка).
    • Утилита xxd — на этапе сборки, для встраивания загрузчика.
    • Внешних зависимостей и git-подмодулей нет: аллокатор tlsf_txn включён в дерево проекта.

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

simplerpc/
├── CMakeLists.txt              # Основной файл конфигурации CMake
├── Makefile                    # Вспомогательный Makefile (обёртка над CMake)
├── doc/
│   └── ARCHITECTURE.md         # Описание внутреннего устройства
├── src/                        # Исходный код ядра библиотеки libsrpc
│   ├── libsrpc.h               # Публичное API
│   ├── libsrpc_rpc_functions.h # X-макрос RPC_LIST — список RPC-функций
│   ├── libsrpc.c               # Кодогенерация обёрток, диспетчер, реестр функций
│   ├── libsrpc_daemon.c        # Запуск демона (spawn) и его главный цикл
│   ├── libsrpc_loader.c        # Исходный код встроенного загрузчика
│   ├── libsrpc_unix_socket.c   # Транспортный уровень на базе UNIX-сокетов
│   ├── 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       # Обёртки POSIX-семафоров (pshared)
│   ├── libsrpc_errno.c         # Коды и строки ошибок libsrpc
│   ├── libsrpc_local.h         # Внутренние структуры: контекст, запрос, ответ
│   ├── libsrpc_private.h       # Идентификаторы RPC и реестр функций
│   └── macro.h, argfunc.h      # Препроцессорная кодогенерация
├── libs/                       # Сторонние исходники (в дереве, без подмодулей)
│   └── tlsf_txn/               # Транзакционный аллокатор TLSF v4.1 (+ тесты, README)
├── example/                    # Демонстрационные приложения
│   ├── app.c                   # Вызывающий процесс (caller)
│   ├── log.c                   # Исполнитель log_write()
│   ├── clc.c                   # Исполнитель calc_add(), он же вызывающий log_write()
│   └── all_exit.c              # Широковещательная остановка исполнителей
└── bench/                      # Замеры производительности
    ├── bench_ipc_baseline.c    # Базовые линии: pipe, socketpair
    ├── bench_srpc_server.c     # Процесс-исполнитель для замеров
    ├── bench_srpc_client.c     # Замеры задержки и пропускной способности
    └── run_bench.sh            # Сценарий запуска замеров

🚀 Сборка и установка

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

1. Клонирование репозитория

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

2. Сборка проекта

В проекте предусмотрен Makefile, который автоматизирует вызов CMake.

make all

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

Для ручной сборки через CMake:

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

Очистка: make clean (удаляет каталог build).

Режимы сборки и параметры

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

  • release (по умолчанию): оптимизация -O3.
  • debug: отладочная информация -O0 -ggdb3 -DDEBUG (включает подробный вывод DBG_PRINT).

Дополнительно поддерживаются следующие параметры CMake:

Параметр По умолчанию Назначение
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 Ёмкость MPMC-очереди как степень двойки (2^10 = 1024)
SHMEM_SIZE_KB 4096 Размер разделяемой памяти в килобайтах
SHMEM_BASE_VADR 0x200000000000 Базовый виртуальный адрес отображения разделяемой памяти

Пример:

cmake -DBUILD_TYPE=debug -DDBG_LVL=3 -DSHMEM_SIZE_KB=16384 ..

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

💡 Пример использования

Объявление 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, RPC_SEND_FIRST, RPC_SEND_LAST, RPC_SEND_RR. Функции с переменным числом параметров не допускаются.

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

Отдельного «серверного» API нет. Процесс, который определил функцию из RPC_LIST в своём коде, автоматически становится её исполнителем — сильный символ перекрывает слабый alias из 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);          /* уйдёт по RPC во все исполнители */

/* Сбор результатов от всех исполнителей (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) printf("pid[%d]=%d\n", i, pid);
}
libsrpc_shmem_free(str);

Указатели, передаваемые в RPC-функции, должны адресовать разделяемый пул (libsrpc_shmem_malloc) — библиотека копирует только сами аргументы, но не данные за указателями.

Таймаут ожидания ответа задаётся в микросекундах: libsrpc_timeout_oneshot_set() (следующий вызов), libsrpc_timeout_func_set() (конкретная функция), libsrpc_timeout_global_set() (все вызовы). По умолчанию 1 с.

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

Запуск примеров

В директории example/ представлены четыре приложения. Фоновый демон-координатор запускать не нужно — он поднимается автоматически первым же процессом, слинкованным с libsrpc.so, и завершается после отключения последнего клиента.

  1. log — процесс-исполнитель, предоставляет log_write().
  2. clc — процесс-исполнитель calc_add(), при этом сам вызывает log_write() по RPC.
  3. app — вызывающий процесс: выделяет строку в разделяемом пуле, вызывает log_write() и calc_add(), печатает результаты.
  4. all_exit — вызывает all_exit(), широковещательно останавливая всех исполнителей.

Запуск исполнителей (в отдельных терминалах):

./build/example/log
./build/example/clc

Запуск клиента:

./build/example/app

Остановка исполнителей:

./build/example/all_exit

В процессе выполнения клиент выделит память в разделяемом пуле (libsrpc_shmem_malloc), запишет туда строку и инициирует RPC-вызов функции логирования, которая выполнится в процессе log. Функция calc_add() при этом исполнится в процессе clc.

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

Каталог bench/ содержит замеры задержки RPC-вызова в сравнении с базовыми механизмами IPC:

make all
./bench/run_bench.sh build 4000

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

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

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

📜 Лицензия

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