C na GNU/Linux - 2. časť

27.08.2008 23:33 | kabel

Po nepísavosti trvajúcej približne pol druha mesiaca sa vraciam s druhou časťou seriálu o programovaní s využitím GLIBC knižníc na GNU/Linux systémoch, tentokrát aj s diakritikou. Tak, ako v minulej časti, aj teraz sa od čitateľa očakáva aspoň minimálna znalosť programovania v jazyku C.

V komentári v minulej časti vid spísal zopár vecí, odporúčam si tento komentár prečítať. Taktiež na jeho radu na začiatok dávam ODKAZ NA GLIBC MANUÁL.

K minulej časti - exit

Na úvod spomenieme niečo k minulej časti, konkrétne k normálnemu ukončeniu programu. Písalo sa tam, že s funkciou exit "Vase programy vsak mozu vracat aj ine cisla." Vstupný argument tejto funkcie je int status, ale toto číslo môže byť iba 8 bitové (tj. od 0 do 255 vrátane). Manuál nás varuje, aby sme sa nepokúšali ako exit status vrátiť počet chýb - ak by bol totiž počet chýb 256, dolných 8 bitov tohto čísla sú samé nuly a výsledný status by bol 0.

K minulej časti - binárne a textové streamy

Vo svojom komentári vid tiež spomenul čosi o rozdielnosti binárnych a textových streamov. O tejto rozdielnosti viem toľko, že na GNU systéme sa nerozlišuje. Ak je potrebné použiť binárny stream na systémoch, kde sa rozlišuje, do argumentu mode volania funkcie fopen sa za prvý znak vloží znak b, teda "rb", "wb", "ab", "rb+", "wb+", "ab+". Dedukciou možno dôjsť k tomu, že ak chceme portabilnosť našej aplikácie z GNU systému na spomínaný iný systém, budeme b používať. V GNU systéme totiž vždy pracujeme akoby s binárnym streamom (je jedno, aké znaky píšeme do súboru - všetkých 256 možných). Pri textových streamoch ide v podstate o čítanie/písanie textu po riadkoch a menenie ukončenia riadkov z CRLF (\r\n) na LF (\n) alebo naopak. Ak niekto vie o tejto téme napísať viac, nech sa prosím realizuje v komentároch (respektíve - ak to presiahne medzu stanovenú tvojim rozumom, napíš to ako článok).

Ošetrovanie chýb - errno

Pri funkciách, ktoré môžu zlyhať viacerými spôsobmi, je dôležité, aby program vedel, prečo konkrétne funkcia zlyhala. Header súbor errno.h definuje premennú volatile int errno, ktorá je určená práve na tento účel. Po volaní funkcie sa do tejto premennej zapíše hodnota chybového kódu - chybové kódy majú určené svoje makrá (pozri sem). Keďže errno je definované s volatile, prerušenia programu vo forme signálových handlerov na to musia dávať pozor (popísané nižšie v sekcii Signály).

Na začiatku programu má errno nulovú hodnotu. Funkcie knižnice nastavujú tejto premennej nenulovú hodnotu - chybový kód - keď dosiahnú chybový stav. Nie je ale definované, že keď prebehnú bezchybne, premennú errno vynulujú! Toto závisí na jednotlivých funkciách - funkcie svojou návratovou hodnotou informujú program, že prebehli bez chýb, alebo sa vrátili z dôvodu chyby. Ak sa funkcia vráti z dôvodu chyby, program môže informácie o tejto chybe získať z errno. Sú aj funkcie (napríklad sqrt, atan), u ktorých z návratovej hodnoty nemožno zistiť, či došlo k nejakej chybe. Chybu môžme kontrolovať tak, že pred použitím takejto funkcie nastavíme errno na nulu a po jej vrátení ho skontrolujeme.

Tiež je potrebné zmieniť sa o portabilite: ISO C definuje errno ako makro, nie ako premennú. Tomuto makru môže byť pridelené volanie nejakej funkcie, napríklad *_errno(). V skutočnosti je to takto na samotnom GNU systéme.

Príklad 1: spracovanie chýb s errno

K príkladu: Takéto spracovanie chýb je vhodné pre programy, ktoré majú pre rozličné chyby rozličné kusy kódu. Funckia open môže podľa manuálu skolabovať s desiatimi rozličnými chybovými kódmi. Programátorovi užívateľskej aplikácie však zvyčajne stačí preložiť chybový kód na text a program skončiť. Preto boli vytvorené tieto funkcie:

char * strerror(int errnum)
Priradí k chybovému kódu errnum reťazec popisovacej správy. Keďže táto funkcia vráti smerník na staticky alokovanú pamäť, nemali by ste tento reťazec modifikovať. Funkcia je deklarovaná v string.h

char * strerror_r(int errnum, char * buf, size_t n)
Podobne ako strerror priradí errnum reťazec popisovacej správy, ale vráti kópiu súkromnú pre thread. Kópiu zapíše do miesta buf, najviac n bytov (spolu s NULL znakom). Túto funkciu odporúčajú používať v multi-threadovaných programoch, pretože sa nedá garantovať, že reťazec vrátený strerrorom, naozaj patrí poslednému volaniu threadu. Je deklarovaná v string.h

void perror(const char * message)
Táto funkcia zapíše chybovú správu aktuálnej hodnoty errno na stderr (pridá tiež znak \n). Ak message je nenulový, najprv vypíše message, pridá dvojbodku a medzeru a potom zapíše chybovú správu. Deklarovaná je v stdio.h.

Príklad 2: spracovanie chýb s funkciami perror, strerror a strerror_r
K príkladu: Ako vidíte, do premennej error_z_fopen sa okamžite po zistení, že fopen schybilo, uloží aktuálna hodnota errno. Toto zvyčajne nie je potrebné, ale pre prípad tohto príkladu áno, pretože funkcie, ktoré používame ďalej (printf, malloc) môžu tiež schybiť a zmeniť errno. Keby sme teda volali iba funkciu perror, môžeme tento kus kódu vynechať.
Perlička: pri písaní som mal priradenie int errno_z_fopen = errno; napísané až po volaní perror a chybové hlásenia perror a strerror, strerror_r sa líšili. Konkrétne fopen zlyhalo z dôvodu No such file or directory, ale hodnota errno po volaní funkcie perror ukazovala na chybové hlásenie Illegal seek. Z toho vyplýva, že aj samotná funkcia perror nejakým spôsobom mení hodnotu errno.

Formátovaný vstup/výstup

Často používaná funkcia printf a funkcie jej podobné - o čo vlastne ide? Formátovaný výstup je určený na prevádzanie rôznych typov premenných na reťazce a následné vloženie týchto reťazcov do jedného celku, spolu s predpísaným vzorom.
Skúsme si predstaviť situáciu, že chceme vypísať správu retazec [tu bude vstupny retazec] ma [tu bude cislo] znakov. Riešenie by vyzeralo takto:
1. vypíšeme "retazec "
2. vypíšeme náš reťazec
3. vypíšeme " ma "
4. dĺžku nášho reťazca prevedieme z číselného typu na reťazec znakov
5. vypíšeme tento reťazec znakov
6. vypíšeme " znakov"
Máme tu 6 bodov na takto primitívnu úlohu. Okrem toho na prevedenie číselného typu na reťazec potrebujeme funkciu itoa, ktorá ale nie je súčasťou štandardu (odkaz vedie na stránku, kde je možné si pozrieť kód tejto funckie v C). Toto všetko sa dá spraviť jednoduchšie pomocou funkcie printf takto:

char * retazec = "ahoj, ja som retazec";
printf("retazec %s ma %i znakov", retazec, strlen(retazec));

Po tomto príklade by ste mali chápať podstatu formatováneho výstupu. Teraz si stručne vysvetlíme jednotlivé popisovače konverzie (všetky sú uvádzané znakom %).
d, i - konverzia int ako dekadické číslo so znamienkom (signed)
u - konverzia int ako dekadické číslo bez znamienka (unsigned)
f, F - vytlačia desatinné číslo typu double
e, E - vytlačia double v exponenciálnej forme: ([-]d.ddd e[+/-]ddd)
g, G - vytlačia double v normálnej alebo exponenciálnej forme, podľa toho čo sa viac hodí
x, X - vytlačia unsigned int ako hexadecimálne číslo, spolu s 0x alebo 0X na začiatku
o - konverzia unsigned int do osmičkovej sústavy
s - vypíše reťazec znakov
c - vypíše char
p - vypíše smerník void *, napríklad 0x12345678
% - %% vypíše znak %

Na stránke http://en.wikipedia.org/wiki/Printf#printf_format_placeholders sú tieto popisovače rozobrané podrobnejšie. Môžte sa tam dočítať o komplikovanejších popisovačoch - o šírke reťazca, o presnosti desatinného čísla a podobne.

Funkcie formátovaného výstupu:

int printf(const char * template, ...)
Vypíše nepovinné argumenty (...) podľa vzoru template do streamu stdout. Vráti počet zapísaných znakov alebo záporné číslo, v prípade chyby.

int fprintf(FILE * stream, const char * template, ...)
Rovnaká ako printf, ale zapisuje do streamu stream.

int sprintf(char * s, const char * template, ...)
Zapíše nepovinné argumenty podľa vzoru template do pamäte popísanej smerníkom s. Správanie v prípade slučky (napríklad keď je ako argument samotný s ako %s) je nedefinované. Použitie môže byť nebezpečné, ak je v s naalokované miesto menšie, ako bude použité! Môže nastať pretečenie reťazca! Funkcia vráti počet zapísaných znakov.

int snprintf(char * s, size_t size, const char * template, ...)
Táto funkcia je podobná funkcii sprintf, ibaže po zapísaní size znakov (terminovací NULL znak nie je počítaný) skončí. Takto je možné predísť pretečeniu reťazca. Funkcia vráti počet znakov, ktoré by boli vygenerované pre daný vstup (nepočítajúc NULL znak). Ak je tento počet väčší alebo rovný ako size, nie všetky znaky výsledku boli uložené do s. V tomto prípade môžete realokovať reťazec na veľkosť návratová_hodnota+1 a funkciu zavolať znovu.

Formátovaný vstup funguje opačne: prevádza reťazce podľa určitého vzoru na rôzne typy premenných. Funkcie ignorujú medzery a iné biele znaky (\n, tabulátory, ...) ako vo vzore, tak aj vo vstupe. Všetky iné znaky sa berú do úvahy - ak nastane nerovnosť, funkcia skončí. Funkcia vracia počet priradených hodnôt - podľa tohto počtu môžeme zistiť kedy a kde nastala chyba. Popíšeme niekoľko popisovačov konverzií (tiež sú uvádzané znakom %):
d - číslo v dekadickej sústave
i - číslo v dekadickej, hexadecimálnej (0x) a oktálnej (0) sústave
o, u, x - unsigned čísla v oktálnej, dekadickej a hexadecimálnej sústave
Preddefinovaný typ pre konverziu čísel je int * alebo unsigned int *. Môžete použiť tieto modifikácie pre špecifikáciu typu:
hh - %hh[dioux] - unsigned/signed char *
h - %h[dioux] - unsigned/signed short *
l - %l[dioux] - unsigned/signed long int *
ll, L, q - unsigned/signed long long int *, q pretože sa tomu nadáva aj quad

Pre koverziu desatinných čísel existujú %e, %f, %g, %E, %G (je jedno ktoré použijete). Preddefinovaný typ je ale float * (líši sa to od printf, kde je preddefinovaný double). Na špecifikáciu typu sa používa:
l - %lf pre double *
L - %Lf pre long double *

Čísla sa niekedy píšu s bodkami alebo čiarkami medzi tisíckami (podľa lokály). Ak za znakom % pridáte znak ', vstupný reťazec bude formátovaný podľa pravidiel danej lokály.

Keďže pri formátovaní reťazcov je pamäťové miesto na uloženie obmedzené, je potrebné určovať maximálnu akceptovateľnú šírku reťazca, alebo povedať funkcii, aby alokovala miesto sama.

%DLZKAs - formátuje reťazec s maximálnou dĺžkou DLZKA. Skončí, keď dosiahne biely znak alebo DLZKA bytov. V smerníku musí byť naalokované miesto DLZKA+1 (ukladá sa aj terminovací NULL znak)

%as - naalokuje potrebné miesto v smerníku sám. Toto miesto je potom možné uvoľniť pomocou free

%[a-zA-Z0-9] - [znaky] môžete použiť miesto znaku s - špecifikujete jednotlivé znaky, ktoré môžu byť v reťazci obsiahnuté.

%n - neformátuje nič, iba uloží počet formátovaných znakov do premennej
%p - prečíta smerník (0x12345678)

Je potrebné si uvedomiť, že funkcie formátovaného vstupu potrebujú smerníky na premenné, do ktorých chceme uložiť jednotlivé vstupy. Používame teda referenciu premenných &.

Príklad 3: Použitie printf a scanf

Nakoniec niekoľko funkcií formátovaného vstupu:

int scanf (const char * template, ...)
Číta zo streamu stdin a podľa vzoru template uloží do smerníkov na premenné jednotlivé vstupy. Vráti počet úspešných priradení.

int fscanf(FILE * stream, const char * template, ...)
Rovnaké ako scanf, ibaže číta zo streamu stream.

int sscanf(const char * s, const char * template, ...)
Rovnaké ako scanf, ibaže znaky majú pôvod v NULL terminovanom reťazci s.

Funkcie formátovaného vstupu aj výstupu sú definované v stdio.h.

Signály

Signál je prerušenie doručené procesu. Systém používa signály na hlásenie výnimočných udalostí. Existuje niekoľko typov signálov - niektoré hlásia chyby, iné sú poslané nejakým procesom (signály patria tiež do medziprocesnej komunikácie). Ak chce nejaký proces poslať inému signálu, musíme mať nato právo. Príjmací proces má právo v závislosti od typu signálu spracovať ho (handling). Existujú signály, ktoré sa spracovať nedajú (napríklad SIGKILL, SIGSTOP). Spracovanie signálov spočíva v registrovaní funkcie (signálový handler), ktorá bude zavolaná, až proces dostane signál. Vtedy budú všetky thready procesu zastavené a vykoná sa signálový handler. Po jeho skončení sa vykonávanie procesu obnoví. Proces si môže tiež nastaviť, aby signály určitého typu ignoroval, alebo mu môže nastaviť defaultuný handler.

Ak signál, ktorý hlási chybu, terminuje nejaký proces, zapíše tiež core dump súbor, ktorý hovorí o stave procesu pri terminácii (coredump možno študovať debuggerom (napríklad gdb)). Takýto signál môže byť procesu poslaný aj explicitne (nie z dôvodu chyby, ale iným procesom).

Pri handlovaní signálov sa môže vyskytnúť problém s errno. Ak totiž signálový handler zmení jeho hodnotu medzi volaním nejakej systémovej funkcie a kontrolovaním errno, kontrola môže zlyhať. Preto by signálové handlery mali na začiatku uložiť hodnotu errno do dočasnej premennej a na konci ju vrátiť.

Príklad 4: registrovanie signálového handleru pre SIGINT
K príkladu: program predefinuje handler pre signál SIGINT (CTRL + C z klávesnice). Ak teda budete chcieť ukončiť program s CTRL + C, nepomôžete si. Cyklus while sa skončí, keď stlačíte p a ENTER.

V signal.h sú definované tieto funkcie (includujte aj sys/types.h):

sighandler_t signal(int signum, sighandler_t handler)
Táto funkcia zmení handler signálu signum. handler môže byť definovaná funkcia (void handler(int signum)), alebo SIG_IGN, ak má byť signál ignorovaný, SIG_DFL ak má byť použitý defaultný handler. Funkcia vracia predchádzajúcu hodnotu signal handleru.

int raise(int signum)
Pošle signál signum volajúcemu procesu (sám sebe).

int pause()
Zastaví vykonávanie procesu až do príchodu nejakého signálu. Po vykonaní handlera vráti -1 (hodnotu neúspechu, pretože úspešnosť je zastaviť program navždy). errno sa potom nastaví na EINTR (Interrupted system call).

int kill(pid_t pid, int sig)
Táto funkcia sa používa na posielanie signálov procesom alebo procesným skupinám.
Ak pid > 0, signál sa pošle procesu pid.
Ak pid = 0, signál sa pošle procesom v procesnej skupine volajúceho procesu.
Ak pid = -1, signál je poslaný všetkým procesom okrem procesu 1 (init) na ktoré je posielanie povolené.
Ak pid < -1, signál sa pošle procesom v procesnej skupine -pid.
Ak sig = 0, nepošle sa žiadny signál, ale kontrola práv prebehne.

Signály môžu procesu dôjsť v I/O funkciách, ako napríklad read/write, pokiaľ sa čaká na na I/O zariadenie. V takej situácii po skončení signal handlera je operačný systém postavený pred otázku: čo spraviť teraz? POSIX špecifikuje, že v takejto situácii má byť operácia ukončená a errno nastavené na EINTR. Toto je potom zdrojom mnohých chýb v programoch, keď sa nekontroluje EINTR (nepredpokladá sa). Zakaždým ho kontrolovať by výrazne zhoršilo programovanie celej aplikácie. GLIBC knižnica definuje makro TEMP_FAILURE_RETRY(výraz), ktoré vykoná výraz, hodnotu ktorú vráti preštuduje ako long int a ak je táto hodnota rovná -1, skontroluje errno. Ak errno == EINTR, toto makro vykoná funkciu znova, a znova, a znova, až kým neprebehne úspešne. Hodnota, ktorú vráti TEMP_FAILURE_RETRY sa rovná hodnote, ktorú vráti výraz. Osobne som TEMP_FAILURE_RETRY nikdy nepoužil.

Pomocou zložitejšej funkcie sigaction, ktorú však nebudem popisovať (môžete sa o nej dočítať v manuále), sa dá signál nastaviť tak, že po skončení signálového handlera sa systém vráti do systémového volania na miesto, kde bolo prerušené.

Programátorom, ktorých táto problematika zaujíma viac, odporúčam prečítať manuál. Zaujímavá je aj časť o stacku signálového handlera.

K zoznamu signálov sa môžete dostať čítaním manuálových stránok:

$ man 1 kill
$ man 7 signal


Dátum a čas

Pri programovaní často pracujeme s časom. S kalendárnym časom sa pracuje nasledovne:

time_t - tento typ vyjadruje čas - v POSIX systéme je to celé číslo (32 alebo 64 bitov široké), ktoré reprezentuje čas Unixovou epochou (počet sekúnd od 1. januára 1970, 00:00:00 UTC)

time_t time(time_t * result)
Vráti aktuálny čas. Ak smerník result nie je nulový, čas bude uložený do *result.

int stime(time_t * newtime)
Nastaví aktuálny čas systémových hodín na *newtime. Proces musí mať na túto akciu práva. Vráti 0 ak nastavenie prebehlo úspešne, -1 v opačnom prípade.

Presnejšiu dĺžku časového intervalu a časové pásmo určujú nasledovné štruktúry:

struct timeval {
        long int tv_sec; // pocet sekund
        long int tv_usec; // pocet mikrosekund. Vzdy mensie ako milion
}
struct timespec {
        long int tv_sec; // pocet sekund
        long int tv_nsec; // pocet nanosekund. Vzdy mensie ako bilion
}
struct timezone {
        int tz_minuteswest; // pocet minut zapadne od UTC
        int tz_dsttime; // nenulove, ak sa v tomto pasme posuva cas (napr. na letny)
}

(Štruktúra timezone je zastaralá a nepoužíva sa.) Na presné určenie času používajte funkcie:

int gettimeofday(struct timeval * tp, struct timezone * tz)
Táto funkcia získa aktuálny čas s presnosťou väčšou ako 1 sekunda. Ak je tz NULL, informácie o časovom pásme sa neukladajú.

int settimeofday(const struct timeval * tp, const struct timezone * tz)
Nastaví systémový čas na čas definovaný s tp. Ak je tz NULL, informácie o časovom pásme sú ignorované. Používať túto funkcie smie iba superuser (root).

Kalendárny čas reprezentovaný s time_t je vhodný na počítanie, ale nie je vhodný pre ľudí. Na druhej strane broken-down time reprezentuje čas v "ľudských" hodnotách (deň, mesiac, rok, ...) a tento zasa nie je vhodný na počítanie. Na prevod z jedného formátu do druhého je potrebné poznať štruktúru tm:

struct tm {
        int tm_sec; // pocet sekund od zaciatku minuty
        int tm_min; // pocet minut od zaciatku hodiny
        int tm_hour; // pocet hodin od polnoci
        int tm_mday; // den v mesiaci
        int tm_mon; // mesiac v roku
        int tm_year; // pocet rokov od roku 1900
        int tm_wday; // pocet dni od nedele (nedela = 0, sobota = 6)
        int tm_yday; // pocet dni od zaciatku roka
        int tm_isdst; // hodnota je kladna ak sa cas posuva (Daylight Save Time), nulova ak nie a zaporna ak informacia nie je znama
        long int tm_gmtoff; // toto nastavenie popisuje casove pasmo poctom sekund, ktore treba pripocitat k UTC aby sme dostali lokalny cas
        const char * tm_zone; // tento retazec popisuje casove pasmo
}

Čas potom prevádzame pomocou týchto funkcií:

struct tm * localtime(const time_t * time)
Funkcia prevedie čas reprezentovaný v type time_t na "ľudský" broken-down time. Výsledok uloží do statickej štruktúry, ktorá je zmenená ďalšími volaniami funkcie localtime, ctime alebo gmtime. Okrem toho táto funkcia nastaví tzname, teda získate informáciu o časovom pásme.

struct tm * localtime_r(const time_t * time, struct tm * resultp)
Funguje rovnako ako localtime, ibaže výsledok uloží na miesto, na ktoré ukazuje smerník resultp.

struct tm * gmtime(const time_t * time)
Rovnaká ako localtime, ibaže čas vyjadruje v UTC (nultý poludník - svetový čas).

struct tm * gmtime_r(const time_t * time, struct tm * resultp)
Táto funkcia vráti čas vyjadrený v UTC a výsledok uloží do *resultp.

time_t mktime(struct tm * brokentime)
Prevádza broken-down time na jednoduchú reprezentáciu času. Normalizuje štruktúru brokentime nastavením wday a yday (tieto nastavenia pri prevode ignoruje, na začiatku nemusia byť nastavené). Ak sa prevod nemôže z nejakého dôvodu uskutočniť, mktime nezmení nič v brokentime a vráti (time_t) -1. Funkcia tiež nastaví tzname.

time_t timegm(struct tm * brokentime)
Robí to isté čo mktime, ibaže vstupné hodnoty chápe ako UTC bezohľadu na časové pásmo.

Formátovanie kalendárneho času na reťazec:
char * asctime(const struct tm * brokentime)
Táto funkcia prevedie brokentime na reťazec v štandardnej forme (Tue May 21 13:46:22 1991\n). Návratová hodnota ukazuje na staticky alokovaný reťazec, ktorý je ďalšími volaniami asctime alebo ctime zmenený.

char * asctime_r(const struct tm * brokentime, char * buffer)
Robí to isté čo asctime, výsledok uloží do *buffer.

char * ctime(const time_t * time)
Rovnaké ako asctime(localtime(time))

char * ctime_r(const time_t * time, char * buffer)
Výsledok uloží do *buffer.

A nakoniec trochu formátovaného výstupu:
size_t strftime(char * s, size_t size, const char * template, const struct tm * brokentime)
Táto funkcia funguje podobne ako sprintf, ale má svoje vlastné popisovače konverzie:

%a - skrateny nazov dna v tyzdni
%A - nazov dna v tyzdni
%b - skrateny nazov mesiaca
%B - nazov mesiaca
%c - preferovana reprezentacia kalendarneho casu podla aktualnej lokaly
%C - storocie
%d - den v mesiaci s uvodnou nulou (01 - 31)
%D - datum formatu %m/%d/%y
%e - den v mesiace bez uvodnej nuly (1 - 31)
%F - datum formatu %Y-%m-%d
%g - rok podla tyzdna (%V) bez storocia (1997 -> 97, 2001 -> 01)
%G - rok podla tyzdna (%V - ak tyzden patri predchadzajucemu alebo nasledujucemu roku, pouzije sa tento rok)
%h - %b
%H - hodina v dni - 24 hodinovy format (00 - 23)
%I - hodina v dni - 12 hodinovy format (01 - 12)
%j - den v roku (001 - 366)
%k - %H bez uvodnej nuly (0 - 23)
%l - %I bez uvodnej nuly (1 - 12)
%m - mesiac ako cislo (01 - 12)
%M - minuta ako cislo (00 - 59)
%n - \n
%p - AM alebo PM
%P - am alebo pm
%r - cas formatu %I:%M:$S %p
%R - cas formatu %H:%M
%s - pocet sekund od epochy
%S - sekunda ako cislo (00 - 60 (60 pri priestupnych sekundach))
%t - \t
%T - cas formatu %H:%M:%S
%u - cislo dna v tyzdni (1 - pondelok, 7 - nedela)
%U - cislo tyzdna v roku (prvy tyzden sa zacina prvou nedelou) (00 - 53, 00 pre dni pred prvou nedelou)
%V - cislo tyzdna v roku (tyzden zacina pondelkom a konci nedelou) (01 - 53)
%w - cislo dna v tyzdni (0 - nedela, 6 - sobota)
%W - cislo tyzdna v roku (prvy tyzden sa zacina prvym pondelkom) (00 - 53)
%x - preferovana reprezentacia datumu podla aktualnej lokaly
%X - preferovana reprezentacia casu v dni podla aktualnej lokaly
%y - rok bez storocia (00 - 99)
%Y - rok podla Gregorianskeho kalendara
%z - casove pasmo (napriklad -0600 alebo +0100)
%Z - skratena reprezentacia casoveho pasma (prazdne ak je pasmo neurcene)
%% - znak %

K formátovanému výstupu času existuje tiež formátovaný vstup - o tom ale písať nebudem, môžte si o tom prečítať tu.

Zmienim sa ešte o funkciách spánku:
unsigned int sleep(unsigned int seconds)
Táto funkcia uspí proces a zobudí ho po seconds sekundách alebo ak dôjde signál. Ak skončí z dôvodu príchodu signálu, vráti zostávajúci čas, v opačnom prípade 0. Definovaná v unistd.h.

int nanosleep(const struct timespec * requested_time, struct timespec * remaining)
Táto funkcia uspí program na čas s veľkou presnosťou (na nanosekundy). Interval by mal byť dlhší, pretože systém si ho zaokrúhli nahor na hodnotu, ktorú môže rozoznať. Do remaining sa uloží zostávajúci čas (ak bola funkcia prerušená signálom - ak nie, v remaining budú nulové hodnoty). Ak funkcia skončí z dôvodu vypršania intervalu, vráti 0. Ak vráti -1, nastaví errno (EINTR ak bola prerušená signálom, EINVAL ak hodnota nanosekúnd obsahuje nepovolené hodnoty (väčšie alebo rovné ako bilión alebo záporné)). Definovaná v time.h.

Čakanie na I/O

V aplikácii môže niekedy nastať situácia, kedy je potrebné prijať vstup z viacerých kanálov, podľa toho ktorý príde prvý. Nemôžete normálne použíť read, pretože proces bude zablokovaný, až kým nepríde vstup z jedného deskriptoru. Môžete nastaviť neblokovaný mód a skúšať jednotlivé deskriptory, ale takéto riešenie je veľmi neefektívne. Lepším riešením je použitie select.

Ešte pred popisom funkcie si popíšeme dátovy typ fd_set. Tento typ reprezentuje množinu file deskriptorov pre select funkciu. Je to bitové pole. Konkrétne (u mňa) funguje tak, že procesu je dovolené mať otvorených maximálne 1024 file deskriptorov, ktoré sa číslujú od 0 do 1023 - v tomto poli sa pri nastavení určitého deskriptoru zmení hodnota prislúchajúceho bitu z 0 na 1.
Makro int FD_SETSIZE definuje hodnotu maximálneho počtu file deskriptorov, ktoré môže spracovať fd_set (u mňa 1024).
void FD_ZERO(fd_set * set) vynuluje fd_set *set.
void FD_SET(int filedes, fd_set * set) pridá deskriptor filedes do fd_set *set.
void FD_CLR(int filedes, fd_set * set) vynuluje deskriptor filedes z fd_set *set.
int FD_ISSET(int filedes, const fd_set * set) vráti nenulovú hodnotu (true) ak deskriptor filedes je nastavený vo fd_set *set, v opačnom prípade nulu (false).

A konečne:
int select (int nfds, fd_set * read-fds, fd_set * write-fds, fd_set * except-fds, struct timeval * timeout)
Funkcia zablokuje volajúci proces, kým nevznikne aktivita na nejakom deskriptore z daných množín, alebo kým neuplynie časový interval timeout.
O file deskriptoroch v read-fds je zisťované, či sú pripravené na čítanie. Podobne funguje write-fds - kontroluje, či je jeden z deskriptorov pripravený na zápis a except-fds - kontroluje, či je jeden z deskriptorov pripravený na výnimočné stavy. Môžete použiť NULLový smerník do hociktorého z týchto argumentov, ak nechcete kontrolovať prísluchajúcu podmienku.
File deskriptor je pripravený na čítanie, ak operácia read nebude blokovaná. Serverový socket je pripravený na čítanie, ak existuje klient čakajúci na accept. Klientový socket je pripravený na písanie, ak je pripojenie k serveru stabilizované. Výnimočné stavy kontrolujú napríklad urgentné správy na socketoch.
Funkcia select kontroluje iba prvých nfds deskriptorov. Často sa tu dáva FD_SETSIZE. Časový interval timeout špecifikuje maximálny čas na čakanie. Ak zadáte NULLový smerník, zablokujete proces, až kým nebude pripravená operácia. Ak chcete iba zistiť ktoré deskriptory sú pripravené, zadávajte vynulovaný timeout.
Normálne funkcia vracia počet pripravených file deskriptorov zo všetkých 3 množín. Každá z množín je potom prepísaná informáciou o pripravených deskriptoroch. Pomocou FD_ISSET môžete potom zistiť, či je daný deskriptor pripravený. Ak funkcia skončí, pretože uplynul časový interval, vráti 0. Ak nastane chyba, funkcia nenastavuje množiny, vráti -1 a nastaví errno (EBADF - jeden z file deskriptorov je nesprávny, EINTR - čakanie bolo prerušené signálom, EINVAL - nesprávna hodnota v timeout).

Príklad 5: Jednoduché použitie select

Minule som napísal, že v tejto časti bude spomínaná aj problematika termio a socketov. Z dôvodu dĺžky článku a zopár ďalších nemenovaných to presúvam do ďalšej časti. Dúfam, že sa na mňa nikto nenahnevá.

    • Re: C na GNU/Linux - 2. časť 28.08.2008 | 15:59
      Avatar blackhole   Návštevník

      Tento serial si zatial uzivam !! K obsahu nemam pripomienky .. Uroven, na korej je clanok pisany presahuje moje skusenosti s jazykom C a pritom otestovanie zdrojovych kodov a ich porozumenie, resp. porozumenie textu celkovo je jednoduche ..

      Bol by som rad, keby isiel serial takym smerom, aby som na jeho konci bol schopny zacat pisat svoje vlastne C aplikacie. Jednoduche. Nechcem aby ma naucil programovat, chcem aby mi ukazal cestu. A aby mal koncept -> to je, aby diely na seba nadvazovali a aby nasledujuci diel staval na tom predoslom .. Bol by som nerad, keby sa tu iba preberali kniznice a funkcie bez ladu a skladu. Ak sa ma vysvetlit vyznam funkcie (napr. 'scanf', 'fscanf,'sscanf'), tak za ucelom vyuzitia funkcie v dasom texte/dieloch. Toto uvadzam iba ako priklad, pretoze tie funkcie su zaklad a kazdy ich pozna ..

      Opakujem, zatial sa mi to paci, ale som si vedomy, ze su to zaklady. Verim v pokracovanie serialu ktory bude mat ciel a koncept ;)

      Len tak pre moju info, kolko dielov planujes?

      • Re: C na GNU/Linux - 2. časť 28.08.2008 | 21:04
        Avatar kabel   Používateľ

        Svoje vlastné C aplikácie môžeš začať písať už teraz. Základnú syntax jazyka vidíš v príkladoch (a môžeš k nej prísť hocikde). Ak chceš zlepšiť svoju znalosť C, musíš programovať. Najlepšie sa programuje za nejakým účelom - buď riešiš príklady do školy, alebo riešiš vlastné problémy. Ak potrebuješ napísať aplikáciu, ktorá má používať niečo, čo nepoznáš, vždy sa dá dostať k informáciám, ktoré ti to objasnia. Cieľom mojich článkov je predovšetkým spísanie informácií, ktoré ti môžu pomôcť vyriešiť problémy na GNU/Linux.

        Áno, viem, že sa moje písanie zvrháva na objasňovanie funkcií, ale keď si to prečítaš a raz budeš riešiť problém, v ktorom budeš musieť nejakým spôsobom použiť niektoré tieto funkcie, spomenieš si na tento článok (možno si ho znova prečítaš) a problém vyriešiš.

        Píšem o tom, čo ma zaujíma a o čom si myslím, že by sa niekomu mohlo zísť. Neviem, koľko častí napíšem a neviem, či sa všetkým hodí názov C na GNU/Linux. V budúcnosti by som chcel objasniť aj kompilovanie a linkovanie programov, vytváranie skriptov configure a make a ďalšie veci, ktoré zaujímajú predovšetkým mňa.
        ----
        "For everything in nature, there is a balance." - Jor-El of Krypton