Минималистичный подход к межпроцессному взаимодействию в Linux для Delphi/Pascal
Введение
В этой статье мы рассмотрим различные способы организации межпроцессного взаимодействия (IPC) между небольшими консольными программами на Linux с использованием Object Pascal (Delphi/Free Pascal). Особое внимание уделим минималистичным подходам, которые позволяют эффективно передавать простые данные (например, булево значение) между процессами с минимальными накладными расходами.
Постановка задачи
Как видно из обсуждения на форуме, у пользователя Curt Carpenter возникла следующая задача:
- Имеются две независимые консольные программы PA и PB
- Программа PB должна предоставлять программе PA доступ к одному булеву значению (true/false)
- Для PA значение должно быть доступно только для чтения
- PB может изменять это значение в любой момент
- Требуется реализовать это взаимодействие с минимальным использованием ресурсов в Linux
Возможные решения
1. Использование файлов-семафоров
Самый простой подход, предложенный dbannon, предполагает использование временного файла:
// Программа PB (запись)
var
f: TextFile;
begin
AssignFile(f, '/tmp/ipc_semaphore');
Rewrite(f);
WriteLn(f, 'True'); // или 'False'
CloseFile(f);
end;
// Программа PA (чтение)
var
f: TextFile;
s: string;
value: Boolean;
begin
if FileExists('/tmp/ipc_semaphore') then
begin
AssignFile(f, '/tmp/ipc_semaphore');
Reset(f);
ReadLn(f, s);
CloseFile(f);
value := (s = 'True');
end;
end;
Плюсы:
- Простота реализации
- Не требует специальных библиотек
Минусы:
- Задержки из-за операций ввода-вывода - Проблемы с блокировкой файлов при параллельном доступе
- "Грязный" подход с точки зрения системного администрирования
2. Использование Shared Memory (разделяемая память)
Более эффективное решение, предложенное TRon, использует системные вызовы shmget/shmat:
// Общий модуль для PA и PB
unit SharedBool;
{$mode objfpc}{$H+}
{$modeswitch advancedrecords}
interface
uses
BaseUnix, SysUtils;
type
TSimpleSharedBool = record
private
FKey: TKey;
FId: cint;
FStore: PBoolean;
procedure AllocateSMS;
procedure AttachSMS;
function GetValue: Boolean;
procedure SetValue(AValue: Boolean);
public
class function Create(AKey: cint): TSimpleSharedBool; static;
property Value: Boolean read GetValue write SetValue;
end;
implementation
class function TSimpleSharedBool.Create(AKey: cint): TSimpleSharedBool;
begin
Result.FKey := AKey;
Result.AllocateSMS;
Result.AttachSMS;
Result.FStore^ := False;
end;
procedure TSimpleSharedBool.AllocateSMS;
begin
FId := shmget(FKey, SizeOf(Boolean), IPC_CREAT or &666);
if FId < 0 then
Halt(1);
end;
procedure TSimpleSharedBool.AttachSMS;
begin
FStore := shmat(FId, nil, 0);
if PtrInt(FStore) = PtrInt(-1) then
Halt(2);
end;
function TSimpleSharedBool.GetValue: Boolean;
begin
Result := FStore^;
end;
procedure TSimpleSharedBool.SetValue(AValue: Boolean);
begin
FStore^ := AValue;
end;
end.
Плюсы:
- Максимальная производительность (работа в памяти)
- Минимальные накладные расходы - Нативная поддержка в Linux
Минусы:
- Более сложная реализация
- Требует работы с системными вызовами
3. Использование SimpleIPC
Библиотека SimpleIPC, входящая в состав Free Pascal, предоставляет более высокоуровневый интерфейс:
// Программа PB (сервер)
uses
SimpleIPC;
var
Server: TSimpleIPCServer;
begin
Server := TSimpleIPCServer.Create(nil);
try
Server.ServerID := 'my_ipc_server';
Server.Global := True;
Server.StartServer;
Server.BooleanMessage['flag'] := True; // или False
// Ожидаем соединений
while True do
Sleep(1000);
finally
Server.Free;
end;
end;
// Программа PA (клиент)
uses
SimpleIPC;
var
Client: TSimpleIPCClient;
FlagValue: Boolean;
begin
Client := TSimpleIPCClient.Create(nil);
try
Client.ServerID := 'my_ipc_server';
if Client.ServerRunning then
begin
FlagValue := Client.BooleanMessage['flag'];
WriteLn('Flag value: ', FlagValue);
end;
finally
Client.Free;
end;
end;
Плюсы:
- Простота использования - Кроссплатформенность
- Встроенная поддержка в FPC
Минусы:
- Больший расход памяти (как отметил Curt Carpenter)
- Использует DBus в Linux, что может быть избыточно для простых задач
4. Использование Unix Domain Sockets
Альтернативный подход с использованием сокетов:
// Программа PB (сервер)
uses
Sockets, BaseUnix;
var
Socket: LongInt;
Addr: TUnixSockAddr;
Value: Boolean;
begin
Socket := fpSocket(AF_UNIX, SOCK_STREAM, 0);
Addr.sun_family := AF_UNIX;
StrPCopy(Addr.sun_path, '/tmp/ipc_socket');
fpBind(Socket, @Addr, SizeOf(Addr));
fpListen(Socket, 1);
Value := True; // Устанавливаем значение
while True do
begin
// Принимаем соединение и отправляем значение
var ClientSocket := fpAccept(Socket, nil, nil);
fpSend(ClientSocket, @Value, SizeOf(Value), 0);
fpClose(ClientSocket);
end;
end;
// Программа PA (клиент)
uses
Sockets, BaseUnix;
var
Socket: LongInt;
Addr: TUnixSockAddr;
Value: Boolean;
begin
Socket := fpSocket(AF_UNIX, SOCK_STREAM, 0);
Addr.sun_family := AF_UNIX;
StrPCopy(Addr.sun_path, '/tmp/ipc_socket');
if fpConnect(Socket, @Addr, SizeOf(Addr)) = 0 then
begin
fpRecv(Socket, @Value, SizeOf(Value), 0);
WriteLn('Received: ', Value);
end;
fpClose(Socket);
end;
Плюсы:
- Гибкость (можно расширить для передачи большего количества данных)
- Нативная поддержка в Linux
- Хорошая производительность
Минусы:
- Более сложная реализация, чем SimpleIPC
- Требует управления соединениями
5. Использование inotify (отслеживание изменений файлов)
Решение, предложенное duralast, основано на механизме inotify:
uses
BaseUnix, SysUtils;
procedure WatchFile;
var
fd, wd: cint;
buf: array[0..4095] of char;
len: cint;
begin
fd := fpinotify_init();
if fd < 0 then Exit;
wd := fpinotify_add_watch(fd, '/tmp/ipc_flag', IN_MODIFY);
if wd < 0 then Exit;
while True do
begin
len := fpRead(fd, @buf, SizeOf(buf));
if len > 0 then
begin
// Файл был изменен, можно читать значение
var f: TextFile;
AssignFile(f, '/tmp/ipc_flag');
Reset(f);
var Value: Boolean;
ReadLn(f, Value);
CloseFile(f);
WriteLn('Value changed to: ', Value);
end;
end;
fpinotify_rm_watch(fd, wd);
fpClose(fd);
end;
Плюсы:
- Реагирует только на изменения, не требует polling
- Нативная поддержка в Linux
Минусы:
- Все еще требует работы с файлами
- Более сложная реализация, чем простой polling файла
Оптимальное решение для задачи Curt Carpenter
Исходя из требований Curt Carpenter (минимальное использование ресурсов, необходимость уведомления о "новой почте"), наиболее подходящими решениями являются:
Shared Memory (shmget/shmat) - абсолютный минимум по использованию ресурсов, максимальная производительность.
Unix Domain Sockets - немного больше накладных расходов, но более гибкое решение.
SimpleIPC - если размер исполняемых файлов не критичен.
Для конкретной задачи передачи одного булева значения я рекомендую использовать разделяемую память, как это показано в примере TRon. Это дает:
- Минимальные накладные расходы
- Мгновенную передачу данных между процессами
- Нативную поддержку в Linux
Расширенный пример с использованием Shared Memory
Приведу более полный пример с обработкой ошибок и корректным освобождением ресурсов:
unit SharedBool;
{$mode objfpc}{$H+}
{$modeswitch advancedrecords}
interface
uses
BaseUnix, SysUtils;
type
TSharedBool = record
private
FKey: TKey;
FId: cint;
FStore: PBoolean;
procedure CreateSharedMemory;
procedure OpenSharedMemory;
procedure AttachSharedMemory;
procedure DetachSharedMemory;
function GetValue: Boolean;
procedure SetValue(AValue: Boolean);
public
constructor Initialize(AKey: Integer);
destructor Finalize;
property Value: Boolean read GetValue write SetValue;
class function GenerateKey(const Path: string; ProjectID: Byte): TKey; static;
end;
implementation
constructor TSharedBool.Initialize(AKey: Integer);
begin
FKey := AKey;
FId := -1;
FStore := nil;
// Пытаемся открыть существующий сегмент
FId := shmget(FKey, SizeOf(Boolean), 0);
if FId = -1 then
CreateSharedMemory
else
OpenSharedMemory;
AttachSharedMemory;
end;
destructor TSharedBool.Finalize;
begin
if Assigned(FStore) then
DetachSharedMemory;
end;
procedure TSharedBool.CreateSharedMemory;
begin
FId := shmget(FKey, SizeOf(Boolean), IPC_CREAT or IPC_EXCL or &666);
if FId = -1 then
raise Exception.Create('Failed to create shared memory segment: ' + SysErrorMessage(fpGetErrNo));
end;
procedure TSharedBool.OpenSharedMemory;
begin
FId := shmget(FKey, SizeOf(Boolean), 0);
if FId = -1 then
raise Exception.Create('Failed to open shared memory segment: ' + SysErrorMessage(fpGetErrNo));
end;
procedure TSharedBool.AttachSharedMemory;
begin
FStore := shmat(FId, nil, 0);
if PtrInt(FStore) = PtrInt(-1) then
raise Exception.Create('Failed to attach shared memory: ' + SysErrorMessage(fpGetErrNo));
// Инициализируем значение, если это новый сегмент
if FStore^ <> False then
FStore^ := False;
end;
procedure TSharedBool.DetachSharedMemory;
begin
if shmdt(FStore) = -1 then
raise Exception.Create('Failed to detach shared memory: ' + SysErrorMessage(fpGetErrNo));
FStore := nil;
end;
function TSharedBool.GetValue: Boolean;
begin
if not Assigned(FStore) then
raise Exception.Create('Shared memory not attached');
Result := FStore^;
end;
procedure TSharedBool.SetValue(AValue: Boolean);
begin
if not Assigned(FStore) then
raise Exception.Create('Shared memory not attached');
FStore^ := AValue;
end;
class function TSharedBool.GenerateKey(const Path: string; ProjectID: Byte): TKey;
begin
Result := ftok(PChar(Path), ProjectID);
if Result = -1 then
raise Exception.Create('Failed to generate key: ' + SysErrorMessage(fpGetErrNo));
end;
end.
Пример использования:
// Программа PB (устанавливает значение)
var
SharedBool: TSharedBool;
begin
SharedBool.Initialize(TSharedBool.GenerateKey('/tmp', 42));
try
SharedBool.Value := True; // или False
WriteLn('Press Enter to exit...');
ReadLn;
finally
SharedBool.Finalize;
end;
end;
// Программа PA (читает значение)
var
SharedBool: TSharedBool;
begin
SharedBool.Initialize(TSharedBool.GenerateKey('/tmp', 42));
try
WriteLn('Current value: ', SharedBool.Value);
WriteLn('Press Enter to exit...');
ReadLn;
finally
SharedBool.Finalize;
end;
end;
Заключение
Для реализации минималистичного межпроцессного взаимодействия в Linux на Delphi/Free Pascal существует несколько подходов:
Для самых простых случаев - файлы-семафоры или inotify
Для максимальной производительности - разделяемая память (shmget/shmat)
Для баланса между простотой и функциональностью - Unix Domain Sockets или SimpleIPC
В контексте задачи Curt Carpenter по созданию "межпроцессной сети" для множества небольших программ оптимальным выбором является использование разделяемой памяти, так как этот подход:
- Обеспечивает минимальные накладные расходы
- Позволяет мгновенно передавать данные между процессами
- Нативно поддерживается Linux
- Масштабируется для более сложных сценариев
Примеры кода, приведенные в статье, можно адаптировать для конкретных нужд, добавляя обработку ошибок и дополнительные функции по мере необходимости.
Статья описывает различные методы межпроцессного взаимодействия в Linux для Delphi/Pascal, уделяя особое внимание минималистичным подходам, таким как файлы-семафоры, разделяемая память, SimpleIPC, Unix Domain Sockets и inotify.
Комментарии и вопросы
Получайте свежие новости и обновления по Object Pascal, Delphi и Lazarus прямо в свой смартфон. Подпишитесь на наш Telegram-канал delphi_kansoftware и будьте в курсе последних тенденций в разработке под Linux, Windows, Android и iOS
Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.