LINUX.ORG.RU

Simplerpc: Простой RPC на Си для Linux-приложений

 , ,


0

2

Есть классическая проблема: изолированные процессы — хорошо для надёжности, но хочется иногда позвать функцию из соседа, как будто она лежала в той же библиотеке. Стандартные IPC — это всегда протокол: описал интерфейс, сгенерировал стабы, поднял сервер, прописал адреса. simplerpc предлагает путь короче: ты пишешь обычную функцию на Си, линкуешь библиотеку — и она автоматически становится RPC-методом, доступным другим процессам на той же машине.

Терминал один: ./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.

Архитектура держится на трёх решениях. Во-первых, данные едут не через сокет, а по разделяемой памяти — SHMEM. Аргументы вызова лежат в общем сегменте, отображённом по одному виртуальному адресу во всех процессах, и копирования между адресными пространствами нет. Сокет используется только для сигналов: регистрация функций, передача дескриптора памяти (SCM_RIGHTS), обнаружение смерти клиента. Во-вторых, свой аллокатор — транзакционный TLSF с откатом при падении процесса. Межпроцессный мьютекс встроен прямо в структуру пула, а если владелец мьютекса погибает, следующий автоматически откатывает незавершённую транзакцию. Пул консистентен без внешнего сторожа. В-третьих, вместо стандартных примитивов синхронизации — POSIX-семафоры в разделяемой памяти и lock-free MPMC-очередь для передачи задач от клиента к воркерам.

Но самое интересное — как демон оказывается в системе. Исполняемый файл демона не лежит на диске. Он встроен в libsrpc.so в виде C-массива, записывается в анонимный memfd целиком в оперативной памяти и запускается через fexecve(). При загрузке библиотеки любым приложением автоматически форкается демон. Если процессов несколько — проигравший гонку bind() на абстрактном Unix-сокете молча завершается. Когда последний клиент отключается — демон выходит, а память исчезает вместе с последней ссылкой на memfd. Никакого PID-файла, никакого init-скрипта, никакого сервиса. Библиотека сама себя разворачивает.

Библиотека организована так: guard daemon (фоновый координатор), транспорт (Unix-сокеты и SCM_RIGHTS), аллокатор TLSF с транзакциями, сборщик мусора в разделяемой памяти с hazard pointers, RPC через X-макросы (один файл — единственный источник истины), динамическая линковка через weak alias, дескрипторы процессов и потоков, lock-free очереди и синхронизация. Всё это — в bench/ можно найти бенчмарки, если захотите сравнить сами.

Попробовать три команды: mkdir build && cd build && cmake .. && make, затем в одном терминале ./log, в другом ./app. Готово.

Ограничения: только Linux, только локальные процессы на одной машине, ранняя стадия развития — API стабильностью не отличается. Функции с переменным числом аргументов не поддерживаются, указатели осмыслены только если адресуют разделяемый пул.

Репозиторий: github, лицензия Apache-2.0. Если интересно — смотрите код на Си.



Проверено: hobbit ()
Последнее исправление: hobbit (всего исправлений: 10)

memory_order_release

А ты хорошо разбираешься в тонкостях acquire/release, чтоб не было гонок? :)

dynamic_cast
()

Сколько возни ради решения ненужной задачи. Подозреваю что ещё и с проблемами.

firkax ★★★★★
()
/* TZ: DeepSeek (Chat)
 * TZ fix: Claude Sonnet 5 Medium (Chat)
 * Implementation & fix TZ: MiMo V2.5 Free (Agent) */

Новости нужен тег [вайбкодинг].

shdown ★★
()

Емнип обычно это через сокеты было, а очередь/протокол потом быстренько переносил в shmem если надо, всё похоже было... Но тут пришёл дипсик!

Shadow ★★★★★
()
Ответ на: комментарий от Shadow

Но тут пришёл дипсик!

Взял вашу разработку, и оп-па, имеем simplerpc. Ловкость рук, и никакого мошенничества.

VIT ★★★
()

Самый лучший rpc это memcpy структуры в сокет. Никаких либ не нужно.

Reset ★★★★★
()
Последнее исправление: Reset (всего исправлений: 1)

а сериализация как устроена? сложные вложенные структуры? указатели?

ergo ★★★★
()
Ответ на: комментарий от Reset

Самый лучший rpc это memcpy структуры в сокет.

Это всё же не rpc, а метод сериализации. Чтобы он записи структуры в сокет произошёл rpc, нужен ещё какой-то код

annulen ★★★★★
()
Ответ на: комментарий от Reset

Самый лучший rpc это memcpy структуры в сокет. Никаких либ не нужно.

Лучше сразу в память нужного процесса! А чтобы вообще без либ и побыстрее - без проверок существования процесса, проверок безопасности, проверок границ памяти, мониторинга, контроля и чтобы без возможности воспроизвести руками :)

И называть не «насрать байтами», а лучший и эффективный rpc :)

skyman ★★★★★
()
Ответ на: комментарий от Reset

memcpy структуры в сокет

Это так?

{
  struct stat x;
  int fd;
  fd = socket(AF_UNIX, SOCK_STREAM, 0);
  memcpy(&fd, &x, sizeof(x));
}

Или так?

{
  struct stat x;
  int fd;
  fd = socket(AF_UNIX, SOCK_STREAM, 0);
  memcpy((void*)fd, &x, sizeof(x));
}

Или так?

{
  struct stat x;
  int fd;
  fd = socket(AF_UNIX, SOCK_STREAM, 0);
  memcpy(mmap(NULL,sizeof(x),PROT_WRITE,MAP_SHARED,fd,0), &x, sizeof(x));
}

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)
Ответ на: комментарий от firkax

Фиркакс довольно категоричен в высказываниях, но тут он скорее прав.

изолированные процессы — хорошо для надёжности

и безопасности тоже! И простоты архитектуры. И эти три фактора как бы переводят сабж в разряд наколеночной поделки.

seiken ★★★★★
()

Если не хочешь описание своего велосипеда с квадратными колёсами сам написать, то мало кто захочет этот электробред читать.

Bfgeshka ★★★★★
()
Ответ на: комментарий от firkax

Так лучше

struct Msg {
    uint64_t id;
    double value;
};

constexpr size_t BUF_SIZE = 4096;

alignas(64) std::byte buffer[BUF_SIZE];

iovec iov{
    .iov_base = buffer,
    .iov_len = BUF_SIZE
};

io_uring ring;
io_uring_queue_init(256, &ring, 0);
io_uring_register_buffers(&ring, &iov, 1);

Msg msg{42, 3.14};

std::memcpy(buffer, &msg, sizeof(msg));

io_uring_sqe* sqe = io_uring_get_sqe(&ring);
io_uring_prep_send(sqe, sockfd, buffer, sizeof(msg), 0);
sqe->buf_index = 0;

io_uring_submit(&ring);
Reset ★★★★★
()
Ответ на: комментарий от Reset

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

Dark_SavanT ★★★★★
()
Ответ на: комментарий от Dark_SavanT

с чего это выравнивание поменяется? если поля никто не переставляет/не меняет выравнивание само по себе не поменяется, а размер структуры можно обложить через  static_assert

Reset ★★★★★
()

Вообще, могло бы выстрелить что-то типа rabbit mq в виде простой в использовании либы (как sqlite на фоне постгреса и mysql), а вот сабж вообще не нужно.

Shadow ★★★★★
()
Ответ на: комментарий от Reset

32 vs 64

А еще можно big endian vs little endian совместимость организовать.

firkax ★★★★★
()
Последнее исправление: firkax (всего исправлений: 1)
Ответ на: комментарий от firkax
__attribute__((packed, scalar_storage_order("little-endian")))

Только scalar_storage_order не распространяется рекурсивно на вложенные struct и union.

shdown ★★
()

Счастливой отладки

Manhunt ★★★★★
()

Я увидел что у вас очередь lock-free и мне стало сразу интересно, потому, что написать lock_free очередь у меня, в свое время не получилось.

Под нагрузкой все ломалось.

А у вас, судя по всему, получилось.

Вопрос у меня возник.

Вот у вас написано

lf_queue_result_t lf_mpmc_queue_try_dequeue(lf_mpmc_queue_t* q, void** data) {
    size_t pos = atomic_load_explicit(&q->tail, memory_order_relaxed);

    for (;;) {
        size_t idx = pos & q->mask;
        size_t seq_old = atomic_load_explicit(&q->buffer[idx].seq, memory_order_acquire);
...
...
...

Вопрос по поводу чтения pos первым atomic_load() : вы уверены, что там должен быть relaxed, а не acquire? Хочу разобраться в атомиках, расскажите, пожалуйста, алшоритм выбора третьего параметра в _explicit-версиях atomic_load/atomic_store.

Спасибо.

ЗЫЖ Я не троллю, хотя не без этого, конечно, но но Интеле\АМД вы никогда не наступите на грабли с мемори ордер. Повезло с архитектурой. Там хоть везде пиши-relaxed.

vvb333007
()
Последнее исправление: vvb333007 (всего исправлений: 1)
Ответ на: комментарий от BruteForce

Ну там довольно оптимально написан код. Иногда это выливается в тыщи строк. Лок-фри у него там везде. По стилю - написано или старым профессиональным Си-программистом или ИИшница постаралась. Щас автор придет, расскажет. Может быть.

vvb333007
()
Последнее исправление: vvb333007 (всего исправлений: 1)
Ответ на: комментарий от Reset

на x86_64 и arm64 выравнивания одинаковые, а других архитектур не существует

Вы чуть-чуть напутали.

На всех архитектурах выравнивания одинаковые: 1,2,4,8 или 16 байт. других выравниваний не существует.

vvb333007
()
Ответ на: комментарий от Shadow

Вообще, могло бы выстрелить что-то типа rabbit mq

Чел же сделал упор на том, что simple. А кролик ваш слона напоминает, если что.

vvb333007
()
Ответ на: комментарий от vvb333007

Если хотите разобраться в атомиках, то посмотрите лекции магистерского курса Константина Владимирова

indie
()
Ответ на: комментарий от vvb333007

Это и есть натуральное выравнивание для 32-байтного типа «вектор из восьми float». Там нигде нет __attribute__((packed)) или __attribute__((aligned)). Это будет компилироваться везде, не только на x86-64, только выравнивание может быть другим: https://godbolt.org/z/4Gsae5b7x.

shdown ★★
()
Последнее исправление: shdown (всего исправлений: 1)
Ответ на: комментарий от shdown

Я просто не знаком с этим аттрибутом. Почему GCC выравнивает на 16 - надо разбираться. Скорее всего, потому что не генерирует инструкций, которым такое (32) выравнивание нужно.

Но да, для векторных инструкций в SIMD, например, если вектор - 256 бит, то и выравнивание будет 256 бит.

vvb333007
()
Последнее исправление: vvb333007 (всего исправлений: 2)
Ответ на: комментарий от Shadow

Емнип обычно это через сокеты было, а очередь/протокол потом быстренько переносил в shmem

Так у него похожим образом и сделано, если прочитать описание. Сокеты для сигналлинга, дата - в shmem

vvb333007
()
Ответ на: комментарий от vvb333007

Я не точно выразился - это всё делалось чуть ли не по man socket или как-то так, т.е. для таких простых случаев там вообще ничего не надо использовать.

Shadow ★★★★★
()
Для того чтобы оставить комментарий войдите или зарегистрируйтесь.