InquirerPNP probes source of fake DLSU-D mass shooting reportThe Jerusalem PostUS Senate unanimously passes bipartisan resolution honoring American October 7 victimsPunchOyo agency seizes 20 cows over open grazing, crop destructionBollywood HungamaNushrratt Bharuccha to undergo spine surgery: Team issues official statement amid accident reportsESPN Deportes¿Por qué se celebra el Gran Premio de Bahréin de F1 en Malasia?20 MinutenDas musst du zum Fall um die «Cornell Seven» wissenZDF heuteEntdecken Sie das ZDF-NachrichtenstudioKhaosod EnglishThailand launches THIM app as digital gateway for foreignersCollider8 Netflix Shows That Deserve a Place Among TV's All-Time Greatest, RankedLa PresseTransports en direct | Rien à signalerRTL BoulevardBekende Nederlanders in actie voor acceptatie homoseksualiteitGhaflaUS death row inmate survives two lethal injection doses during failed execution attempt
The Daily Newsstand · Free, Always
Friday, October 2, 2026

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

Translate

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

Зачем это нужно

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

Архитектура

Архитектура держится на трёх решениях.

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

Во-вторых, свой аллокатор — транзакционный TLSF с robust-mutex, встроенным прямо в структуру пула. Обратная сторона удобства единого адресного пространства в том, что любой из процессов может в любой момент “умереть” — по exit(), SIGKILL или segfault — и оставить после себя либо недоделанную операцию с аллокатором, либо блоки, которые больше никому не нужны. Первая часть этой проблемы решается самим аллокатором: перед каждой записью в метаданные пула старое значение сохраняется в журнал отката, и следующий процесс, захвативший мьютекс и получивший EOWNERDEAD, возвращает пул в состояние до начала прерванной операции. Внешний контроллёр для этого не нужен, пул консистентен сам по себе.

В-третьих, вместо стандартных примитивов синхронизации — POSIX-семафоры в разделяемой памяти и lock-free MPMC-очередь для передачи задач от клиента к исполнителям.

Вторая часть проблемы — утечки блоков, оставшихся после умершего владельца, — решается отдельным механизмом владения и сборкой мусора; ему посвящён отдельный раздел ниже.

Как демон оказывается в системе

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

Модель владения разделяемой памятью

При использовании несколькими приложениями аллокатора из общего пула возникает задача: как гарантировать, что блок, на который ссылается один процесс, не будет освобождён из-под него другим процессом или демоном — в том числе если владелец блока внезапно “умрёт”.

Сборка мусора и UID-владение

Для предотвращения утечек, возникающих из-за смерти процесса, реализован сборщик мусора (GC), возвращающий в пул блоки, выделенные умершим процессом. Каждый блок в пуле помечен UID процесса-владельца, а демон узнаёт о смерти клиента по обрыву Unix-сокета, после чего обходит кучу по UID умершего и освобождает его пользовательские блоки. Служебные блоки запросов так сразу не освобождаются, потому что их ещё может читать исполнитель в другом процессе: они помечаются на отложенную очистку и достаются GC только тогда, когда ни один поток их больше не держит.

Сама модель владения устроена просто. В заголовке каждого блока есть массив владельцев на двенадцать UID. libsrpc_shmem_malloc() записывает туда UID выделившего процесса, а в пул блок возвращается только тогда, когда из массива вычеркнуты все владельцы. libsrpc_shmem_link() добавляет в массив UID вызывающего процесса, а libsrpc_shmem_free() вычёркивает только UID того, кто его вызвал — если после этого владельцы ещё остались, блок остаётся выделенным. Отсюда получается способ передать блок другому процессу без копирования данных: владелец отдаёт указатель соседу, сосед делает link, после чего прежний владелец делает free и “отцепляется” от блока, а данные остаются лежать там же, где и лежали, но принадлежат уже соседу.

Блокировка блоков от GC

Такая “чистоплотность” GC вынуждает решать ещё одну задачу — гарантированный доступ к выделенным блокам чужого процесса, ведь он может внезапно завершиться во время обращения к его данным, и демон освободит блок прямо из-под читающего. Для этого в версии 0.3.0 добавлен механизм блокировки блоков от освобождения их GC в случае завершения другого процесса.

После RPC-вызова вызывающий процесс может выполнить libsrpc_shmem_proc_lock(idx, ptr): библиотека берёт исполнителя, ответившего в слоте idx последнего запроса, проверяет, что блок ptr действительно принадлежит ему, и записывает его UID в тело запроса. Пока UID там записан, пользовательские блоки этого процесса после его “смерти” в пул не возвращаются, а складываются в список GC и ждут снятия блокировки через libsrpc_shmem_proc_unlock() или libsrpc_shmem_proc_all_unlock(). Блокировка вешается на процесс целиком, а не на один указатель, поэтому достаточно предъявить любой его блок, чтобы защитить и все остальные. На один запрос можно удержать до четырёх UID.

Удержание привязано к потоку, а не к процессу: UID записывается в блок запроса, который библиотека хранит в thread-local переменной и переиспользует для всех RPC-вызовов этого потока. Поэтому снять удержание может только тот поток, который его взял, а соседний поток того же процесса работает со своим блоком запроса и чужих удержаний не видит. Последующие RPC-вызовы потока удержание не сбрасывают, оно остаётся в силе до явного unlock, но idx в libsrpc_shmem_proc_lock() всегда относится к последнему запросу, так что исполнителя нужно удерживать сразу после нужного вызова, пока следующий вызов не перезаписал слоты ответов.

При этом удержание умирает вместе с держателем. Решая, можно ли уже вернуть в пул блоки умершего процесса, демон смотрит только запросы живых процессов, а блок запроса потока освобождается при завершении самого потока. Если держатель “умрёт” или просто завершит поток, забыв снять удержание, отложенные блоки на ближайшем проходе GC всё равно вернутся в пул, и забытый unlock не превращается в вечную утечку.

Этот механизм не защищает от освобождения блока чужим процессом по free, поэтому “живые” процессы должны договариваться о механизме владения памятью, например через robust-mutex, размещённый в той же разделяемой памяти рядом с данными.

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

Всё это продемонстрировано в примере ./example/list/, где разделяемый между процессами двусвязный список хранит строки. Пример состоит из двух программ: list_db определяет у себя RPC-функцию list_api() и тем самым становится её исполнителем, а list_cli эту функцию не определяет, поэтому её обычный вызов уходит по RPC в list_db. Функция принимает код операции и указатель: DAL_LIST_OP_GET_HEAD возвращает голову списка, DAL_LIST_OP_INSERT вставляет узел, DAL_LIST_OP_REMOVE удаляет его.

При старте list_db проверяет через libsrpc_fnreg_num_get(), не зарегистрирован ли уже другой исполнитель list_api(), и если да — завершается, так как список в примере один. Затем выделяет голову списка в разделяемой памяти и инициализирует в ней robust-mutex с атрибутом PTHREAD_PROCESS_SHARED, через который все процессы договариваются о доступе к ссылкам списка. Голова принадлежит list_db, поскольку выделил её он, после чего процесс просто ждёт вызова all_exit().

Добавление строки (--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), после чего у узла два владельца, и только потом узел под мьютексом вставляется в список. Вернувшись из RPC-вызова, list_cli делает libsrpc_shmem_free(node), но блок при этом не освобождается, а лишь теряет одного из владельцев, и узел остаётся в списке, принадлежа теперь только list_db. Если list_cli после этого завершится, его “смерть” на список никак не повлияет.

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

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

Запускается пример так же, как и остальные, демон поднимается автоматически первым же процессом, слинкованным с libsrpc.so. Терминал один: ./build/example/list_db, в нём будут видны приходящие вызовы list_api. Терминал два — клиент, новые узлы вставляются в начало списка, поэтому печатаются в обратном порядке:

$ ./build/example/list_cli --add hello --add world --print
size: 30, data: world
size: 30, data: hello
$ ./build/example/list_cli --del hello --print
size: 30, data: world
$ ./build/example/all_exit

Последняя команда рассылает all_exit() всем, кто его определил, и list_db завершается.

Из чего состоит библиотека

Библиотека организована так: 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 стабильностью не отличается. Функции с переменным числом аргументов не поддерживаются, указатели осмыслены только если адресуют разделяемый пул. Блокировка от GC не спасает от чужого free, владельцев у блока не больше двенадцати, а удержанных UID на один запрос — не больше четырёх.

Репозиторий

github.com, лицензия Apache-2.0. Примеры в example/.

Расскажите, с какими сценариями работы в разделяемой между процессами памяти сталкивались, и как решали возникающие задачи.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.