C pod Unixom - tutorial

Obsah:
1 C pod UNIX: C je stále iba C
2 Parametre príkazového riadku a ich spracovanie
3 Všetci sme hriešni... errno, perror, exit, stderr, ...
4 Nech sa páči, vidlička: fork (wait, exit, getpid)
5 Spúšťanie externých programov: exec()
6 Súbory: streamy, low level
7 Rúry - pipe(), dup()
8 Prehľadávanie adresárov
9 Prihlásení používatelia
10 Záver

Download



---------------------------------------- ----------------------------------------


1 C pod UNIX: C je stále iba C


1.1 Úvod
C je stále iba C, ako jazyk má isté normy (ANSI, POSIX, ...), ktoré platia multiplatformne, či robíte v C pod MS-DOS, Windows, Linux alebo Unix. Samozrejme, nie úplne všetko, na čo ste zvyknutí, je rovnaké. Základná množina zostáva, ale rozdiely sú vždy. Aj to je jeden z cieľov predmetu Operačné Systémy - naučiť vás rozdiely vnímať a poznať ich pôvod.
Na zvládnutie tohoto predmetu však potrebujete spĺňať isté predpoklady - už musíte čosi vedieť, aby ste na tom mohli stavať ďalej.
Jazyk C máte ešte v čerstvej pamäti, takže vo svete deklarácií, funkcií, polí a štruktúr budete ako doma. Aby sa vám dobre pracovalo, musíte poznať C-čko aspoň na 'slušnej' úrovni. Aby ste nemali problémy so základmi, je dobré mať poruke aj nejakú referenčnú príručku jazyka C.
Inou kapitolou je práca v OS UNIX samotnom - veľa princípov vám je známych z iných OS, potrebný zvyšok získate na prednáškach a cvičeniach. Bez znalosti princípov a postupov špeciálnych pre OS v ňom nemôžete programovať, to je predsa jasné...

Spôsob programovania v C pod UNIXom je klasický, čo sa týka životného cyklu programu. Zabudnime teraz na časť návrhu a povedzme, že vieme aký program spraviť:

PROGRAM 1.1 hello.c

#include <stdio.h>
main()
{
printf("Hello World !\n");
}

Tento asi poznáte :-) Nuž, každý program treba napísať, skompilovať, (prípadne odladiť) a potom používať. Možno ste z iných implementácií C zvyknutí na užívateľské rozhranie - Borland C, Visual C, atď. Tieto prostredia zabezpečujú pomoc pri editácii zdrojového kódu, manažment súborov so zdrojovými textami, preklad kódu, spustenie programu, ladenie. Štandartne sa však k Unixu nedodáva žiadne komplexné prostredie. Nám ho ani nebude treba, každý náš program bude v jedinom krátkom súbore, ktorý sa dá normálne editovať; preklad a spustenie sú otázkou dvoch príkazov. (Téma ladenie príde neskôr.) Navyše sa učíme pracovať s operačným systémom, preto budeme vyrábať programy "nízkej úrovne", čo sa týka ich rozhrania aj ich určenia. Rozhranie na používateľa bude mať program pomocou argumentov a štandartných vstupov/výstupov. A hlavný dôvod, prečo budeme programy robiť, je naučiť sa systémové rozhranie operačného systému UNIX, takže sa sústredíme na jeho služby (prepáčte, žiadne program! ovanie hier :-).




1.2 Preklad programu
Nasledujúci riadok preloží program v súbore hello.c a vytvorí spustiteľný súbor "hello":
>cc hello.c -o hello
ktorý stačí už len spustiť:
>./hello
Hello World !
Prepínač -o znamená "output" a meno za ním uvedené je meno výstupného spustiteľného programu.
(Nezabúdajte, že všetko sa deje na príkazovom riadku shellu vo vašom domovskom adresári.)

"cc" je skratka z "C compiler" a je to štandartný kompiler pod Unix. Existujú aj iné nástroje, napríklad GNU C compiler - gcc. Môžete používať ktorý chcete, jedinou podmienkou je, že vaše zadania musia byť preložiteľné na stroji osa.dcs.elf.stuba.sk - taktiež spôsob prekladu musíte uviesť ako komentár do zdrojového textu.

Prekladač má samozrejme množstvo iných prepínačov, ale asi jediný, ktorý ešte budete potrebovať, je -g, ktorý zabezpečí pridanie debuggovacích informácií ku programu.




1.3 Knižnice, manuálové stránky
Ako ste si asi všimli, aj v našom jednoduchom programe bolo nutné, aby sme funkciu printf() zalúčili do programu pomocou použitia knižnice <stdio.h>. Aj v implementácii C pod Unixom používame štandartné ANSI knižnice. Pre nás to znamená, že môžeme používať funkcie z dobre známych knižníc, napríklad:
<stdio.h>
<string.h>
ale budeme používať aj zatiaľ pre vás asi neznáme
<sys/dir.h>
<sys/wait.h>
a zopár iných.
Celý čas budeme pracovať s malou skupinou funkcií a knižníc, takže sa nemusíte báť, že by vás ich množstvo preťažilo. Niekedy vzniknú problémy s tým, že funkcia je implementovaná v dvoch knižniciach - ktorú teraz používať ? Alebo, viete síce čo funkcia robí, ale to vám iba povedal kamarát a ono to ozaj funguje, ale iba niekedy... Knižnice sú tu na to, aby ste ich používali, ale používali správne. Nestačí sa iba domnievať, čo funkcia robí. V Unixe existuje systém manuálových stránok, kde sú funkcie dopodrobna opísané, tiež sú spomenuté rozdiely v jednotlivých knižniciach. Tento výukový text vám tiež ponúkne vysvetlenie ku dôležitým funkciám, ale jedine manuály a vlastná skúsenosť vás naučia "krotiť hadov". (Funkcie, ktoré nemáte nacvičené, sa presne tak správajú - dlho nič a zrazu ham...)

Manuálové stránky asi už poznáte, jedine treba poznamenať, že oproti C-shellu sa zvyčajne opisy súvisace s C nachádzajú vo vyšších kapitolách. Niektoré mená sú také isté ako mená príkazov v C-shelli alebo externých programov, takže treba zadať aj číslo kapitoly, napr.:

>man 3 exec
>man 2 wait




1.4 Linux a FreeBSD
Študenti si po minulé roky zvykli pracovať doma na systéme LINUX a potom zadania nosiť do školy. Nezabúdajte, že vaše zadanie musí správne bežať pod FreeBSD, preto si 'Linuxáci' musia byť vedomí všetkých rozdielov (čo v podstate znamená prácu navyše, aj keď sa pritom niečo naučíte zároveň o Linuxe aj FreeBSD). Spomenuté sa týka používaných prekladačov, odchýliek v službách operačného systému, inkludovaných knižníc.
V súčasnej dobe nie je problém zohnať si CD s FreeBSD, ktoré sa dá nainštalovať rovnako pohodlne ako Linux. FreeBSD takisto 'pozná' XWINDOWS, KDE, GNOME a tisícky balíkov s utilitami, takže ani z tejto stránky nebudete ukrátení (iba o cca 700 MB diskového priestoru).



1.5 Ladenie v krátkosti
V Unixe máte k dispozícii debugger zvaný gdb (pamätajte si to ako Geniálny DeBugger). Je to tzv. riadkový nástroj, po jeho spustení máte k dispozícii príkazový riadok programu gdb a veľký počet príkazov. Využijete asi málo z nich, napríklad beh programu až po určité miesto v zdrojovom kóde, alebo príkaz "next", ktorým sa hýbe po riadkoch programu, alebo "print", ktorým si vypisujete hodnoty premenných. Gdb má dobrý help, takže by ste sa stratiť nemali.




2 Parametre príkazového riadku a ich spracovanie
Poďme si upraviť náš malý program hello.c takto:
PROGRAM 2.1 hello2.c

#include <stdio.h>
main(int argc, char *argv[]) // dalo by sa aj (...,char **argv)
{
printf("Hello %s !\n",argv[1]);
}

a poďme skúsiť, čo to urobí:

>cc hello2.c -o hello2
>./hello2 Baltazar
Hello Baltazar !

Takže sme spracovali prvý argument programu. Čo sa však stane, ak argument neuvedieme a náš program ho neuvážene použije ?

>./hello2
Hello (null) !

Nič strašné sa nestalo, ale aj tak je to chyba: nezadali sme predsa meno 'null' a napriek tomu sa vypísalo. O tom, že argv[1] je nulový pointer sa ani nezmieňujem.

Takže počet argumentov treba kontrolovať (napr. pomocou argc). Ak bude mať program viac parametrov - rôznych prepínačov, číselných hodnôt aj reťazcov, tak sa klasické použitie argv a argc natiahne na dlhý a málo prehľadný kód (ak neviete ozaj "pekne" programovať).

Máme však aj elegantnejšie riešenie: Spracovanie parametrov je úlohou takmer každého programu, a preto bola spravená funkcia getopt. Aj keď vás spočiatku bude vábiť použitie argc a argv (to je priamočiarejšie, všakže), odporúčam čo najskôr si zvyknúť na mechanizmus getopt. Dokonca na teste vám bude predložený program obsahujúci funkciu main() a kostru spracovania parametrov pomocou getopt, takže je výhodné (ale nie povinné) sa toto naučiť.
Poďme si ukázať klasický spôsob práce s getopt:

PROGRAM 2.2 getopt1.c

#include <stdio.h>
#include <unistd.h>

main(int argc, char *argv[])
{
int o;
opterr = 0;
while ((o = getopt(argc, argv, "hp:")) != -1)
switch (o)
{
case 'h':
printf("Tu bude help\n");
break;

// prepinac s jednym parametrom
case 'p':
printf("Output: '%s': parameter prepinaca -p\n", optarg);
break;

default:
printf("Error: '%c': nespravny prepinac", optopt);
printf(" alebo zly argument prepinaca\n");
exit(1);
} // koniec: switch aj while

argc -= optind;
argv += optind;

// samotny program ...
}

Podľa konvencie musia byť na príkazovom riadku uvedené najprv prepínače a až potom "neprepínačové" argumenty, ako napríklad mená súborov na spracovanie. Getopt vám pomôže oddeliť možné prepínače od tých, ktoré program neobsahuje; tiež pomôže získať parametre jednotlivých prepínačov.
Funkcia skrýva v sebe porovnania a testy, podľa nich nastaví obsah premenných opterr, optarg, optopt, optind.
Funkcii getopt odovzdáme počet argumentov a pointer na pole argumentov, tiež reťazec, v ktorom opíšeme, ako má vyzerať náš formát parametrov: ktoré sú povolené, ktoré majú argumenty a podobne.
Názorne si ukážme, ako sa program bude správať:
>./getopt1 -h
Tu bude help

>./getopt1 -w
Error: 'w': nespravny prepinac

>./getopt1 -p 100
Output: '100': parameter prepinaca -p

Po tomto školení by nemal pre vás byť problém pridať spracovanie ďalších parametrov. Samozrejme, pozrite si aj manuál ku getopt a najmä opis formátu našich prepínačov v stringu odovzdávanom getopt.




3 Všetci sme hriešni... errno, perror, exit, stderr, ...
Čírou náhodou nie je nadpisom myslené, že akurát vo vašom programe budú chyby. Ide o to, že chyby sú prirodzenou súčasťou života programu (aj programátora). Chyby môžu nastať nesprávnym použitím programu, ale aj vašim nesprávnym použitím knižničnej funkcie. Navyše každý užívateľ je obmedzovaný, napríklad môže mať naraz spustený iba určitý počet procesov. Chyby potom vznikajú, keď sa takéto hranice prekročia.
S niektorými chybami sa budete musieť vysporiadať, s niektorými nie. Všeobecne sa dá povedať, že dôležité veci treba kontrolovať, teda ak používate výstup z nejakej funkcie v ďalšej práci, je treba si aj zistiť, či funkcia tento výstup poskytla, alebo skončila s chybou.
(Príklad - testovanie, či sme ozaj otvorili súbor - ak nie, nemá zmysel ďalej s ním pracovať).
A nezabúdajte, že vaše programy budú na vznik chyby testované :-)
Postupne si budeme ukazovať, ktoré chyby sú závažné a ako ich ošetrovať. Najskôr si však povieme niečo o spracovaní chýb všeobecne.



3.1 errno
Takmer každá funkcia má nejaký spôsob oznámenia, že pri jej použití nastala chyba. Jednoducho sa to robí tak, že funkcia vracia hodnotu bool, int alebo pointer nejakého typu, ktorá indikuje chybu: zvyčajne, ak sa vráti true (0, nenulový pointer), tak funkcia prebehla správne. V prípade vzniku chyby však vieme iba, že chyba nastala, nevieme jej príčinu.
Preto funkcie nastavujú hodnotu globálnej premennej errno, ktorá obsahuje číslo chyby. Z manuálových stránok sa dozviete, aké chybové kódy funkcia nastavuje .
Treba ešte trošku objasniť konvenciu nastavovania errno - errno sa nastaví, iba ak chyba nastala, ak nenastala, tak sa s errno neurobí NIČ. To značí, že treba najprv kontrolovať návratovú hodnotu funkcie, ak funkcia zlyhá, tak až potom pracovať s errno.
Pozor, nie všetky funkcie sa správajú slušne, čo sa týka oznamovania chýb. Zase je dobré nakuknúť do manuálu, či funkcia nastavuje errno.




3.2 perror, stderr, exit
Ak chyba nastala, tak sa s ňou treba nejako vysporiadať, v našich jednoduchých programoch ju budeme zvyčajne len oznamovať. Samozrejme, budeme sa sťažovať na tom správnom úrade: Štandartný chybový výstup - jeho skratka však nie je ŠCHV, ale stderr.
Je viacero spôsobov, ako poslať výpis na stderr:
perror("chyba na riadku 150");.
Podľa premennej errno sa ku nášmu reťazcu pripojí opis chyby (oddelený " :") a toto všetko sa vypíše na stderr.

fprintf(stderr, "chyba %d\n",errno);.
V kapitole o súboroch a streamoch si povieme viac o tom, co je toto zac - zatial staci vediet,ze okrem prvého parametra sa ku funkcii správame ako ku printf - teda máme formátovací reťazec a zoznam vypisovaných parametrov. Výpis ide tiež na stderr.

Ak je chyba závažná, nezostáva nám nič iné, ako program ukončiť. Funkcia exit(cislo_chyby); spôsobí ukončenie procesu (teda vášho programu). Proces vráti chybovú hodnotu, ktorú ste zadali ako číslo chyby.





4 Nech sa páči, vidlička: fork (wait, exit, getpid)
Termín "proces" by sa dal jednoducho vysvetliť ako "spustená inštancia programu". Samozrejme, vysvetlenie je zložitejšie, ale to nie je predmetom tohoto textu. Budem predpokladať, že proces a s ním zviazané záležitosti sú vám známe.
Ukážeme si volania, ktoré súvisia s prácou s procesmi.

4.1 fork()
V Unixe každý nový proces vzniká delením - iba ten úplne prvý to mal zložitejšie. fork() je systémové volanie (ktoré robí proces napr. A), je to žiadosť operačnému systému, aby vytvoril nový proces (napr. proces B), ktorý bude kópiou pôvodného (teda procesu A). Proces B sa zvykne nazývať "detský proces", "dieťa", "dcérsky proces". Anglicky: child process.
Znamená to, že nový proces dostane do vienka od pôvodného napríklad obsah všetkých premenných, ale nie ony samotné - nový proces má už "svoje vlastné", nezávislé. Až na pár výnimiek, ktoré spomenieme, sú to dve kópie, dva nezávislé procesy.

V programe to vyzerá troška nezvyčajne: zvykne sa hovoriť, že fork() sa vráti dva krát: raz v rodičovskom procese, vtedy funkcia fork() vráti PID dieťaťa, druhý krát sa fork() vráti v detskom procese, kde vráti 0. Defacto detský proces "začína" až po volaní fork(), avšak majte na pamäti, že dieťa dedí CELÝ program od rodiča, aj tú časť, ktorá predchádzala fork-u. Je dôležité si to uvedomiť, napríklad keď budete písať nejaké rekurzívne programy.

Samozrejme, také potrebné volanie ako fork() sa správa slušne a vracia -1 v prípade chyby (a tiež nastaví errno). Ukážme si, ako to vyzerá v programe:


PROGRAM 4.1 fork.c

main()
{
int child_pid;

printf("Zaciatok\n");

child_pid = fork();
switch(child_pid)
{
case 0:
printf("Decko\n");
exit(0);
case -1:
perror("fork");
exit(-1);
default:
printf("Ocko: deckov PID=%d\n",child_pid);
}
}

Rodič vypíše cca:"Zaciatok" a potom "Ocko ...", dieťa len "Decko".
Ak teraz nechápete tento princíp, odporúčam si to odskúšať, pre ďalšiu prácu je veľmi dôležité vedieť, ako fork pracuje.




4.2 getpid()
Samotné volanie fork() by nám ale na zmysluplnú prácu nestačilo. Máme ešte napríklad funkciu getpid(), ktorá nám vráti PID aktuálneho procesu a getppid() - vráti PID rodiča.
4.3 wait(), exit()
Končiaci proces zvyčajne vráti svojmu rodičovi tzv. exit-status, v ktorom oznámi, či jeho činnosť prebehla úspešne. Toto urobí pomocou volania exit(). Rodič však obdrží trocha viac informácií, napríklad či dieťa skončilo samo, alebo násilným prerušením a podobne. (Bližšie man 2 wait, hlavne poslená časť o makrách.).
No a to som vlastne prezradil volanie, ktorým si rodič od dieťaťa preberie jeho status:
int wait(int * status)
Toto volanie pozdrží vykonávanie rodičovského procesu, až kým dieťa neskončí a neodovzdá status. Ak dieťa zabudne odovzdať status explicitne pomocou exit(), potom sa status odovzdá implicitnym zavolanim exit() po navrate z funkcie main().
Foriem volania wait je viac, proces môže čakať na jedno dieťa, ale aj na konkrétne dieťa z viacerých. Na začiatok však postačí, keď si pocvičíte wait. Skúste nasimulovať aj ZOMBIE správanie - k tomu vám poslúži fukcia sleep(int n), ktorá pozastaví vykonávanie procesu na n sekúnd.





5 Spúšťanie externých programov: exec()
Stáva sa, že chcete z vášho programu zavolať iný program, aby pre vás vykonal nejaké zložitejšie služby. Existuje volanie system(), ktoré máte na predmete OS zakázané: má to dobrý dôvod, učíme sa programovať systémové volania iba istej úrovne. Budeme si musieť pomôcť inak.
Volanie exec() by sa vlastne ani exec volať nemuselo: skôr sedí 'nahradenie'. Volanie totiž naplní celý obraz procesu novým procesom, ktorý vytvorí spustením daného programu. Jednoduchšie povedané, starý program prestáva po volaní exec existovať a miesto neho sa spustí nový program, ktorý ste zadali ako parameter funkcie exec.

No a teraz prichádza ťažšia časť: funkcia exec má viac foriem, ani v jednej to nejde tak jednoducho: spusť program xyz. Ukážme si to:

Program 5.1 execl.c

main()
{
execlp("ls","ls" ,0); // treba aj cestu
printf("sem nikdy neprideme...\n");
}

Vypíše sa obsah adresára, ale printf nikdy nevypíše svoj pesimistický reťazec.
Execl znamená EXEC & LEAVE, teda spusti program a ukonči - teda spustí sa ls a náš program sa nedokončí. Máme aj viac foriem ako execl, túto si vysvetlíme a ostatné sú veľmi podobné.
Prečo sme museli zadať ls dva razy ? Prečo tá nula na konci ? Lebo kompletný popis funkcie je:
int execl(const char * path, const char * arg, ...)
path: pointer na reťazec obsahujúci meno príkazu, alebo jeho plnú cestu
arg,...: čiarkami oddelené pointre na reťazce obsahujúce argumenty príkazu, avšak prvý z nich musí byť meno programu - podľa manuálu "aspoň jeho posledná časť", teda názov samotného súboru s programom. Toto je však iba konvencia. Navyše je tu podmienka, že posledný argument musí byť NULL, aby sa vedelo, kedy končí zoznam argumentov.
Je to jednoduché, stačí si pozrieť príklad:
execlp("ls","ls" ,"-al",NULL);
ktorý samozrejme vykoná "ls -al".

Funkcie typu exec sa nikdy nevrátia, pokiaľ sa podarilo program spustiť. Ak sa vrátia, návratová hodnota je -1 a je nastavené errno.

Ostatné volania typu exec sú podobné, napríklad execlp() hľadá dotyčný program aj v "path" - v ceste pre spustiteľné súbory.

Poznámka na koniec: nie všetko z pôvodného programu je volaním exec "stratené a prepísané", viac o tom je v kapitole o súboroch. (Zostávajú napríklad deskriptory otvorených súborov, PID, ...)





6 Súbory: streamy, low level
So súbormi sa dá pracovať dvoma spôsobmi:
Nízkoúrovňovo - neformátovaný I/O, nebufferovaný
Pomocou streamov (prúdov) - bufferovaný a formátovaný I/O


6.1 Nízkoúrovňový prístup
Ako už bolo spomenuté, tento prístup je nebufferovaný, teda každý zápis do súboru je priamy (ak neuvažujeme cache, ale to je iná kapitola). Prístup je tiež neformátovaný, proste zapisujeme bajty.
Ku súboru pristupujeme pomocou tzv. deskriptorov. Deskriptor je malé nezáporné číslo, označujúce "handle" súboru - pri otvorení súboru obdržíme deskriptor, ktorý označuje náš súbor, ďalej potom pracujeme označujeme náš súbor len týmto číslom.

Máme tu kompletnú množinu príkazov na prácu so súborom, tento môžme vytvoriť, otvoriť, čítať z neho, zapisovať do neho, zatvoriť ho. Zmazanie súboru si nechajme až na kapitolu o adresároch.


6.1.1 open()
int open(char *filename, int flag, int perms)
Volanie open otvorí súbor s daným menom (plná cesta) a vráti jeho deskriptor, alebo vráti -1, ak sa súbor nepodarilo otvoriť (v tom prípade nastaví errno).

flag je označenie spôsobu, ako sa má súbor otvoriť. Spôsoby sa dajú pomocou OR-u kombinovať, spomeniem len niektoré:
O_CREAT - vytvor súbor
O_APPEND - otvor súbor na zápis, bude sa zapisovať na koniec súboru
O_RDONLY - otvor súbor iba na čítanie
O_RDWR - otvor na čítanie aj zápis

perms sú prístupové práva súboru, tvorené rovnako ako tie, na ktoré ste zvyknutí zo C-shellu (napr. osmičkové číslo 0700 apod.). Používajú sa iba pri O_CREAT.

S každým súborom je zviazaná "aktuálna pozícia" - budem písať len o pozícii. Keď sa súbor vytvorí, pozícia je na začiatku, po zápisoch (čítaniach) sa posúvame ku koncu súboru, pozícia sa zväčšuje. Viac o pozícii pri funkcii lseek().

Bližšie, samozrejme, man 2 open, man 2 chmod, man 2 umask




6.1.2 creat(), close()
Dve nasledovné funkcie sú jednoduché: creat() vytvorí súbor (parametre má podobné ako open) a close() zatvorí súbor s daným deskriptorom (s daným handle). Ak sa súbor nepodarilo vytvoriť (zatvoriť), vráti sa -1.
int creat(char *filename, int perms)
int close(int handle)




6.1.3 read(), write(), lseek()
int read(int handle, char *buffer, unsigned length)
int write(int handle, char *buffer, unsigned length)

Funkcia write zapisuje do súboru na aktuálnu pozíciu. Zapisuje bajty z buffera, na ktorý odovzdáme pointer, a dĺžka dát je "length" bajtov.
Podobne pracuje read - načíta do buffera "length" bajtov. Funkcie samozrejme posunú aktuálnu pozíciu v súbore.
Obe funkcie vracajú počet zapísaných/načítaných bajtov, alebo vrátia -1 v prípade chyby.

int lseek(int filedesc, int offset, int whence)
lseek() mení aktuálnu pozíciu v súbore, ktorý je identifikovaný svojim deskriptorom filedesc. Parameter whence určuje, ako sa má parameter offset interpretovať:

whence = SEEK_SET - pozícia sa nastaví na hodnotu offset
whence = SEEK_CUR - pozícia sa nastaví na hodnotu: aktuálna pozícia + offset
whence = SEEK_END - pozícia sa nastaví na hodnotu: koniec súboru + offset
pričom koniec súboru znamená jeho aktuálnu veľkosť.

lseek vráti novú pozíciu ako počet bajtov od začiatku súboru, alebo -1 (aj errno) pri chybe.

Je možné nastaviť pozíciu v súbore aj za jeho koniec - normálne sa tam dá zapisovať, veľkosť súboru sa zodpovedajúco zväčší. Ak by ste potom čítali z 'diery', kam vlastne žiadne data neboli zapísané, načítanie bude úspešné a buffer bude naplnený nulami.




6.1.4 A čo na to fork a exec ?
Dieťa pri volaní fork() zdedí všetky deskriptory súborov od rodiča. Ak pred forkom bol súbor otvorený, po fork-u bude mať ku nemu prístup rodič aj dieťa. Prejaví sa to ale tak, že KAŽDÝ z nich môže posúvať aktuálnu pozíciu, ak teda zapisuje jeden aj druhý do toho istého súboru, výstup bude "mixovaný".
Toto však používať nebudeme, budeme používať len dedenie deskriptorov.
Správanie exec-u je podobné: deskriptory otvorené vo volajúcom procese zostavajú otvorené aj v novom procese, okrem tých, ktore majú nastavený flag 'close on exec'.




6.2 Streamy
Streamy (prúdy) znamenajú bufferovaný a formátovaný prístup ku súboru. Používajú sa ale širšie: aj pri zariadeniach alebo pri rúrach.
Výhody bufferovania streamov sú jasné, nevýhodou je, že zápis nie je okamžitý, čo nám môže robiť problémy, ale aj s tým sa vysporiadame.
Formátovaním vstupu/výstupu si dosť uľahčíme prácu, všetko bude zobrazené tak, ako chceme.
Aj tu existuje podobná množina funkcií ako v predošlej kapitole:
otvorenie súboru - fopen
výpis do súboru - fprintf,fputs,fputc
vstup zo súboru - fscanf,fgets,fgetc
ošetrenie chýb - feof
vynútený zápis buffera - fflush zatvorenie súboru - fclose

Spôsob formátovania je ten istý, ako pri funkcii printf.
So súbormi pracujeme podobne, ako v predošlej kapitole - ibaže teraz nemáme jeden int, ale máme ukazovateľ na typ FILE:

FILE * subor;

Otvorenie súboru:
FILE * fopen(const char * path, const char * mode)

požaduje, aby sme zadali plnú cestu k súboru a mód otvorenia: je to reťazec, v ktorom by na začiatku malo byť jedno z nasledovných:
"r" - otvor súbor len na čítanie, pozícia sa nastaví na začiatok
"r+" - otvor na čítanie aj zápis, pozícia sa nastaví na začiatok
"w" - budeme zapisovať, vytvor nový súbor (al. prepíš starý), pozícia sa nastaví na začiatok
"w+" - ako "w", ale máme aj zápis
"a" - pridávanie do súboru, iba zápis, pozícia sa nastaví na koniec súboru, všetky zápisy idú na koniec súboru bez ohľadu na nastavenie pozície funkciou fseek()
"a+" - ako "a", ale máme aj čítanie

a potom môžu nasledovať ďalšie upresnenia módu, bližšie man fopen.
Ak sa súbor nepodarilo otvoriť, fopen vráti NULL a nastaví errno.

Práca so streamom teda prebehne nasledovne:


PROGRAM 6.1 streams.c

main()
{
FILE * subor;

subor = fopen("/home/janko/prvy.txt" ,"w");
if (subor == NULL)
{
perror("fopen: /home/janko/prvy.txt");
exit(1);
}

fprintf(subor, "Toto je obsah suboru, dame sem nejake ciselko %d\n", 543);
fclose(subor);
}

Máme ešte jednu zaujímavú funkciu, ktorá prekonvertuje existujúci deskriptor na stream:

FILE * fdopen(int filedesc, const char * mode)

Mód streamu musí byť kompatibilný s módom deskriptora. Keď zatvoríme stream pomocou fclose, filedesc bude tiež zatvorený.

A nakoniec ku bufferovaniu streamov: výpis sa na svoje miesto určenie dostane, až keď sa v prúde vyskytne znak '\n', alebo sa zavolá funkcia fflush() (alevo fclose()). Treba si to hlavne uvedomiť, keď budete mať viac procesov a výstupy sa vám budú mixovať 'nesprávne' - ak nebudete vynucovať zápisy, poradie výstupov môže byť nesprávne kvôli buffrom. Používajte '\n'.


stdin, stdout, stderr
Každý program má hneď od začiatku k dispozícii tri pointre na streamy, ktoré nemusí otvárať:
FILE * stdin
FILE * stdout
FILE * stderr

Netreba dúfam vysvetľovať, čo ktorý znamená.





7 Rúry - pipe(), dup()
Dostávame sa k ďalšej téme, ktorou je komunikácia dvoch procesov. Existuje viacero spôsobov, ako si dva procesy môžu vymieňať údaje, jedným z nich je tzv. rúra - pipe.
Rúrou sa nazýva preto, lebo údaje ňou "tečú" - čo sa na jeden "koniec" rúry zapíše, to sa z druhého konca dá prečítať. Už asi tušíte, že jeden koniec rúry bude mať pridelený jeden proces a druhý koniec druhý proces.



7.1 pipe()
Systémová služba pipe() vytvára I/O mechanizmus nazývaný dátovod (pipe).
#include <limits.h> /*Definicia PIPE_MAX*/
pipe(int filedes[2])

Oba deskriptory vrátené touto službou môžu byť použité vo funkciách 'read','write'. Oba deskriptory sú tie najmenšie voľné z tabuľky pre daný proces. Ak je do dátovodu zapisované pomocou deskriptoru filedes[1], až PIPE_MAX bytov je bafrovaných, kým je zapisovací proces pozdržaný.
Dáta sa z dátovodu prečítajú pomocou 'read' s deskriptorom filedes[0]. Použitie [0] na čítanie a [1] na zápis je konvencia.

Predpokladá sa, že potom, ako bol dátovod vytvorený, dva (alebo viac) spolupracujúcich procesov (vytvorených následným volaním služby fork) si vymieňajú dáta cez dátovod funkciami 'read' a 'write'.

Ak vytvorenie dátovodu bolo úspešné, pipe() vráti 0, inak vráti -1 a nastaví errno.

Koncept vymieňania si údajov je jasný, poďme si objasniť, ako sa rúry používajú v programoch.
Ak si pamätáte, písal som, že po fork-u zostávajú otvorené deskriptory rodičovi, aj dieťaťu. Ak máme najprv pipe(filedesc[2]) a potom fork(), obaja budú môcť používať oba deskriptory. Vezmime si príklad, keď rodič posiela dáta dieťaťu: Podľa konvencie toku dát filedesc[1] --> filedesc[0] nebude treba tomu, kto zapisuje (teraz rodič) filedesc[0], čítajúcemu (teraz dieťa) zase filedesc[1]. Tie zavrieme.
Ak teraz rodič spraví write(fildes[1],...), dieťa môže pomocou read(filedesc[0],...) dáta načítať. Ak dieťa vykoná takýto read a rodič ešte nič nezapísal, dieťa je blokované, až kým mu niečo od rodiča nepríde.
O konci zapisovaných dát rozhoduje rodič - keď už nemá nič na zápis, zatvorí zapisovací deskriptor. Dieťa následne na to obdrží informáciu, že nastal 'koniec súboru'.

Nasledujúci program zhŕňa, čo sme si doteraz povedali - ide o jednoduchú komunikáciu rodič-dieťa. Po spustení zadajte reťazec ukončený ENTERom, koniec zápisu je pomocou CRTL-D.


PROGRAM 7.1 pipe.c
#include <limits.h>
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

main(int argc, char * argv[])
{
int fildes[2];
int c;

if (-1==pipe(fildes)) // vytvorme si ruru
{
printf("Can't create interprocess channel!\n");
return 2;
}

switch (fork())
{
case -1:
printf("Can't create child process!\n");
return 1;

case 0:
close(fildes[1]); // som dieta a tento koniec rury mi netreba
printf("Child: I am here!\n");
while (1)
{
if (!read(fildes[0],&c,1)) // po znakoch citam z rury, co mi rodic posiela
{
printf("Child: All data have been read.\n");
close(fildes[0]); // pekne po sebe zavrieme
return 0;
}
printf("Child: Received character '%c'.\n",c); // vypis co ti doslo
}

default:
close(fildes[0]); // som rodic a tento koniec rury mi netreba
printf("Parent: I am here!\n");
while (1)
{
if (!read(0,&c,1)) // uzivatel zada znak zo stdin (stdin=0)
{ // ak je EOF (CTRL-D), koncime
close(fildes[1]); // pekne po sebe zavrieme
// tym sme dali vediet decku, ze koncime s rurou
printf("Parent: All data have been sent.\n");
wait(0); // pockaj na skoncenie dietata
return 0;
}
printf("Parent: Writing character '%c'.\n",c);
write(fildes[1],&c,1); // zapis do rury (posli decku)
}
}
}
Ak ste pochopili predošlú časť, môžme postúpiť ďalej, ale najskôr si potrebujeme vysvetliť volanie dup().



7.2 dup()
newd = dup(oldd)
int newd, olddd;
alebo druhá forma:

dup2(oldd, newd)
int oldd, newd;

Systémová služba 'dup' zduplikuje existujúci deskriptor. Argument 'oldd' je malý, nezáporný celočíslený index do tabuľky deskriptorov patriacej danému procesu. Nový deskriptor, 'newdd', vrátený volaním tejto služby,je najmenší voľný deskriptor.

Objekt referencovaný deskriptorom sa správa rovnako pri použití deskriptora 'oldd' aj 'newd'. Takže, ak 'newd' a 'old' sú duplikovanou referenciou na otvorený súbor, funkcie 'read','write' a 'lseek' modifikujú spoločný ukazateľ do súboru. Ak sa vyžadujú dva rozdielne ukazatele do toho istého súboru, druhá referencia na súbor musí byť opäť získaná pomocou volania 'open'.

Druhá forma systémového volania, 'dup2', špecifikuje hodnotu 'newd' ako svoj druhý argument. Ak je deskriptor 'newd' už použitý, je najprv dealokovaný, akoby sa naň zavolala služba 'close'.

Ak sa počas volania vyskytla nejaká chyba, je vrátená hodnota -1 a nastavená errno.




7.3 pipe + dup + exec
Nie vždy je to také jednoduché, že máme 'svojho' rodiča aj dieťa. Stáva sa, že chceme použiť iný program, ako náš, napríklad grep alebo find a jeho výstup potrebujeme nejako spracovať. Teraz si ukážeme, na akom princípe si pomocou pipe môžme odchytiť výstup programu (alebo programu ako štandartný vstup ponúknuť výstup nášho programu).
Vieme, že dup() použije ako nový deskriptor najmenšie číslo v tabuľke deskriptorov. Vieme, že štandartný vstup má číslo 0, štandartný výstup má 1. Ak urobíme rúru (pipe(pipedesc)), zatvoríme štandartný výstup (close(1)), a potom spravíme dup(pipedesc[1]) - teda na zapisovací koniec, dostaneme stav, kedy všetky zápisy na stdout pôjdu rovno do rúry. Toto by sme mohli nejako využiť aj v našom programe (zatiaľ nepadlo ani slovo o exec), ale u nás sa deje to, čo chceme my - a my si priamo môžeme povedať, kam chceme zapisovať. Ale 'vonkajší' program zapisuje na SVOJ stdout a my mu nemôžme prikázať nič iné. Tak ho oklameme :-). Ak si pamätáte, tak pri exec-u dedí program všetky deskriptory - zdedí teda aj takýto stav. Tak si proces fork-neme, po forku v dieťati nastavíme, aby štandartný výstup išiel do zapisovacieho konca rúry a spravíme exec na želaný program. Program bude na zápis (akože na stdout-teda on si to myslí) používať deskriptor 1, ktorý je ale zduplikovaný do rúry, nuž a tak pošle cez rúru výstup nám.

Obdobný postup sa použije pre presmerovanie vstupu, teda keď nejaký externý program číta vstup iba zo svojho štandartného vstupu a my mu chceme nanútiť ten svoj.

Ak vás náhodou napadlo, "že prečo to neurobiť cez '|' ako v C-shelli" - nebuďťe na omyle, že sa dá niečo ako
execlp("ls","ls" ,"|","wc",0);
to vás musím sklamať, ale rúry si musíte vytvoriť sami....

Ono totiž práve shell robí túto prácu za vás, znakom '|' mu dáte príkaz, aby vytvoril rúry a pospájal vstupy a výstupy programov. Volanie execl iba spúšťa konkrétny program, nie je to príkaz shellu, preto znak rúry je iba ďalší parameter programu. Tento znak je špeciálny iba pre shell, ktorý si ho interpretuje po svojom.




7.4 Prečo zatvárame deskriptory
Dobrá rada na koniec: nezabudnite zatvárať všetky nepoužívané deskriptory ! Prečo ?
Rúra sa vytvorí ako dvojica zviazaných deskriptorov. Spolu v dvoch procesoch máme po dedení (dedíme aj deskriptory) štyri deskriptory. My však používame na prácu dva, jeden zapisovací a jeden čítací a navyše každý v inom procese. V každom procese nám teda zostáva jeden nevyužitý deskriptor. Takže máme tu logický dôvod na zatvorenie. Ale dôležitejšie je zatvárať deskritory kvôli spôsobu, akým zapisovateľ oznamuje čítajúcemu, že dáta skončili: zatvorí zapisovací koniec. Keď sa nám však deskriptory zduplikujú, zatvorenie jedného "zapisovacieho" deskriptora neznačí, že sa zapisovací koniec zatvorí - toto sa stane, až keď sú zatvorené obidva deskriptory. Takže ak zabudnete zatvoriť zapisovací koniec u čitateľa, tento sám seba "vyšachuje", lebo si myslí, že mu niekto ešte bude zapisovať do rúry, ale on jediný má ešte otvorený zapisovací koniec, teda čaká len sám na seba.
Takže buďte policajti a zatvárajte a zatvárajte.




8 Prehľadávanie adresárov
Nakoniec sme si nechali dve také ľahšie kapitolky - prvá z nich je o prehľadávaní adresárov.

8.1 Sekvenčný prístup
Pomocou funkcií opendir, readdir,... môžeme ku vstupom adresára pristupovať sekvenčne: pomocou opendir si otvoríme adresár, potom cez readdir budeme postupne načítavať jednotlivé vstupy.
opendir() vráti ukazovateľ (DIR *), pomocou ktorého budeme ďalej identifikovať naše prehľadávanie. Funkcii readdir (a ostatným, je ich samozrejme viac) budeme odovzdávať tento pointer.
struct dirent * readdir(DIR * dirp)

budeme ešte mať aj druhý pointer, už na konkrétny adresárový vstup, a tento pointer nám bude vracať funkcia readdir(). Keď funkcia readdir vráti NULL, skončil sa adresár (alebo nastala chyba).
Najlepšie bude si to ukázať na príklade, ktorý vypíše celý adresár:


PROGRAM 8.1 readdir.c

#include <sys/types.h>
#include <dirent.h>
#include <stdio.h>

main()
{
DIR *dp;
struct dirent *dir;

// Otvorime aktualny adresar prikazom opendir

if ((dp = opendir(".")) == NULL) {
fprintf(stderr, "cannot open directory.\n");
exit(1);
}

//V cykle precitame vsetky polozky adresara (prikaz readdir).

while ((dir = readdir(dp)) != NULL)
{
if (dir->d_ino == 0) // Preskocime subory, ktore su vymazane.
continue;
printf("%s\n", dir->d_name); // Ostane subory vypiseme. (len mena).

}

// Na konci prace s adresarom ho treba uzavriet! (closedir)
closedir(dp);
exit(0);
}

Okrem funkcií opendir, readdir, closedir ešte máme:
telldir - vráti pozíciu v adresári
seekdir - nastaví sa na konkrétny adresárový vstup
a zopár iných, ale spomenuté by nám na prácu s prehľadávaním adresárov mali stačiť.
(teraz je dobrý čas pozrieť si man 5 dir, man opendir)



8.2 Informácie o súboroch
Readdir nám stačiť nebude, keď budeme chcieť získať viac informácií o konkrétnom súbore, ktorý v adresári nájdeme. V štruktúre dirent toho veľa nenájdeme, zaujíma nás hlavne meno súboru. Potom, toto meno odovzdáme funkcii stat(), ktorá vráti o kompletné informácie o súbore.
int stat(const char * path, struct stat * sb)

Samozrejme, definujeme si nejakú struct stat filestat;, aby sme ju stat-u mohli odovzdať, on nám ju naplní. V tejto štruktúre sa toho nachádza mnoho, zase nás prevažne budú zaujímať len niektoré položky.
filestat.st_mode - obsahuje informáciu, či vstup je adresár, súbor apod. Nebudeme toto pole deliť na jeho bity, použijeme makrá S_ISDIR(.st_mode), S_ISREG(.st_mode), tieto nám povedia, či sa jedná o adresár alebo súbor.

Stat sa správa tak, že nasleduje linky. Ak budeme chcieť získať informáciu o linku samotnom, nie o tom, kam link ukazuje, musíme použiť lstat(), ktorý sa používa rovnako ako stat.

Ak vás zaujímajú aj iné možnosti práce s adresárovou štruktúrou, pozrite si aj nasledovné funkcie:
readlink()
scandir()
fts()
povolenú máte síce iba readlink(), ale nezaškodí vedieť trošku viac.

Pri čítaní mien súborov a adresárov do vlastných reťazcov nezabúdajte, že dĺžky mien sú obmedzené konštantami definovanými v /sys/syslimits.h. Takže maximálna dĺžka cesty je PATH_MAX a mena súboru NAME_MAX. Pre orientáciu - hodnota PATH_MAX je napr. 2048 bajtov.





9 Prihlásení používatelia
Keď vám niekto zadá za úlohu zisťovať niečo o používateľoch prihlásených do systému, najskôr vás asi napadne riešiť to cez spustenie "who" alebo "ps". Máme však aj inú možnosť, pretože Unix poskytuje prístup k zoznamu aktuálne prihlásených používateľov a to pomocou súboru /var/run/utmp.
V tomto súbore sú v binárnom tvare za sebou dáta o každom prihlásenom užívateľovi. Štruktúra binárnych dát je známa, je vám poskytnutá štruktúra struct utmp, definovaná v súbore utmp.h. (Treba samozrejme pripojiť #include <utmp.h> a tiež #include <sys/types.h>)
Spôsob práce s utmp je jednoduchý, proste čítate položky od začiatku súboru až do konca. V súbore nie sú priamo žiadne informácie o tom, koľko užívateľov je prihlásených a pod., súbor musíte prehľadávať sekvenčne a ďalšie informácie získať sami, napríklad počet aktuálne prihlásených užívateľov zistíte sčítaním načítaných položiek.


PROGRAM 9.1 utmp.c

#include <stdio.h>
#include <time.h>
#include <utmp.h>

main()
{
FILE *fp;
struct utmp u;
char tmp[UT_NAMESIZE+1];

if ((fp = fopen(_PATH_UTMP, "r")) == NULL)
{
perror(_PATH_UTMP);
exit(1);
}

// citaj vsetky zaznamy
while (fread(&u, sizeof(u), 1, fp) != NULL)
{
// preskoc nenalogovanych
if (u.ut_name[0] == NULL) continue;

// nech je meno ukoncene NULL
strncpy(tmp, u.ut_name, UT_NAMESIZE);

printf("%s\n", u.ut_name);
}

fclose(fp);
exit(0);
}

Podobne, ako sa pracuje s /var/run/utmp, sa pracuje aj s WTMP, čo je súbor s informáciami o tom, koľko boli ktorí užívatelia do systému nahlásení za "posledný čas" - teda odkedy operačný systém tieto informácie zaznamenáva. (Spomeňte si na príkaz 'last')




10 Záver
Predchádzajúci text mal za úlohu oboznámiť vás so základmi práce, nie s kompletnou problematikou. Takisto neodhalil všetky zákutia a problémy, ktoré vás môžu stretnúť. Odporúčam vám ďalšiu prácu postaviť predovšetkým na vlastnej skúsenosti - a nebojte sa experimentovať, tvoriť vlastné príklady, od jednoduchých až ku zložitým. Takto si najlepšie osvojíte používanú množinu funkcií a volaní a postupne prejdete na zložitejšie zadania.
Hojne používajte dostupnú dokumentáciu: systémové manuály, dokumenty na predmet OS, ale tiež aj dostupné webové zdroje.

Prajem veľa výdrže a úspechov pri práci.






---------------------------------------- ----------------------------------------

Download
hello.c Program "Hello World !"

hello2.c Druhý "Hello World !"

getopt1.c Spracovanie parametrov príkazového riadka

fork.c Rozmnožovanie procesov

execl.c Spustenie externého programu

streams.c Práca so súbormi pomocou prúdov

pipe.c Komunikácia dvoch procesov cez rúru

utmp.c Prihlásení používatelia