Synchronizácia v Linuxe - Semafóry

Tomáš Plch  /  01. 08. 2006, 00:00

Keď programujeme vlákna, prípadne processy, ktoré zdieľajú prostriedky (väčšinou pamäť), je dôležité vždy mať na pamäti synchronizáciu. Semafóry sú jeden z prosriedkov, ktoré nám dáva jadro operačného systmému Liux k dispozícii, aby sme zabezpečili vzájomnu medziprocesovú, prípadne medzivláknovu synchronizáciu.

Na úvod ďalšieho pokračovania seriálu o vláknach a multithreadovom programovaní zabrdneme do semfórov. Semfóry, ktoré si predstavíme sú tzv. SystemV semafóry. Okrem vláken ich môžeme použiť na vzájomnú synchronizáciu processov. SystemV semafóry su jedným z mála prostriedkov na medziprocessovú komunikáciu (IPC - interprocess communication).

Na začiatok trošku teórie. Povieme si, čo to vlastne semafór je a čo je jeho úlohou. Začneme prv tým, čo je úlohou semafóru. Už z predchádzajúcich dielov sme sa poučili, že existujú oblasti kódu nazývané kritické sekcie. Kedže nevieme, kedy môže dôjsť k transparetnému preplánovaniu, musíme zabezpečiť aby sa v týchto, takzvaných kritických sekciách, nachádzal presne kontrolovný počet vláken. Na toto nám slúžia synchronizačné primitíva, medzi ktoré patrí aj semafór.

V jednoduchosti je semafór počítadlo, ktoré sa operáciami zvyšuje a znižuje. Avšak nikdy neklesne pod nulu. Postupne sa táto predstava trošku upresní, ale na začiatok by to mohlo úplne stačiť.

Z teoretického pohľadu na vec to v programe vyzerá asi takto:velmi dolezity kod
operation_P(semafor)
  kriticka sekcia
operation_V(semafor)
dalsi este dolezitejsi kod
Ako si pozorný čitateľ stihol všimnúť, so semafórmi sa pracovalo pomocou operácii P a V. Názvy týchto operácii pochádzajú od vynálezcu semafórov, pána Edsger Wybe Dijkstra. V je skratka slova verhoog, čo znamená v holandštine zvýšiť. P je podla pána Dijkstru skratka za vymyslené slovo prolaag. Čo to slovo ale chce znamenať to si nájdite na wikipedii.

Samotné operácie sú principiálne veľmi jednoduché a predpokladá sa, že je zabezpečená ich atomicita (nedojde k preplánovaniu počas ich vykonávania). Atomicita sa v takomto prípade zabezpečuje rôznymi prostriedkami (spin-locks, zakázanie prerušenia, HW podpora v inštrukčnej sade).

Operácia P vyzerá takto:

if (semafor.value == 0)
{
    pridaj volajuceho do fronty cakajucich
    sleep
}else
{
    semafor.value--
}
Operácia V je podobná, ale z druhého konca

if (semafor.value == 0 )
{
    vyber niekoho z fronty cakajucich a zobud ho
}else
{
    semafor.value++;
}

Ako sa dá jednoducho poznať, ak má semafór nulovú hodnotu, tak je ten čo sa pokúsi zavolať operáciu P uspaný. Naopak ked sa volá operácia V tak je niekto z "čakajúcich" znova zobudený a "vpustený" do kritickej sekcie.

Toľko teórie okolo semafórov. Prakticky je semafór reprezentovaný trošku nešikovne, avšak ak si na rozhranie, ktoré je určené na ich ovládanie, zvyknete, bude to brnkačka.

Pre použitie semafórov musíme pravdaže aj niečo urobiť. Prvým krokom je zaincludovať správne hlavičkové súbory.

 #include <sys/types.h>
 #include <sys/ipc.h>
 #include <sys/sem.h>

Druhým dôležitým krokom je získanie semafóru. Kedže je semafór určený na medziprocessovú komunikáciu (ale to nebráni v jeho použití i pri vláknach), musí reálne ležať mimo adresový priestor processu. Jedná sa o "objekt" jadra (objekt je veľmi zle zvolené slovo, ale dúfam, že čitateľ získa predstavu o čo ide) o ktorý si musíme zažiadať systémovým volaním int semget(key_t key, int nsems, int semflg);

Funkciu semget si rozoberieme trošku dopodrobna. Ale prv si objasníme ako sú semafóry vnímané v Linuxe. Semafór nieje len jeden "objekt", ale je to sada objektov (tzv. set), ktorý je identifikovaný unikátnym identifikátorom. V takejto sade môžete mať viacero semafórov a identifikujete ich poradím (začínajúc od nuly). Operácie sa vykonávajú atomicky nad touto sadou(o tom trošku neskôr).

Sadu semafórov si môžete veľmi ľahko predstaviť ako pole jednotlivých semafórov, pričom sa jedná o jeden "objekt" jadra (je uložený v kernel memory space, pri operáciach sa prepne kontext na kontext kernelu...). Unikátny identifikátor ktorý daný objekt identifikuje (v tomto prípade sadu semafórov) je číslo (integer).

Dôležité je dodať, že k prístupu k tomuto "objektu" treba mať určité práva. Manuálové stránky daných funkcíí dostatočne túto nie príliž dôležitú tému rozoberajú (práva k objektom zachovávajú klasický tvar USER,GROUP,OTHER).

Prvým parametrom funkcie semget je identifikačný kľúč key_t key. Načo taký kľúč slúži nemusí byť očividne jasné. Keď kernel požiadate o sadu semafórov, on pravdepodobne zoberie a vytvorí túto sadu a predá vám identifikátor na ňu (návratová hodnota semget). Avšak čo robiť keď chcete získať rovnakú sadu ako získal iný proces pre vzájomnú synchronizáciu (komunikáciu)? Oba procesy môžu identifikovať požadovaný objekt pomocou kľúča. V prípade, že sada semafórov s daným kľúčom už existuje, nevytváara sa nová, ale poskytne sa už vytvorená sada.

Čo je nevýhoda, je skutočnosť, že oba procesy musia poznat spoločný kľúč. Jednou možnosťou je použiť dopredu známu hodnotu, napríklad pomocou #define v zdrojovom kóde. Toto prináša pravdaže ďalšie nevýhody. Existuje funkcia  key_t ftok(const char *pathname, int proj_id), ktorá sa používa na generovanie kľúčov podľa špecifických kritérií.

Naopak kľúč môže byť prostriedok, ako zabezpečiť unikátnu sadu semafórov, kde je malá pravdepodobnosť, že niekto do nej bude hrabať (zvolí rovnaký kľúč). Čitatel sa určite bez problému dovtípi, že použitie PIDu(process ID) je dobrým začiatkom. Mr.Linux však myslel aj na toto a ako kľúč môžeme použiť hodnotu IPC_PRIVATE, ktorá zabezpečí, že je vytvorený nový objekt, ktorý nieje so žiadným existujúcim asociovaný.

Druhým parametrom semget je int nsems, ktorý reprezentuje počet semafórov v sade. A posledným parametrom je  int semflg, čo sú príznaky vytvorenia danej sady. Dokumentácia spomína dva príznaky - IPC_CREAT a IPC_EXCL. Chovanie týchto flagov je veľmi podobné správaniu sa funkcie open. V prípade použitia IPC_CREAT je vytvorenému setu nastavený vlastník a skupina na skupinu a ID volajúceho processu. V prípade použitia IPC_EXCL spolu s IPC_CREAT funkcia v prípade existenice sady zlyhá.

Okrem toho sa semflag používajú na stanovenie prístupových práv. Spodných 9 bitov nastavuje tieto hodnoty presne v štýle aký je používaný u súborov. Pre presnejšie informácie treba konzultovať manuálové stránky ("man chmod").

Ešte sa vrátime k funkcii ftok. Funkcia vygeneruje rovnaké kľúče, v prípade, že sa prvý i druhý parameter zhodujú s iným volaním tejto funkcie.

Na záver tejto sekcie si ukážeme krátky kód, ktorý nám vytvorí sadu troch semafórov.
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <stdio.h>
#define SEM_KEY 4455

int semCtl = 0;

int main ( int argc , char ** argv)
{

    semCtl = semget ( SEM_KEY, 3, IPC_CREAT | 0666 );
    if (semCtl < 0){
        printf ("chyba pri vytarani semafor. Exit. n");
        return -1;
    }
   
    return 0;
}

Ako si pozornejši z vás urcite všimli, nikde v programe nedošlo k uvolneniu vyžiadaného objektu sady semafórov. Funkciu ktorá toto zabezpečuje si predstavíme až na koniec, kedže sa jedná o veľmi komplikovanú záležitosť.

Čo je ale dôležité povedať. Odstraňovanie sady semafórov (rovnako ako aj iných zdieľaných objektov) by mala byť premyslená operácia ktorá má veľmi presne vymedzené pôsobenie. Dobré je zachovať pravidlo - kto vytvoril zodpovedá za odstránenie, prípadne zodpovedá za určenie zodpovedného.

A ako teda realizovať operácie nad semafórmi? Jednoducho, pomocou funkcie int semop(int semid, struct sembuf *sops, unsigned nsops) prípadne int semtimedop(int semid, struct  sembuf  *sops,  unsigned
       nsops, struct timespec *timeout).
Rozdiel medzi semtimedop a semop je len v tom, že v prípade semop sa na vykonanie operácii čaká "nekonečne" dlho a v druhom prípade je toto čakanie obmedzené časovo.

Pre pochopenie nám teda stačí si vysvetliť funkciu semop. Prvým parametrom je identifikátor sady semafórov - semid - nad ktorou sa má previesť sada operácií. Druhým parametrom - sembuf - je pointer na pole operácií. Posledným parametrom je tradične počet položiek v poli.

Čo obsahuje typ struct sembuf.
unsigned short sem_num;
short sem_op; 
short sem_flg;  
Položky štruktúry by si čitateľ domyslel aj sám, ale prečo ich nevysvetliť. Položka sem_num určuje, nad ktorým semafórom sady sa má urobiť daná operácia.
Položka sem_op je číslo určujúce typ operácie a položka sem_flg môže obsahovať jeden z dvoch príznakov SEM_UNDO alebo IPC_NOWAIT. Ich význam si vysvetlíme za chvíľku.

Prv si povieme ako semop narába s množinou operácií. V prípade, že nieje príznakmi v operácii povedané inak, všetky operácie sa vykonajú atomicky a vykonajú sa len vtedy ak je možné ich všetky vykonať zároveň. V prípade, že to možné nieje, semop uspí volajúci proces.

V prípade, že u operácie je príznak SEM_UNDO nieje úplne pochopiteľný, avšak znamená asi toľko, že operácie ktoré sú vykonané s týmto príznakom sú navrátené (odčinené) po ukončení procesu. Táto operácia má zabezpečiť prípadné uchovanie konzistencie semafóru v prípade, že proces sa ukončí počas vykonávania kritickej sekcie.

V prípade IPC_NOWAIT je to docela naopak pochopiteľné. V prípade, že sa medzi operáciami narazí na operáciu, ktorá by vyžadovala uspatie procesu (nemôže byť totiž vykonaná) a zároveň má daná operácia príznak IPC_NOWAIT, tak funkcia neuspí volajúci process ale len vráti chybovú hodnotu -1 a v errno sa bude nachádzať EAGAIN.

A teraz by nebolo zlé si vysvetliť samotnú podstatu operácií.

Operácie sa rozdeľujú na aditívne, subtraktívne a wait-for-zero. Aditívna operácia k semafóru pridá hodnotu, ktorá je v sem_op. Táto hodnota musí byť kladná. Táto operácia je vždy úspešná a nikdy neuspáva volajúceho.

Subtraktívna operácia odčíta od hodnoty semafóru absolútnu hodnotu parametru sem_op. Aby operácia bola považovaná za subtraktívnu, musí byť hodnota v sem_op záporná. V prípade, že hodnota semafóru nemá dostatočnú hodnotu a odčítanie by zmenšilo hodnotu semafóru pod NULU, je operácia neúspešná (a prípadne je volajúci uspaný). Úspešnou môže byť až vtedy ak je hodnota semafóru dostatočná na prípadne odčítanie hodnoty sem_op.

Wait-for-zero operácia naopak čaká na semafóre (pokiaľ nieje nastavený príznak IPC_NOWAIT) dovtedy, kým sa daný semafór nedostane na NULU. Tento efekt sa dá využiť veľmi prefíkaným sposobom (možno si ukážeme akým).

Predstavili sme si už 2/3 celej plejády funkcií, už len poslednú a najhoršiu funkciu a tou je int semctl(int semid, int semnum, int cmd, ...). Pomocou tejto funkcie sa nastavujú a získavajú hodnoty semafórov, prípadne sa semafóry odstraňujú. Nebudeme si komplet všetky možnosti príkazov popisovať, len si predstavíme tie najdôležitejšie a ukážeme si použitie.

Dôležité je na začiatok upozorniť, že do svojich hlavičkových súborov musíte pridať tento kus kódu (prípadne ho dať do samostatného hlavičkového súboru a ten includnuť).
#ifndef _SEMUN_H
#define _SEMUN_H
union semun {
    int val;
    struct semid_ds *buf;
    unsigned short * array;
};
#endif
Táto únia sa používa na prípadne predávanie hodnôt funkci pri modifikovaní semafórov, alebo na prípadny návrat hodnot ak chceme niečo o semafóroch zistiť.

Parametre funkcie semctl sú podobné ako u ostatných funkcií. Parameter semid určuje, s ktorou sadou semafórov sa má pracovať. S tým je spojený parameter semnum, ktorý naopak určuje, s ktorým semaforom z danej sady sa má pracovať (začinajúc od NULY). Parameter cmd určuje o aký z príkazov sa pri danej operácii jedná. Operácia je vykonaná atomicky. v prípade, že sa chcete oboznámiť so všetkými operáciami, odporúčam manuálové str=anky funkcie semctl. My si spomenieme len dva "príkazy" a to SET_VAL a IPC_RMID.

Určite vás napadla otázka, prečo má funkcia variabilný počet parametrov (tie tri bodky). Odpoveď je jednoduchá, niekedy môže mať tri a niekedy štyri parametre. A kedže Cčko nepodporuje preťažovanie, prečo nepoužiť toto.

Ako posledný parameter sa použije union semun. Pozor, nie pointer.

V prípade príkazu SET_VAL sa jedná o nastavenie hodnoty semafóru a v prípade IPC_RMID o jeho odstránenie. Oba príkazy si ukážeme na príklade.

Použitie SET_VAL, kde funkcia nastaví semafór na pozícii semord v sade určenej pomocou semset na hodnotu semval.

int setsemval ( int semset, int semord, int semval)
{
    union semun sem_un;
    sem_un.val = semval;
    if (semctl(semset, semord, SET_VAL, sem_un) == -1) return -1;
    return 0;
}

A tu si ukážeme odstránenie sady semafórov. V prípade, že na daných semafóroch niekto spí, je prebudený a je mu oznámená chyba.

int semkill ( int semset )
{
    if (semctl (semset, 0, IPC_RMID) == -1) return -1;
    return 0;
}

Tak to by bolo na dnes z témy semafórov všetko. Dúfam, že nebudete čakat na križovatke kým sa váš semafór nezdvihne nad nulu.

<= predchádzajúci článok     nasledujúci článok =>

Neprehliadnite: