C na GNU/Linux

© 13 Júl 2008 by Marek Behún, pôvodne na blackhole.sk

Nazov clanku moze trosku zavadzat, na vystiznejsi a pritom strucny som neprisiel. Tento serial sa bude tykat najma pouzitia GLIBC kniznic a kompilovania a linkovania programov napisanych v C na GNU/Linux systemoch. Od citatela sa ocakava minimalna znalost programovania v C. Ak vsak mate stipku logiky a ovladate zaklady programovania, serial je z istej casti urceny aj vam.

Ak sa v clankoch dostanete k niecomu, comu nebudete rozumiet, pytajte sa v komentaroch. Pre teoriu jazyka odporucam Uvod do programovania v jazyku C.

Vstupne argumenty pre program

Ked spustame program z prikazoveho riadku, zvycajne piseme nazov programu a parametre pre program (napriklad ls -al). Aby sa k tymto argumentom dostal proces, kernel (jadro systemu) ich zapise do isteho miesta pamate procesu a funkcii main ich priradi ako vstupne hodnoty.
Funkcia main dostava 3 vstupne hodnoty: pocet argumentov, smernik (pointer) na argumenty a smernik na enviromentalne nastavenia (vysvetlenie nizsie). Nulty argument (prvy v poli) je nazov, ktorym bol program spusteny.

Priklad 1: Vypis argumentov

// kompilacia: gcc -o priklad1 priklad1.c
#include <stdio.h>
int main (int argc, char ** argv) {
        int i;
        for (i = 0; i < argc; i++) {
                printf("%i : %s\n", i, argv[i]);
        }
}
Spustenie a priklad vystupu:
user@hostname:~/c$ priklad1 a b c
0 : priklad1
1 : a
2 : b
3 : c
user@hostname:~/c$ ./priklad1 ahoj
0 : ./priklad1
1 : ahoj
Ako vidiet, nulty argument (spustaci nazov) nie je konecny nazov suboru, ale obsahuje aj cestu k tomuto suboru. Toto je dolezity fakt pri programoch, ktorych cinnost zavisi aj od nazvu, akym boli spustene - napriklad gunzip je zvycajne symbolicky odkaz na gzip, robi vsak odlisnu cinnost. Pri zistovani teda nestaci porovnat argv[0] s retazcom "gunzip", je potrebne odstranit z retazca argv[0] cestu ku suboru a zanechat iba nazov suboru (z retazca "/bin/gunzip" treba ziskat retazec "gunzip"). V string.h je na tento ucel definovany prototyp funkcie basename.

Priklad 2: Zistenie nazvu suboru, ktorym bol program spusteny

// kompilacia: gcc -o priklad2 priklad2.c
#include <stdio.h>
#include <string.h>
int main (int argc, char ** argv) {
        char * progname = basename(argv[0]);
        printf("%s\n", progname);
}
Spustenie a priklad vystupu:
user@hostname:~/c$ priklad2
priklad2
user@hostname:~/c$ ./priklad2
priklad2

Vyssie spomenute enviromentalne nastavenia su take nastavenia, ktore su narozdiel od argumentov globalnejsie - su rovnake pre viac procesov. Obsahuju informacie napriklad o domovskom adresari uzivatela, type terminalu, nastavenej lokale (jazyku)... Ich vypis mozno ziskat prikazom export v prikazovom riadku.
Enviromentalne nastavenia vo svojom nazve nemozu obsahovat znak =, pretoze tymto znakom sa oddeluje nazov od ich hodnoty.

Priklad 3: Vypis enviromentalnych nastaveni

// kompilacia: gcc -o priklad3 priklad3.c
#include <stdio.h>
int main (int argc, char ** argv, char ** envp) {
        int i = 0;
        while (1) {
                if (envp[i] == NULL)
                        break;
                printf("%s\n", envp[i]);
                i++;
        }
}
Z prikladu mozno dedukovat ze pole envp je zakoncene NULL bunkou (jadro nam neposkytne pocet tychto nastaveni, na ich konci vsak prida hodnotu NULL a preto pri ich vypise neostaneme v nekonecnej slucke).

Na pracu s enviromentalnymi nastaveniami GLIBC ponuka prototypy funkcii v stdlib.h - ak ich raz mienite pouzit, odporucam tuto stranku manualu (anglicky).

V unistd.h je definovana premenna char ** environ, ktora obsahuje environmentalne nastavenia bez toho aby ich tam daval uzivatel. Nemusite v deklaracii funkcie main pouzit tretiu hodnotu, v nastaveni environ tieto nastavenia budu definovane. GLIBC musi mat environmentalne nastavenia uloznene napriklad pri volani execv (sekcia Procesy).

Normalne ukoncenie programu

V doterajsich prikladoch sa nase programy koncili jednoduchym ukoncenim funkcie main. Niekedy je vsak potrebne program ukoncit vo vetve (napriklad pri zisteni chyby). Nato je urcena funkcia exit(int status), ktora je definovana v stdlib.h. Funkcia exit ziada parameter status - vysledok programu. Definovane su makra EXIT_SUCCESS a EXIT_FAILURE s hodnotamy 0 a 1. Vase programy vsak mozu vracat aj ine cisla. Vysledna hodnota statusu sa pouziva najma pri programovani v shell jazykoch.

Priklad 4: Normalne ukoncenie programu

// kompilacia: gcc -o priklad4 priklad4.c
#include <stdio.h>
#include <stdlib.h>
int main (int argc, char ** argv, char ** envp) {
        if (argc < 2) {
                printf("nezadal si argument\n");
                exit(EXIT_FAILURE);
        }
        printf("%s\n", argv[1]);
        exit(EXIT_SUCCESS);
}
Tento priklad vypise prvy argument a vrati status 0 (EXIT_SUCCESS), ak bol zadany minimalne jeden argument okrem argv[0]. V opacnom pripade - ak argv[1] nie je definovane - vypise sa chybove hlasenie a program vrati hodnotu 1 (EXIT_FAILURE).
user@hostname:~/c$  priklad4
nezadal si argument
user@hostname:~/c$ echo $?
1
user@hostname:~/c$ priklad4 ahoj
ahoj
user@hostname:~/c$ echo $?
0
(Premenna $? v bashi oznacuje status naposledy vokonaneho programu.)

Vacsie cisla ako 1 vracia napriklad zip - programy pouzivaju zip na zbalenie, respektive rozbalenie archivu. Pocet moznych chyb je vacsi ako 1, preto sa pouzivaju vyssie cisla (pozri napriklad man zip, sekcia DIAGNOSTICS).

IO - input/output

Dalsia vec, ktorej sa budeme venovat, je IO - API pre citanie a zapisovanie. V GNU systeme existuju 2 koncepty pre IO - stream (prud) a file descriptor (popisovac suboru).
Pre system je jednoduchsi file descriptor, ten je vlastne jedinym popisovacom IO entity z hladiska kernelu. Je to cele cislo, ktoremu je v kerneli pre dany proces priradena IO entita.
Stream je popisovac IO entity iba z hladiska procesu - funkcie pri praci s entitou v konecnom dosledku pouziju skryty file descriptor - ten je ulozeny v strukture streamu.

Priklad 5: Zapisanie retazca do suboru

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
int main () {
        int fd; // pamat pre file descriptor
        FILE * fp; // pamat pre stream
        fd = open("subor1", O_WRONLY | O_CREAT, 0644);
        if (fd < 0) {
                perror("subor1");
                exit(EXIT_FAILURE);
        }
        write(fd, "nieco 1", 7);
        close(fd);
        fp = fopen("subor2", "w");
        if (!fp) {
                perror("subor2");
                exit(EXIT_FAILURE);
        }
        fprintf(fp, "nieco 2");
        fclose(fp);
        exit(EXIT_SUCCESS);
}
Ako vidime, v priklade sme pouzili obidva koncepty:

Na prevadzanie file descriptoru na stream a spat sa pouzivaju funkcie fdopen a fileno.
Priklad 6: Pouzitie fdopen a fileno

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
int main () {
        int fd;
        FILE * fp;
        fd = open("foo", O_WRONLY | O_CREAT, 0644);
        if (fd < 0) {
                perror("foo");
                exit(EXIT_FAILURE);
        }
        write(fd, "prvy riadok\n", 12);
        fp = fdopen(fd, "w");
        fd = -1000; // teraz uz mozme zabudnut hodnotu fd
        if (!fp) {
                perror("foo");
                exit(EXIT_FAILURE);
        }
        fprintf(fp, "druhy riadok\n");
        fflush(fp);
        fd = fileno(fp); // naspat ziskame hodnotu fd
        if (fd < 0) {
                perror("fileno");
                exit(EXIT_FAILURE);
        }
        write(fd, "treti riadok\n", 13);
        close(fd);
        exit(EXIT_SUCCESS);
}
Priklad otvori subor foo cez file descriptor, zapise donho retazec, potom s pomocou funkcie fdopen ziska stream na tento subor, zapise don retazec cez stream, potom znovu zo streamu pomocou funkcie fileno ziska file descriptor, zapise treti retazec a subor zatvori. Je potrebne si vsimnut funkciu fflush, ktora je pouzita po zapisani do suboru cez stream. Jej funkcia bude popisana nizsie v stati o bufferingu streamov.

Zmena pozicie v subore

Pri citani dat zo suboru alebo pri zapisovani don casto potrebujeme zacat citat/pisat nie od zaciatku, ale od nejakej pozicie v subore. Niekedy je tiez potrebne viackrat menit tuto poziciu. Na zmenu pozicie pri file descriptoroch sluzi funkcia lseek, pri streamoch fseek.

Priklad 7: Pouzitie lseek a fseek

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
int main () {
        // najprv do suboru zapiseme 30 znakov X
        int fd;
        fd = open("priklad7_test", O_RDWR | O_CREAT, 0644);
        write(fd, "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", 30);
        // teraz sa presunime na poziciu 10 od zaciatku
        // suboru a prepiseme 10 znakov medzetami
        lseek(fd, 10, SEEK_SET);
        write(fd, "          ", 10);
        close(fd);
        // a+ otvori subor na citanie aj zapis,
        // ukazovatel je na konci suboru (30)
        FILE * fp;
        fp = fopen("priklad7_test", "a+");
        // presunieme sa na poziciu 15,
        // precitame 10 znakov a vypiseme ich
        fseek(fp, 15, SEEK_SET);
        char buffer[11];
        fread(buffer, 1, 10, fp);
        buffer[10] = '\0';
        printf("%s\n", buffer);
        fclose(fp);
        exit(EXIT_SUCCESS);
}
Funkcie lseek a fseek potrebuju 3 hodnoty: file descriptor alebo stream, pozicia v subore, typ zmeny pozicie.
Pre typ zmeny pozicie su definovane 3 makra:
SEEK_SET - zmena pozicie absolutne - od zaciatku suboru,
SEEK_CUR - zmena pozicie relativne - od aktualnej pozicie,
SEEK_END - zmena pozicie relativne - od konca suboru.

Buffering streamov

Pri pouziti funkcie write s file descriptorom je ziadost okamzite poslana kernelu a zapis je prevedeny pri volani. Pri streamoch je vsak defaultne nastavene, ze data ktore sa zapisu cez fwrite, fprintf a ine, su najprv ulozene do bufferu v pamati a zapisane do suboru su az ked velkost dat v bufferi prekroci iste cislo (fully buffered) alebo ked sa dosiahne znak \n (line buffered). Data su teda pri streamoch zapisovane v blokoch. Ak chceme vyprazdnit buffer a data v nom zapisat do suboru, pouzijeme funkciu fflush(FILE * stream). Ak stream je NULL, vyprazdnene budu vsetky otvorene streamy.

Priklad 8: Pouzitie fflush

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>
int main () {
        printf("text");
        write(1, "abc", 3);
        fflush(stdout);
        write(1, "def\n", 4);
        exit(EXIT_SUCCESS);
}

Keby sme nevedeli o bufferingu streamov, predpokladali by sme ze vystup prikladu bude textabcdef. Kedze stream stdout ma defaultne zapnuty line buffering, pri pouziti printf su data najprv zapisane do bufferu
tohto streamu. Pouzitie funkcie write(1, "abc", 3) ihned vypise retazec "abc", fflush(stdout) nasledne vyprazdni buffer (vypise sa "text") a potom write vypise "def\n".

Ak chcete vypnut stream buffering pre dany stream, pouzite setbuf(FILE * stream, NULL).
Pre kompletne vysvetlenie kontrolovania bufferingu pozri tieto stranky manualu.

Zhrnutie funkcii

int open(const char * filename, int flags[, mode_t mode])
otvorenie suboru, vrati file descriptor

int close(int filedes)
zatvorenie suboru identifikovaneho file descriptorom

ssize_t read(int filedes, void * buffer, size_t size)
citanie size bytov z filedes do buffer, vrati pocet precitanych bytov (ak nebolo mozne ziskat size bajtov), -1 v pripade chyby

ssize_t write(int filedes, void * buffer, size_t size)
zapisanie size bytov z buffer do filedes, vrati pocet zapisanych bytov, -1 v pripade chyby

int lseek(int filedes, long int offset, int whence)
zmeni poziciu vo filedes na offset typom whence

FILE * fopen(char * filename, char * opentype)
otvorenie suboru, vrati stream

int fclose(FILE * stream)
zatvori stream

int fcloseall()
zatvori vsekty otvorene streamy, vrati 0 pri uspechu, EOF pri chybe

FILE * freopen(char * filename, char * opentype, FILE * stream)
kombinacia fclose a nasledne fopen

int getc(FILE * stream)
precita 1 znak zo streamu ako unsigned char a ulozi ho ako int. Ak bol dosiahnuty koniec suboru, vrati EOF

int getchar()
getc zo standardneho vstupu - getc(stdin)

char * fgets(char * s, int count, FILE * stream)
cita zo stream az kym nedosiahne znak noveho riadku \n, alebo ak dosiahne count-1 precitanych znakov. s musi mat velkost count bytov, vysledok ulozeny don obsahuje aj nulovaci znak \0

char * gets(char * s)
tato funkcia cita zo standardneho vstupu az kym nedosiahne znak noveho riadku. Vysledok ulozi do znakoveho pola s. POZOR: nakolko tato funkcia nerobi ziadnu kontrolu o velkosti pola s, je mozny BUFFER OVERFLOW UTOK na toto pole. POUZITIE TEJTO FUNKCIE JE VELMI NEBEZPECNE A NEODPORUCA SA!

size_t fread (void * data, size_t size, size_t count, FILE * stream)
precita count objektov velkosti size do pola data zo stream. Vrati pocet precitanych objektov. Ak sa chyba alebo EOF vyskytne v strede nejakeho objektu, vrati pocet kompletnych objektov a necely objekt zahodi.
Priklad: fread(buffer, 1, 10, stdin) - precita 10 jednobytovych objektov (10 znakov) zo stdin.

site_t fwrite (void * data, size_t size, size_t count, FILE * stream)
zapise count objektov velkosti size z pola data do stream. Vrati pocet uspesne zapisanych objektov.

int fflush(FILE * stream)
vyprazdni buffer streamu. Ak stream je NULL, vyprazdni bufferi vsetkych streamov.

Pre subory vacsie ako 232 bytov (4 GB) sa pouzivaju funkcie so suffixom 64: open64, lseek64, fopen64, freopen64.
Funkcie read64/write64 neexistuju - nie su potrebne, nakolko je malo pravdepodobne ze sa jednym volanim takejto funkcie bude citat/zapisovat viac ako 4 GB dat.

Procesy

Vytvaranie procesov je zaklad dnesnych operacnych systemov. Kazdy proces ma svoj vlastny adresovaci priestor. Proces vykonava program - mozte mat viac procesov vykonavajucich ten isty program, kazdy proces ma svoju vlastnu kopiu tohto programu a vykonava ho nezavisle od ostatnych kopii.
Procesy su zorganizovane hierarchicky. Kazdy proces ma svoj rodicovsky proces (parent process) - proces, ktory ho vytvoril alebo z ktoreho vznikol. Proces vytvoreny z nejakeho procesu sa nazyva dcersky proces (child process). Dcersky proces dedi mnoho vlastnosti po svojom rodicovksom procese.

int system(char * command)
tato funkcia spusti prikaz cez shell /bin/sh. Vrati -1 ak nebolo mozne prikaz vykonat, v opacnom pripade vrati navratovu hodnotu vykonavaneho programu.

Priklad 9: Pouzitie system na vykonanie ls -al a ulozenie vypisu adresara do priklad9out

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main () {
        int status;
        status = system("ls -al >priklad9out");
        printf("status = %i\n", status);
        exit(EXIT_SUCCESS);
}

Kazdy proces ma svoje identifikacne cislo - process id, resp. PID. Zivotnost procesu pominie ked je jeho terminacia oznamena jeho rodicovskemu procesu. Vtedy su vsetky informacie o procese uvolnene. Procesy sa vytvaraju systemovym volanim fork. Dcersky proces vytvoreny s fork je kopia svojho rodicovkeho procesu, az nato, ze ma svoje vlastne PID. Novy forkovany dcersky proces pokracuje s vykonavanim toho isteho programu ako rodicovksy proces na mieste, kde sa vracia volanie fork. Navratova hodnota fork sa pouziva na rozoznaie dcerskeho od rodicovskeho procesu.

Dcersky proces moze potom zmenit program ktory vykonava niektorou s exec funkcii. Program, ktory proces vykonava, nazyva sa image procesu. Zmena imagu procesu sposobi absolutne zabudnutie na predchadzajuci image procesu. Ak novy program exituje, ukonci sa cely program, namiesto vratenia sa k predchadzajucemu procesu.

pid_t getpid() - vrati PID procesu
pid_t getppid() - vrati PID rodicovskeho procesu

pid_t fork()
funkcia vytvory novy proces. Pre dcersky proces vrati hodnotu 0, pre rodicovksy vrati PID dcerskeho procesu. Ak sa nepodari vytvorit novy proces, vrati -1.

Priklad 10: Jednoduche pouzitie fork

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
int main () {
        int pid;
        pid = fork();
        if (pid == -1) {
                perror("fork");
                exit(EXIT_FAILURE);
        }
        if (pid) {
                printf("rodicovksy proces ; PID dcerskeho procesu je %i\n", pid);
        } else {
                printf("dcersky proces\n");
        }
        exit(EXIT_SUCCESS);
}

int execv (char * filename, char * argv[])
funkcia sa pokusi zmenit image procesu na program filename - vykona filename. Argument argv je NULL terminovane pole retazcov, ktore sa pouziju ako argv argument pre main funkciu vykonavaneho programu. Posledny prvok tohto pola musi byt NULL. Prvy prvok tohto pola by mal byt nazov spustacieho suboru programu. Environmentalne nastavenia pre novy image su take iste, ako environmentalne nastavenia sucasneho imagu.

int execve (char * filename, char * argv[], char * env[])
funkcia je podobna funkcii execv, ibaze environmentalne nastavenia si programator urci sam.

int execvp (char * filename, char * argv[])
ak filename neobsahuje znak / (absolutnu cestu), program je hladany v adresaroch, ktorych zoznam je v environmentalnom nastaveni PATH. Uzitocna je na spustanie systemovych utilit.

Na kompletny funkcii zoznam sa mozte pozriet tu.

Priklad 11: Spustenie ls -al s execv.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
int main () {
        char * argumenty[] = {"ls", "-al", NULL};
        execv("/bin/ls", argumenty);
        exit(EXIT_SUCCESS);
}

pid_t waitpid(pid_t pid, int * status, int options)
funkcia ziska status jedneho zo svojich dcerskych procesov a ulozi ho do miesta, na ktore ukazuje smernik status. Vrati PID procesu ktoreho status zistovala. Ak je argument pid nastaveny na konkretne PID, waitpid bude ignorovat ostatne svoje dcerske procesy.
Hodnota WAIT_ANY (-1) v pid hovori, ze waitpid ma zistit status hociakeho zo svojich dcerskych procesov a WAIT_MYPGRP (0) hovori, ze ma zistit status dcerskych procesov ktore su v tej istej procesnej skupine (bude vysvetlene v niektorej z buducich casti) ako volajuci proces.
Argument options je logickym OR scitane bitove pole - vacsinou 0. Prepinac WHOHANG zaisti, ze ak nie je dostupny ziadny dcersky proces, waitpid nebude cakat na ukoncenie jedneho z nich, miesto toho vrati 0. Prepinac WUNTRACED zabezpeci, ze waitpid vrati informaciu o hociakom dcerskom procese ktory bol bud ukonceny alebo stopnuty.

pid_t wait(int * status)
wait(&status) je ekvivalent s waitpid(-1, &status, 0)

Status ktory ulozi waitpid je mozne vyparsovat s tymyto makrami:
int WIFEXITED(status)
vrati nenulovu hodnotu ak bol program normalne ukonceny (s exit)

int WEXITSTATUS(status)
ak WIFEXITED(status) je nenulova hodnota, toto makro vrati dolnych 8 bitov statusu - navratovu hodnotu procesu

int WIFSIGNALED(status)
vrati nenulovu hodnotu ak bol proces terminovany signalom, ktory nebolo mozne vybavit (signaly budu vysvetlene v jednej z buducich casti)

int WTERMSIG(status)
ak WIFSIGNALED(status) je nenulova hodnota, toto makro vrati cislo signalu, ktory proces terminoval

int WCOREDUMP(status)
vrati nenulovu hodnotu ak bol proces terminovany a vyprodukoval core dump

int WIFSTOPPED(status)
vrati nenulovu hodnotu ak bol dcersky proces stopnuty

int WSTOPSIG(status)
ak WIFSTOPPED(status) je nenulova hodnota, toto makro vrati cislo signalu, ktory proces stopol

Priklad 12: Vytvorenie dcerskeho procesu, spustenie programu ping v dcerskom procese, zistenie statusu dcerskeho procesu.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <sys/wait.h>
#include <errno.h>
int main () {
        int pid;
        pid = fork();
        if (pid == -1) {
                perror("fork");
                exit(EXIT_FAILURE);
        }
        if (pid == 0) {
                // dcersky proces - pockame 5 sekund
                sleep(5);
                // a spustime "ping neexistujucihost"
                char * args[] = {"ping", "neexistujucihost", NULL};
                execvp("ping", args);
                // ak sa execvp nepodarilo, vratime hodnotu 50
                exit(50);
        }
        // rodicovsky proces
        int status;
        waitpid(pid, &status, 0);
        if (WIFEXITED(status)) {
                printf("dcersky proces skoncil s navratovou hodnotou %i\n", WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
                printf("dcersky proces bol terminovany signalom cislo %i\n", WTERMSIG(status));
        } else if (WCOREDUMP(status)) {
                printf("dcersky proces vyprodukoval core dump\n");
        } else if (WIFSTOPPED(status)) {
                printf("dcersky proces bol stopnuty signalom cislo %i\n", WSTOPSIG(status));
        }
        exit(EXIT_SUCCESS);
}


Na zaver nieco k dalsej casti - bude o problematike signalov medzi procesmi, cakania na IO, termio - terminalove IO a socketom.