Elementy biblioteki C postanowiłem opisać przed specyfikami bibliotek C++ z paru względów, mianowicie m.in. dlatego, że jej konstrukcja jest o wiele prostsza (choć w rezultacie - co jest normalne w takich przypadkach - jest bardziej toporna w używaniu). W opisie biblioteki iostream będzie parę odwołań do opisanych tutaj rzeczy z biblioteki stdio. Powód tego jest taki, że biblioteka standardowa C++ sama czasem do niej nawiązuje, zwłaszcza że kiedy ją pisano nie sposób było zignorować faktu istnienia stdio.
Większość biblioteki C została napisana dość dawno i zawiera wiele różnych ciekawych i użytecznych funkcji. Postaram się opisać najważniejsze rzeczy, które mogą być potrzebne. Ponieważ nikt również nie płaci za ten papier (tak mi się wydaje :*), opiszę również funkcje, których już się w C++ nie używa (ze względu na istnienie lepszych odpowiedników). Może być to pomocne przy pracy nad kodem w C.
Nie będę się oczywiście tutaj skupiał na zbyt szczegółowym opisie; to będzie tylko taki "overview". Dla dodatowych informacji proszę zapoznać się ze szczegółami w plikach pomocy (na unixach dostępne również pod poleceniem man).
Oto pliki należące do standardowej biblioteki C (włacznie z ISO C 9X). Nie mam całkowitej pewności co do iso646.h, aczkolwiek to są wszystkie zaadoptowane przez C++:
assert.h - makra do asercji ctype.h - klasyfikacje znaków typu char (isspace, isalpha itd.) errno.h - deklaracja errno fenv.h - środowisko dla liczb zmiennoprzecinkowych (ISO C 9X) float.h - definicje specjalne dla liczb zmiennoprzecinkowych limits.h - makra określające granice dla typów ścisłych locale.h - definicje lokali math.h - funkcje matematyczne setjmp.h - funkcje setjmp i longjmp signal.h - sygnały stdarg.h - narzędzia dla funkcji o zmiennej liście argumentów stddef.h - standardowe definicje (ptrdiff_t i size_t głównie) stdio.h - operacje wejścia/wyjścia stdlib.h - zespół funkcji użytkowych string.h - funkcje operujące na tablicach znaków time.h - narzędzia do odczytywania, interpretacji i prezentacji czasu wchar.h - obsługa "szerokiego" (wide-char) zestawu znaków wctype.h - wersja `ctype.h' dla szerokich znaków
W C++ należy używać nie nazw tu wymienionych, tylko odpowiedników bez końcówki `.h', poprzedzone literą `c', np. nie `math.h', ale `cmath'. Poniższe już nie są objęte tą regułą (tzn. nie ma dla nich wrapperów C++; używa się na własne ryzyko):
complex.h - dodatkowe narzędzia dla liczb zespolonych (w C++ niejednoznaczne; większość kompilatorów C++ ma własny `complex.h' z uwagi na zgodność wsteczną z ARM, przez co ten staje się niedostępny) inttypes.h - konwersje formatu dla liczb całkowitych iso646.h - ten nagłówek jest trochę dziwny; istnieje do niego co prawda odpowiednik ciso646, ale w iso646.h na początku jest #ifndef __cplusplus stdbool.h - definicja typu bool (niezdatne w C++) stdint.h - definicje typów w rodzaju int8_t, int32_t, int64_t itd. tgmath.h - generyczne typy matematyczne
Poniższe nie należą do ISO C 9X i są specyficzne dla unixa, niemniej przedstawię ich zawartość, gdyż opisane tam funkcje mogą być przydatne (na windowsach niektóre z tych funkcji istnieją, ale jest z tym różnie):
dirent.h - obsługa katalogów fcntl.h - funkcje open i fcntl wraz z odpowiednimi definicjami flag unistd.h - standardowe funkcje UNIXa sys/select.h - funkcja select
Oczywiście nie będę tutaj opisywać wszystkich funkcji bibliotecznych, a jedynie co niektóre.
Obsługa błędów w funkcjach systemowych UNIX-a jest zunifikowana. Jednak korzysta z niej wiele funkcji standardowych; opiszę więc tutaj, jak to wygląda.
Różne są sposoby powiadomienia użytkownika, że wykonanie funkcji się nie powiodło. Funkcje te jednak (będę zaznaczał, które) w razie takiego błędu, ustawiają jeszcze jego numer, aby uzyskać zunifikowany "kod błędu". Numer ten przechowywany jest w zmiennej `errno'.
Proszę oczywiście określenia `zmiennej' nie traktować zbyt serio! Zewenętrznie oczywiście zachowuje się jak zmienna, ale aby mieć do niej dostęp należy wczytać plik nagłówkowy `errno.h'. W żadnym wypadku nie wolno deklarować jej jako `extern int errno' (co można spotkać w wielu programach) !
Nagłówek ten udostępnia również maksymalną wartość liczbową kodu błędu (sys_nerr), jak również tablicę z komunikatami, odpowiadającymi konkretnemu błedowi (sys_errlist), będąca wielkości sys_nerr. Jednak zaleca się korzystanie z odpowiednich funkcji:
Numery błędów są oznaczone odpowiednimi stałymi, które są zebrane zazwyczaj w pliku sys/errno.h (na niektórych systemach, zwłaszcza na tych najbardziej zabałaganionych jak np. Linux, w tym pliku będzie tylko odniesienie - czyli wczytanie dyrektywą #include - innego pliku; tu akurat będzie to asm/errno.h lub bits/errno.h zależnie od wersji i dystrybucji). Jedne z najczęstszych to:
O 'assert' wspomnę tylko zlekka. Otóż jest to nie funkcja, lecz makro i jego definicja zależy od zdefiniowania makra NDEBUG. Jeśli takie makro jest zdefiniowane, to assert jest pustym makrem. Właśnie dlatego nie wolno w nim umieszczac żadnych wyrażeń, które sa afektywne! Takie wyrażenia należy wykonać wcześniej, a do assert podawać tylko zapamiętane wyniki.
Zachowanie się makra assert zależy od implementacji. Implementacje takoż różnią się kwestią czy program należy całkiem wysypać, czy tylko rzucić ostrzeżeniem; jak również czy wypisać to na strumieniu, czy wyświetlić okienko z informacją.
Ta biblioteka zawiera operacje na plikach, w tym również na konsoli. Typem danej, na którym się tutaj operuje jest FILE. Jest to struktura, do której nie warto zaglądać (może być na każdym systemie zdefiniowana inaczej, aczkolwiek nie ma problemu; w C NIE DA się niczego ukryć). Wiemy tylko tyle, że na niej operuje większość podanych tu funkcji. Oczywiście należy pamiętać, że ową strukturę otrzymujemy od funkcji fopen przy otwieraniu pliku i przekazać musimy do fclose chcąc go zamknąć (oczywiście przez wskaźnik).
Przede wszystkim, na razie skorzystamy z trzech podstawowych strumieni plikowych, które są predefiniowane (jako stałe typu FILE*):
stdin - "standardowe wejście" stdout - "standardowe wyjście" stderr - "standardowe wyjście diagnostyczne"
Przy uruchamianiu programów na konsoli, stdin to wejście z klawiatury, a stdout i stderr są kierowane na ekran. Oczywiście przy uruchamianiu programu można dokonać przekierowania, jeśli wywołujemy program przez polecenie w linii komend ( < dla stdin i > dla stdout; unixowe powłoki pozwalają też na przekierowanie stderr, np. w sh i pochodnych jest to 2>).
1) funkcje podstawowe
Najprostszą funkcją wypisującą cokolwiek na danym strumieniu jest fputs o następujących argumentach (trzeba przyznać, że ścisłość schematów funkcji jest w C dość - delikatnie mówiąc - zadziwiająca):
fputs( napis, strumien );
Dla stdout jest krótsza forma, puts, która wymaga tylko pierwszego argumentu.
Można też wysłać tylko jeden znak przy pomocy funkcji fputc (o podobnych argumentach). Funkcja putc też istnieje, ale jest właściwie odpowiednikiem fputc (w dzisiejszych czasach istnieje praktycznie tylko ze względu na kompatybilność i nie zaleca się jej używać), natomiast odpowiednikiem fputc( c, stdout ) jest putchar( c ).
Istnieją podobne funkcje do wczytywania ze strumienia. Pojedyncze znaki wczytuje się funkcją fgetc (argumentem jest tylko strunień, a znak jest zwracany jako wynik). Podobnie jak przy fputc, odpowiednikiem fgetc z argumentem stdin jest getchar() (bez argumentów). Należy jednak pamiętać, że konsola ma zazwyczaj liniowy (czyli buforowany liniowo), a nie znakowy sposób wczytywania znaków (na unixie można to zmienić); toteż nawet jeśli oczekuje się wprowadzenia jednego znaku, to należy po jego wprowadzeniu klepnąć Enter.
Wczytywany jest oczywiście znak (a więc dana typu char), natomiast funkcje te zwracają int. Jest to użyteczne, gdyż część typu int nie używana przez char pozostaje w ten sposób zawsze zerem. Umożliwia to stwierdzenie końca pliku, co jest sygnalizowane przez zwrócenie jako wyniku specjalnej stałej EOF; nie odpowiadającej żadnemu znakowi. Oczywiście koniec pliku można też sprawdzić wywołując funkcję feof() dla danego strumienia.
Znak po wczytaniu może zostać "cofnięty"; używa się w tym celu funkcji ungetc( znak, strumien ). Aby nie wprowadzać zamieszania, należy jednak jako argument podawać ten znak, który ostatnio wprowadzono.
Można wczytać również całą linię. Służy do tego funkcja `fgets':
fgets( tablica, ile, strumien );
Istnieje również funkcja `gets', która posiada tylko pierwszy z tych argumentów (strumieniem wejściowym jest stdin). Jednak nawet nagłówek z GNU C ostrzega wyraźnie: NIE UŻYWAJ TEJ FUNKCJI! Nie jest niczym ograniczona ilość pobieranych znaków (nawiasem dodam, że funkcja ta umożliwiła wiele włamów na serwery sieciowe, słynny tzw. "buffer overflow").
Jednymi z najbardziej przydatnych funkcji obsługujących "porcje" danych są fread i fwrite. Mają dość podobne nagłówki:
size_t fread( void* ptr, size_t s, size_t n, FILE* stream ); size_t fwrite( const void* ptr, size_t s, size_t n, FILE* stream );
Funkcja fread wczytuje ze strumienia `n' elementów wielkości `s' do tablicy podanej jako `ptr'. Funkcja fwrite - zapisuje, wedle tych samych reguł. Wartością zwracaną jest ilość poprawnie wczytanych lub zapisanych elementów (nie znaków!), czyli jest <=n. Nie da się oczywiście odróżnić błędu operacji od końca pliku; należy to sprawdzić przy pomocy feof() i ferror().
2) formatowane wyjście i wejście
Ponieważ często na strumieniu wypisuje się skomplikowane dane, wymaga się formatowania. Najprostszą funkcją jest tutaj - znana pewno wielu - printf:
int printf( const char* format, ... );
Funkcja ta wypisuje na standardowym wyjściu (stdout) sformatowany napis. Pierwszym argumentem jest napis formatujący, który m.in. zwykł zawierać odpowiednie znaczniki, rozpoczynające się znakiem `%'. Na każdy taki znacznik (tzn. prawie na każdy; istnieją takie znaczniki, które mają trochę bardziej skomplikowane znaczenie) funkcja oczekuje jednej danej, którą pobierze z listy argumentów podanej po tym napisie:
Przed taką literą może wystąpić jeszcze litera będąca modyfikatorem dla typu int: `h' oznaczające short i `l' oznaczające long. Zatem %hu będzie oczekiwało typu unsigned short int.
%e, %f, %g - wypisanie liczby typu double. Znacznik `e' wypisuje w postaci naukowej, `f', w postaci zwykłej, natomiast `g' w jednej lub drugiej, w zależności od tego, co będzie lepiej pasować. Litery e i g mogą być też duże, co wymusi dużą literę E w notacji naukowej.
Oczywiście - czego się można domyślić - żeby zażądać umieszczenia znaku % na wyjściu, należy po prostu napisać go dwukrotnie.
Jedynym znacznikiem o specjalnym znaczeniu, który powoduje pobieranie raczej, niż wysyłanie danych, jest %n. Oczekuje się w tym miejscu wskaźnika (!) na wartość typu int. Pod wskazany adres wpisywana jest ilość znaków dotychczas wypisanych.
Ponieważ kwestie te mogą być przydatne, opiszę jeszcze dodatkowe elementy formatowania, których oznaczenia umieszcza się w znacznikach (nie wszystkie oczywiście). Domyślne wartości stanowią, że:
Zatem umieszczenie (bezpośrednio lub jako kolejny argument znacznika) po znaku `%' wymienionych znaków oznacza:
Zalecam dużą ostrożność przy korzystaniu z tej rodziny funkcji. Typy podane w znacznikach i w argumentach muszą (!) się zgadzać, a tej poprawności kompilator nie jest w stanie sprawdzić (GNU C++ jedynie ma taki bajer, że rozpoznaje rodzinę funkcji printf po nazwie i ostrzega przy źle podanych typach - istnieje też możliwość zarejestrowania dowolnej funkcji jako printfo-podobnej). Jeśli do funkcji będzie brakować wymaganych argumentów, funkcja się o tym nie dowie i potraktuje jakiś fragment pamięci jako dane. Będę to jeszcze dokładniej omawiał przy funkcjach o zmiennej liście argumentów (stdarg).
W tej samej konwencji, co printf napisane są funkcje:
sprintf( char* buf, const char* fmt, ... );
fprintf( FILE* strumien, const char* fmt, ... );
Istnieją również funkcje o tych nazwach poprzedzonych `v'. Tak naprawdę, dopiero one są tymi "właściwymi" funkcjami, bo te powyższe to są tylko wrappery (tak dokładnie to "matką" wszystkich funkcji printfo-podobnych jest vsprintf). Różnią się od powyższych tym, że w miejscu `...', mają one normalny argument, którym jest zainicjalizowana zmienna lista argumentów (pokażę to dokładniej przy stdarg).
Ostatnie standardy (czyli ISO C 9X i Unix 98) dodają jeszcze funkcję snprintf. Po wskaźniku na tablicę otrzymuje jeszcze argument typu size_t, oznaczający rozmiar tej tablicy. Naprawia to mały błąd funkcji sprintf, polegający na braku kontroli wielkości zapisywanej tablicy.
Rozszerzenie GNU udostępnia jeszcze funkcję asprintf (która sama przydziela odpowiednią tablicę malloc'iem a pierwszy argument jest wskaźnikiem na zmienną wskaźnikową, do której ten adres jest wpisywany), oraz dprintf (której pierwszym argumentem jest deskryptor pliku - omówię to za chwilę).
Wszystkie te funkcje jako wynik zwracają ilość poprawnie zwartościowanych argumentów.
Podobnie, jak formatowane wyjście, mamy też formatowane wejście. Choć to jest może drobna przesada z możliwościami (w porównaniu ze zwykłym prostym pobraniem argumentu), ale często się przydaje. Oto funkcja scanf:
int scanf( const char* fmt, ... );
Funkcja ta wczytuje pierwszy argument ze standardowego wejścia, rozkłada go na czynniki pierwsze według schematu podanego w napisie formatującym, po czem "rozparsowane" elementy wpisuje do podanych zmiennych. Proszę się oczywiście nie przerażać - najczęstszą postacią łańcucha formatującego jest "%i" lub "%f".
Funkcja scanf ma podobne (tzn. dające się skojarzyć), ale o innym znaczeniu flagi znaczników. Oczywiście wiele flag było w printf rozróżnianych ze względu na odmienny sposób wyprowadzania i w przypadku scanf ich znaczenie jest identyczne. Zatem X jest tym samym, co x, natomiast e, E, g i G - tym samym, co f. Podobnie jak w printf, `%%' oznacza, że w polu wejściowym ma się znaleźć znak `%'. Podobnie jak tam również, argumenty mogą być poprzedzone `l' i `h' wymuszające odpowiednie typy long i short. Ważna jest również flaga `*', która oznacza, że dany argument jest oczekiwany w polu wejściowym, ale ma zostać zignorowany. Można też przed znacznikiem podać liczbę określającą maksymalną ilość znaków oczekiwanych dla danego elementu (domyślnie "w nieskończoność"). Następnie:
W języku C ta funkcja jest często używana do pobierania liczb ze strumienia, gdyż jest to jedyna możliwość; toteż często podaje się jako argument zmienną z pobraniem jej adresu. Tu ostrzegam jeszcze mocniej, niż przy printf - funkcja ta jest jeszcze bardziej od niej niebezpieczna; nie sprawdzi bowiem, czy użytkownik nie zapomniał o znaku `&' przed nazwą zmiennej. Na dodatek znacznik `%s' (i jest to też możliwe dla `%c') nie wymaga tego znaku, bo tablice są typowo równoważne wskaźnikom.
Przypominam też, że - jak to napisałem - funkcja pobiera pierwszy ARGUMENT ze standardowego wejścia, czyli wszystko do pierwszego białego znaku (tabulacji lub spacji). Do pobierania całej linii służy inna funkcja (wymieniona wcześniej).
Podobnie jak w przypadku `printf' istnieją funkcje fscanf, sscanf, vsscanf itd.. Tutaj funkcja sscanf bardzo się przydaje. Zresztą, mimo istnienia iostream, w C++ funkcji sprintf (czy raczej snprintf) i sscanf nadal często się używa (dlatego też wprowadzono rozszerzenie w GNU C++, które udostępnia te właściwości dla iostream, aczkolwiek usunieto je juz od wersji 3.0).
3) deskryptor pliku
Oczywiście, funkcje obsługi plików z tej biblioteki obsługują tzw. buforowanie. Oszczędza się dzięki temu na wywołaniach funkcji systemowych, które zajmują więcej czasu, niż wywołania funkcji użytkownika. Struktura FILE zatem określa buforowanie (z którego można oczywiście zrezygnować), ale jedną z najważniejszych rzeczy przy kojarzeniu pliku jest deskryptor.
Deskryptor jest to liczba całkowita, skojarzona z plikiem. Za jej pośrednictwem funkcje systemowe operują na fizycznym pliku. Jest on zawarty w strukturze FILE. Można go też z tej struktury pobrać funkcją fileno(). Funkcje te będą omówione przy nagłówkach UNIX-owych (to, że unixowe nie oznacza bynajmniej, że nie ma ich na innych systemach!).
Standard (tu drobny ukłon w stronę Qrczaka) nie zakłada co prawda istnienia deskryptora pliku jak i wielu funkcji z niego korzystających, tak też wszelkie odniesienia do deskryptorów plików mają sens tylko w przypadku systemów Unixo-podobnych. Takoż, wszelkie operacje odwołujące się do systemu plików nie będą przenośne w ramach standardu języka C++, a co najwyżej w ramach POSIX-a.
Oczywiście jeśli używa się funkcji stosujących buforowanie, odradzam stanowczo mieszanie w plikach za pośrednictwem deskryptora; może to spowodować bardzo niebezpieczne efekty pomieszania sterowania wskaźnikiem do pliku.
Zaznaczę jednak zrazu dla ciekawostki, że strumienie stdin, stdout i stderr mają ZAWSZE deskryptory o numerach 0, 1 i 2. Te deskryptory są dostępne ZAWSZE w każdym programie zaraz po uruchomieniu (oczywiście, można je zamknąć).
4) obsługa plików
Możemy stworzyć własny strumień, który będzie skojarzony z plikiem. Jest to obiekt dynamiczny typu FILE, a więc trzymany przez wskaźnik. Zwraca go funkcja fopen o takiej postaci:
FILE* fopen( const char *path, const char *mode );
Tryb dostępu jest to napis, w którym występują odpowiednie oznaczenia, definiujące ów sposób dostępu (np. "r+b"). Oto one:
Argument `mode' pochodzi z dawnych czasów, kiedy na UNIX-ach istniały tylko funkcje systemowe open i creat (do otwierania pliku istniejącego i do tworzenia pliku); wtedy open przyjmowała podobne argumenty. Obecnie funkcja open przejęła wszystkie czynności związane z otwieraniem pliku, a wszelkie flagi dotyczące trybu otwarcia są podawane jako flagi bitowe. Spowodowało to oczywiście małe zamieszanie; w rezultacie lepiej do takiej postaci open dostosowana jest biblioteka iostream.
Istnieją jeszcze trochę podobne funkcje:
Funkcje fopen, fdopen i freopen zwracają wskaźnik do struktury FILE, jeśli kojarzenie się powiodło. W razie błędu zwracane jest 0 (jak to się mówi, NULL), a errno przyjmuje wartość błędu (najczęściej ENOENT).
Jeśli kończymy korzystanie ze strumienia, należy go zamknąć; służy to tego funkcja fclose. Zamyka ona strumień i niszczy podany obiekt typu FILE.
5) buforowanie strumienia
Jak wspomniałem, strumień FILE to strumień buforowany. Zatem operacje odczytu i zapisu są najpierw notowane w buforze i dopiero w przypadku zapełnienia całego bufora lub skończenia się w nim danych (również w przypadku zamykania strumienia), wykonuje się odpowiednią operację przez funkcje systemowe. Właściwie najbardziej nas owa kwestia interesuje podczas zapisu pliku; wtedy "dorównanie" przez fizyczną postać pliku postaci zapisanej w buforach, nazywa się `synchronizacją'. Zaznaczę od razu, że mówię tu o synchronizacji SYSTEMOWEJ, a nie SPRZĘTOWEJ; na współczesnych systemach bowiem mamy do czynienia z buforowaniem na dwóch poziomach: systemowym, który zapewnia FILE i sprzętowym, który zapewnia system. Dopiero sprzętowa synchronizacja doprowadza do właściwej postaci plik zapisany na urządzeniu zewnętrznym (zwracam tu uwagę, że w wielu współczesnych systemach operacyjnych system plików jest połączony z zarządzaniem pamięcią, zatem w pliku na dysku może być fizycznie to, co w nim powinno być nawet dopiero w momencie zamykania systemu). Archaiczne systemy, jak np. DOS, nie posiadają oczywiście buforowania systemowego i tam jest tylko jeden poziom buforowania.
Żeby więc zsynchronizować (względem systemu) dany plik, należy wywołać funkcję `fflush' dla danego strumienia (zaznaczę od razu, że na UNIXie synchronizację z kolei sprzętową wykonuje się poleceniem sync lub funkcją systemową sync(); powoduje ona opróżnienie wszystkich systemowych buforów).
Można też ustawić własne buforowanie. Wykonuje to funkcja setbuf:
void setbuf( FILE* stream, char* buf );
Nie mamy oczywiście możliwości ustawiania wielkości bufora! Bufor jest zawsze wielkości BUFSIZ (a przynajmniej procedury tak myślą :*).
6) Jeżdżenie po pliku
Kiedy operujemy na pliku, mamy coś takiego jak wskaźnik bieżącej pozycji w pliku. Przesuwa się ona do przodu za każdym razem po wykonaniu jakiejś operacji, która na niego wpływa (tzn. czytaniu lub pisaniu). Możemy ją jednak zapamiętywać i dowolnie ustawiać. Służą do tego następujące funkcje:
int fseek( FILE* stream, long int offset, int whence ); long int ftell( FILE* stream ); void rewind( FILE* stream );
Funkcja ftell odczytuje i zwraca bieżącą pozycję pliku (względem początku oczywiście). Z kolei funkcja rewind "przewija" plik, czyli ustawia się na jego początku (odpowiada fseek( stream, 0, SEEK_SET ) ).
7) Funkcje dodatkowe
Mamy w tej bibliotece jeszcze parę drobnych funkcji odpowiadających właściwie zazwyczaj poleceniom powłoki. Chyba nie muszę wyjaśniać, co oznaczają:
int remove( const char* ); int rename( const char*, const char* );
char* tmpnam( char* );
8) Przekierowywanie procesów
Ten zestaw funkcji nie istnieje w standardzie, a jedynie na systemach unixowych (tzn. dokładnie to implementują je systemy zgodne z POSIX2, BSD lub System V). Ich obsługa jest właściwie identyczna jak plików, różnice są tylko w ich otwieraniu i zamykaniu.
FILE* popen( const char* command, const char* mode );
Zwracana struktura jest normalnym strumieniem, na którym można wykonywać określone operacje. Po zakończeniu korzystania z potoku należy go zamknąć, używając w tym celu funkcji pclose (argument jak fclose).
Występują tutaj funkcje o różnych zastosowaniach. Omówię je więc w poszczególnych kategoriach.
1) Interpretacja liczbowa argumentu tekstowego
Jest to grupa funkcji najczęściej używana do interpretacji argumentów funkcji main. Najprostsze to:
int atoi( const char * ); long atol( const char* ); double atof( const char* );
Nieco bardziej skomplikowane są następujące funkcje. Argument `endptr' jeśli nie jest zerem, wskazuje na zmienną wskaźnikową, do której wpisywany jest wskaźnik na pierwszy znak, który przez funkcję już nie został zinterpretowany.
double strtod( const char *nptr, char **endptr ); long strtol( const char *nptr, char **endptr, int base ); unsigned long strtoul( const char *nptr, char **endptr, int base );
W tych funkcjach argument base dodatkowo oznacza system liczbowy, w jakim dana wartość ma być zinterpretowana i może przyjmować wartości od 2 do 36 lub mieć specjalną wartość 0. W przypadku base == 16 lub base == 8, liczba może mieć nagłówek odpowiednio 0x i 0. Jeśli base == 0, liczba jest interpretowana jako szesnastkowa (jeśli ma nagłówek 0x), ósemkowa (jeśli ma nagłówek 0) lub dziesiątkowa (jeśli nie ma żadnego z tych nagłówków).
2) Generator liczb losowych
Tutaj mamy właściwie tylko dwie funkcje:
int rand( void ); // zwraca liczbę losową z przedziału <0, RAND_MAX> void srand( unsigned int ); // inicjalizuje generator
3) Przydział i zwalnianie pamięci
Funkcje służące do przydziału pamięci (w C) to:
void* malloc( size_t ); // "Allocate Memory" void* calloc( size_t, size_t ); // "Allocate and Clear" void* realloc( void*, size_t ); // "Reallocate"
Pamięć można oczywiście zwolnić przez:
void free( void* );
Na linuxie np. jest tak nie zawsze. W dodatku istnieje tam funkcja (wzięta z SVID2 i XPG4) zwana `mallopt', która pozwala na określanie, ile minimalnie pamięci musi być żądane do zwolnienia, żeby program oddał ją do dyspozycji systemowi, ile minimalnie pamięci jest żądane od systemu (tzn. jeśli jest to np. 1kB, to po żądaniu np. 4 bajtów pamięci malloc przydzieli 1kB, 4 odda do dyspozycji, a pozostała wielkość będzie dla przyszłych malloc'ów) i tak dalej. I standardowo są ustawione tam jakieś sensowne wartości (niestety nie pamiętam, jakie; zresztą - jak zauważyłem - różnią się nawet w różnych dystrybucjach). Jednak należy dobrze się zapoznać ze sposobem współpracy procesów z systemem w dziedzinie przydziału i zwalniania pamięci, gdyby zamierzało się conieco szaleć z ilością pamięci; jeśli takiej współpracy nie ma w ogóle, proszę się bardzo ostrożnie obchodzić z przydziałem pamięci, bo to oznacza, że system odzyska pamięć dopiero gdy proces się zakończy; niekiedy więc może ją zechcieć spróbować odzyskać "na chama" (zwykle zresztą w wyniku odpowiedniej interwencji administratora systemu :*).
4) Przerywanie programu
Te dwie funkcje służą do natychmiastowego opuszczenia programu:
void exit( int ); void abort( void );
Funkcja abort oznacza totalnie nienornalne zakończenie programu. Na systemach unixowych w dodatku powoduje (jeśli możliwe) zrzut zawartości pamięci (porównaj SIGABRT, który będzie opisany przy sygnałach).
Następująca funkcja z kolei:
int atexit( void (*)( void ) );
W C funkcja exit jest dość popularna, z kolei w C++ prawie NIGDY nie wolno jej używać (no chyba, że nie używało się właściwości dodatkowych C++ i w tym stwierdzeniu mieści się też m.in. niezamykanie otwieranych strumieni iostreamowych). Nie oznacza to jednak, ze C++ nie posiada mechanizmu, które takie rzeczy umożliwiają - będzie to opisane przy wyjątkach.
Z funkcji abort z kolei - jak się niedługo dowiemy - korzysta w C++ jedna z funkcji systemowych, ale dopiero po obsłużeniu wszystkiego, co niezbędne (co też zostanie opisane przy wyjątkach).
5) Obsługa środowiska
char* getenv( const char *name ); int setenv( const char *name, const char *value, int replace ); void unsetenv( const char *name ); int putenv( const char *string );
Dość zunifikowaną postać prezentuje `putenv' (o ile mi wiadomo nie istnieje w standardzie, ale jest fajna:*). Wykonuje to samo, co setenv pod warunkiem, że podany napis będzie postaci "NAZWA=WARTOŚĆ". Jeśli nie będzie `=', putenv usunie ze środowiska zmienną o podanej nazwie.
Oczywiście nie zapominajmy o trzecim argumencie funkcji main - envp. Jest to tablica tablic napisowych zawierających zmienne środowiskowe (w postaci "NAZWA=WARTOŚĆ").
Warto też wspomnieć o funkcji system:
int system( const char* );
6) Funkcje matematyczne dla liczb całkowitych
Z najprostszych:
Poniższe funkcje są już trochę bardziej skomplikowane i obsługują tablicę składającą się ze wskaźników (do dowolnego typu).
typedef int (*compar_fn_t)( const void*, const void* );
void* bsearch( const void* key, const void* base, size_t nmemb, size_t size, compar_fn_t compar );
void qsort( void* base, size_t nmemb, size_t size, compar_fn_t compar );
Zajmiemy się najpierw sygnałami. Sygnał jest to specjalny, wyjątkowy komunikat wysłany przez system do programu (co prawda nie wszystkie systemy umieją wysyłać sygnały, jest to też bodajże domena POSIX-a; jeśli nie, to co najwyżej program może go wysłać sam do siebie). Wysłanie tego sygnału do programu powoduje (zazwyczaj) natychmiastowe przerwanie wykonywania programu - chyba, że zażądano jego zignorowania (nie każdy się da oczywiście :*).
Funkcji `signal' chyba nie muszę już przedstawiać :*). Ma ona następujące argumenty:
signal( numer_sygnału, sposób_obsługi );
Argument numer_sygnału jest jedną z wartości symbolicznych zebranych - normalnie (czyli nie dotyczy to linuxa :*) - w pliku sys/signal.h (na linuxie bywa różnie, ostatnio znalazłem w bits/signum.h; tam zazwyczaj należy się zwykle przegrzebywać przez sterty dyrektyw #include i trzeba mieć trochę nosa; zwłaszcza, że sys/signal.h zawiera tylko `#include <signal.h>/ :*)) - bardzo przydaje się tu grep). Oto spis najważniejszych sygnałów. Większość powoduje natychmiastowe zakończenie programu, w przypadku innych standardowych reakcji będzie to oznaczone. Niektóre powodują zrzut zawartości pamięci, co będzie oznaczone jako (zrzut):
SIGHUP - odcięcie od terminala sterującego SIGINT - przerwanie (zazwyczaj naciśnięcie Ctrl-C na terminalu) SIGQUIT - wyjście z programu (zrzut) (osiągalne przez Ctrl-\) SIGABRT - abort, inwokowany przez funkcję abort (zrzut) SIGFPE - wyjątek zmiennoprzecinkowy; również dzielenie przez zero (zrzut) SIGKILL - zabicie programu; sygnał nieprzechwytywalny SIGTERM - żądanie zakończenia programu SIGSEGV - naruszenie ochrony pamięci (zrzut) SIGUSR1, SIGUSR2 - sygnały do zdefiniowania przez użytkownika SIGPWR - krytyczny stan zasilania SIGALRM - upłynął czas nastawiony przez alarm() (w sekundach) SIGCHLD lub SIGCLD - proces potomny zakończył działanie.
Tu uwaga - jeśli zamierzasz korzystać z procesów potomnych: Standardowo sygnał SIGCLD oznacza, że proces potomny się zakończył, pozostały po nim "zwłoki" i te "zwłoki" są właśnie do dyspozycji (funkcja wait() pobierze kod powrotny takiego procesu i "zwłoki" zostaną usunięte). Nie wpływa to oczywiście na proces macierzysty, który otrzymał sygnał. Jeśli Cię kod powrotny procesu nie interesuje, ustaw ignorowanie tego sygnału, w przeciwnym razie owe "zwłoki" zaczną się piętrzyć jeśli się nimi nie zajmiesz. Jeśli ustawisz jakąś szczególną obsługę tego sygnału, to jego kod powrotny będzie dostępny po wykonaniu wait(), która w tym przypadku - wbrew nazwie - na nic nie będzie czekać.
Argument sposób_obsługi jest najczęściej funkcją przyjmującą argument int (wpisywany jest do niego numer sygnału), a dana funkcja zostanie wywołana w momencie nadejścia sygnału. Można też ustawić jedną z dwóch następujących wartości: SIG_DFL - domyślny sposób obsługi sygnału oraz SIG_IGN - zignorowanie sygnału
Mamy jeszcze w zestawie wiele interesujących funkcji (jak np. obsługujące maski dla sygnałów w danym procesie), ale poprzestanę tylko na tych:
raise( sygnał ); kill( pid_procesu, sygnał );
Należy jednak pamiętać o następujących rzeczach:
Z powodu tej drugiej rzeczy trudno jest przerywać wykonywanie funkcji, która nadaje się tylko do przerwania. Dlatego obmyślono skoki długie.
int setjmp( jmp_buf env ); void longjmp( jmp_buf env, int val );
Po wywołaniu longjmp, przywracane jest środowisko zapamiętane w `env' i następuje tak jakby "powrót" z setjmp, z tym że tym razem zwraca ona wartość podaną jako drugi argument w longjmp (tylko nie 0; w przypadku podania w longjmp zera setjmp zwraca 1).
Funkcje setjmp i longjmp są z natury bardzo niebezpieczne; w C++ stanowczo odradza się korzystanie z tych funkcji mniej więcej z tych samych powodów (i również to samo stosuje się w zastępstwie), co exit i abort. C++ posiada dużo bezpieczniejsze i wygodniejsze narzędzia jakim są wyjątki. Osobiście mam nawet pozytywne doświadczenia z podpinania wyjątków do sygnałów.
W tej części zostanie opisane, jak obsługiwać funkcje o zmiennej liście argumentów. Używanie ich to niestety dość `hackerskie' podejście, które w C++ powinno być raczej - z racji istnienia w nim lepszych rozwiązań - minimalizowane.
Funkcja o zmiennej liście argumentów musi mieć jakiś argument jawny (w C++ nie musi, ale taka konstrukcja służy do czego innego i praktycznie tylko w połączeniu z extern "C") a na końcu listy umieszcza się element `...'. Przykładów takich funkcji mieliśmy już kilka, jak choćby printf. Nagłówek stdarg.h zawiera narzędzia pozwalające obsługiwać te dodatkowe argumenty.
Wewnątrz funkcji, należy najpierw zadeklarować sobie zmienną, która będzie stanowić początek listy argumentów niejawnych:
va_list ap;
va_start( ap, last );
va_arg( ap, typ );
Standard podaje jeszcze jedno makro, `va_end( ap )' i podobno musi ono wystąpić po zakończeniu przeglądania listy argumentów. Choć na wszystkich widzianych przeze mnie kompilatorach jego deasygnatem jest `(void*)0', co już w ogóle nie wygląda mi na instrukcję, to jednak radzę go używać. Być może w niektórych implementacjach będzie trzeba np. zwolnić jakąś pamięć. Zatem trzymanie się zasad przenośności może wymagać użycia tego makra.
Zwracam uwagę, że w funkcjach o zmiennej liście argumentów mamy informacje o typach jawnych i początku obszaru pamięci, która - BYĆ MOŻE - zawiera oczekiwane przez nas dane. Nie ma ogólnie absolutnie ŻADNEGO mechanizmu, który możnaby było użyć w celu sprawdzenia, jakiego typu obiekty przekazano do funkcji i ile ich przekazano. Pod tym względem (jeśli założymy, że wszystkie obiekty są tego samego typu) przypomina to tablicę o nieznanym rozmiarze (np. przekazaną do funkcji przez wskaźnik); musimy albo ten rozmiar (lub jego symbol) dodatkowo podać, albo podać daną mającą oznaczać koniec argumentów. W sytuacji, kiedy każda dana może być innego typu, nie będzie danej "uniwersalnej" i lepiej jest zrobić łańcuch formatujący w stylu printf. Zresztą jeśliby nawet funkcja założyła, że wszystkie elementy są jednego typu, to kompilator i tak nie jest w stanie niczego takiego zagwarantować. Typ va_list jest to najczęściej... void* (choć kompilatory mające własne dodatkowe wspomaganie statycznego systemu typów - np. GNU C++ - mają własny, wewnętrzny deasygnat takiego aliasa, aczkolwiek to i tak niewiele pomaga).
I jeszcze jedno – przez "..." nie da się przekazać żadnego typu innego, niż ścisły (czy inaczej POD-type).
Nagłówek ten zawiera zestaw funkcji obsługujących tablice typu char, czyli napisowe. Jak wiemy, w takich tablicach obowiązuje zasada, że koniec napisu oznaczany jest bajtem zerowym. Wiele tych funkcji wykonuje różne, dość skomplikowane czynności, jak np. parsowanie na elementy, znajdowanie nagłówków itd., jednak opiszę tutaj tylko najważniejsze.
dest +
strlen( dest )
' (gdyby wartość zwracana przez strcpy nie była tak
idiotycznie dobrana, funkcja ta nie byłaby w ogóle potrzebna; zademonstruję
to przy właściwościach obiektowych)
Wiele z tych funkcji posiada jeszcze wariant "z `n'", a w nazwie owo `n' znajduje się za `str' (czyli np. strncpy). Te funkcje posiadają jeszcze dodatkowy argument, który oznacza maksymalną wielkość tablicy, do której zapisują (pomaga kontrolować zakresy tablicy).
Dodatkowo można jeszcze wyszukać w tablicy określony znak (strchr, również od końca - strrchr) lub napis (strstr). Dostępne jest również porównywanie dwóch tablic char (strcmp, która jest funkcją spełniającą warunki dla opisanych wyżej bsearch i qsort).
W tym pliku istnieją też surowe operacje na pamięci o trochę podobnych nazwach: memcpy, memstr, memcmp itd., ale jedną z najważniejszych jest memset, która powoduje zapisanie wycinka pamięci odpowiednią wartością (często używana do wyzerowania jakiegoś obszaru pamięci, zwłaszcza, że jest dość szybka). W C++ jednak odradzam stanowczo jej używanie (tzn. można używać, ale z rozsądkiem; jeśli chcemy zerować strukturę, to najlepiej jest skupić w jednym miejscu wszelkie pola typów ścisłych, wyznaczyć zakres i ten zakres zerować), gdyż operuje ona na nieinterpretowanym wycinku pamięci, co może naruszać spójność niektorych typów w C++ (niektóre np. opakowują wskaźnik i - zabraniając komukolwiek dostępu do niego - chciałyby gwarantować, że ten wskaźnik nigdy nie będzie zawierał zera; w takim przypadku użycie memset byłoby wręcz chamstwem!).
Biblioteka ta zawiera jeszcze mnóstwo różnych użytecznych funkcji, pozwalających na rozkładanie napisu na czynniki, wyróżnianie nagłówków (strspn), tokenów (strtok) itd.. Jeśli kogoś to interesuje, polecam zapoznanie się z tymi funkcjami, ja jednak nie będę ich tutaj opisywać.
Funkcje opisane tutaj oficjalnie nie istnieją w standardzie. Nie wiem, być może standard milcząco zakłada, że nie wszystkie systemy muszą być wyposażone w katalogi. W sumie jest to nawet słuszne; jeśli program w C++ wykonuje się np. w głowicy pocisku nuklearnego, cóż mogą go interesować jakieś katalogi (ja to mówię całkiem poważnie; świat przecie nie kończy się na komputerach). Jednak wydaje mi się, że raczej zastosowania komputerowe najbardziej nas tu będą interesować, dlatego przedstawię parę drobnostek na temat dirent.h.
Pierwsza uwaga na dobry początek - dirent.h nie jest jedyną biblioteką do obsługi katalogów; istnieje też coś takiego jak "direct.h". Funkcje tam umieszczone (i - przede wszystkim - struktury) mogą się nieco różnić od tych, które występują w dirent.h, ale nie są to aż tak rażące różnice, żeby były jakiekolwiek problemy z ich przetłumaczeniem.
Jedną z najważniejszych rzeczy jest struktura o nazwie `DIR', która odpowiada niejako strukturze FILE (opisywać jej w sumie nie ma sensu; przyznam zresztą, że mimo najszczerszych chęci, po kilku próbach wyszukania szczegółów tej struktury na linuksie poddałem się). Uzyskujemy ją przez opendir:
DIR* opendir( const char* name );
int closedir( DIR* );
Teraz parę słów o strukturze dirent, która jest tzw. "pozycją w katalogu". Wszędzie, gdzie tylko to jest dostępne, posiada ona pole o nazwie `d_name'. Informacje te pobieramy przy pomocy funkcji readdir:
dirent* readdir( DIR* );
Struktura dirent poza d_name zawiera jeszcze jakieś inne pola, ale są one już charakterystyczne dla systemu. Zresztą mając nazwę pliku i tak można reszty sprawdzenia dokonać odpowiednimi metodami.
Aha, jeszcze jedno. Uważni pewnie zaobserwują coś takiego jak pole d_reclen w strukturze dirent. Ponieważ obiekty tej struktury są różnej długości, to właśnie pole d_reclen oznacza jej długość. Z tej długości można wyznaczyć długość napisu wskazanego przez d_name. Oczywiście napis pod d_name jest też kończony zerem.
Opisane tu funkje są zgodne ze standardem POSIX 2.10. Są to funkcje systemowe, z których funkcje biblioteczne C korzystają (zatem umiar i rozsądek przy korzystaniu z tych funkcji jest surowo wskazany!). Większość opisanych tu funkcji znajduje się w unistd.h, z wyjątkiem funkcji open i fcntl (i pochodnych), które znajdują się w fcntl.h. Proszę zapamiętać jeszcze jedną rzecz, dość standardową dla tych funkcji. Praktycznie wszystkie funkcje (jeśli nie są `noreturn', tzn. ktokolwiek miałby szanse sprawdzić jej wartość zwracaną) zwracają coś przez wartość typu int. Jedyną zastrzeżoną wartością zwracaną jest `-1', które oznacza błąd; wtedy należy sprawdzić errno. Jeśli funkcja z założenia nie zwraca żadnej sensownej wartości, wtedy na znak że się powiodła zwraca 0. Jeśli funkcja nie będzie odstawać od tej reguły, nie będzie to specjalnie oznaczone.
1) Obsługa plików
Do utworzenia połączenia z plikiem (lub urządzeniem) służy funkcja open:
int open( const char* path, int oflag [, int mode] );
`Oflagi' mogą być następujące:
Jedną z ciekawych funkcji, jaką można wykonać na pliku jest `unlink', która powoduje usunięcie danego pliku z katalogu. Plik jednak fizycznie usuwany jest dopiero wtedy, kiedy jakikolwiek proces przestaje z niego korzystać (wywoła close lub zakończy się). Oczywiście plik może mieć wiele dowiązań z nazwą (tzn. zależy na jakim systemie plików, na unixowych tak :*), a unlink usuwa tylko podaną. Jak się można oczywiście domyślić, `link' z kolei dodaje pozycję katalogową przypisaną danemu plikowi (to samo, co polecenie `ln') i też oczywiście tylko na tych systemach plików (nie systemach operacyjnych!), które wspierają wiele nazw do pliku.
Kiedy mamy już utworzony plik, możemy wykonywać na nim operacje. Podstawowymi operacjami są read i write.
int read( int fd, char* buf, int n ); int write( int fd, const char* buf, int n );
Zatem, funkcja read, wczytuje ze strumienia o deskryptorze `fd' maksymalnie `n' znaków do `buf'. Zwraca ilość faktycznie wczytanych znaków (łatwo się domyślić, że jeśli ta ilość jest mniejsza od `n', może to oznaczać koniec pliku). Podobnie, funkcja write powoduje wysłanie `n' znaków z `buf' do strumienia `fd'. No i podobnie zwraca ilość faktycznie wypisanych znaków, z tym że znaczenie częściej jest on równy `n' (jeśli nie będzie miejsca na zapisanie wymaganej ilości danych, funkcja write nie wykona się).
Gdyby ktoś chciał wykonać testy czasowe, mógłby się przekonać, że funkcja write jest zadziwiająco szybka (oczywiście mówię o systemach, które mają dobrze dopracowaną wielozadaniowość!). I to wielokrotnie szybsza, niż pozwala na to sprzęt. Jak to się dzieje? Oczywiście, system nas tutaj troszeczkę oszukuje. Tak naprawdę bowiem funkcja write nie pilnuje fizycznego zapisu pliku, a jedynie przekazuje systemowi zadanie do wykonania. Sprawdzane są jedynie warunki wstępne tzn. poprawność deskryptora i ilość wolnego miejsca, nie przewiduje się jednak żadnych błędów podczas transmisji.
Oczywiście nikt nie przysięgnie, że błędy transmisji na pewno nie wystąpią. Tylko co użytkownik jest w stanie zrobić w takiej sytuacji, kiedy program jest już kilka instrukcji do przodu? A co z procesami, które już się zakończyły? System oczywiście próbuje coś wypisać na ekranie w takiej sytuacji, choć to pewnie marna pociecha.
Istnieje zatem wśród flag flaga O_SYNC, której ustawienie powoduje, ze funkcja write czeka, aż transmisja się zakończy. Nie jest jednak specjalnie dobrym pomysłem korzystanie z tej flagi, gdyż ma ona parę wad.
Jest ona przede wszystkim czymś nienaturalnym na uniksach. Często wprowadzają one małe opóźnienie w zapisywaniu fizycznym (spowodowane głównie priorytetowością zadań, ale czasem jest to opóźnienie całkiem celowe). Dzięki temu szybciej wykonują się wszelkie operacje, które coś kombinują z plikiem (tzn. w efekcie nie każda zmiana dokonana w pliku jest odwzorowywana fizycznie na zawierającym go urządzeniu); gdyby np. przed fizycznym zapisem pliku jakiś proces zażądał do niego dostępu, system przekaże mu dane z pamięci buforowej. Zatem zapis synchroniczny musiałby oznaczać w efekcie albo odwzorowanie tego opóźnienia w programie (dzięki czemu write będzie działać wielokrotnie wolniej, niż pozwalałby na to sprzęt), albo przyznanie zadaniu fizycznego zapisu danych wyższy priorytet, co może się odbić na funkcjonowaniu całego systemu.
Na niektórych systemach są problemy z implementacją O_SYNC; na ostatnich jądrach linuksa jest ona dość świeża i działa tylko na urządzeniach blokowych i plikach zlokalizowanych na systemie plików ext2. Na systemach komercyjnych (np. Solaris) powinna jednak działać bez zarzutu. Z moich doświadczeń mogę powiedzieć, że nie miałem specjalnych problemów nt. błędów transmisji, aczkolwiek zdarzały się one przy połączeniach przez NFS. Oczywiście problem był mało znaczący, bo te pliki zapisywały się tam co trochę.
No to może w końcu zamkniemy tą dyskusję zaprezentowaniem funkcji close, która służy do zamknięcia połączenia strumieniowego i zwolnienia deskryptora.
Przemieszczanie się po pliku realizujemy funkcją lseek, która ma argumenty podobne jak fseek.
int lseek( int fd, long offset, int whence );
Microsoft wśród wad Linuxa (jak i pozostałych uniksów) wymienia wiele rzeczy, między innymi to, że nie potrafi obsługiwać pamięci operacyjnej powyżej 2GB i plików o rozmiarze powyżej 2 GB. Istnieje jednak funkcja o nazwie `lseek64'. Nie dopatrzyłem się dokładnie, ale prawdopodobnie zastępuje ona `lseek' na maszynach obsługujących 64-bitowe liczby całkowite. Z tego zresztą, co słyszałem te ograniczenia zostały już dawno przełamane.
2) Operacje plikowe na wyższym poziomie
Mamy tutaj parę dość odległych od siebie funkcji, toteż trudno trochę dobrać dla nich kategorię. Są to:
int chown( const char* file, int uid, int gid );
int chdir( const char* );
char* getcwd( char* buf, size_t len );
int rmdir( const char* name );
3) Funkcje kontrolujące czas
Tutaj mamy kilka funkcji, które pozwalają efektywniej wykorzystać czas procesora, tzn. zmniejszyć ilość "bzdurnych pętli", marnujących moc procesora. Dokładnie to pozwalają zrezygnować z pewnej ilości czasu procesora, jeśli się tego czasu nie zamierza efektywnie wykorzystać. Mamy tu następujące funkcje:
4) Wieloprocesowość i komunikacja wieloprocesowa
Najbardziej surowym narzędziem komunikacji wieloprocesowej są potoki (czy strumienie, jak tam zwał), choć najadekwatniejsza dla nich nazwa to - tłumacząc z angielskiego "na chama" - "rury". Taka "rura" może być przyłączona do różnych rzeczy, z których najprostszym (do obsługi) jest plik. Rura jest oczywiście rurą, a proces ma dostęp do jednego z jej końców, który określa się liczbą całkowitą, zwaną deskryptorem. Można też oczywiście wymusić połączenie końców dwóch "rur" uruchamiając polecenie w odpowiedniej składni; np. na uniksie będzie to:
polecenie1 | polecenie2
które utworzy rurę podłaczoną do deskryptora 1 polecenia 1 i deskryptora 0 polecenia 2. To znaczy, że to co normalnie by się "wylało" na ekran idzie do tej końcówki rury, do której normalnie się "wlewa" z ekranu. Ale to nam daje jeszcze za małe możliwości, chcielibyśmy bowiem nie korzystać z konieczności wywołania specjalnego polecenia powłoki. Poznajmy zatem uniwersalną funkcję, która tworzy po prostu samą rurę (ang. pipe):
int pipe( int fds[2] );
Jak poprzednio, nagłówek jedynie sugeruje sposób użycia funkcji, nie jest to kopia oryginalnego nagłowka. Funkcja ta oczekuje tablicy typu int o wielkości 2, do której wpisuje deskryptory końcówek utworzonej rury. Kierunek przepływu danych przez rurę jest określony: dane wpycha się przez fds[1], a pobiera przez fds[0] (jeśli używa się tej funkcji, warto zdefiniować sobie takie stałe: const int R = 0, W = 1). Jak widać więc, w pojedynczym procesie nie ma ona żadnego zastosowania, jest za to podstawowym środkiem komunikacji międzyprocesowej.
Jak wiadomo, w systemie operacyjnym (a nie nakładce na sprzęt pozwalającej uruchamiać programy) istnieją identyfikatory procesów. Identyfikator danego procesu (PID) można pobrać następującą funkcją:
int getpid();
Istnieje również hierarchia procesów. Proces może utworzyć swój proces potomny i w ten sposób staje się korzeniem hierarchii procesów (w uniksach mówi się o tzw. grupie procesów i jej przywódcy, ale staram się nie używać określenia `grupa procesów' na rzecz `hierarchii' właśnie, może się to bowiem mylić z pojęciem `grupy użytkowników', w której utrzymane są pliki). Identyfikator tej hierarchii to po prostu identyfikator procesu przywódczego. Oczywiście identyfikator hierarchii identyfikuje wyłącznie proces macierzysty danego procesu. Procesem "najbardziej macierzystym" wszystkich bowiem procesów jest `init'. Staje się on również zawsze procesem "bezpośrednio macierzystym" wszystkich "osieroconych" procesów. Poznajmy zatem podstawowe funkcje do tworzenia i obsługi procesów potomnych.
int fork( void );
Funkcja ta klonuje bieżący proces. System tworzy dokładną kopię procesu, z pamięcią dynamiczną włącznie. Proces-klon jest procesem potomnym i wykonuje się on od dokładnie tego samego miejsca, co proces macierzysty, tzn. fork wywołuje proces macierzysty, ale z funkcji powracają już dwa procesy. Aby mogły się one odróżnić, w procesie potomnym `fork' zwraca zero, natomiast w procesie macierzystym - PID procesu potomnego.
W systemach zgodnych z BSD 4.3, X/Open i POSIX, jest jeszcze taka funkcja `vfork'. Zazwyczaj na Linuxach była definiowana jako alias do `fork', aczkolwiek w nagłówkach kompilatora jest napisane trochę coś innego. Funkcja `vfork' bowiem nie kopiuje całej przestrzeni adresowej procesu. Proces wywołujący jest zawieszany aż do chwili zakończenia procesu potomnego lub wywołania funkcji z grupy exec (patrz niżej).
W uniksie nie da się jednak za bardzo utworzyć procesów i potem zechcieć je połączyć - przynajmniej przy pomocy "rur". Jednak pamiętajmy, że proces potomny dziedziczy deskryptory; zatem możemy najpierw utworzyć rurę, a potem się fork-nąć.
W komunikacji międzyprocesowej często jednak używa się programów użytkowych, które nie są przewidziane do żadnej wyspecjalizowanej komunikacji. Wiemy jednak, że każdy proces po uruchomieniu ma otwarte deskryptory 0, 1 i 2. Istnieje w uniksie taka możliwość, żeby zająć nowy deskryptor, który będzie wskazywał na już otwartą rurę (tzn. dana końcówka rury ma wtedy dwa deskryptory). Deskryptor oryginalny można wtedy normalnie zamknąć i efektem będzie tylko zwolnienie danego deskryptora. Poznajmy zatem funkcję dup2:
int dup2( int fd, int fdto );
Funkcja ta duplikuje deskryptor `fd' na żądany `fdto'. Jeśli `fdto' jest zajęty, jest przedtem zamykany. W starszych unixach istniała tylko funkcja `dup', która duplikowała podany deskryptor na pierwszy wolny (stawał się takim każdy pierwszy w kolejności deskryptor, jeśli został zamknięty). Można zatem przy pomocy tej funkcji podmienić rury przypisane do odpowiednich deskryptorów w procesie potomnym. Zanim jednak przejdziemy do przykładu, poznajmy jeszcze jedną funkcję, czy raczej grupę funkcji (podam raczej schemat, niż nagłówek):
execl[e|p]( nazwa, argv[0], argv[1] ... argv[n], 0 [, envp] ); execv[e|p]( nazwa, argv [, envp] );
Ogólnie to się nazywa `funkcje exec' (istnieje nawet polecenie powłoki `exec' o bardzo podobnym znaczeniu). Jak widać, funkcja ta jest interfejsem do wszelkich wywołań. Powoduje ona jednak PRZEKSZTAŁCENIE bieżącego procesu w nowy (zatem po jej wywołaniu bieżący program przestaje istnieć). Jedyna zatem wartość, jaka może zostać zwrócona, to -1. Zatem jeśli z exec nastąpił normalny powrót, to znaczy, że exec się nie powiodła.
Jak widać funkcje exec mają następujące nazwy: execl, execv, execle, execve, execlp, execvp. Litery `l' i `v' jak widać specyfikują sposób podawania listy argumentów: przy `l' należy podawać argumenty po przecinku, przy `v' - normalnie tablicę napisów. Łatwo się chyba zorientować, że tak przekazaną tablicę następnie dostaje jako argument funkcja main wywoływanego procesu (nie sugeruję bynajmniej, że program musi być napisany w C lub C++; przekazywanie argumentów do programu jest rzeczą uniwersalną i inne języki zazwyczaj mają swoje sposoby interpretacji podanych argumentów). A co za tym idzie, lista argumentów musi być zakończona zerem (jak to podano w pierwszym schemacie).
Dodatkowo można dać litery `e' lub `p' (nie obie na raz!). Litera `p' oznacza, że system ma przeszukać katalogi zawarte w zmiennej środowiskowej `PATH', jeśli nazwa nie zawiera "slaszy" (/), w przeciwnym razie podaną nazwę traktuje się jak ścieżkę do pliku z programem. Natomiast litera `e' oznacza podanie procesowi środowiska i wtedy należy dodać zmienną `envp'. W przeciwnym razie, środowisko w procesie potomnym będzie odziedziczone.
Zatem, aby np. skorzystać z programu sort do posortowania tablicy napisów, można zrobić tak:
#include <iostream> #include <unistd.h> using namespace std; const int R = 0, W = 1; // indeksy dla pipe int main() { char* tsrc[20]; char* tdst[20]; ... ; // Załóżmy, że mamy dwie tablice, tsrc i tdst. // Pierwsza z nich zawiera nieposortowane napisy, // które należy posortować i wpisać do drugiej // Tworzymy sobie rurę int rura_to[2]; // rura, która w procesie macierzystym ma być OUT int rura_from[2]; pipe( rura_to ); pipe( rura_from ); // Teraz się forkniemy int cldpid = fork(); // W procesie potomnym dokonujemy podmiany deskryptorów i uruchamiamy // polecenie `sort' if ( cldpid == 0 ) { dup2( rura_to[R], 0 ); dup2( rura_from[W], 1 ); close( rura_to[0] ); // są zduplikowane lub niepotrzebne close( rura_to[1] ); close( rura_from[0] ); close( rura_from[1] ); execlp( "sort", "sort", (char*)0 ); } // W procesie macierzystym natomiast, zamykamy niepotrzebne końce rury: close( rura_to[R] ); close( rura_from[W] ); // I teraz można już wykonać komunikację ofstream pout( rura_to[W] ); ifstream pin( rura_from[R] ); for ( int i = 0; i < 20; i++ ) pout << tsrc[i] << endl; pout.close(); for ( int i = 0; i < 20; i++ ) pin >> tdst[i]; ...; }
Oczywiście od razu małe ostrzeżenie. Program `sort' należy do takich programów, które muszą odczytać wszystko, co im się poda na wejście, żeby rozpocząć działanie. Jednak nie wszystkie programy tak działają. Niektóre z nich odczytają trochę i wysyłają od razu na bieżąco. W takich sytuacjach może powstać zakleszczenie, które jest problemem nie łatwo dającym się rozwiązać.
Gdyby zamiast `sort' był jakikolwiek inny program, który wczytuje linie i po przerobieniu od razu ją wysyła, mielibyśmy trochę problem. Gdyby bowiem tak robić jak tutaj pokazałem, to przy zbyt dużej ilości danych nastąpi taka akcja: proces potomny przetwarzając nadchodzące dane, zapcha rurę wyjściową, gdyż proces macierzysty nie odbiera z niej danych. W związku z tym proces potomny wstrzyma pracę, aż rura się przepcha. Ponieważ w efekcie oznacza to, że proces potomny również nie będzie odbierał danych z rury wejściowej, to i proces macierzyty zapcha rurę wejściową. Proces macierzysty będzie czekał na odkorkowanie rury wejściowej, ale tą blokuje proces potomny, który czeka na odkorkowanie rury wyjściowej, którą blokuje proces macierzysty, oczekując na ... i tak dalej.
Do takich programów lepiej stosować taktykę "linijka po linijce", tzn. po wysłaniu jednej linijki odbieramy jedną linijkę. To rozwiązanie jednak też nie jest uniwersalne. Wyobraźmy sobie sytuację najbardziej abstrakcyjną - nie ma w ogóle przetwarzania tekstowego ani żadnego podziału na linijki, a wyniki produkowane przez proces potomny są absolutnie niekorespondentne do przekazywanych mu danych pod względem ilościowym. Przyda się nam tutaj zatem flaga O_NDELAY, która stanowi, że operacje nie będą blokowane. Niestety wcale nam to tak znowu nie załatwia sprawy; system może co prawda przechować dane, które nie mieszczą się nam w kolejce, ale wcale to nie oznacza, że system to worek bez dna.
Oczywiście, nie ma co się szczypać: wiadomo, że procesy muszą się komunikować między sobą wedle ściśle określonego protokołu, który będzie dostosowany do ich sposobów pracy (konkretnie to do sposobu pracy procesu potomnego).
5) Programy wieloużytkowe (serwery)
Uniksy mają wiele różnych innych technik komunikacji między procesami. Dla komunikacji "rurowej" jednak istnieje jeszcze jeden pakiecik, który ułatwia procesowi obsługiwanie kilku rur na raz (staje się niejako serwerem). Nie jest to jednak typowy sposób obsługi serwera, bo popularniejsze (zwłaszcza wśród daimonów) jest forkowanie się i obsługiwanie (pojedynczego!) użytkownika przez proces macierzysty. Ten pakiet umożliwia jednak inną rzecz: obsługę kilku użytkowników na raz (również więc na upartego komunikację między użytkownikami!):
int select( int fdmax, fd_set* in_set, fd_set* out_set, fd_set* exc_set, timeval* timeout ); void FD_ZERO( fd_set* set ); void FD_SET( int fd, fd_set* set ); void FD_CLR( int fd, fd_set* set ); bool FD_ISSET( int fd, fd_set* set );
Te deklaracje wymagają oczywiście <sys/select.h>.
Typ `fd_set' jest typem, który - w założeniu - jest tablicą bitów, w której każdy bit odpowiada jednemu deskryptorowi. Logicznie możnaby taką tablicę określić jako zestaw "lampek kontrolnych" odpowiadających konkretnym deskryptorom. Jeśli chce się używać select, należy zadeklarować sobie (w dowolnej klasie pamięci) zmienne tego typu odpowiadające wejściu, wyjściu i sygnalizacji błędów.
Pierwszy argument funkcji select to maksymalny numer deskryptora, jaki jest brany pod uwagę. Kolejne trzy to zestawy "lampek" oznaczające odpowiednio: gotowość danej rury do przyjmowania danych, istnienie danych gotowych do odbioru, oraz wystąpienie błędu dla danej rury. Ostatni argument oznacza maksymalny czas, jaki funkcja ma czekać na zgłoszenie się odpowiednich deskryptorów. Jak widać więc, niezapalenie się lampki dla danego deskryptora oznacza, że gdyby próbować wykonać na takim deskryptorze odpowiadającą danemu kierunkowi operację, zakończyłaby się zwróceniem -1 i ustawieniem errno. Jeśli dodatkowo taka lampka pali się w zestawie `exc_set', to znaczy że błąd jest poważny (ale nie wiemy jaki; jednocześnie może palić się wiele lampek w `exc_set', więc errno nie jest tu w ogóle możliwe do wykorzystania).
Typ `timeval' to struktura o następującej definicji:
struct timeval { int tv_sec; // sekundy int tv_usec; // mikrosekundy (`u' naśladuje greckie `my') };
Wszystkie argumenty, zarówno zestawy lampek jak i `timeout' są opcjonalne; można ich "nie podać" tzn. w ich miejsce podać zero. Można zatem wyróżnić trzy tryby pracy tej funkcji:
6) Podsumowanie
Wielu funkcji uniksowych nie opisałem tutaj; opisałem jedynie te, które uważam za najważniejsze i najbardziej podstawowe. Ze swojej strony - jeśli ktoś chce dowiedzieć się więcej na ten temat, polecam książkę pt. "Komunikacja procesów w Unixie" (niestety nie pamiętam autora ani wydawnictwa; chyba Arkana, ale głowy nie dam). Pominąłem tutaj również funkcje specyficzne dla BSD, aczkolwiek starałem się zawrzeć większość funkcji zgodnych z Unix98.
_____________________________________________________________________________