No tedy jĂĄ tenhle blog vedu. Teda spĂ­ĹĄ ve ĹĄkole je to docela nĂĄročnĂŠ. Nebo spĂ­ĹĄ si to ve ĹĄkole trochu nĂĄročnĂŠ dělĂĄm. V uplynulĂŠm tĂ˝dnu mne plně vytěžovala semestrĂĄlka na předmět 36OSY – operačnĂ­ systĂŠmy. Tato semestrĂĄlka byla zaměřenĂĄ na pouĹžitĂ­ systĂŠmovĂ˝ch volĂĄnĂ­ unixu (vlastně to jsou funkce jako kaĹždĂŠ jinĂŠ, jen v man strĂĄnkĂĄch je najdete v sekci 2 a jen nejsou větĹĄinou tak hezky user friendly, jako jejich knihovnĂ­ nadstavby). KonkrĂŠtně jsem jako zadĂĄnĂ­ dostal (nebo spĂ­ĹĄ jsem si vybral z těch co jeĹĄtě zbĂ˝valy):

Napište program talk, který umožní vzájemnou komunikaci více uživatelů v rámci jednoho počítače. Ke komunikaci mezi procesy použijte System V IPC sdílenou paměť.

Povoleno bylo jak komunikovat po znacích tak po řádcích (jako třeba u IRC). Při tom pročítání zadání jsem si vzpomněl, že komunikací mezi procesy se zabývá poměrně podrobně seriál o programovaní v časopise Linux+, kterého jsem šťastným odběratelem. No a doma jsem skutečně našel docela hezké povídání o semaforech, sdílené paměti a zasílání zpráv v Linux+ 2/2004. Napsal to dokonce nějaký student felu, dnes v šestém ročníku. Asi si vzpomněl na třeťák ;o) Součástí článku byl i listing kódu producent/konzument, což je typický příklad uváděný v souvislosti se sdílenou pamětí a semafory. Já jsem si ho u ségry na počítači naskenoval a OCR programem převedl do textu (pravdou je, že bych to asi měl rychleji přepsané).

V tom příkladě jde o to, Ĺže jeden proces do sdĂ­lenĂŠ paměti zapisuje a druhĂ˝ z nĂ­ čte – čili v prvnĂ­m přiblĂ­ĹženĂ­ přesně to, co jsem potřeboval. Mimochodem Ăşloha producent/konzument byla takĂŠ jednĂ­m ze zadĂĄnĂ­. ZĂĄkladnĂ­ myĹĄlenka řeĹĄenĂ­ je v tom, Ĺže producent naalokuje sdĂ­lenou paměť pomocĂ­ funkce shmget nějak takhle:

shm_id=shmget(key,sizeof(struct shmem),IPC_CREAT|0666)

kde první parametr je klíč pod kterým ten kus paměti bude hledat i konzument (tedy nejspíš nějaká konstanta při překladu), následuje velikost paměti která nás zajímá a poslední je bitové logické nebo přesnějšího určení chování (IPC_CREAT znamená pokud ještě nic takového naalokovaného není tak vytvoř) a přístupových práv k takové paměti. Vrácenou hodnotu shm_id hned použijeme v funkci, kterou si připojíme sdílenou paměť do svého adresového prostoru:

pshmem=(struct shmem *)shmat(shm_id,NULL,0))

A abych nezapomněl – potřebujete mĂ­t pro prĂĄci se sdĂ­lenou pamětĂ­ v hlavičce

#include <sys/ipc.h>
#include <sys/shm.h>

navíc budeme potřebovat pracovat se semafory (hned vysvětlím) a tak ještě přidáme:

#include <sys/sem.h>

Ono totiĹž se sdĂ­lenou pamětĂ­ je docela potĂ­Ĺž v tom, Ĺže nikdy nevĂ­me kdy kterĂ˝ proces se dostane k jejĂ­mu zĂĄpisu a k jejĂ­mu čtenĂ­. V dĹŻsledku toho by pak mohly vznikat nekonzistence (u příkladu konzument/producent se třeba mĹŻĹže stĂĄt, Ĺže producent, jeĹĄtě do paměti nezapsal vĹĄechno, ale konzument byl rychlejĹĄĂ­ a přečetl z paměti co tam zatĂ­m bylo). DalĹĄĂ­ moĹžnĂ˝ příklad chyb vzniklĂ˝ch pokud nebudeme nějak synchronizovat čtenĂ­ a zĂĄpis vznikne třeba tehdy, kdyĹž producent jeĹĄtě nic novĂŠho do paměti nezapsal, ale producent v tom svĂŠm čtecĂ­m cyklu uĹž znovu doĹĄel k mĂ­stu s přečtenĂ­m sdĂ­lenĂŠ paměti. Existuje vĂ­ce zpĹŻsobĹŻ jak chrĂĄnit tzv „kritickou sekci“, neboli mĂ­sto v kĂłdu kde se nějak manipuluje se sdĂ­lenou pamětĂ­. U IPC to jsou semafory, pomocĂ­ kterĂ˝ch se dĂĄ řeĹĄit větĹĄina problĂŠmĹŻ. Představa je prostě takovĂĄ, Ĺže při vstupu do kritickĂŠ sekce proces koukne na semafor a pokud tam je zelenĂĄ, tak rozsvĂ­tĂ­ červenou a vleze do vnitř. Pokud svĂ­tĂ­ červenĂĄ tak čekĂĄ na zelenou a aĹž pak rozsvĂ­tĂ­ zase červenou a vejde dovnitř. Zelenou pak nesmĂ­ zapomenout rozsvĂ­tit, kdyĹž kritickou sekci opouĹĄtĂ­.

Problém by ale mohl vzniknout i tady a to tehdy, když by se k lizu dostal druhý proces mezi tím, kdy se ten náš podíval na zelenou a tím kdy se ji pokusil nastavit. Proto jsou tyhle 2 akce operačním systémem neoddělitelné (tzv atomické).

No tak abych se vrĂĄtil k tomu popisu producenta – ten si tedy zaalokuje potřebnĂ˝ počet semaforĹŻ (tady budeme potřebovat 2) coĹž se dělĂĄ – světe div se – pomocĂ­ funkce semget, kterou volĂĄme nějak takhle:

sem_id=semget(key,2,IPC_CREAT|0666))

TentokrĂĄt je snad uĹž vĹĄechno jasnĂŠ, ne? NĂĄsleduje inicializace hodnot semaforĹŻ – to se dělĂĄ pomocĂ­ funkce semctl, kterĂĄ jako jeden argument dostane

union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
} sem_arg;

kde nás vlastně zajímá hlavně to val, kam nahrajeme na co chceme příslušní semafor inicializovat:

sem_arg.val=0;
semctl(sem_id,0,SETVAL,sem_arg)

sem_arg.val=1;
semctl(sem_id,1,SETVAL,sem_arg)

Tím máme přípravné práce za sebou a před námi je výkonný cykl. Ten začíná podíváním se na semafor, jestli na něm svítí zelená a pokud ano, tak rozsvícení červené a pokračováním. To se realizuje tak, že do struct sembuf sops; následující:

sops.sem_num=1; /* kterĂ˝ semafor */
sops.sem_op=-1; /* typ operace */
sops.sem_flg=0; /* dalĹĄĂ­ parametry operace */

semop(sem_id,&sops,1)

Jestli se díváme na zelenou, nebo naopak za sebou zelenou rozsvěcíme se řídí právě parametrem sem_op a to takhle:

if (sem_op < 0) { /* podle toho jestli svítí zelená nebo červená se zachovej */
if (semval < 0) /* svĂ­tĂ­ zelenĂĄ */
semval–; /* tak rozsviĹĽ červenou a pokračuj */
if (semval >=0) /* svítí červená */
čekej dokud nebude (semval < 0);
}
if (sem_op > 0) { /* po odchodu z kritickĂŠ sekce */
semval++; /* za sebou rozsviĹĽ zelenou */
}
if (sem_op == 0) { /* pouŞívĂĄ se jako bariĂŠra – počkat aĹž sem doběhnou vĹĄechny procesy */
čekej dokud nebude (semval == 0);
}

Samozřejmě existují nějaké další větve tohohle rozvětveného stromu (například příchod signálu proces probudí a může se vykonat jeho obsluha i když fakticky stojí na semaforu), ale tohle jsou ty nejzákladnější případy. Více viz man semop.

V tom cyklu producent vždy nejdřív koukne na semafor 1 (na začátku je nastavený na 1 tady pokračuje), pak zapíše do sdílené paměti a nakonec nastaví zelenou (tedy sem_op=1) na semaforu 0, což odblokuje konzumenta, který to přečte a až to přečte, tak nastaví zelenou na semaforu 1, na kterém do té doby čeká producent.

Docela jednoduché, co říkáte ;o) No ale pořád je to daleko lepší než se patlat s nějakým aktivním čekáním, které by ale stejně někdy náhodně nefungovalo. Tohle je zkutečně bezpečná cesta, jak programově ošetřit tu nekonzistenci dat ve sdílené paměti.

V tom časopise byl ještě navíc zdrojáček pro uvolnění sdílené paměti. Na to nezapomínejte, protože pak vám jednak nemusí sedět hodnoty semaforů a jednak velikost možné sdílené paměti je přeci jen omezená a mě se skutečně na těch sunech ve škole stalo, že ta paměť (ne tedy mojí vinou) došla. Jinak k vypsání sdílených prostředků slouží příkaz ipcs a pro uvolnění pak ipcrm {shm | msg | sem} id.

No a teď k té mojí semestrálce. Opravdu mne nenapadlo, jak to udělat jinak abych zároveň mohl čekat na vložení něčeho třeba funkcí gets() a přitom moci vypsat zprávu, která přišla od jiného uživatele, než tak, že jsem svůj proces po inicializacích rozdělil na 2, z nichž předek představoval producenta a potomek pak konzumenta. V unixu se rozdělení procesů realizuje pomocí fork, konkrétněji této konstrukce:

x = fork(); /* tady se proces rozdvojĂ­ */
if (x == 0) {
/* tady bude tělo potomka */
} else { /* protože předek v x má teď PID potomka */
/* tak tady bude tělo předka */
}

Pak ale nastane problĂŠm s tĂ­m, aby jednak na jeden zĂĄpis producenta připadlo proběhnutĂ­ vĹĄech spuĹĄtěnĂ˝ch konzumentĹŻ a přitom kaĹždĂ˝ to ale musĂ­ přečíst jen jednou. Nejprve tedy musĂ­m vědět kolikrĂĄt je mĹŻj program spuĹĄtěn – mĂ­t counter v sdĂ­lenĂŠ paměti se přímo nabĂ­zĂ­. NavĂ­c se mi ten counter hodĂ­ na to, abych při ukončovĂĄnĂ­ věděl, jestli nejsem nĂĄhodou poslednĂ­ a tedy mohu bez obav vrĂĄtit sdĂ­lenou paměť a semafory. No a pak prostě počet instancĂ­ nastavĂ­m truct do přísluĹĄnĂŠho semaforu. To mi ale neřeĹĄĂ­ problĂŠm s tĂ­m, Ĺže jeden konzument bude tak rychlĂ˝, Ĺže vypĂ­ĹĄe tu zprĂĄvu ve sdĂ­lenĂŠ paměti několikrĂĄt a tĂ­m pĂĄdem se na některĂŠ nedostane. K tomu jsem prĂĄvě vyuĹžil tu bariĂŠru. VĹĄechny procesy, kterĂŠ uĹž vypsali aktuĂĄlnĂ­ zprĂĄvu čekajĂ­ na konci cyklu do tĂŠ doby, neĹž tam dorazĂ­ vĹĄichni producenti. KonkrĂŠtně nastavĂ­m v producentovi do semaforu 3 počet instancĂ­ a pak při kaĹždĂŠm prĹŻchodu čtenĂ­m v producentovi odečtu jedničku (ale pořád semval zĹŻstĂĄvĂĄ kladnĂĄ takĹže pokračuje) a hned za tĂ­m nĂĄsleduje volĂĄnĂ­ semop s parametrem 0, tedy čekej dokud nebude 0.

Na tohle než jsem přišel tak jsem si způsobil pořádné bolení hlavy. Výsledný program, který jsem nazval stalk navíc ještě obsahuje jednoduchoučké curses uživatelské rozhraní, které mne ovšem nakonec nejspíš stálo bod. Vycházel jsem z toho, že nevím o jiném způsobu jak ošetřit situaci kdy do rozepsané zprávy přijde zpráva někoho jiného. Logicky z toho vyplývá, že potřebuju jednu část terminálu aby sloužila jako vkládací pro producenta a druhou část jako vypisovací pro konzumenta.

Velice dobrĂ˝ tutoriĂĄl (teda on popisuje ncurses, ale to je nĂĄslednĂ­k curses a při svĂ˝ch začátečnickĂ˝ch pokusech jsem vyjma makra box() nenarazil na nic co by neumělo i samostatnĂŠ curses) je NCURSES Programming HOWTO. ProblĂŠm s curses (btw anglicky to znamenĂĄ „kletby“ a musĂ­m říct Ĺže uĹž vĂ­m proč) byl hlavně v tom, Ĺže nenĂ­ stavěnĂŠ na to, aby ho zĂĄroveň pouŞívaly 2 procesy. UrčitĂ˝m řeĹĄenĂ­m by byly thready (na venek se aplikace chovĂĄ jako jeden proces a vĹĄechny podprocesy majĂ­ sdĂ­lenou celou svojĂ­ paměť) ale ty jsem pouŞít nemohl protoĹže jsem mezi volĂĄnĂ­m jĂĄdra nenaĹĄel jak je jednoduĹĄe vytvořit. JedinĂŠ jednoduchĂŠ řeĹĄenĂ­ pomocĂ­ syscallĹŻ mne napadlo pouĹžitĂ­ signĂĄlĹŻ.

To je mechanizmus asynchronní kontroly procesů. Vlastně něco jako INT v hardwaru, nebo softwarový INT v assembleru. Třeba pokud používáte nějakou aplikaci a stisknete CTRL+C tak aplikace dostane SIGINT a většinou obsluha tohoto signálu ukončí běžící aplikaci. Ale obsluha většiny signálů se dá předefinovat a dokonce ve standardu jsou 2 signály SIGUSR1 a SIGUSR2 které jsou přímo určeny k uživatelskému předefinování jejich obsluh.

Na věci okolo signĂĄlĹŻ jsem se ptal na cvičenĂ­ z předmětu unix (přednĂĄĹĄku kde se braly signĂĄly najdete tady) a ten cvičícĂ­ mi doporučil aĹĽ si stĂĄhnu knihu Advanced Linux Programming. Mimochodem vyĹĄla i v čeĹĄtině, ale ta se uĹž pokud vĂ­m stĂĄhnout z webu nedĂĄ 🙁

Aby se při přijetí signálu pustila vaše obsluha zajistíte takhle:

signal(SIGUSR1,printkonz);

kde ten druhý argument je jmÊno funkce s takovýmhle prototypem:

void printkonz(int cislo_signalu)

To číslo signálu se sakra hodí v případě, že používáte stejnou obsluhu pro více přerušení (nicméně i když nepoužíváte, tak tam být musí aby funkce signal poznala že je to ta obsluha). Ono totiž po prvním příchodu signálu se ta vaše obsluha zase odhlásí a musíte jí znovu přihlásit pokud chcete aby se při příštím přijetí signálu znovu pustila tahle obsluha.

No a to je vlastně všechno co jsem si chtěl zaznamenat. Snad se v tom vyznám, až to někdy budu zase potřebovat a snad se to bude hodit někdy i někomu jinému.

Tags

Napsat komentář