Windows - Meziprocesní komunikace (Interprocess communication, IPC)

Meziprocesní komunikace (IPC) jsou mechanizmy umožňující komunikaci a sdílení dat mezi procesy. Některé formy IPC fungují mezi několika speciálními procesy, jiné mezi počítači na síti.
Typicky, aplikace používají IPC jako server nebo jako klient. Klient je aplikace nebo proces, který požaduje službu od jiné aplikace nebo procesu. Server je aplikace nebo proces, který odpovídá na požadavky klientů. Spousta aplikací se chová jako server i jako klient.

Win32 API poskytuje tyto mechanizmy IPC: schránku, Dynamic Data Exchange (DDE), mapování souboru, poštovní schránky, OLE, roury, RPC, Windows Sockets, WM_COPYDATA.

Aplikace většinou využívají více mechanizmů IPC. Minimálně schránku, pak DDE a OLE. Odpovědi na tyto otázky ukáží, zda je pro aplikaci výhodné použít nějaký mechanizmus IPC:

Mapování souboru (File mapping)

Mapování souboru umožňuje procesu nahlížet na obsah soubor jako na kus paměti v jeho adresovém prostoru. Proces pak může jednoduše přes ukazatele číst a modifikovat soubor. Když více procesů přistupuje ke stejnému mapování souboru, každý dostane ukazatel do vlastní paměti. Procesy musí používat synchronizaci, např. semafor, pro vyvarování se poškození dat.
Speciální případ mapování souboru je pojmenovaná sdílená paměť. Pokud se jako mapovaný soubor zadá systémový swapovací soubor, je mapovaná paměť sdílenou pamětí.
Mapování souboru lze použít jen mezi procesy na lokálním počítači.

Operační systém vytvoří objekt mapování souboru (file-mapping object). File view je kus paměti procesu, zpřístupňující obsah souboru. Funkce mapování souboru dovolují procesu vytvořit objekt i view. Mapovaný soubor může být jakýkoliv soubor nebo swapovací soubor.
Objekt mapování souboru může obsahovat celý nebo jen část souboru. View může obsahovat celý nebo jen část objektu. Proces může vytvořit více view pro jeden objekt.

Mapování souboru má dvě výhody:

Nejdříve je potřeba otevřít soubor pomocí funkce CreateFile. Aby ostatní procesy nemohly do tohoto souboru zapisovat, v parametru fdwShareMode se zadá 0. Handle souboru musí být otevřený tak dlouho, dokud bude potřeba objekt mapování.

CreateFileMapping
  • vytvoří objekt mapování souboru (při prvním mapování souboru) nebo vrátí handle již existujícího objektu
  • vrátí handle objektu, při chybě NULL
  • hFile specifikuje handle souboru, pokud je to 0xFFFFFFFF, mapuje se systémový swapovací soubor
  • musí "sedět" práva přístupu v parameru flProtect jako u funkce CreateFile:
    • PAGE_READONLY - přístup jen pro čtení, při CreateFile musí být GENERIC_READ
    • PAGE_READWRITE - čtení i zápis, při CreateFile GENERIC_READ a GENERIC_WRITE
  • velikost objektu není závislá na velikosti souboru, soubor se při větším objektu zvětší
  • pomocí dwMaximumSizeHigh a dwMaximumSizeLow se určí počet mapovaných bytů, pokud jsou oba 0, namapuje se přesně celý soubor
  • lpName specifikuje jméno objektu, může být NULL


HANDLE hMapFile;

hMapFile = CreateFileMapping(hFile,    // Current file handle. 
    NULL,                              // Default security. 
    PAGE_READWRITE,                    // Read/write permission. 
    0,                                 // Max. object size. 
    0,                                 // Size of hFile. 
    "MyFileMappingObject");            // Name of mapping object. 
 
if (hMapFile == NULL) { 
    ErrorHandler("Could not create file-mapping object."); 
} 

Př. Napište program, který vytvoří objekt mapování souboru pro čtení i zápis.

MapViewOfFile
MapViewOfFileEx
  • vytvoří view objektu mapování souboru, zadaného v hFileMappingObject, v adresovém prostoru procesu
  • vrací ukazatel na view, přes který lze přistupovat k souboru, při chybě NULL
  • musí "sedět" práva přístupu v parametru dwDesiredAccess jako u funkce CreateFileMapping:
    • FILE_MAP_READ - přístup jen pro čtení, při CreateFileMapping musí být PAGE_READONLY nebo PAGE_READWRITE
    • FILE_MAP_WRITE nebo FILE_MAP_ALL_ACCESS - čtení i zápis, při CreateFileMapping PAGE_READWRITE
  • lpBaseAddress (u MapViewOfFileEx) specifikuje adresu počátku view, musí být násobek stránky paměti, tu vrací funkce GetSystemInfo
  • velikost view dwNumberOfBytesToMap musí být rovna nebo menší než velikost objektu mapování, při 0 se mapuje celý soubor
  • dwFileOffsetHigh a dwFileOffsetLow specifikují offset mapování v souboru, musí být násobek stránky paměti


LPVOID lpMapAddress;
lpMapAddress = MapViewOfFile(hMapFile, // Handle to mapping object. 
    FILE_MAP_ALL_ACCESS,               // Read/write permission 
    0,                                 // Max. object size. 
    0,                                 // Size of hFile. 
    0);                                // Map entire file. 
 
if (lpMapAddress == NULL) { 
    ErrorHandler("Could not map view of file."); 
} 

Př. Vytvořte view objektu pro zápis a něco do mapovaného souboru uložte.

FlushViewOfFile
  • okamžitě zapíše změny view do souboru
  • zadá se adresa v lpBaseAddress a velikost v dwNumberOfBytesToFlush, při 0 se zapíše vše

UnmapViewOfFile
  • zruší všechny view k objektu mapování

Pro zrušení objektu mapování se jednoduše použije funkce CloseHandle ještě před uzavřením souboru.

Př. Před ukončením programu zrušte view i objekt mapování souboru.

OpenFileMapping
  • otevře existující pojmenovaný (lpName) objekt mapování souboru
  • dwDesiredAccess specifikuje přístup, stejné jako u MapViewOfFile
  • vrací handle objektu, při chybě NULL


HANDLE hMapFile;
LPVOID lpMapAddress;

hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, // Read/write permission. 
    FALSE,                             // Do not inherit the name
    "MyFileMappingObject");            // of the mapping object. 
 
if (hMapFile == NULL) { 
    ErrorHandler("Could not open file-mapping object."); 
} 
 
lpMapAddress = MapViewOfFile(hMapFile, // Handle to mapping object. 
    FILE_MAP_ALL_ACCESS,               // Read/write permission. 
    0,                                 // Max. object size. 
    0,                                 // Size of hFile. 
    0);                                // Map entire file. 
 
if (lpMapAddress == NULL) { 
    ErrorHandler("Could not map view of file."); 
} 

Pokud má více procesů namapován stejný soubor, říká se mu pojmenovaná sdílená paměť. Musí koordinovat přístup k souboru pomocí semaforů, mutexů, událostí, atd.

Př. Napište druhý program, který otevře existující objekt mapování souboru (z předchozího programu), vytvoří view pro čtení, přečte, co tam první program zapsal a vypíše to.

Roury (Pipes)

Win32 API poskytuje dva druhy rour pro obousměrnou komunikaci: nepojmenované (unnamed, anonymous) a pojmenované (named) roury (pipes). Nepojmenované roury umožňují výměnu informací mezi dvěma příbuznými procesy. Typicky jsou používány pro přesměrování std. vstupu a výstupu potomka, takže může komunikovat s rodičem. Jeden proces může zapisovat a druhý číst z roury. Nepojmenované roury nelze použít přes síť ani mezi nepříbuznými procesy. Pojmenované roury se používají pro přenos dat mezi dvěma nepříbuznými procesy nebo mezi procesy na různých počítačích. Typicky named-pipe server vytvoří rouru se známým jménem. Named-pipe client ji na druhém konci otevře. Pak mohou oba číst i zapisovat do roury.

Roura má dva konce. Jednosměrná roura umožňuje procesu na jednom konci zapisovat do ní, procesu na druhém konci z ní číst. Obousměrná (duplex) roura umožňuje oběma procesům číst i zapisovat.

Nepojmenované roury (Anonymous pipes)

Nepojmenovaná roura je jednosměrná roura pro přenos dat mezi rodičem a potomkem.

CreatePipe
  • vytvoří nepojmenovanou rouru
  • vrátí oba její konce (handly) - čtecí do hReadPipe a zapisovací do hWritePipe
  • ve struktuře na lpPipeAttributes musí být nastavené bInheritHandle, aby se konce roury mohly dědit
  • nSize určuje velikost bufferu, při 0 se použije defaultní hodnota

Pro čtení z roury se používá funkce ReadFile, pro zápis WriteFile. ReadFile vrací 0, pokud je uzavřený zapisovací konec roury. Zápis je bufferovaný, pokus chceme zapsat více dat a buffer je plný, funkce WriteFile čeká dokud se něco nepřečte, pak se pošlou další data. Roura existuje dokud nejsou uzavřeny oba její konce pomocí CloseHandle. Všechny handly rour jsou uzavřeny při ukončení procesu. Dědění handlu lze určit ještě pomocí DuplicateHandle a CreateProcess.

Pokud chceme přesměrovat např. std. výstup potomka do zapisovacího konce roury, uděláme to takto:

Postup pro přesměrování std. vstupu potomka na čtecí konec roury je obdobný.

PŘÍKLAD (rodič a potomek)

Pojmenované roury (Named pipes)

Pojmenovaná roura je jednosměrná nebo obousměrná roura pro komunikaci mezi jedním serverem a jedním nebo více klienty. Všechny instance pojmenované roury sdílejí jméno roury, ale každá má vlastní buffery a handly. Každý proces může vystupovat jako server nebo klient. Server je potom ten, který rouru vytvoří, klient se připojí na instanci roury.

Pro jméno roury se používá tvar \\ServerName\pipe\PipeName, kde ServerName je jmého počítače nebo tečka (lokální počítač). Server nemůže vytvořit rouru na jiném počítači.

CreateNamedPipe
  • vytvoří instanci roury lpName
  • vrací hadle serverového konce instance roury, při chybě INVALID_HANDLE_VALUE
  • dwOpenMode specifikuje přístupové módy:
    • PIPE_ACCESS_INBOUND - server čte a klient zapisuje
    • PIPE_ACCESS_OUTBOUND - server zapisuje a klient čte
    • PIPE_ACCESS_DUPLEX - oba čtou i zapisují
  • dwPipeMode specifikuje módy typu, čtení a čekání:
    • PIPE_TYPE_BYTE - data jsou do roury zapisována jako proud bytů, systém nerozlišuje byty z různých zápisových operací, defaultní
    • PIPE_TYPE_MESSAGE - byty z operace zápisu tvoří jednotku zprávy
    • PIPE_READMODE_BYTE - data jsou čtena z roury jako proud bytů, jediné, pokud je PIPE_TYPE_BYTE, defaultní
    • PIPE_READMODE_MESSAGE - data jsou čtena jako proud zpráv, úspěšné přečtení je jen přečtení celé zprávy
    • PIPE_WAIT - blokující mód, operace čtení, zápisu a připojení jsou blokovány, defaultní
    • PIPE_NOWAIT - neblokující mód
  • nMaxInstances udává při prvním volání maximální počet instancí roury, další instance se vytvoří znova voláním této funkce (se stejným číslem), PIPE_UNLIMITED_INSTANCES je nekonečný počet
  • nOutBufferSize a nInBufferSize udávají velikosti bufferů v bytech

Klienti používají CreateFile pro připojení k rouře. Musí specifikovat příslušný přístupový mód v parametru dwDesiredAccess. Např. GENERIC_READ pro rouru vytvořenou s PIPE_ACCESS_OUTBOUND. Handle roury, který vrací CreateFile je vždy PIPE_READMODE_BYTE. ReadFile při blokujícím módu čeká, až bude z roury co číst. WriteFile při blokujícím módu čeká, až bude v bufferu roury místo pro celý zápis. Při neblokujícím se ihned vrátí a nezapíše nic nebo zapíše, co se do bufferu vleze. ConnectNamedPipe při blokujícím módu čeká, až se klient připojí k rouře.

SetNamedPipeHandleState
  • nastavuje módy čtení a čekání roury hNamedPipe
  • lpMode ukazuje na slovo s módy

GetNamedPipeHandleState
  • vrací, jaký je mód čtení a čekání (do lpState), počet instancí (do lpCurInstances) roury hNamedPipe

ConnectNamedPipe
  • umožní serveru připojení klientů na instanci roury hNamedPipe

Když server komunikuje s více klienty přes více instancí roury, jedna ze strategií je vytvořit pro každou instanci roury samostatné vlákno, které zabezpečuje komunikaci s jedním klientem.

CallNamedPipe
  • připojí klienta k rouře lpNamedPipeName (která byla vytvořena s PIPE_TYPE_MESSAGE), zapíše a přečte z roury a uzavře ji
  • lpInBuffer je ukazatel na buffer s daty pro zápis, nInBufferSize jeho velikost, lpOutBuffer na buffer, který dostane data z roury, nOutBufferSize jeho velikost
  • skončí s chybou, pokud byla roura vytvořena s PIPE_TYPE_BYTE

WaitNamedPipe
  • čeká, až je dostupná instance roury (lpNamedPipeName) pro připojení

PeekNamedPipe
  • čte z roury (hNamedPipe) bez jejího vyprazdňování do bufferu na lpBuffer, velikosti nBufferSize

Při ukončení práce s instancí roury by server ještě před odpojením od klienta měl zavolat funkci FlushFileBuffers, která čeká, až klient přečte zbytek obsahu roury.

DisconnectNamedPipe
  • odpojí serverový konec instance roury (hNamedPipe) od klienta

GetNamedPipeInfo
  • vrací informace o rouře (hNamedPipe), jako např. typ (lpFlags), velikost bufferů (lpOutBufferSize a lpInBufferSize), maximální počet instancí (lpMaxInstances)

Př. Napište program (server), který vytvoří pojmenovanou rouru, ze které bude číst a vypisovat proud bytů. Pak napište klienta, který do roury něco zapíše.

PŘÍKLAD (server a klient)

Další funkce týkající se rour jsou:



Jan Outrata
outrata@phoenix.inf.upol.cz