Linux - zdieľaná pamäť (Shared memory)

Tomáš Plch  /  01. 05. 2007, 00:00

Zdieľaná pamäť je jedným z najdôležitejších prostriedkov komunikácie medzi procesmi - IPC (InterProcess Communication). V tomto článku si v jednoduchosti objasníme, ako tento mechanizmus funguje a ako ho používať. Znalosť synchronizačných primitív je výhodou, ale nie nutnou podmienkou. V tomto článku sa budeme pohybovať v prostredí operačného systému Linux.

Na to aby sme pochopili ako zdieľaná pamäť (Shared memory) funguje, objasníme (zopakujeme) si najprv pohľad procesu na pamäť a celkovo zdroje (resources), ktoré proces používa.

Dva podstatné mechanizmy operačných systémov sú virtuálna pamäť a preempcia.

Preempcia umožňuje procesu vnímať transparentne, že procesor patrí výhradne jemu. Samotný kód nedokáže vnímať preplánovanie operačným systémom a nemal by ho predpokladať ani s týmto javom nijak pracovať. Skúsenejší programátori budú namietať, že posledná veta je viacmenej nepravdivá, ale pre zjednodušenie (the sake of simplicity) si s týmto vysvetlením postačíme.

Druhý podstatný mechanizmus je virtualizácia pamäte. Tá umožňuje, že proces vníma celý pamäťový priestor ako jemu vlastný a tým pádom odpadajú určite podstatné problémy s manažmentom procesov (a iné pribúdajú). Čiže každý proces ma k dispozícii pamäť od adresy 0x0h až po určitú hranicu stanovenú nastavením operačného systému. Z aktuálne používaných 4GB (2^32) si operačný systém vyhradí spodných 1-2GB, väčšinou sú to tie 2GB.

Pozorný čitateľ si mohol všimnúť, že objem virtuálnej pamäte čo i len jedného procesu môže byť väčší, ako dostupná operačná pamäť. Mechanizmus stránkovania  je nad rámec tohoto článku. Avšak pre pochopenie si povieme, že tento mechanizmus mapuje virtuálne bloky pamäte na fyzické bloky, často o veľkosti 4KB. Každý proces ma svoju tabuľku virtuálnych stránok a každý prístup do pamäte (cez adresu do virtuálnej pamäte) sa preloží na adresu vo fyzickej pamäti. Ak dôjdu fyzické stránky, jednoducho operačný systém vyhodí niektorú málo používanú stránku do "swapu" a nahradí ju. V prípade Že chce niekto stránku zo "swapu" použije sa podobný mechanizmus. Ale to už som príliš zabehol do témy, ktorú som ani nechcel rozoberať. Možno inokedy v inom článku.

Pred praktickým úvodom si trošku rozoberieme mechanizmus akým sa so zdieľanou pamäťou pracuje. Prvým a podstatným krokom je zdieľanú pamäť získať. Keďže sa jedná o mechanizmus ktorý sa realizuje mimo proces - v kerneli - treba zavolať API kernelu. Podstatou tohoto mechanizmu je "registrácia", podobne ako u semafórov, kedy poskytnete nejaký identifikačný kľúč a operačný systém vám vráti identifikátor na narábanie s danou zdieľanou pamäťou - teda volanie iných funkcií API ktoré pracujú so zdieľanou pamäťou. Prípadne vám bude vrátená chyba, ale to si objasníme neskôr.

Keď získate takýto identifikátor, môžete si zdieľanú pamäť pripojiť (attach) pomocou ďalšej funkcie k svojmu adresovému priestoru (čiže sa vám namapuje do vašej virtuálnej pamäte). Toto si môžete vyžiadať aby bolo realizované na špecifické miesto vo vašom adresnom priestore. Pozor, toto sa nemusí nutne podariť. Ak sa to však podarí, táto pamäť bude rešpektovaná ako obsadená (napríklad z pohľadu alokátora).
Pravdaže s pamäťou sa dajú robiť rôzne operácie, nastavovať práva prístupu.

Avšak čo je dôležitejšie je ako sa zdielanej pamäte zbaviť. I v tomto mechanizme sú dva podstatné kroky. Jedným je odpojenie(dettach) zdieľanej pamäte. Toto sa realizuje buď implicitne, alebo na požiadanie pomocou API. Avšak zdieľanú pamäť treba ešte odstrániť - zmazať. Túto operáciu musí aspoň jeden z procesov realizovať. Ak by sa tak nestalo, kus pamäte (stránky) ostanú zaregistrované a budú eventuálne umiestnené do swapu. A takto zbytočne spotrebovávať pamäť je nepekné.

V princípe platí, že pamäť sa po poslednom odpojení zmaže(ak je označená za vhodnú na odstránenie). Pamäť sa dá teda dopredu označiť za zmazanú, pričom je ale špecifikum Linuxu, že i napriek nedefinovanému správaniu (podľa štandardu POSIX) v takom prípade je možné si takú pamäť pripojiť. Nie všetky implementácie POSIXu toto podporujú (a teda sa na to netreba spoliehať). Ak k pamäti nikto nieje pripojený a je označená na vhodnú na zmazanie, je zmazaná. Je to trošku komplikovanejšie, ale uvedomte si, že medzi ľubovolnou inštrukciou vášho kódu môže dôjsť k preplánovaniu (a to je už pekná schizofrénia, že? :) )

Teraz sa vrhneme na samotné funkcie.

Dôležitou informáciou je ktoré hlavičkové súbory nutne potrebujete:
#include <sys/ipc.h>
#include <sys/shm.h>
Začneme funkciou, ktorá nám získa identifikátor na zdieľanú pamäť. Pozor, to ešte nieje samotná zdieľaná pamäť, avšak po tomto kroku ju treba niekedy zmazať(remove).

int shmget(key_t key, size_t size, int shmflg);

V prípade neúspechu vráti hodnotu -1 a do premennej errno naplní druh chyby aký sa vyskytol.

Parameter key_t key predstavuje kľúč (číslo) pomocou ktorého chceme blok zdieľanej pamäte identifikovať. V prípade, že sa takto identifikovaný blok už existuje, a použité príznaky(flagy) a prístupové práva to dovoľujú, je vrátený identifikátor na tento blok zdieľanej pamäte. V prípade, že žiadna zdieľaná pamäť s týmto identifikátorom neexistuje, je vytvorená.


Pozorný čitateľ sa určite zamyslel: "Ako ale budú mať dva procesy rovnaký kľúč." V linuxe existujú rôzne možnosti ako kľúč dostať od procesu k procesu - buď za pomoci IPC (zdieľaná pamäť, rúry(pipy), semafory, atď ) alebo iné metódy (sockety, súbory). Iná možnosť je priamo tzv hardcoded - čiže pomocou nejakého pevne daného čísla, či už priamo číselnej hodnoty alebo klauzuly #define. Ale pozor, tu sa stráca celá flexibilita a v prípade, že niekoho napadne rovnako pekné číslo 666, môžu nastať nepekné problémy.

Existuje funkcia key_t ftok(const char * pathname , int proj_id), ktorá vráti systémovo unikátny identifikátor pre každú kombináciu pathname a proj_id. A tu už patrične vidíte, že trafiť sa do dvoch spoločných hodnôt pri správnom použití, je viacmenej nepravdepodobné.

Ak použijete kľúč reprezentovaný pomocou IPC_PRIVATE, je zaručené, že sa vám vytvorí nový blok zdieľanej pamäte (pravdaže ak ste neprekročili nejaký limit). Inými slovami, nezohľadní sa kľúč.

Parameter size_t size určuje veľkosť zdieľanej pamäte v bajtoch. Pozor, ak sa ne veľkosti nezhodnete a vami požadovaná veľkosť je väčšia ako veľkosť už vytvoreného bloku zdielanej pamäte. Na veľkosť sa však nemôžte úplne spoliehať, operačný systém vám môže prideliť o niečo väčší kus ako ste chceli (zaokrúhli to na špecifický násobok preddefinovanej hodnoty). Ale komu by vadilo viac pamäte.

Ešte podstatné upozornenie je, aby ste veľkosť neúmerne nepreháňali. Maximálna i minimálna hodnota tohoto č9sla je závislá od hodnôt v zdrojákoch kernelu, a pokiaľ potrebujete viac než cca 5MB na jeden zdieľaný segment, tak máte trošku problém a možno by ste mali uvažovať nad iným druhom IPC.

Parameter int shmflg sú príznaky. Rozoberieme jeden po druhom, pričom platí, že tieto príznaky môžu byť vzájomne spojené pomocou operátoru "or" (čiže '|').
IPC_CREAT - tento príznak znamená, že chcete vytvoriť zdieľanú pamäť, v prípade, že ho neuvediete a pamäť pred vami niekto nezaregistroval, funkcia ohlási chybu.
IPC_EXCL - je príznak, ktorý zabezpečí, že pokiaľ je na daný kľúč už zaregistrovaná zdieľaná pamäť, volanie API zlyhá.
Ešte sú príznaky SHM_HUGETLB a SHM_NORESERVE, ktoré majú špecifické použitie, ktoré vyžaduje hlbšie pochopenie fungovania operačného systému.

Spodných 9 bitov parametru shmflg sú prístupové práva. Pravdaže tie sa aplikujú len pri vytváraní, ak k nemu dôjde. Štandardne je použitá konvencia Process-Group-Others, pričom jednotlivé hodnoty reprezentujú kombinácie prístupových práv. Príznak execute je aktuálnou implementáciou ignorovaný. Je to presne ten istý koncept ako pri prístupových právach k súborom.

Z chybových hodnôt sú najzaujímavejšie
EACCES - proces nemá dostatočné práva na prístup k danému identifikátoru
EEXIST - pri použití IPC_CREAT | IPC_EXCL príznakov nastala situácia kedy už daná zdieľaná pamäť bola zaregistrovaná
EINVAL - bol prekročený minimálny(SHMMIN) alebo maximálny(SHMMAX) limit dostupnej pamäte pre vytvorenie zdielanej pamäte alebo nastala situácia, kedy už pre daný kľúč bola zaregistrovaná zdieľaná pamäť ale veľkosť pri volaní je väčšia ako tá už zaregistrovanej pamäte
ENOENT - IPC_CREAT nebol použitý ako flag a pamäť nikto nezaregistroval.

Ďalšou funkciou v poradí je funkcia na pripojenie získanej zdieľanej pamäte do adresového priestoru volajúceho procesu

void *shmat(int shmid, const void *shmaddr, int shmflg);

Parameter int shmid je identifikátor, ktorý sme získali pomocou nejakého úspešného volania funkcie shmget().

Parameter const void *shmaddr určuje adresu, na ktorú by ste chceli aby bola zdieľaná pamäť pripojená. Dôvodom môžu byť napríklad pointre v rámci zdieľanej pamäte. Umiestnenie na presne stanovenú adresu sa nemusí z objektívnych dôvodov podariť. Ak je táto hodnota NULL operačný systém rozhodne sám kam pripojí zdieľanú pamäť.


Parameter int shmflg určuje "ako" chcete danú zdieľanú pamäť pripojiť. Možnosti, ktoré sú pre nás aktuálne zaujímavé sú SHM_RDONLY a SHM_REMAP. V prípade SHM_RDONLY je zdieľaná pamäť namapovaná len pre čítanie a pravdaže proces musí mať práva na čítanie. A prípade, že tento príznak nieje nastavený, je zdieľaná pamäť namapovaná na čítanie i zápis a proces musí mať práva na zápis a čítanie. V prípade príznaku SHM_REMAP je možné pripojiť zdieľanú pamäť na miesto(ľubovolnej časti rozsahu ktorý pokrýva) kde sa nachádza iná zdieľaná pamäť. V prípade nastavenia tohoto príznaku nesmie byť shmaddr nastavené na hodnotu NULL.
Ďalším príznakom, ktorý si len spomenieme je SHM_RND. Tento príznak ma súvislosť so zarovnaním adresy shmaddr a vyžaduje si lepšiu predstavu stránkovania.

Funkcia vráti adresu do adresového priestoru procesu, kam bola zdieľaná pamäť pripojená. Ak vráti NULL, v errno je chyba, ku ktorej došlo.

Chyby, ktoré sa môžu vyskytnúť v errno sú nasledovné

EACCES - nedostatočné práva volajúceho procesu
EINVAL - hodnota shmid je neplatná, prípadne nebolo možné pripojiť zdieľanú pamäť na špecifikované miesto
ENOMEM - vyčerpala sa pamäť pre interné štruktúry procesu. Toto je veľmi závažná ale dosť ojedinelá chyba. Často naznačuje skrytú a veľmi nepríjemnú chybu vo vašom kóde.
K tejto funkcii je potrebné poznať funkciu

int shmdt(const void *shmaddr);

Táto funkcia slúži na popredné odpojenie zdieľanej pamäte. Toto sa zrealizuje pri likvidácii procesu(ak to nebolo dopredu realizované), ale prečo by sme nemohli kus nepotrebnej pamäte uvolniť skôr.

V prípade chyby je návratová hodnota -1(errno sa nastavený na EINVAL ak je chybný parameter shmaddr) a ak je všetko v poriadku tak 0.

Tu treba poznamenať, že interná štruktúra ohľadom zdieľanej pamäte si drží počet "pripojení" a v prípade, že dosiahne 0 a je tento blok pamäte označený vhodný na zmazanie, odstráni sa.


Poslednou dôležitou funkciou je funkcia

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

Jedná sa o taký allaround nástroj na narábanie so zaregistrovanými zdieľanými pamäťmi.


Parameter int shmid má úplne rovnaký význam ako v prípade shmat().
Parameter int cmd je kontrolný príkaz ktorý hovorí, čo sa má vykonať s danou zdieľanou pamäťou.
Parameter struct shmid_ds *buf je pointer na štruktúru, ktorá sa použije, prípadne naplní v prípade niektorých príkazov. Vyzerá nasledovne

struct shmid_ds {
struct ipc_perm shm_perm; /* Ownership and permissions */
size_t shm_segsz; /* Size of segment (bytes) */
time_t shm_atime; /* Last attach time */
time_t shm_dtime; /* Last detach time */
time_t shm_ctime; /* Last change time */
pid_t shm_cpid; /* PID of creator */
pid_t shm_lpid; /* PID of last shmat()/shmdt() */
shmatt_t shm_nattch; /* No. of current attaches */
...
};

pričom struct ipc_perm vyzerá nasledovne
struct ipc_perm {
key_t key; /* Key supplied to shmget() */
uid_t uid; /* Effective UID of owner */
gid_t gid; /* Effective GID of owner */
uid_t cuid; /* Effective UID of creator */
gid_t cgid; /* Effective GID of creator */
unsigned short mode; /* Permissions + SHM_DEST and
SHM_LOCKED flags */
unsigned short seq; /* Sequence number */
};
V prípade, že vám kompilátor hlási niektoré podivné chyby, napríklad neexistujúcu položku key_t key; oplatí sa kuknúť do zdrojákov sys/ipc.h prípadne vás to môže dostať až k zdrojáku bits/ipc.h.
U mňa je napríklad namiesto key_t key; položka key_t __key;
Ale netreba sa toho zľaknúť.

Kontrolné príkazy sú
IPC_STAT
Získa informáciu o identifikátorom identifikovanej zdieľanej pamäti.Patrične naplní štruktúru struct shmid_ds *buf.
IPC_SET
Opak IPC_STAT, kde nastavením niektorých hodnôt v struct shmid_ds *buf môžete ovplyvniť nastavenia u ipc_perm
uid_t uid; -
uid vlastníka
gid_t gid; -
gid vlastníka
unsigned short mode; -
flagy ako predurčenie k zmazaniu, zamknutie a práva
IPC_RMID
Je to príkaz nastavenia zdieľanej pamäte ako zmazateľnej. Toto sa realizuje potom ako posledný proces odpojí zdieľanú pamäť zo svojho adresového priestoru (a počítadlo pre pripojenia sa zníži na 0). V príznakoch v položke unsigned short mode sa objaví aktívny flag SHM_DEST.

Tento príkaz musí aspoň jeden proces zo zúčastnených zavolať (buď to je vlastník, tvorca alebo privilegovaný užívateľ).
Pre Linux špecifické sú 
IPC_INFO
Vráti informáciu ohľadom IPC (medzi procesovej komunikácie) a hlavne limitov. Vráti ich ako pointer v buf, avšak tento treba pretypovať na pointer na štruktúru struct shminfo

struct  shminfo {
unsigned long shmmax; /* Max. segment size */
unsigned long shmmin; /* Min. segment size; always 1 */
unsigned long shmmni; /* Max. # of segments */
unsigned long shmseg; /* Max. # of segments that a
process can attach; unused */
unsigned long shmall; /* Max. # of pages of shared
memory, system-wide */
};

SHM_INFO
Podobná vo fungovaní ako IPC_INFO s tým rozdielom, že sa jedná o spotrebu zdieľanej pamäte v systéme a pointer treba pretypovať na štruktúru struct shm_info (no nezabili by ste niektorých ľudí)

struct shm_info {
int used_ids; /* # of currently existing
segments */
unsigned long shm_tot; /* Total number of shared
memory pages */
unsigned long shm_rss; /* # of resident shared
memory pages */
unsigned long shm_swp; /* # of swapped shared
memory pages */
unsigned long swap_attempts; /* Unused since Linux 2.4 */
unsigned long swap_successes; /* Unused since Linux 2.4 */
};
SHM_STAT
Asi najvecsia prasárnička široko ďaleko. Vráti pointer do poľa štruktúr struct shmid_ds a to je pravdaže interne v kerneli.
SHM_LOCK
Tento príkaz je vyžaduje lepšiu znalosť stránkovania. Principiálne sa jedná o zabránenie vo vystránkovaní danej zdielanej pamäte.
SHM_UNLOCK
Opačný mechanizmus proti SHM_LOCK

Takže ešte raz si zopakujeme
Pamäť sa dá zaregistrovať (shmget()) a označiť za vhodnú na zmazanie(shmctl()). Do aderesného priestoru sa pripojí pomocou shmat() a odpojí pomocou shmdt(). Nezmaźe sa však až do momentu, kým nieje odpojený posledný proces. Je možné pripojiť sa k pamäti, ktorá je označená na zmazanie, ale nemalo by sa to robiť, pretože je to nedefinované správania.

Na záver ešte drobná poznámka k synchronizácii. Zdieľaná pamäť ako mechanizmus nemá ochranu tohoto druhu. Všetci pristupujú k zdieľanej pamäti len s obmedzeniami ich prístupových práv. Procesy sa nemôžu spoliehať na výhradnosť prístupu ani na to, že nebudú v najhoršom možnom momente preplánované a konkurenčný proces sa pustí do menenia nekonzistného obsahu zdielanej pamäte.

Preto treba prístup k zdielanej pamäti synchronizovať známymi synchronizačnými primitívami.

Príjemné zdieľanie...

Neprehliadnite: