Linux - Procesy (Processes) Běžící instance programu se nazývá proces. Například dvě okna terminálu jsou instancemi stejného terminálového programu, ale jsou to dva různé procesy. V každém okně běží shell, který je zase jiný proces. Spuštěním programu v tomto shellu se vytvoří další nový proces. Více spolupracujících procesů v jedné aplikaci jí umožňuje dělat více věcí najednou. #include
typedef int __pid_t; typedef __pid_t pid_t; getpid(2) #include pid_t getpid(void); •
vrací jedinečné pid aktuálního procesu
Př. Napište program, který vypíše pid svého procesu. Program nemažte, budete ho rozšiřovat o další probírané funkce! getppid(2) #include pid_t getppid(void); • •
vrací jedinečné pid rodiče aktuálního procesu každý proces má rodiče (kromě procesu init), procesy jsou uspořádány do stromové struktury s procesem init jako vrcholem
Př. Vypište pid rodiče. ps(1) ps [options] • •
defaultně (bez parametrů) vypíše procesy kontrolované terminálem, ve kterém byl ps spuštěn přepínači lze určit, jaké procesy a informace o nich se mají vypsat, např. -e pro všechny procesy, -u user pro procesy uživatele user, -l pro dlouhý formát výpisu, -o pro vlastní formát, atd.
Př. Vypište si všechny své procesy ve tvaru STAT USER PID PPID NICE CMD kill(1) kill pid ... •
ukončí procesy pid ... (pošle jim signál SIGTERM)
Př. Spusťte si program top a ukončete ho pomocí SIGTERM.
Vytvoření procesu system(3) #include <stdlib.h> int system (const char * string); • • •
spustí příkaz ve string voláním /bin/sh -c string její používání není bezpečné (blokuje některé signály), doporučuje se používat fork a exec vrací 127, pokud shell nelze spustit, -1 při jiné chybě, jinak návratový kód příkazu
Př. Spusťte příkaz ls -la. fork(2) #include pid_t fork(void); • • •
vytvoří stejný proces jako svého potomka, který se liší jen v pid a ppid rodičovský proces i jeho potomek pokračují ve vykonávání programu od místa volání fork v rodiči vrací pid potomka, v potomkovi nulu, při chybě -1 v rodiči, potomek se nevytvoří
Př. Vytvořte potomka, který vypíše svoje pid. exec(3) execve(2) #include int int int int int int
execl( const char *path, const char *arg, ...); execlp( const char *file, const char *arg, ...); execle( const char *path, const char *arg , ..., char * const envp[]); execv( const char *path, char *const argv[]); execvp( const char *file, char *const argv[]); execve (const char *filename, char *const argv [], char *const envp[]); • • • • • • •
tyto funkce nahradí aktuální program jiným první argument je soubor (včetně cesty), který se má vykonat const char *arg, ... jsou arg0, ... spuštěného programu, poslední prvek seznamu musí být NULL char *const argv[] je seznam arg0, ... spuštěného programu, poslední prvek pole musí být NULL char * const envp[] je seznam proměnných prostředí spuštěného programu, poslední prvek seznamu musí být NULL, prvky seznamu jsou očekávány ve tvaru "PROMENNA=hodnota" funkcím execvp a execlp stačí jen jméno souboru ke spuštění, který se hledá ve vyhledávací cestě (proměnná PATH) pokud se funkce ukončí (vždy -1), je to chyba
Při vytváření nového procesu se nejdříve zavolá fork, který vytvoří kopii aktuálního procesu, a pak exec, který nový proces změní na instanci programu, který chceme spustit. Př. Spusťte příkaz date +%s pomocí fork a exec. Linux plánuje procesy nezávisle, není zaručeno, který poběží dřív, jak dlouho poběží. nice(1) nice [OPTION]... [COMMAND [ARG]...] • • • •
spustí program s jinou prioritou (defaultně má nový proces prioritu 0) rozsah priorit je -20 (nejvyšší) až 19 (nejnižší) pro novou prioritu je přepínač -n jenom root může spustit proces se zápornou prioritou
Př. Spusťte program yes s nice 5. Zkontrolujte si to pomocí ps. Zkuste -5. renice(1) renice priority [[-p] pid ...] [[-g] pgrp ...] [[-u] user ...] • • • •
změní prioritu běžících procesů procesy lze určit pomocí pid nebo všechny procesy uživatele user (defaultně pid) priority může být [+-]číslo, priority = nice jenom root může zvyšovat prioritu (snižovat nice)
Př. Zvyšte nice programu yes na 19. Zkontrolujte si to pomocí ps. Zkuste nice snížit. nice(2) #include int nice(int inc); • • •
přidá inc k prioritě volájícího procesu jenom root může zadat záporný inc vrací 0, při chybě -1
Př. Zvyšte nice potomka. sleep(3) #include unsigned int sleep(unsigned int seconds); • •
"uspí" proces na seconds sekund, nebo ho probudí neignorovaný signál vrací 0, pokud čas uplynul, jinak zbývající počet sekund
Př. Pozdržte potomka 2 sekundy a vypište datum znovu.
Ukončení procesu exit(3) #include <stdlib.h> void exit(int status); • •
normálně ukončí program, status je vrácen rodiči funkce se neukončí
Př. Ošetřete neúspěšný fork pomocí exit. int main(int argc, char *argv[], char *envp[]); • •
ukončení main je normální ukončení, návratová hodnota vrácena rodiči návratová hodnota funkce main je sice int, ale používat by se měly jen hodnoty 0-127, kódy nad 128 včetně mají speciální význam - když je proces ukončen signálem, návratová hodnota je 128 + číslo signálu
Př. Ukončete korektně rodiče i potomka návratem z main. abort(3) #include <stdlib.h> void abort(void); • •
abnormálně (s core souborem) ukončí program funkce se neukončí
Př. Ošetřete neúspěšný exec pomocí abort. Potomek může proběhnout až po ukončení předka, protože oba procesy jsou plánovány nezávisle. To je někdy nežádoucí, někdy chceme, aby rodič počkal, až se potomek ukončí. wait(2) #include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status) • •
zastaví proces, dokud se neukončí nějaký potomek, nebo pokud proces dostane ukončující signál nebo se volá nějaký signal-handler pokud už potomek skončil (zombie), funkce se hned ukončí, všechny prostředky systému přidělené potomkovi
• •
jsou uvolněny pokud status není NULL, na místo, kam ukazuje, se uloží informace o potomkovi vrací pid potomka, při chybě -1
Př. V rodiči počkejte na potomka. waitpid(2) #include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int *status, int options); • • • • •
zastaví proces, dokud se neukončí potomek s ID pid, nebo pokud proces dostane ukončující signál nebo se volá nějaký signal-handler pokud už potomek skončil (zombie), funkce se hned ukončí, všechny prostředky systému přidělené potomkovi jsou uvolněny pokud status není NULL, na místo, kam ukazuje, se uloží informace o potomkovi pokud je pid -1, funkce se chová jako wait vrací pid potomka, při chybě -1, nebo 0, pokud žádný potomek neskončil a v options bylo WNOHANG
Ze status (int) lze získat různé informace o potomkovi těmito makry: WIFEXITED(status) •
vrací nenulové číslo, pokud potomek spončil normálně
WEXITSTATUS(status) • •
vrací návratový kód potomka smí se vyhodnotit, jen pokud WIFEXITED je nenulové
Př. Vypište návratový kód potomka. WIFSIGNALED(status) •
vrací nenulové číslo, pokud potomka ukončil signál
WTERMSIG(status) • •
vrací číslo signálu, který ukončil potomka smí se vyhodnotit, jen pokud WIFSIGNALED je nenulové
Zombie procesy Co se stane, když potomek skončí a rodič na něj nečeká voláním wait? Informace o jeho ukončení budou dočasně ztraceny a z něj se stane zombie. Zombie je proces, který skončil, ale ještě nebyl ze systému odstraněn, to je povinnost rodiče. Potom, co se proces ukončí, stane se z něj zombie, dokud si informace o jeho ukončení nevyzvedne jeho rodič pomocí wait. Pokud si rodič tyto informace nevyzvedne nikdy (ani po svém ukončení), zombie adoptuje proces init a odstraní jej ze systému. OTÁZKA: Co se s potomkem stane, když rodič skončí dřív než on? Bude bez rodiče? PŘÍKLAD Další funkce týkající se procesů jsou: • • • • • • •
getuid(2) / setuid(2) geteuid(2) / seteuid(2) getgid(2) / setgid(2) getegid(2) / setegid(2) clone(2) vfork(2) getpriority(2) / setpriority(2)
• •
wait3(2) wait4(2)
Linux - Signály (Signals) Signály je mechanizmus pro komunikaci a manipulaci s procesy. Signál je speciální zpráva zaslaná procesu. Signály jsou asynchronní, tj. když jej proces obdrží, ihned ho obslouží, bez dokončení aktuální funkce (ta se dokončí až pak). Každý signál je specifikován svým číslem, v programech se však používají jména definovaná v . Všechny signály jsou pak popsány v signal(7).
Zaslání signálu raise(3) #include <signal.h> int raise (int sig); • •
pošle signál sig vlastnímu procesu vrací 0, při chybě nenulové číslo
Př. Vytvořte program, který si pošle SIGTERM a tím se tedy ukončí. Program nemažte, budete ho rozšiřovat o další probírané funkce! Proces může poslat signál jinému procesu. Např. ho ukončit zasláním SIGTERM nebo SIGKILL. OTÁZKA: Jaký je rozdíl mezi SIGTERM a SIGKILL? kill(1) kill [signal] pid ... • •
pošle každému procesu (pid) SIGTERM (pokud není signal zadán), nebo signál signal přepínač -l vypíše všechny signály
Př. Spusťte si program top a zabijte ho pomocí SIGKILL. kill(2) #include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig); • •
pošle signál sig procesu s ID pid vrací 0, při chybě -1
Př. Nahraďte funkci raise funkcí kill. Pro zaslání příkazu procesu jsou rezervovány dva uživatelské signály, SIGUSR1 a SIGUSR2. Někdy se pro tento účel používá i SIGHUP, na "probuzení" nebo "restart" programu. strsignal(3) #define _GNU_SOURCE #include <string.h> char *strsignal(int sig); •
vrátí řetězec popisující singál s číslem sig
psignal(3) #include <signal.h> void psignal(int sig, const char *s); •
napíše na stderr zprávu tvaru s: sig_d, kde sig_d je popis signálu sig
pause(2) #include int pause(void); • •
"uspí" proces, dokud nepřijde signál při přerušení (signálem) vrací -1
Př. Uspěte program a z jiného terminálu mu pošlete SIGCONT.
Obdržení signálu Co proces udělá po obdržení signálu záleží na určení signálu. Každý signál má defaultní určení, která určuje, co se stane, když program nestanovuje jinak. Pro většinu signálů jej může program ignorovat nebo zavolat speciální funkci (signal-handler). Při volání handleru se program zastaví, vykoná se handler a pak program pokračuje. Např. SIGBUS (bus error), SIGSEGV (segmentation violation) a SIGFPE (floating point exception) jsou zaslány procesu, když se pokouší provést nepřípustnou akci. Defaultní určení signálu je ukončení procesu a vytvoření core souboru. signal(2) #define _GNU_SOURCE #include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t action); • •
• • • • •
instaluje nový signal-handler pro signál s číslem signum action může být: • signal-handler funkce • SIG_IGN pro ignorování signálu • SIG_DFL pro defaultní určení signálu parametr signal-handler funkce je číslo signálu defaultní určení signálů jsou popsány v signal(7) nelze instalovat handler na SIGKILL a SIGSTOP sighandler_t je GNU rozšíření vrací předchozí signal-handler, nebo při chybě SIG_ERR
Př. Napište signal-handler, který bude bude počítat, kolik signálů SIGTERM program dostane. Při SIGQUIT se ukončí a vypíše počet obdržených signálů SIGTERM. Pošlete programu pár signálů SIGTERM a pak SIGQUIT. Protože signály jsou asynchronní, program by mohl být při zpracovávání signálu v nekonzistentním stavu a proto by se v signal-handleru neměly volat knihovní a systémové funkce a vykonávat I/O operace. signal-handler by měl vykonat jen práci nezbytně nutnou k ošetření signálu. Je možné, že signal-handler bude přerušen obdržením jiného signálu. Pokud se to stane, je velmi těžké to debugovat, pokud je tam chyba, proto by se měl dávat velký pozor na to, co se v signal-handleru provádí. Nebezpečné je dokonce i přiřazení do globální proměnné, protože může probíhat ve více instrukcích, a druhý signál se může vyskytnout mezi nimi. Tato globální proměnná by měla být typu sig_atomic_t, do kterého se přiřazuje v jedné instrukci (protože je to int). #include <signal.h> typedef int __sig_atomic_t; typedef __sig_atomic_t sig_atomic_t; Př. Proměnnou, ve které počítáte počet signálů SIGTERM, změňte na typ sig_atomic_t. alarm(2) #include unsigned int alarm(unsigned int seconds); • • •
pošle procesu SIGALRM za seconds sekund pokud seconds je 0, nic se nenačasuje vrací zbývající počet sekund do zaslání signálu, nebo 0, pokud není načasován žádný alarm
Př. Zařiďte, aby se program sám ukončil po 10 sekundách běhu.
Procesy a signály Proces může skončit abnormálně, jako odpověď na signál. Např. SIGBUS, SIGSEGV a SIGFPE způsobí ukončení procesu. Další signály slouží k explicitnímu ukončení. SIGINT je procesu zaslán, když se jej uživatel snaží ukončit pomocí C-C. SIGTERM posílá kill. Při volání funkce abort si proces sám pošle SIGABRT, což jej také ukončí s core souborem. Nejmocnější je SIGKILL, který jej okamžitě ukončí a program tomu nemůže nijak zabránit (blokovat, ingnorovat, ošetřit). Čekání na potomka pomocí wait proces blokuje, dokud se potomek neukončí. Většinou ale chceme, aby i rodič pokračovat dál. Jak se ale postarat o potomka ihned po jeho ukončení, tak, aby nezůstávali zombie? Jedna možnost je periodicky volat wait3 nebo wait4, které lze volat i v neblokujícím módu. Mnohem lepší je ale rodiče informovat o ukončení potomka, pomocí signálu. Když se ukončí potomek, systém pošle rodiči signál SIGCHLD, který defaultně nemá žádné určení. Funkci wait tedy zavoláme v signal-handleru signálu SIGCHLD. PŘÍKLAD Další funkce týkající se signálů jsou: • • • • • • • • • •
sigaction(2) sigprocmask(2) sigpending(2) sigsuspend(2) sigblock(2) siggetmask(2) sigsetmask(2) sigmask(2) siginterrupt(2) sigpause(2)
Linux - Vlákna (Threads) Vykonávání programu může být rozděleno do více "proudů", vláken, které běží paralelně. Vlákna tedy, stejně jako procesy, umožňují aplikaci dělat více věcí najednou. Vlákno existuje uvnitř procesu. Při spuštění programu se vytvoří proces a v něm jedno vlákno, které může spustit další. Všechna tato vlákna běží paralelně ve stejném procesu, ale mohou vykonávat různé části (kód) programu. Dvě různá vlákna téhož procesu sdílejí stejnou paměť, deskriptory souborů a jiné systémové zdroje přidělené celému procesu. Protože proces a všechna jeho vlákna vykonávají jeden program, když jedno vlákno zavolá exec, všechna ostatní vlákna jsou ukončena. GNU/Linux implementuje API vláken podle standardu POSIX (pthreads). Všechny funkce a datové typy jsou deklarovány v . Funkce nejsou ve standardní C knihovně, ale v knihovně libpthread, takže je potřeba linkovat s parametrem -lpthread.
Vytvoření vlákna #include typedef unsigned long int pthread_t; pthread_self(3) #include pthread_t pthread_self(void); •
vrací id vlákna
Př. Napište program, který vypíše ID svého hlavního vlákna. Program nemažte, budete ho rozšiřovat o další probírané funkce! pthread_equal(3) #include
int pthread_equal(pthread_t thread1, pthread_t thread2); •
vrací nenulové číslo, když thread1 a thread2 jsou id téhož vlákna, jinak 0
Po spuštění každé vlákno vykonává funkci vlákna. Když tato funkce skončí, skončí i vlákno. Má jediný parametr void*, vrací také void*. pthread_create(3) #include int pthread_create(pthread_t * thread, pthread_attr_t * attr, void * (*start_routine)(void *), void * arg); • • • •
vytvoří nové vlákno, které vykoná funkci start_routine s parametrem arg attr specifikuje atributy vlákna (viz pthread_attr_init), pokud je NULL, jsou defaultní: joinable, defaultní priorita vrací 0 a id vlákna na místo, kam ukazuje argument thread, při chybě nenulové číslo funkce se vrací ihned, obě vlákna pak pokračují asynchronně
Př. Vytvořte vlákno s defaultními atributy a bez parametrů funkce vlákna. Při vytváření atributů vlákna se postupuje takto: • • • • •
vytvoří se struktura pthread_attr_t pomocí pthread_attr_init se inicializuje na defaultní hodnoty změní se hodnoty ve struktuře zavolá se pthread_create a předá se jí tato struktura pthread_attr_destroy strukturu zruší, ale nedealokuje (pokud je vytvořena dynamicky)
S jednou strukturou je možné vytvořit více vláken a je možné ji zrušit po vytvoření vlákna. pthread_attr_init(3) #include int pthread_attr_init(pthread_attr_t *attr); • •
inicializuje strukturu atributů defaultními hodnotami vrací 0, při chybě nenulové číslo
pthread_attr_destroy(3) #include int pthread_attr_destroy(pthread_attr_t *attr); • • •
zruší strukturu atributů, před dalším použitím se musí znova inicializovat vrací 0, při chybě nenulové číslo v Linuxu nedělá nic
pthread_attr_setdetachstate(3) pthread_attr_getdetachstate(3) #include int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate); • • •
nastaví(vrací) atribut z(do) druhého parametru vrací 0, při chybě nenulové číslo atribut detachstate může být: • PTHREAD_CREATE_JOINABLE - vlákno bude joinable, defaultní • PTHREAD_CREATE_DETACHED - detached
Joinable vlákno není po skončení automaticky odstraněno ze systému, jeho status ukončení je udržován, dokud si ho nevyzvedne jiné vlákno pomocí pthread_join (stejné jako zombie u procesu). Detached vlákno je naopak hned po skončení automaticky odstraněno a nelze získat jeho informace o ukončení pomocí pthread_join.
Př. Změňte vytvářené vlákno na detached. Typ argumentu funkce vlákna je void*, nelze tedy poslat data (>4B) přímo. Předává se ukazatel na strukturu dat. Tato struktura ale nesmí být lokální v jednom vláknu (př. hlavním), protože by mohlo skončit dříve než ostatní, která tuto strukturu používají. Pokud je lokální, možným řešením je ve vláknu počkat, až ostatní vlákna skončí. Struktura se nesmí dealokovat dřív, než vlákna, která ji používají, skončí. Př. Předejte funkci vlákna 2 řetězce jako parametry, ty spojte a výsledek vraťte jako návratovou hodnotu funkce vlákna. pthread_join(3) #include int pthread_join(pthread_t th, void **thread_return); • • • • •
pozastaví vlákno, dokud neskončí vlákno specifikované pomocí th pokud thread_return není NULL, vrátí návratovou hodnotu funkce vlákna na místo, kam ukazuje, pokud je vlákno zrušeno, je tam PTHREAD_CANCELED jen jedno vlákno může čekat na skončení jiného vlákna vrací 0, při chybě nenulové číslo vlákno nesmí čekat samo na sebe, to je deadlock!
Př. Počkejte na dokončení vlákna a vypište výsledek spojení. pthread_detach(3) #include int pthread_detach(pthread_t th); • •
"přepne" vlákno na detached, pokud jiné vlákno nečeká na jeho ukončení vrací 0, při chybě nenulové číslo
Ukončení vlákna pthread_exit(3) #include void pthread_exit(void *retval); • •
ukončí vlákno s návratovým kódem retval nikdy se neukončuje
pthread_cancel(3) #include int pthread_cancel(pthread_t thread); • •
pošle vláknu, specifikovanému pomocí thread, požadavek na ukončení vrací 0, při chybě nenulové číslo
Vlákno často vykonává kód, který nesmí být přerušen. To je zajištěno tím, že vlákno samo si může určit místa, kde může být zrušeno a kde ne, pomocí svého stavu: • • •
asynchronně zrušitelné - může být zrušeno kdekoliv synchronně zrušitelné - může být zrušeno až v určitých místech (body zrušení), požadavek je zaznamenán, defaultní při vytvoření vlákna nezrušitelné - požadavky na zrušení jsou ignorovány
pthread_setcanceltype(3) #include int pthread_setcanceltype(int type, int *oldtype); • •
změní typ reakce na zrušení vlákna na type type může být: • PTHREAD_CANCEL_ASYNCHRONOUS - asynchronní
• •
• PTHREAD_CANCEL_DEFERRED - synchronní pokud oldtype není NULL, na místo, kam ukazuje, je uložen předchozí typ vrací 0, při chybě nenulové číslo
pthread_testcancel(3) #include void pthread_testcancel(void); • •
vytvoří bod zrušení existují implicitní body zrušení (viz man), př. pthread_join
Př. Změňte vlákno na synchronně zrušitelné a vytvořte body zrušení před a po spojení. pthread_setcancelstate(3) #include int pthread_setcancelstate(int state, int *oldstate); • •
• • •
změní stav zrušení vlákna na state state může být: • PTHREAD_CANCEL_ENABLE - zrušitelný stav • PTHREAD_CANCEL_DISABLE - nezrušitelný pokud oldstate není NULL, na místo, kam ukazuje, je uložen předchozí stav vrací 0, při chybě nenulové číslo umožňuje vymezení kritických sekcí - nepřerušitelný kód
Př. Uzavřete spojení do kritické sekce. PŘÍKLAD
Lokální uložení dat Vlákna sdílejí stejný adresový prostor. Někdy je ale potřeba, aby každé vlákno mělo vlastní kopii určité globální proměnné. Každé vlákno poskytuje lokální uložení dat (thread-specific data - TSD - area), kde se ukládají kopie globálních proměnných specifické pro každé vlákno. Proměnné v tomto uložení jsou indexovány přes klíče. Klíče jsou společné všem vláknům, hodnota proměnné je v každém vláknu jiná. pthread_key_create(3) #include int pthread_key_create(pthread_key_t *key, void (*destr_function) (void *)); • • •
vytvoří nový TSD klíč a uloží ho, kam ukazuje key, asociovaná hodnota je NULL pokud destr_function není NULL, je to funkce, která se zavolá po ukončení vlákna s hodnotou asociované proměnné jako parametrem, před jejím voláním je ke klíči asociován NULL vrací 0, při chybě nenulovou hodnotu
pthread_key_delete(3) #include int pthread_key_delete(pthread_key_t key); • •
zruší TSD klíč vrací 0, při chybě nenulovou hodnotu
pthread_setspecific(3) #include int pthread_setspecific(pthread_key_t key, const void *pointer); •
změní hodnotu proměnné asociované s klíčem key na pointer
•
vrací 0, při chybě nenulovou hodnotu
pthread_getspecific(3) #include void * pthread_getspecific(pthread_key_t key); •
vrací hodnotu proměnné asociované s klíčem key, při chybě NULL
PŘÍKLAD OTÁZKA: Procesy versus vlákna? Najděte co nejvíc rozdílů. Další funkce týkající se vláken jsou: • • • • •
pthread_cleanup_push(3) / pthread_cleanup_pop(3) pthread_setschedparam(3) / pthread_getschedparam(3) pthread_kill(3) pthread_sigmask(3) sigwait(3)
Linux - Meziprocesní komunikace (Interprocess communication, IPC) Nejjednodušší forma IPC je, že rodič může zjistit, jak skončil jeho potomek. Rodič může potomkovi sdělit informace také přes jeho argumenty a proměnné prostředí. Žádný z těchto mechanizmů ale rodiči neumožňuje s potomkem komunikovat za jeho běhu a už vůbec neumožňuje komunikaci dvou procesů, které nejsou ve vztahu rodič-potomek. Za určitý primitivní druh komunikace se dají považovat signály, ale "skutečná" komunikace znamená výměna informací čili dat. Např. klasické balení souboru dvojicí tar+gzip, tar cf - * | gzip - > soubor.tar.gz, využívá mechanizmu roury (pipes). Druhy IPC se liší těmito kritérii: • • • •
zda komunikují dva příbuzné (mající společného rodiče) nebo zcela cizí procesy zda komunikující proces může jen číst či jen zapisovat data počet procesů zapojených do komunikace zda jsou komunikující procesy synchronizovány, např. čtecí proces čte, až je co číst
Sdílená paměť (Shared memory) Jednou z nejjednodušších metod IPC je sdílená paměť, která umožňuje dvěma a více procesům přistupovat ke stejné oblasti paměti. Sdílená paměť je nejrychlejší formou IPC, protože procesy sdílí stejnou paměť a pro přístup do ní není potřeba žádné systémové volání. Protože ale systém neposkytuje žádnou formu synchronizace, musíme si ji zajistit sami. Např. dva procesy by neměly zapisovat zároveň na stejné místo v paměti. Tradičním nástrojem pro synchronizaci jsou semafory. Pro použití sdílené paměti ji jeden proces musí alokovat. Všichni si ji pak "připojí". Po používání ji "odpojí" a ten proces, který ji alokoval, ji dealokuje. Sdílená paměť se alokuje po celočíselných násobcích velikosti stránky paměti. getpagesize(2) #include size_t getpagesize(void); •
vrací velikost (počet bytů) stránky paměti systému
shmget(2) #include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, int size, int shmflg); • •
je vytvořen úsek paměti velikosti nahoru zaokrouhlené size na násobek stránky, pokud v key je IPC_PRIVATE anebo ke key není asociován žádný úsek sdílené paměti, a v shmflg je IPC_CREAT key je libovolný zvolený číselný klíč
•
• •
v shmflg může být: • IPC_CREAT - vytvoří nový úsek, pokud toto není, najde se úsek asociovaný ke klíči key • IPC_EXCL - používá se s IPC_CREAT, chyba pokud úsek existuje • mode_flags - specifikuje práva k úseku, definována v <sys/stat.h>, může být: • S_IRUSR, S_IWUSR - čtení, zápis pro vlastníka úseku • S_IROTH, S_IWOTH - pro ostatní vrátí identifikátor sdílené paměti asociovaný ke klíči key, při chybě -1 existují limity pro úsek sdílené paměti, viz man
Př. Napište program, který vytvoří sdílenou paměť o velikosti stránky paměti s právy zápisu pro sebe a čtení pro ostatní. Potomek po fork zdědí připojené úseky, po exec a exit jsou všechny připojené úseky odpojeny, ale ne dealokovány! shmat(2) #include <sys/types.h> #include <sys/shm.h> void *shmat ( int shmid, const void *shmaddr, int shmflg ); • • • • •
připojí úsek sdílené paměti identifikovaný identifikátorem shmid na adresu shmaddr pokud je shmaddr NULL, systém si sám najde volnou adresu pokud je v shmflg SHM_RND, adresa se zaokroulí dolů na násobek stránky, jinak adresa musí být už zaokrouhlená pokud je v shmflg SHM_RDONLY, pameť je připojena jen pro čtení, jinak i pro zápis (pokud to práva dovolují) při chybě vrací -1, jinak adresu připojené paměti
Př. Uložte něco (např. text) do sdílené paměti. shmdt(2) #include <sys/types.h> #include <sys/shm.h> int shmdt ( const void *shmaddr); • •
odpojí sdílenou paměť na adrese shmaddr, paměť musí být připojená při chybě vrací -1, jinak 0
Př. Před ukončením programu sdílenou paměť odpojte. shmctl(2) #include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf); • • •
•
zjistí informace o úseku (identifikovaného pomocí shmid) sdílené paměti, mění práva a dealokuje úsek informace vrací do struktury shmid_ds, viz man cmd může být: • IPC_STAT - vrací informace, potřebuje právo na čtení úseku • IPC_SET - nastavuje práva • IPC_RMID - označí úsek pro zrušení, ten se zruší až po posledním odpojení, zrušit úsek může jen ten, kdo ho vytvořil nebo root vrací 0, při chybě -1
Př. Vytvořte potomka, který si zjistí informace o této paměti a z nich vypíše její velikost, pak přečte, co tam rodič zapsal a vypíše to. Každý úsek sdílené paměti musí být explicitně zrušen, protože systém ji nezruší ani po ukončení procesů! Př. Před ukončením programu sdílenou paměť zrušte.
ipcs(8) ipcs -m •
vypíše úseky sdílené paměti
Př. Podívejte se, zda v systému nezůstaly úseky nepoužívané sdílené paměti. ipcrm(8) ipcrm shm id... •
zruší úseky sdílené paměti specifikované pomocí id
Př. Zrušte úseky nepoužívané sdílené paměti. PŘÍKLAD
Mapovaná paměť (Mapped memory) Mapovaná paměť umožňuje různým procesům komunikovat přes sdílený soubor. Může se zdát, že je to stejné jako sdílená paměť, ale jsou zde technické rozdíly. Mapovaná paměť se dá použít jak pro komunikaci, tak pro jednoduchý přístup do souboru. Linux rozdělí soubor na paměťové stránky a ty zkopíruje do paměti, takže proces k nim může přistupovat jako do paměti, číst i zapisovat. mmap(2) #include #include <sys/mman.h> caddr_t mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset); • •
•
•
namapuje length bytů na offsetu offset ze souboru specifikovaném deskriptorem fd na adresu start (v praxi se ovšem dává NULL - systém sám vybere adresu) prot specifikuje práva: • PROT_EXEC - z paměti lze spouštět • PROT_READ - číst • PROT_WRITE - zapisovat flags určuje typ mapování, může být jen jedno z: • MAP_SHARED - sdílení mapování souboru s ostatními procesy, které ho mapují také, zápis je nebufferovaný, okamžitý • MAP_PRIVATE - privátní mapování vrací ukazatel na mapovanou paměť, při chybě -1
munmap(2) #include #include <sys/mman.h> int munmap(void *start, size_t length); • • •
zruší mapování paměti na adrese start délky length vrací 0, při chybě -1 mapovaná paměť se automaticky zruší po skončení procesu
Různé procesy mohou komunikovat použitím mapované paměti téhož souboru a použití MAP_SHARED. Stejně jako u sdílené paměti je potřeba explicitně zajistit synchronizaci, např. semafory nebo zamykáním souboru. Časté použití mapované paměti je také pro rychlé čtení a zápis do souboru nebo ukládání datových struktur do souboru. OTÁZKA: Když ukládaná datová struktura obsahuje ukazatele, po načtení této struktury budou tyto ukazatele neplatné. Proč? Jaké shody okolností by musely nastat, aby byly platné? PŘÍKLAD Další funkce týkající se sdílené nebo mapované paměti jsou: •
msync(2)
Roury (Pipes) Roura umožňuje jednosměrnou komunikaci. Data zapsaná do "zapisovacího konce" jsou čtena z "čtecího konce". Roury jsou sériová zařízení, data jsou čtena ve stejném pořadí, v jakém byla zapsána. Používají se ke komunikaci dvou vláken jednoho procesu nebo mezi rodičovským procesem a potomky. V shellu symbol | vytvoří rouru. Např. ls | less vytvoří dva potomky shellu, ls a less. Shell také vytvoří rouru spojující standardní výstup ls se standardním vstupem less. Kapacita roury je omezená. Pokud zapisující proces zapisuje rychleji než čtecí proces čte a roura už nemůže uchovat data, zapisovací proces je blokován, dokud se neuvolní místo. Pokud se proces pokouší číst z roury, ale není co číst, je blokován, dokud nebude co číst. Roura tedy automaticky synchronizuje dva procesy. pipe(2) #include int pipe(int filedes[2]); • • •
vytvoří dva deskriptory souboru, ukazující na konce roury, a uloží je do filedes filedes[0] je pro čtení, filedes[1] pro zápis vrací 0, pří chybě -1
Deskriptory souboru, které vytvoří pipe, jsou platné jen v procesu a jeho potomcích. Při volání fork jsou deskriptory kopírovány do potomka, proto roura může spojovat pouze příbuzné procesy. Po volání fork mají jak rodič tak i potomek oba konce roury. Roura jako komunikační zařízení má však jen dva konce, proto se musí v rodiči i v potomkovi nepoužívané konce ihned uzavřít. Zvláště existence dvou zapisovacích konců roury způsobuje podivné chování. Při čtení z roury, která má zapisovací konec uzavřený, vrací read hodnotu 0. A při zápisu do roury, která má uzavřený čtecí konec, obdrží proces signál SIGPIPE. Př. Napište program, který vytvoří rouru a potomka. Jeden proces pak do roury něco zapíše, druhý to přečte a vypíše. K otevření konce roury ve formě streamu (FILE *) použijte funkci fdopen, k uzavření zase close. Často je potřeba vytvořit potomka tak, aby jeden konec roury byl jeho standardní vstup nebo výstup. dup(2) dup2(2) #include int dup(int oldfd); int dup2(int oldfd, int newfd); • • • •
funkce vytvoří kopii deskriptoru oldfd starý i nový deskriptor jsou zaměnitelné, sdílejí zámky, pozici, nesdílejí příznak uzavření při ukončení procesu dup2 vytvoří kopii jako newfd, který popřípadě nejdřív uzavře vrací nový deskriptor, při chybě -1
Př. Přesměrujte std. vstup potomka na čtecí konec roury a změňte program potomka na sort. V rodiči do roury zapište několik vět. Roura má buffer určité velikosti, proto po zápisu vět zavolejte fflush. Deskriptor std. vstupu je STDIN_FILENO. Běžné použítí rour je zasílání nebo příjímání dat od programu, který běží jako potomek. K vytvoření tohoto stavu je potřeba volat postupně funkce pipe, fork, dup2, exec a fdopen. Volání všech těchto funkcí nahrazují funkce popen a pclose. popen(3) #include <stdio.h> FILE *popen(const char *command, const char *type); • •
• •
vytvoří proces voláním pipe, fork a spuštěním shellu type může být jen jedno z: • "r" - proces bude číst data od potomka, funkce vrátí standardní výstup potomka • "w" - proces bude zapisovat data pro potomka, funkce vrátí standardní vstup potomka command je příkaz pro shell, spuštěn pomocí /bin/sh -c command vytvořený stream by měl být uzavřen pomocí pclose, ne fclose
•
při chybě (fork, pipe nebo alokace paměti) vrací NULL
pclose(3) #include <stdio.h> int pclose(FILE *stream); • •
uzavře stream vytvořený pomocí popen, čeká, až se spuštěný příkaz ukončí vrací návratový kód spuštěného příkazu, při chybě -1
Př. Poslední úkol (sort) proveďte pomocí popen a pclose. PŘÍKLAD
FIFO FIFO (First In, First Out) je roura, která je souborem. Jakýkoliv proces může číst nebo zapisovat do FIFO, procesy nemusí být příbuzné. FIFO musí být otevřená pro čtení i zápis dřív, než se z ní může něco číst nebo do ní zapisovat. Otevření pro čtení proces blokuje, dokud ji neotevře jiný proces pro zápis a opačně. FIFO se někdy nazývá pojmenovaná roura. mkfifo(1) mkfifo [OPTION] NAME... • •
vytvoří FIFO se jménem NAME volbou -m (--mode) se nastavují práva, která musí obsahovat práva čtení a zápisu
Př. Vytvořte FIFO, např. /tmp/fifo. V jednom terminálu z ní čtěte pomocí cat < /tmp/fifo. V jiném do ní zapisujte pomocí cat > /tmp/fifo a zadávejte věty (ukončené ENTER). Sledujte, jak se věty vypisují v prvním terminálu. Zadávání vět ukončete pomocí C-D. Smažte FIFO. mkfifo(3) #include <sys/types.h> #include <sys/stat.h> int mkfifo ( const char *pathname, mode_t mode ); • • • •
vytvoří FIFO se jménem pathname mode specifikuje práva, která musí obsahovat práva čtení a zápisu, stejné jako u funkce open s FIFO se pracuje jako s obyčejným souborem, tj. open, read, write, close (low-level I/O) nebo fopen, fread, fwrite, fclose (I/O C knihovny) vrací 0, při chybě -1
Z FIFO může číst nebo do ní zapisovat více procesů. Data z každého zapisujícího procesu jsou zapisována atomicky. Další funkce týkající se rour nebo FIFO jsou:
Linux - Synchronizace (Synchronization) Programování vláken není jednoduché. Není způsob, jak předem zjistit, kdy systém spustí jedno vlákno a kdy druhé. Debugování vícevláknového programu je těžké, protože nelze vždy reprodukovat chybné chování. Většina problémů je způsobena tím, že vlákna přistupují ke stejným datům. Na jednu stranu je to velká výhoda vláken, ale na druhou stranu to může být nebezpečné. Když je jedno vlákno v půli cesty při zapisování (čtení) dat a druhé je čte (zapisuje), pravděpodobně vznikne chyba. Často program funguje jen pokud vlákno běží dříve nebo později než jiné vlákno. Chyby, které vznikají v důsledku nesprávného pořadí vykonávání vláken, se nazývají race conditions (chyby souběhu), vlákna soutěží o to, které poběží dřív. K eliminování race conditions je potřeba zajistit, aby se vlákna vykonávala ve správném pořadí nebo aby se některé operace prováděly atomicky, tzn. je potřeba vlákna synchronizovat. Atomická operace je nerozdělitelná a nepřerušitelná, když začne, nebude zastavena nebo přerušena, dokud se neukončí.
Semafory pro vlákna (Semaphores for threads) Semafor je "počítadlo" umožňující synchronizaci více vláken. Systém zaručuje, že zjištění nebo modifikování hodnoty
semaforu je atomická operace. Hodnota semaforu je nezáporné číslo, umožňuje dvě operace: • •
čekání (wait) - sníží hodnotu semaforu o 1. Pokud je hodnota 0, operace je blokována, dokud nebude hodnota kladná. Až bude kladná, sníží se o 1 a čekání končí. nastavení (post) - zvýší hodnotu semaforu o 1. Pokud byla hodnota 0 a nějaká vlákna jsou blokována při čekání na tento semafor, jedno z nich je odblokováno a jeho čekání se ukončí (a hodnota semaforu se vrátí na 0).
Semafory pro vlákna jsou implementovány podle standardu POSIX, hlavičkový soubor je <semaphore.h>. sem_init(3) #include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value); • • •
inicializuje semafor, na který ukazuje sem, na hodnotu value pshared specifikuje, zda je semafor lokální pro aktuální proces (0), nebo zda se má sdílet s ostatními procesy (!0), na Linuxu tohle ale není implementováno, musí být vždy 0 vrací 0, při chybě -1
sem_destroy(3) #include <semaphore.h> int sem_destroy(sem_t * sem); • • •
zruší semafor, na který ukazuje sem žádné vlákno by na semafor nemělo čekat, jinak chyba vrací 0, při chybě -1
sem_wait(3) #include <semaphore.h> int sem_wait(sem_t * sem); • • •
operace wait bod zrušení vlákna vrací vždy 0
sem_trywait(3) #include <semaphore.h> int sem_trywait(sem_t * sem); • •
neblokující operace wait, pokud má semafor hodnotu 0, vrací se s chybou vrací 0, při chybě -1
sem_post(3) #include <semaphore.h> int sem_post(sem_t * sem); • • •
operace post nikdy neblokuje a může se bezpečně používat v asynchronních signal-handlerech vrací 0, při chybě -1
sem_getvalue(3) #include <semaphore.h> int sem_getvalue(sem_t * sem, int * sval); • • •
uloží aktuální hodnotu semaforu na místo, kam ukazuje sval neměla by se používat k rozhodnutí operace se semaforem, může to vést k race condition: jiné vlákno může změnit hodnotu semaforu mezi touto funkcí a operací vrací vždy 0
Př. Napište program, ve kterém vytvoříte vlákno. Jedno vlákno bude něco zapisovat do společného bufferu (naplní ho), druhé z něho číst (vyprázdní ho). Synchronizujte tyto dvě vlákna semafory tak, aby se zapisovalo, jen když je buffer prázdný, a četlo, jen když je plný, a do bufferu v danou chvíli přistupovalo jen jedno vlákno.
Semafory pro procesy (Semaphores for processes) Procesy musí koordinovat přístup např. do společné sdílené a mapované paměti. Semafory jsou alokovány, používány a dealokovány stejně jako úseky sdílené paměti, jsou také součástí systémových volání původního unixového systému System V IPC (poslední jsou fronty zpráv). I když ve většině případů stačí jeden semafor, semafory pro procesy existují po množinách. semget(2) #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semget ( key_t key, int nsems, int semflg ) • •
• • •
vytvoří množinu nsems semaforů ke klíči key, pokud v key je IPC_PRIVATE, nebo pokud ke klíči key již neexistuje množina semaforů a v semflg je IPC_CREAT v semflg může být: • IPC_CREAT - vytvoří novou množinu semaforů, pokud toto není, najde se množina asociovaná ke klíči key • IPC_EXCL - používá se s IPC_CREAT, chyba pokud množina semaforů s klíčem key existuje • práva přístupu - definovaná v <sys/stat.h>, může být: • S_IRUSR, S_IWUSR - čtení, změna pro vlastníka množiny semaforů • S_IROTH, S_IWOTH - pro ostatní nsems může být 0, pak se nic nevytvoří, nebo kladné číslo udávající počet semaforů v množině vrací identifikátor množiny semaforů, jinak -1 existuje limit počtu semaforů v množině, viz man
Semafory existují i když skončí všechny procesy, které je používají. Proces, který semafory vytvořil, je také musí dealokovat, jinak systému dojdou. Kromě alokace je potřeba semafory také inicializovat. semctl(2) #include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semctl (int semid, int semnum, int cmd, union semun arg) • • •
•
zjistí informace o množině semaforů (identifikované pomocí semid) nebo o jednom semaforu, mění její práva, dealokuje ji a inicializuje ji strukturu semun si většinou musíme deklarovat sami, viz man cmd může být: • IPC_STAT - vrací informace do semid_ds struktury arg.buf, viz man, potřebuje právo čtení • IPC_SET - nastavuje práva • IPC_RMID - ihned zruší množinu semaforů (a odblokuje všechny čekající procesy), může jen ten (uživatel), který ji vytvořil nebo vlastník (nebo root) • GETALL - vrátí hodnotu všech semaforů do arg.array • GETVAL - vrátí hodnotu semnum-tého semaforu (první je 0-tý) • SETALL - nastaví hodnoty všech semaforů podle arg.array, čekající procesy jsou odblokovány • SETVAL - nastaví hodnotu semnum-tého semaforu na arg.val, čekající procesy jsou odblokovány při chybě vrací -1, jinak záleží na cmd, při GETVAL vrací hodnotu semaforu
Každý semafor má nezápornou hodnotu a podporuje operace wait a post. Tyto operace se provádějí pomocí funkce semop. semop(2) #include <sys/types.h> #include <sys/ipc.h>
#include <sys/sem.h> int semop ( int semid, struct sembuf *sops, unsigned nsops ) • •
•
provede operaci na vybraných semaforech (počtu nsops) z množiny semid, operace se provedou jen pokud se všechny provedou úspěšně prvky pole sops specifikují operaci pomocí struktury sembuf, která má prvky: • sem_num - číslo ovlivňovaného semaforu, pro první je 0 • sem_flg - když je zde SEM_UNDO, při ukončení procesu je semafor vrácen na svou předchozí hodnotu, platí pro všechna volání této funkce během existence procesu, sem_undo struktury procesu se nedědí při fork, ale při exec ano • sem_op - operace, může být: • kladné číslo - přidá se k hodnotě semaforu, při 1 je to operace post • nula - blokuje proces, dokud hodnota semaforu není 0 nebo množina semaforů není zrušena nebo proces nedostane signál, který se obslouží • záporné číslo - pokud je hodnota semaforu větší nebo rovna absolutní hodnotě tohoto čísla, je tato absolutní hodnota odečtena od hodnoty semaforu, jinak je proces blokován, dokud hodnota semaforu nebude větší nebo rovna absolutní hodnotě nebo nebo množina semaforů není zrušena nebo proces nedostane signál, který se obslouží, při -1 je to operace wait vrací 0, při chybě -1
Př. Synchronizujte rodiče a potomka v programu ze cvičení 'Linux - Meziprocesní komunikace', část 'Sdílená paměť'. Použijte jeden binární semafor tak, aby proces četl, až tam druhý něco zapíše. ipcs(8) ipcs -s •
vypíše pole semaforů a informace o nich
Př. Podívejte se, zda v systému nezůstaly nepoužívané množiny semaforů. ipcrm(8) ipcrm sem id... •
zruší množinu semaforů specifikovanou pomocí id
Př. Zrušte nepoužívané množiny semaforů. PŘÍKLAD Další funkce týkající se semaforů jsou:
Linux - Synchronizace (Synchronization) Mutexy (Mutexes) Řešením většiny race conditions problémů je dovolit v daném čase přístup ke sdíleným datům pouze jednomu vláknu. Když jedno vlákno data mění, žádné jiné by k nim nemělo přistupovat, dokud změna dat neskončí. Mutex (MUTual EXclusion lock) je speciální zámek, který může v daném čase zamčít pouze jedno vlákno. Když se druhé vlákno pokusí zamčít již zamčený mutex, je blokováno, dokud první vlákno mutex neodemče. pthread_mutex_init(3) #include int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr); • •
inicializuje mutex na ukazateli mutex, nastaví atributy podle mutexattr, pokud je NULL, atributy jsou defaultní, viz pthread_mutexattr_init proměnná typu pthread_mutex_t může být inicializována také staticky pomocí konstanty PTHREAD_MUTEX_INITIALIZER, např. pro globální a statické proměnné
•
vždy vrací 0
pthread_mutex_destroy(3) #include int pthread_mutex_destroy(pthread_mutex_t *mutex); • •
zruší mutex, který musí být odemčený, uvolní prostředky systému (v Linuxu nejsou mutexům přiděleny žádné prostředky, tzn. funkce se nemusí používat pro zrušení mutexu) vrací 0, při chybě nenulové číslo
pthread_mutex_lock(3) #include int pthread_mutex_lock(pthread_mutex_t *mutex); • •
zamče mutex, pokud je již zamčený, blokuje vlákno, dokud není odemčen vrací 0, při chybě nenulové číslo
pthread_mutex_trylock(3) #include int pthread_mutex_trylock(pthread_mutex_t *mutex); • •
zamče mutex, pokud je již zamčený, vrací chybu vrací 0, při chybě nenulové číslo
pthread_mutex_unlock(3) #include int pthread_mutex_unlock(pthread_mutex_t *mutex); • • •
odemče mutex a jedno (náhodné) z blokovaných vláken je odblokováno mutex musí být uzamčený stejným vláknem, ze kterého je volána tato funkce vrací 0, při chybě nenulové číslo
Před přístupem (čtení i zápis) ke sdíleným datům si každé vlákno uzamče mutex. Až po dokončení operace s daty mutex odemče. Př. Nahraďte patřičné semafory z příkladu ze cvičení 'Linux - Synchronizace', část 'Semafory pro vlákna', mutexy. Blokování vláken mutexem může způsobit deadlock, který vznikne, když vlákna čekají na něco, co se nikdy nestane. Např. když se vlákno pokusí uzamčít jím zamčený mutex, ale to záleží na typu mutexu: • • •
fast mutex - pokus o uzamčení zamčeného mutexu blokuje, dokud není odemčen, inicializační konstanta je PTHREAD_MUTEX_INITIALIZER, defaultní recursive mutex - může být uzamčen vícekrát, uzamčení stejným vláknem se počítá, až stejný počet odemčení (tím vláknem) jej odemče, inicializační konstanta je PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP error-checking mutex - při pokusu o uzamčení již zamčeného mutexu se vrátí chyba, ale neblokuje se, inicializační konstanta je PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP
pthread_mutexattr_init(3) #include int pthread_mutexattr_init(pthread_mutexattr_t *attr); • •
inicializuje atributy mutexu attr a naplní je defaultními hodnotami vrací vždy 0
pthread_mutexattr_setkind_np(3) #include int pthread_mutexattr_setkind_np(pthread_mutexattr_t *attr, int kind);
• •
•
nastaví typ mutexu v atributech attr na hodnotu kind kind může být PTHREAD_MUTEX_FAST_NP, PTHREAD_MUTEX_RECURSIVE_NP nebo PTHREAD_MUTEX_ERRORCHECK_NP, NP znamená nonportable, tj. nepřenosné rozšíření POSIX standardu vrací 0, při chybě nenulové číslo
pthread_mutexattr_getkind_np(3) #include int pthread_mutexattr_getkind_np(const pthread_mutexattr_t *attr, int *kind); • •
vrátí typ mutexu v atributech attr na adresu kind vrací vždy 0
pthread_mutexattr_destroy(3) #include int pthread_mutexattr_destroy(pthread_mutexattr_t *attr); • •
zruší atributy mutexu, před novým použitím se musí znovu inicializovat vrací vždy 0
Condition variables Condition variable je třetí synchronizační prostředek v Linuxu, který umožňuje implementovat složitější podmínky běhu více vláken. Například funkce vlákna, která vykonává nekonečnou smyčku. Pokud je nastavena určitá vlajka, vykoná se při iteraci nějaká činnost. void* thread_function(void* thread_arg) { while (1) { int flag_is_set; pthread_mutex_lock(&thread_flag_mutex); flag_is_set = thread_flag; pthread_mutex_unlock(&thread_flag_mutex); if (flag_is_set) do_work ();
}
} return NULL;
void set_thread_flag(int flag_value) { pthread_mutex_lock(&thread_flag_mutex); thread_flag = flag_value; pthread_mutex_unlock(&thread_flag_mutex); }
Tohle je správně, ale ne dostatečně. Pokud není vlajka nastavena, spotřebuje se spousta času CPU testováním vlajky a zamykáním a odemykáním mutexu. Je potřeba vlákno uspat, dokud se nestane něco, co nastaví vlajku. Condition variable implementuje podmínku, při které vlákno poběží a naopak podmínku, při které bude vlákno blokováno. Pokud vlákno čeká na condition variable, je blokováno, dokud jiné vlákno nesignalizuje condition variable. Condition variable (na rozdíl od semaforu) není čítač, vlákno na ni musí čekat před tím, než je signalizována. Jinak je signál ztracen a vlákno čeká na další. Zpět k příkladu, pokud není vlajka nastavena, vlákno bude čekat na condition variable. Po nastavení vlajky se condition
variable signalizuje a blokované vlákno otestuje vlajku znovu (úspěšně). Je tu ale jeden problém: race condition mezi testováním vlajky a čekáním na condition variable. Např. vlákno zjistí, že vlajka není nastavena. Plánovač přepne vlákna, vlajka se nastaví a signalizuje se condition variable. Signál je ztracen, plánovač přepne vlákno zpět a to začne čekat na condition variable, které se možná nedočká. Potřebujeme zamčít mutexem vlajku i condition variable. Každá condition variable proto musí být použita spolu s mutexem: • • •
vlákno zamče mutex a otestuje vlajku pokud je vlajka nastavena, odemče mutex a vykoná činnost pokud ne, atomicky odemče mutex a čeká na condition variable
pthread_cond_init(3) #include int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr); • • •
inicializuje condition variable na ukazateli cond, atributy cond_attr nejsou v Linuxu implementovány, tzn. zadává se NULL proměnná typu pthread_cond_t může být inicializována také staticky pomocí konstanty PTHREAD_COND_INITIALIZER, např. pro globální a statické proměnné vždy 0, při chybě nenulové číslo
pthread_cond_destroy(3) #include int pthread_cond_destroy(pthread_cond_t *cond); • •
zruší condition variable, na kterou nesmí žádné vlákno čekat, uvolní prostředky systému (v Linuxu nejsou mutexům přiděleny žádné prostředky, tzn. funkce se nemusí používat pro zrušení condition variable) vrací 0, při chybě nenulové číslo
pthread_cond_signal(3) pthread_cond_broadcast(3) #include int pthread_cond_signal(pthread_cond_t *cond); int pthread_cond_broadcast(pthread_cond_t *cond); • •
signalizuje condition variable, odblokuje nejvýše jedno (náhodné) blokované vlákno nebo všechna blokovaná vlákna (pthread_cond_broadcast) vrací 0, při chybě nenulové číslo
pthread_cond_wait(3) #include int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); • • • •
atomicky odemče mutex a čeká na condition variable, mutex musí být zamčený při signalizování znovu zamče mutex ještě před odblokováním vlákna je to bod zrušení vlákna, před zrušením zamče mutex vrací 0, při chybě nenulové číslo
Při změně podmínky (vlajky), která je chráněna pomocí condition variable, se musí postupovat takto: • • • •
zamče se mutex pro condition variable změní se podmínka (vlajka) signalizuje se condition variable odemče se mutex
Př. Opravte předložený příklad tak, aby se použila condition variable. Condition variable se může použít i bez podmínky, jednoduše pro blokování vlákna, dokud ho jiné "neprobudí". OTÁZKA: Pro tohle se dá použít i semafor. Jaké jsou principiální rozdíly mezi condition variable a semaforem? Další funkce týkající se mutexů nebo condition variables jsou:
• Windows - Procesy (Processes) Aplikace pro Windows se skládá z jednoho nebo více procesů. Proces je jednoduše řečeno spuštěný program. Každý proces poskytuje zdroje nutné k vykonávání programu. Proces má vituální adresový prostor, kód, data, proměnné prostředí, prioritu, atd. #include <windows.h> - pro všechny funkce Win32 API
Vytvoření procesu CreateProcess • • • •
• • •
• •
vytvoří nový proces jako potomka rodičovského procesu, oba běží nezávisle na sobě vrátí strukturu PROCESS_INFORMATION informací o novém procesu do lpProcessInformation, např. handle (který je platný dokud se neuzavře, tedy i po ukončení procesu) vrací nenulovou hodnotu, při chybě 0 vlastnosti nového procesu lze nastavit pomocí struktury STARTUPINFO: • pro GUI procesy: výšku, šířku a pozici okna, které se použijí jako defaultní hodnoty funkce CreateWindow, nCmdShow parametr pro funkci ShowWindow • pro konzolové procesy velikost okna a bufferu (ve znacích), pozici, barvy textu a pozadí, titulek konzole lpCommandLine - příkazový řádek, tj. jméno programu s parametry bInheritHandles - jestli potomek zdědí handly dwCreationFlags - další nastavení, např. • CREATE_NEW_CONSOLE - potomek na novou konzoli, nemůže se používat s DETACHED_PROCESS • DETACHED_PROCESS - pro konzolové procesy je bez konzole rodiče, nemůže se používat s CREATE_NEW_CONSOLE • CREATE_SUSPENDED - hlavní vlákno procesu se vytvoří pozastavené • priorita - defaultní NORMAL_PRIORITY_CLASS, pokud má rodič IDLE_PRIORITY_CLASS, tak tato lpStartupInfo - STARTUPINFO struktura lpProcessInformation - PROCESS_INFORMATION struktura
PŘÍKLAD Př. Napište program a spusťte z něj nový konzolový proces s prioritou IDLE_PRIORITY_CLASS. CreateProcessAsUser •
vytvoří nový proces, který poběží pod jiným uživatelem, specifikovaným pomocí hToken
GetCurrentProcessId •
vrací pid procesu
Př. Vypište pid procesu. OpenProcess • • •
vrací handle procesu na základě pid, při chybě NULL bInheritHandle - jestli může být handle děděn dwProcessId - id procesu
GetCurrentProcess •
vrací pseudo handle procesu, který je použitelný jen v rámci tohoto procesu !, lze jej "zveřejnit" na normální handle pomocí DuplicateHandle
Př. Zjistěte handle, pseudo handle a "zveřejněný" pseudo handle procesu a porovnejte je s handlem z PROCESS_INFORMATION
Informace o procesu GetStartupInfo •
vrací STARTUPINFO strukturu, použitou při vytvoření procesu, do lpStartupInfo
Nový proces může od rodiče zdědit různé vlastnosti a zdroje, a rodičovský proces může určit, co se může zdědit. Potomek může dědit: • • • •
handly otevřených souborů, bufferů, rour, procesů, vláken, mutexů, semaforů, mapování souborů proměnné prostředí aktuální adresář konzoli, pokud rodič není bez konzole nebo pokud si potomek nevytvoří novou
nedědí: • • •
pseudo handly prioritu GDI nebo USER handly, např. HBITMAP nebo HMENU
Aby se handl zdědil, musí se zděditelnost specifikovat při jeho vytváření, otevírání nebo duplikování a při volání funkce CreateProcess s parametrem fInheritHandles. Zděděný handl je stejný jako původní, změna na jednom se projeví i na druhém.
Ukončení procesu ExitProcess •
ukončí tento proces s návratovým kódem uExitCode
TerminateProcess • •
ukončí jiný proces, jehož handle je hProcess, s návratovým kódem uExitCode vrací nenulovou hodnotu, při chybě 0
Proces se též ukončí, když skončí jeho hlavní nebo poslední vlákno. Handly otevřených souborů a jiné jsou při ukončení procesu uzavřeny, ale stále existují, pokud je používají i jiné procesy. Konzolové procesy lze také ukončit pomocí C-C nebo C-Break. GetExitCodeProcess • •
vrací status ukončení procesu, jehož handle je hProcess, do lpExitCode, je STILL_ACTIVE, pokud proces ještě běží, nebo je to návratový kód procesu, který vrací main nebo WinMain funkce vrací nenulovou hodnotu, při chybě 0
Př. Vypište v rodiči návratový kód potomka. TerminateProcess neukončuje korektně vlákna, pro ukončení procesu jiným by se mělo postupovat takto: • •
oba procesy si zaregistrují privátní zprávu pomocí RegisterWindowMessage jeden ukončí druhého posláním této privátní zprávy pomocí BroadcastSystemMessage, př. BroadcastSystemMessage( BSF_IGNORECURRENTTASK, BSM_APPLICATIONS, private message, wParam, lParam );
•
// // // // //
do not send message to this process broadcast only to applications message registered in functionvious step message-specific value message-specific value
proces, co přijme zprávu, se ukončí pomocí ExitProcess
Při ukončení procesu se neukončují jeho potomci. Další funkce týkající se procesů jsou:
• • • • • • • •
GetCommandLine GetCurrentDirectory / SetCurrentDirectory GetEnvironmentStrings GetEnvironmentVariable / SetEnvironmentVariable FreeEnvironmentStrings GetProcessTimes GetPriorityClass / SetPriorityClass GetModuleFileName
•
pthread_cond_timedwait(3)
Windows - Vlákna (Threads) V procesu může běžet jedno nebo více vláken. Vlákno je základní jednotka vykonávání instrukcí programu, které systém přiděluje procesor. Každý proces startuje v primárním vláknu. Všechna vlákna sdílejí stejný adresový prostor a systémové zdroje. Každé vlákno má vlastní handlery výjimek, prioritu a kontext. Kontext vlákna obsahuje registry, zásobník jádra a vlákna a prostředí vlákna v adresovém prostoru procesu. Vlákno může vykonávat libovolnou část kódu programu, včetně částí vykonávaných jiným vláknem.
Vytvoření vlákna CreateThread • • • • • •
vytvoří nové vlákno procesu musí se specifikovat startovací adresa kódu v lpStartAddress, typicky funkce tvaru DWORD WINAPI ThreadFunc( LPVOID ); vrací handle vlákna (který je platný dokud se neuzavře, tedy i po ukončení vlákna) a thread id do lpThreadId, při chybě NULL priorita nového vlákna je THREAD_PRIORITY_NORMAL lpParameter - parametr funkce vlákna dwCreationFlags - další nastavení, např. • CREATE_SUSPENDED - vlákno je vytvořeno pozastavené, např. pro inicializaci, změnu priority, dokud jiné vlákno nezavolá ResumeThread
Je riskantní v parametru funkce vlákna předávat adresu lokální proměnné z vlákna, ze kterého vytváříme nové vlákno, protože původní vlákno může skončit dřív než nové a adresa by byla neplatná. Předávají se adresy na dynamicky alokovanou paměť nebo vlákno počká, až se ukončí nové vlákno. PŘÍKLAD Př. Napište program a vytvořte v něm nové vlákno. CreateRemoteThread •
vytvoří nové vlákno v jiném procesu, jehož handle je hProcess
OTÁZKA: Zjistěte podrobnější informace o vytvoření vlákna v jiném procesu. Např. musí funkce vlákna existovat v tomto nebo v cílovém procesu? GetCurrentThreadId •
vrací thread id vlákna
Př. Vypište id vlákna. GetCurrentThread •
vrací pseudo handle vlákna, který je použitelný jen v rámci tohoto vlákna !, lze jej "zveřejnit" na normální handle pomocí DuplicateHandle
Př. Zjistěte pseudo handle a "zveřejněný" pseudo handle vlákna a porovnejte ho s handlem z CreateThread. SuspendThread
• •
pozastaví vlákno, jehož handle je hThread, přesněji zvýší počet pozastavení vlákna vrací počet pozastavení vlákna, při chybě 0xFFFFFFFF
Př. Pozastavte vlákno (z něj). ResumeThread • •
sniží počet pozastavení vlákna, jehož handle je hThread, při 0 vlákno pokračuje vrací počet pozastavení vlákna, při chybě 0xFFFFFFFF
Př. Pusťte vlákno. Sleep • •
uspí vlákno na dwMilliseconds milisekund když je dwMilliseconds INFINITE, vlákno se uspí navždy
GDI objekty (palety, kontexty zařízení, regiony, atd.) nejsou serializované, proto by se neměly sdílet. Pro zamezení chybám souběhu (race conditions) a zablokování (deadlock) je potřeba synchronizovat přístup více vláken ke sdíleným prostředkům. Ve vláknu je možné vytvořit okno, pak ale musí poskytovat smyčku zpráv, která bude vybírat zprávy z fronty zpráv.
Ukončení vlákna ExitThread •
ukončí toto vlákno s návratovým kódem dwExitCode
TerminateThread • •
ukončí jiné vlákno, jehož handle je hThread, s návratovým kódem uExitCode vrací nenulovou hodnotu, při chybě 0
Vlákno se ukončí i když libovolné vlákno v procesu zavolá ExitProcess nebo TerminateProcess nebo když se ukončí funkce vlákna. GetExitCodeThread • •
vrací status ukončení vlákna, jehož handle je hThread, do lpExitCode, je STILL_ACTIVE, pokud vlákno ještě běží, nebo je to návratový kód funkce vlákna vrací nenulovou hodnotu, při chybě 0
Př. Vypište návratový kód vlákna. TerminateThread neukončuje korektně vlákna, pro ukončení vlákna jiným by se mělo postupovat takto: • • • •
vytvořit objekt události pomocí CreateEvent vytvořit vlákna každé vlákno monitoruje stav události voláním WaitForSingleObject, interval je 0 každé vlákno se ukončí při nastavené události (WaitForSingleObject vrátí WAIT_OBJECT_0)
Lokální uložení vlákna Pomocí lokálního uložení vlákna (TLS) lze vytvořit lokální kopii statických a globálních proměnných vláken. TlsAlloc •
vrací TLS index, při chybě 0xFFFFFFFF
TlsSetValue
• •
uloží hodnotu na lpTlsValue na místo přístupné přes TLS index dwTlsIndex vrací nenulovou hodnotu, při chybě 0
TlsGetValue •
vrací hodnotu na pozici určené TLS indexem dwTlsIndex, při chybě 0
TlsFree • •
uvolní TLS index dwTlsIndex vrací nenulovou hodnotu, při chybě 0
Lokální uložení vlákna se používá takto: • • • •
pomocí TlsAlloc se alokuje TLS index proměnné pomocí TlsSetValue se asociuje index s alokovanou pamětí TlsGetValue vrací ukazatel do alokované paměti na základě indexu po ukončení práce s indexem se uvolní pomocí TlsFree
PŘÍKLAD Další funkce týkající se vláken jsou: • • •
SwitchToThread GetThreadTimes GetThreadPriority / SetThreadPriority
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: • • • • • •
má aplikace komunikovat s jinými aplikacemi na jiných počítačích na síti, nebo jen s aplikacemi na lokálním počítači? má aplikace komunikovat s jinými aplikacemi na jiných počítačích s jiným OS? má uživatel aplikace vybrat aplikace, se kterými bude aplikace komunikovat, nebo si má aplikace sama vybrat partnery? má aplikace komunikovat stejně se všemy jinými aplikacemi, nebo jen s vybranými aplikacemi omezeným způsobem? je výkon důležitý?, všechny mechanizmy IPC mají svoji režii má být aplikace GUI nebo konzolová?, některé IPC mechanizmy potřebují GUI
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: • •
sdílená paměť rychlejší a jednodušší přístup k souboru
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, NULL, PAGE_READWRITE, 0, 0, "MyFileMappingObject");
// // // // // //
Current file handle. Default security. Read/write permission. Max. object size. Size of hFile. 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, 0, 0, 0);
// // // //
Read/write permission Max. object size. Size of hFile. 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, FILE_MAP_ALL_ACCESS, 0, 0, 0);
// // // // //
Handle to mapping object. Read/write permission. Max. object size. Size of hFile. 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: • • • •
handle std. výstupu vrátí funkce GetStdHandle nastavíme handle std. výstupu na zapisovací konec roury pomocí SetStdHandle a vytvoříme potomka uzavřeme v rodiči zapisovací konec roury pomocí CloseHandle potomek pak zapisuje na std. výstup, jehož handle zjistí pomocí GetStdHandle
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: •
TransactNamedPipe
Windows - Synchronizace (Synchronization) Win32 API poskytuje mnoho způsobů koordinace vykonávání více vláken a mechanizmů synchronizace přístupu k prostředku. První metoda je použití synchronizačních objektů v čekacích funkcích. Stav objektu je buď signalizován nebo nesignalizován. Čekací funkce blokují vykonávání vlákna, dokud není nesignalizovaný objekt signalizován. Další synchronizační mechanizmus je např. objekt kritické sekce.
Čekací funkce (Wait functions) Existují tři typy čekacích funkcí: single-object, multiple-object a alertable. Tyto funkce se neukončí, dokud není splněna nějaká podmínka. Když je funkce zavolána, zkontroluje, zda je podmínka splněna. Pokud ne, vlákno vstoupí do čekacího stavu, kde spotřebovává velmi málo času procesoru a čeká, až bude podmínka splněna. Single-object čekací funkce jsou WaitForSingleObject a SignalObjectAndWait. WaitForSingleObject • •
ukončí se, když je synchronizační objekt (hHandle) signalizován nebo vyprší čas v dwMilliseconds (který může být INFINITE, tj. nevyprší nikdy) vrací WAIT_OBJECT_0 při signalizování objektu, WAIT_TIMEOUT při vypršení času nebo WAIT_FAILED při chybě
SignalObjectAndWait • • • •
atomicky signalizuje synchronizační objekt (hObjectToSignal) a čeká, až bude jiný objekt (hObjectToWaitOn) signalizován ukončí se také po vypršení času v dwMilliseconds (který může být INFINITE, tj. nevyprší nikdy) hObjectToSignal může být handle semaforu, mutexu nebo události vrací WAIT_OBJECT_0 při signalizování objektu, WAIT_TIMEOUT při vypršení času nebo 0xFFFFFFFF při chybě
Multiple-object čekací funkce jsou WaitForMultipleObjects a MsgWaitForMultipleObjects. WaitForMultipleObjects • • • •
•
ukončí se, když je některý nebo všechny synchronizační objekty (lpHandles) signalizovány nebo vyprší čas v dwMilliseconds (který může být INFINITE, tj. nevyprší nikdy) v nCount je počet objektů, maximum je MAXIMUM_WAIT_OBJECTS bWaitAll specifikuje, zda se má čekat na signalizaci všech objektů (TRUE), nebo jen na signalizaci libovolného jednoho (FALSE) při čekání na všechny objekty funkce nemění stav objektů, dokud nejsou signalizovány všechny, např. mutex je signalizován, ale funkce se neukončí, dokud nebudou signalizovány všechny objekty, během tohoto čekání může jiné vlákno vrátit mutex do nesignalizovaného stavu atd. vrací hodnotu z intervalu WAIT_OBJECT_0 až WAIT_OBJECT_0 + nCount - 1, po odečtení WAIT_OBJECT_0 je to nejmenší index signalizovaného objektu, WAIT_TIMEOUT při vypršení času nebo WAIT_FAILED při chybě
MsgWaitForMultipleObjects • • •
•
jako WaitForMultipleObjects, ale některé objekty mohou být objekty události vstupu (např. vstup od myši), u kterých se čeká, až přijdou do vstupní fronty vlákna vlákno může použít GetMessage nebo PeekMessage na získání vstupu dwWakeMask specifikuje typ vstupu, pro který bude přidán objekt události vstupu, může být libovolná kombinace např. QS_ALLINPUT (jakákoliv zpráva), QS_HOTKEY (zpráva WM_HOTKEY), QS_INPUT (vstupní zpráva), QS_KEY (klávesové zprávy), QS_MOUSE (zprávy události myši), QS_TIMER (zpráva WM_TIMER) a další vrací WAIT_OBJECT_0 + nCount při příchodu objektu události vstupu, při chybě 0xFFFFFFFF
Před svým ukončením může čekací funkce změnit stav synchronizačního objektu, na který čekala, např. snížit hodnotu semaforu o 1 (ten je nesignalizován, pokud jeho hodnota je 0) nebo nastavit na nesignalizovaný stav mutex, auto-reset událost, change-notification objekt a synchronizační časovač. Čekací funkce se musí používat opatrně ve spojení s DDE. Pokud vlákno vytvoří okno, musí obsluhovat zprávy. DDE
posílá zprávy všem oknům v systému. Pokud vlákno čeká bez časového omezení, nastane deadlock. Vlákna, která tvoří okna, musí používat MsgWaitForMultipleObjects.
Synchronizační objekty (Synchronization objects) Synchronizační objekt se zadává (jeho handle) v čekacích funkcích ke koordinaci vykonávání více vláken. Stejný objekt může využívat i více procesů pro meziprocesní synchronizaci. Pro synchronizaci se používají objekty události, semaforu, mutexu a časovače. Ale mohou se použít i objekty procesu a vlákna, které jsou při běhu nesignalizovány a signalizovány při ukončení.
Objekt události (Event object) Jsou dva typy objektu události: • •
manual-reset - zůstane signalizován, dokud není explicitně nastaven na nesignalizován, když je signalizován, může se odblokovat libovolný počet čekajících vláken auto-reset - zůstane signalizován, dokud není odblokováno jedno čekající vlákno, objekt je pak nastaven na nesignalizován
Objekt události je užitečný pro zaslání signálu pro indikaci, že se objevila nějaká událost (např. se dokončila nějaká operace). CreateEvent • • • • •
vytvoří (nebo vrací) objekt události se jménem lpName (může být NULL), defaultně práva EVENT_ALL_ACCESS a nedědí se bManualReset specifikuje typ objektu, manual-reset při TRUE bInitialState určuje počáteční stav, signalizován při TRUE vrací handle objektu, při chybě NULL pro zrušení stačí CloseHandle, při ukončení procesu se zruší automaticky
OpenEvent • • •
vrací handle existujícího objektu události se jménem lpName, při chybě NULL dwDesiredAccess specifikuje přístupová práva objektu, EVENT_ALL_ACCESS pro všechna, EVENT_MODIFY_STATE pro modifikaci bInheritHandle povoluje dědění (TRUE)
SetEvent •
nastaví stav objektu události (hEvent) na signalizován
PulseEvent •
jako SetEvent, ale po odblokování vrátí objekt na nesignalizovaný stav
ResetEvent • •
nastaví stav objektu události (hEvent) na nesignalizován používá se u manual-reset objektu
Př. Napište program, ve kterém vytvoříte vlákno. Toto vlákno bude monitorovat stav objektu události a při signalizovaném stavu se ukončí. Objekt signalizujte z prvního vlákna.
Semafor (Semaphore object) Semafor je "počítadlo", které má hodnotu od 0 do maximální hodnoty. Hodnota je dekrementována (o 1) po každé čekací funkci a inkrementována, když vlákno semafor uvolní. Když je hodnota 0, čekací funkce vlákno blokuje. Semafor je signalizován, když je jeho hodnota větší než 0, a nesignalizován, když je 0. Semafor je užitečný ke kontrolování sdíleného prostředku, ke kterému má přístup omezený počet uživatelů. Např. limit na počet vytvořených oken. Maximální hodnota semaforu je maximální počet oken, dekrementuje se při vytvoření okna,
inkrementuje při zavření. Před vytvořením okna se volá čekací funkce se semaforem. CreateSemaphore • • • •
vytvoří (nebo vrací) semafor se jménem lpName (může být NULL), defaultně práva SEMAPHORE_ALL_ACCESS a nedědí se lInitialCount určuje počáteční hodnotu semaforu (která je od 0 do maximální hodnoty), lMaximumCount maximální vrací handle objektu, při chybě NULL pro zrušení stačí CloseHandle, při ukončení procesu se zruší automaticky
OpenSemaphore • • •
vrací handle existujícího semaforu se jménem lpName, při chybě NULL dwDesiredAccess specifikuje přístupová práva objektu, SEMAPHORE_ALL_ACCESS pro všechna, SEMAPHORE_MODIFY_STATE pro inkrementaci bInheritHandle povoluje dědění (TRUE)
ReleaseSemaphore • • •
inkrementuje hodnotu semaforu (hSemaphore) o lReleaseCount pokud by hodnota po inkrementaci byla větší než maximální hodnota, k inkrementaci nedojde a funkce vrací FALSE na adresu lpPreviousCount (pokud není NULL) se uloží předchozí hodnota
Počáteční hodnota semaforu je typicky maximální hodnota. Je dekrementována při spotřebovávání chráněného prostředku. Nebo je počáteční hodnota 0 pro blokování přístupu během inicializace aplikace. Po inicializaci se hodnota zvedne na maximální hodnotu. Vlákna nevlastní semafor. Při opakovaném čekání na semafor se tento vždy dekrementuje a při 0 se vlákno samo blokuje. Zvednout hodnotu semaforu může libovolné vlákno. Dekrementování o více než 1 se provede více voláními čekací funkce s tímto semaforem. Uvedení jednoho semaforu vícekrát v multiple-object čekací funkci ho dekrementuje pouze o 1. Př. Napište program, ve kterém vytvoříte vlákno. Jedno vlákno bude něco zapisovat do společného bufferu (naplní ho), druhé z něho číst (vyprázdní ho). Synchronizujte tyto dvě vlákna semafory tak, aby se zapisovalo, jen když je buffer prázdný, a četlo, jen když je plný, a do bufferu v danou chvíli přistupovalo jen jedno vlákno.
Mutex (Mutex object) Mutex je signalizován, když ho nevlastní žádné vlákno, a nesignalizován, když ho vlastní jedno vlákno. Mutex může vlastnit právě jedno vlákno, používá se k exkluzivnímu přístupu ke sdílenému prostředku (MUTtually EXclusive). Např. pro zabránění dvěma vláknům zápisu do sdílené paměti ve stejnou chvíli, obě čekají na vlastnictví objektu a pak jedno zapisuje, potom vlákno mutex uvolní. CreateMutex • • • •
vytvoří (nebo vrací) mutex se jménem lpName (může být NULL), defaultně práva MUTEX_ALL_ACCESS a nedědí se bInitialOwner určuje okamžité vlastnictví (TRUE) vrací handle objektu, při chybě NULL pro zrušení stačí CloseHandle, při ukončení procesu se zruší automaticky
OpenMutex • • •
vrací handle existujícího mutexu se jménem lpName, při chybě NULL dwDesiredAccess specifikuje přístupová práva objektu, MUTEX_ALL_ACCESS pro všechna bInheritHandle povoluje dědění (TRUE)
Vlákno požaduje vlastnictví mutexu pomocí některé čekací funkce. Pokud již mutex vlastní jiné vlákno, čekací funkce vlákno blokuje, dokud to druhé vlákno mutex neuvolní. Když vlákno vlastní mutex, může ho použít v opakovaných voláních čekací funkce bez blokování (vlákno neblokuje samo sebe). Pro uvolnění mutexu ho musí uvolnit stejně početkrát.
ReleaseMutex •
uvolní vlastnictví mutexu (hMutex), může jen vlákno, které mutex vlastní
Př. Nahraďte patřičné semafory z předchozího příkladu mutexy. Další funkce týkající se čekacích funkcí, objektů události, semaforů nebo mutexů jsou: • • •
WaitForSingleObjectEx WaitForMultipleObjectsEx MsgWaitForMultipleObjectsEx
Windows - Synchronizace (Synchronization) Synchronizace procesů (Interprocess synchronization) Více procesů může mít handly objektu události, semaforu, mutexu nebo časovače, proto mohou být tyto prostředky použity i pro synchronizaci procesů. Proces, který objekt vytvoří, používá handle, který vrátí vytvářecí funkce. Ostatní procesy si handle otevřou použitím jeho jména nebo pomocí zdědění nebo duplikace handlu. Jméno objektu může obsahovat jakékoliv znaky kromě zpětného lomítka \ a je case-sensitive. Jména objektů události, semaforu, mutexu, časovače a mapování souboru sdílejí stejný prostor jmen, proto by se pro tyto objekty měla používat unikátní jména. Pokud se při vytvoření objektu zadá jméno objektu jiného typu, vytvářecí funkce bude úspěšná, ale jméno zůstane původnímu objektu (a funkce GetLastError vrátí ERROR_ALREADY_EXISTS), např. v CreateMutex jméno semaforu. Při zadání jména objektu stejného typu vrátí vytvářecí funkce handle tohoto objektu, např. v CreateMutex jméno mutexu. Při vytvoření procesu (pomocí funkce CreateProcess) lze specifikovat zdědění handlu objektu pomocí struktury SECURITY_ATTRIBUTES. Zděděný handle má stejná práva k objektu jako původní handle. Handle se sice zdědí, ale novému procesu se musí nějak sdělit jeho hodnota (např. přes parametr příkazové řádky). Duplikaci handlu provádí funkce DuplicateHandle, hodnota nového handlu se musí nějak sdělit jinému procesu, libovolnou metodou meziprocesní komunikace (např. pojmenovanou rourou nebo přes sdílenou paměť). Př. Synchronizujte programy ze cvičení 'Windows - Meziprocesní komunikace', část 'Mapování souboru'. Použijte jeden binární semafor tak, aby druhý program četl, až tam první něco zapíše.
Objekt kritické sekce (Critical section object) Objekt kritické sekce poskytuje podobnou synchronizaci jako mutex s tím rozdílem, že se může použít pouze pro vlákna v jednom procesu. Mutexy lze takto také použít, ale objekt kritické sekce je rychlejší. Jako mutex, objekt kritické sekce vlastní v daném čase jen jedno vlákno a používá se pro ochranu sdílené proměnné proti vícenásobnému přístupu (s proměnnou se pracuje v tzv. kritické sekci). InitializeCriticalSection • •
inicializuje objekt kritické sekce (typu CRITICAL_SECTION) na adrese lpCriticalSection objekt nelze přesouvat, kopírovat nebo modifikovat
EnterCriticalSection • • •
vlákno si přivlastní objekt kritické sekce (lpCriticalSection), tj. vstoupí do kritické sekce pokud již objekt vlastní jiné vlákno, funkce blokuje (nekonečně dlouho) opakované volání neblokuje (vlákno nezablokuje samo sebe), pro uvolnění se musí uvolnit stejně počet-krát
TryEnterCriticalSection • •
jako EnterCriticalSection, ale neblokuje vrací nenulovou hodnotu, pokud vlákno vstoupí (nebo již je) v kritické sekci, pokud objekt vlastní jiné vlákno, vrací 0
LeaveCriticalSection •
uvolní objekt kritické sekce (lpCriticalSection) z vlastnictví, tj. opustí kritickou sekci
•
pokud vlákno objekt nevlastní, vznikne chyba, která může způsobit nekonečné čekání na vlastnictví tohoto objektu
DeleteCriticalSection • •
zruší objekt kritické sekce (lpCriticalSection), který nevlastní žádné vlákno (žádné vlákno není v kritické sekci), a uvolní prostředky systému může ji zavolat libovolné vlákno procesu, nejen to, které objekt inicializovalo
Pokud objekt kritické sekce vlastní nějaké vlákno (vlákno je v kritické sekci), blokována jsou pouze vlákna, která čekají na vlastnictví tohoto objektu (na funkci EnterCriticalSection), ostatní vlákna normálně pokračují (a mohou vykonávat i kritickou sekci prvního vlákna!). Př. Napište program, ve kterém vytvoříte nové vlákno, jehož hlavní funkce bude mít 2 parametry, řetězce, které funkce spojí a výsledek vypíše. Spojení uzavřete do kritické sekce. Další funkce týkající se synchronizace procesů nebo objektu kritické sekce jsou: