C++ bez cholesterolu

Elementy biblioteki standardowej C

Wstęp

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 dodatkowych informacji proszę zapoznać się ze szczegółami w plikach pomocy (na Unixach dostępne również pod poleceniem man).

Pliki nagłówkowe

Oto pliki należące do standardowej biblioteki C (włącznie 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.

Diagnostyka

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ą.

Operacje wejścia/wyjścia; biblioteka stdio

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*):

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>).

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().

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, ...);

- sformatowany łańcuch wpisuje do podanej tablicy

fprintf(file* strumien, const char* fmt, ...);

- sformatowany łańcuch wpisuje do strumienia. łatwo jest się chyba zorientować, że printf(<x>) to inaczej fprintf(stdout, <x>).

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ę za pomocą malloc 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).

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ąć).

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 );

gdzie: path - ścieżka do pliku, a mode - tryb dostępu.

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.

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 );

Oczywiście istnieją dwa rodzaje buforowania: blokowe i liniowe. Domyślnie dla nowych strumieni przyjmowane jest buforowanie blokowe. Dla stdin i stdout jest buforowanie liniowe, zaś stderr nie jest buforowany. Brak buforowania oczywiście można wymusić przez podanie jako wskaźnika na bufor zera (czyli tzw. null). Ta funkcja akurat ustawia buforowanie blokowe; istnieją też inne funkcje do ustawień buforowania, ale nie jest chyba istotne, żeby je opisywać.

Nie mamy oczywiście możliwości ustawiania wielkości bufora! bufor jest zawsze wielkości bufsiz (a przynajmniej procedury tak myślą :*).

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 fseek ustawia dla strumienia `stream' pozycję `offset' względem pozycji określonej argumentem `whence'. Ten ostatni argument przybiera jedną z wartości: seek_set (uwaga: nie `beg'!), seek_cur i seek_end oznaczające, że podana pozycja jest względem odpowiednio początku pliku, bieżącej pozycji w pliku i końca pliku. Widać więc, że `offset' może przybierać dla seek_cur wartości również ujemne, a dla seek_end - wyłącznie ujemne.

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) ).

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* );

No i mamy też pewną dość użyteczną funkcję, tworzącą nazwę (nie plik!) pliku tymczasowego (funkcja dba o to, żeby nazwa była unikalna). Jako argument możemy podać zero, wtedy nazwa będzie kopiowana do statycznej tablicy, jeśli podamy wskaźnik na napis, tam zostanie ta nazwa skopiowana; następnie ten wskaźnik zostanie zwrócony:

char* tmpnam( char* );

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 );

Funkcja ta uruchamia polecenie `command', podłączając jej wyjście (lub wejście) do pliku, którego struktura buforowa jest zwracana. Po prawdzie, tworzy ona połączenie zwane "potokiem" (ang. pipe) pomiędzy uruchamianym procesem a naszym procesem. Choć oczywiście potok nie ma nigdy określonego kierunku przepływu danych, to jednak potok jest jeden, a uruchamiany proces korzysta przecie - jak wiemy - z aż trzech strumieni. Zatem nie jest możliwe, aby przy pomocy tej funkcji przechwycić wyjście i wejście procesu; mamy do dyspozycji albo rybki, albo akwarium. Argument `mode' tej funkcji określa kierunek przepływu danych i jest podobny do argumentu `mode' funkcji fopen. Problem z ustanowieniem połączenia dwukierunkowego nie wynika bynajmniej z ograniczeń systemu; polega wyłącznie na tym, że przy pomocy tej funkcji pobieramy "jeden koniec rury", a drugi jej koniec jest podłączany do deskryptora 0 (jeśli odczyt) lub 1 (jeśli zapis) uruchamianego procesu, a te strumienie mają już niestety z góry określony kierunek komunikacji (jeśli jednak te możliwości ci nie wystarczają, polecam funkcje systemowe Unixa :*).

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).

Różne funkcje użytkowe - stdlib.h

Występują tutaj funkcje o różnych zastosowaniach. Omówię je więc w poszczególnych kategoriach.

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* );

W przypadku błędu w zapisie tekstowym liczby po prostu zwracane jest zero.

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
);

Funkcja `strtod' zwraca huge_val dodatnią lub ujemną w przypadku przepełnienia (w odpowiednim kierunku), natomiast zero w przypadku niedopełnienia. W obu przypadkach errno jest ustawiana na erange. Podobnie, funkcje strtol i strtoul sprawdzają zakresy odpowiednio <long_min,long_max> i <0,ulong_max>; w przypadku przekroczenia zwracana jest granica zakresu, którą przekroczono i errno jest ustawiane na erange. Oczywiście z tym zerem dla strtoul jest nie dokładnie tak; jeśli bowiem liczba interpretowana przez nią jest ujemna, tzn. zaczyna się minusem, minus jest niejako ignorowany.

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).

Generator liczb losowych

Tutaj mamy właściwie tylko dwie funkcje:

// zwraca liczbę losową z przedziału <0, rand_max>
int rand( void );
void srand( unsigned int );  // inicjalizuje generator

Ze swojej strony dodam oczywiście, że najlepiej się go inicjalizuje odpowiednią wartością odczytaną przy pomocy gettimeofday (będzie omówiona przy nagłówkach Unixowych).

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"

Funkcja malloc przydziela podaną ilość pamięci (zazwyczaj w takim wywołaniu umieszcza się sizeof z odpowiednim typem). Funkcja calloc robi właściwie to samo; przeznaczona jest do przydziału pamięci na tablicę (nie wysilając się nadmienię, że argumenty są po prostu przez siebie mnożone) i - co ważne - zapisuje całą przydzieloną pamięć zerami. Funkcja realoc z kolei powiększa podany obszar pamięci i zwraca wskaźnik do nowego (być może przemieszczonego!) bloku. Zawartość starej części bloku pozostaje oczywiście w stanie nienaruszonym (nienaruszonym, to znaczy został on być może skopiowany za pomocą memcpy - zwracam uwagę, bo w C++ jest to poprawne tylko dla typów pod).

Pamięć można oczywiście zwolnić przez:

void free( void* );

muszę jednak zwrócić uwagę na pewną bardzo ważną rzecz (tym bardziej ważną na systemach Unixowych). Funkcje te to funkcje użytkownika. W pierwotnych Unixach (i nawet w wielu jest tak w dalszym ciągu) free nie zwalniało pamięci do systemu. Wrzucało ją do odpowiedniego obszaru, z którego następnie była przydzielana po ewentualnym wywołaniu malloc/calloc/realloc. W przypadku, gdyby tam nie było pamięci (a tak jest zaraz po uruchomieniu programu), dodatkowa pamięć jest żądana od systemu (na Unixie jest to sbrk).

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ę co nieco 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 :*).

Przerywanie programu

Te dwie funkcje służą do natychmiastowego opuszczenia programu:

void exit( int );
void abort( void );

W funkcji exit argument jest tym samym, co argument instrukcji return w funkcji main. Powoduje ona normalne zakończenie programu (tzn. normalne od strony systemu; od strony programu nigdy to nie jest normalne).

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 ) );

powoduje zarejestrowanie funkcji, która będzie wywołana przed wyjściem z programu po wywołaniu exit.

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).

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 );

Funkcja getenv zwraca wartość podanej zmiennej środowiskowej. Funkcja setenv ustawia wartość zmiennej środowiskowej `name' na `value'; dodaje jako nową jeśli nie istnieje. Jeśli istnieje, to zmienia jej wartość tylko jeśli `replace' jest niezerowe, inaczej nie zmienia. Funkcja unsetenv usuwa podaną zmienną środowiskową.

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* );

przekazuje ona polecenie powłoce do wykonania i oczekuje na jego zakończenie.

Funkcje matematyczne dla liczb całkowitych

z najprostszych:

abs, labs
zwracają wartość bezwzględną podanej liczby (abs dla int, labs dla long)
div(x,y);
dzieli całkowite x przez y i zwraca wynik jako strukturę div_t, zawierającą pola (typu int) quot i rem, oznaczające odpowiednio wynik i resztę z dzielenia. Podobnie ldiv, tylko zwraca ldiv_t i wszystkie typy są long (już nie wspomne o lldiv obsługującej long long).

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* );

Funkcja tego typu występuje w obu poniższych funkcjach i jest obowiązana porównać dwa podane (przez wskaźnik) elementy oraz zwrócić:

Można to nawet tak określić, że funkcja ma niejako "odjąć drugi element od pierwszego" i zwrócić całkowity wynik takiej operacji. Funkcja ta jest podawana jako ostatni argument dwóch poniższych funkcji:

void* bsearch(
  const void* key, const void* base,
  size_t nmemb, size_t size, compar_fn_t compar
);

Funkcja bsearch wyszukuje element `key' w tablicy `base', zawierającej `nmemb' elementów rozmiaru `size' na podstawie kryterium określonego przez `compar'.

void qsort(
  void* base, size_t nmemb,
  size_t size, compar_fn_t compar
);

Funkcja qsort sortuje tablicę `base' o `nmemb' elementach rozmiaru `size' na podstawie kryterium `compar'.

Sygnały i skoki długie - signal.h i setjmp.h

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 );

Z wartości zwracanej rzadko się tu korzysta, chyba, że chce się ustawić inną obsługę sygnału "na chwilę" i nie wie się, co było ostatnio ustawione (bo też jest od czegoś zależne).

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
sygnały do zdefiniowania przez użytkownika
sigusr2
sygnały do zdefiniowania przez użytkownika
sigpwr
krytyczny stan zasilania
sigalrm
upłynął czas nastawiony przez alarm() (w sekundach)
sigchld
proces potomny zakończył działanie.
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ł );

Funkcja `raise' powoduje wysłanie przez program podanego sygnału do samego siebie. Z kolei `kill' (istnieje tylko na systemach, które używają sygnałów) powoduje wysłanie sygnału do procesu identyfikownego przez pid_procesu (jeśli pid_procesu jest ujemne, jej wartość bezwzględna jest identyfikatorem grupy procesów, do których sygnał jest wysyłany, jeśli jest to -1 - do wszystkich procesów użytkownika).

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 );

Funkcja setjmp zapamiętuje w `env' (jmp_buf jest oczywiście wskaźnikiem na strukturę - znów możnaby wyrazić uznanie dla spójności definicji w bibliotece standardowej c) bieżące środowisko tak, aby można było je potem przywrócić. Jeśli nastąpiło normalne wywołanie setjmp, zwraca ona 0.

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. Język 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.

Funkcje o zmiennej liście argumentów - stdarg.h

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;

Argument `ap' jest tu najczęściej używaną nazwą, toteż nie będę odstawał od innych :*). Żeby zainicjalizować tę zmienną (tzn. żeby ona wskazywała początek listy argumentów) należy użyć konstrukcji:

va_start( ap, last );

gdzie `last' jest ostatnim jawnym argumentem. Kiedy mamy już zainicjalizowaną zmienną, możemy pobierać z niej argumenty. Służy do tego makro:

va_arg( ap, typ );

Makro to zwraca wartość następnego argumentu odczytanego z `ap' zinterpretowanego jako typ `typ' (ustawiając ap na następny argument). Zauważ, że sposób tej interpretacji (czyli typ oczekiwanego argumentu) musi być znany przed jego pobraniem. Interpretacji bowiem podlega niejako obszar pamięci. Dlatego właśnie np. znaczniki w funkcjach printf i scanf muszą determinować typ oczekiwanego argumentu. Jak widać, jest to jeden z najlepszych sposobów walki ze statycznym systemem typów.

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).

Obsługa tablic napisowych, string.h

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.

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ć.

Obsługa katalogów - dirent.h

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 );

Podobnie jak w fopen, w przypadku niepowodzenia zwracane jest 0 i należy sprawdzić zmienną errno. Aby tak otwarty katalog zamknąć, należy podać tą strukturę do funkcji closedir:

int closedir( dir* );

Funkcja ta zwraca 0 jeśli się to powiodło, w przeciwnym razie -1.

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* );

Oczywiście oryginalnie nagłówek zawiera `struct dirent', ale nie jest to przecież kurs języka C :*). W przypadku końca katalogu lub błędu w operacji zwracane jest zero (nie udało mi się dowiedzieć, jak sygnalizowany jest błąd w operacji i jak jest odróżniany od normalnego "końca katalogu").

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.

Standardowe funkcje Unixa - unistd.h, fcntl.h oraz sys/select.h

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.

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]
);

Zwracam uwagę, że ten nagłówek jest tylko "orientacyjny" (po oflag tak naprawdę następuje `...'). Funkcja ta otwiera połączenie do pliku o podanej ścieżce i kierunku (i flagach dodatkowych) ustalonych przez zestaw flag bitowych `oflag'. Jeśli w zestawie tym zawiera się o_creat, brany jest pod uwagę trzeci argument, którym jest tryb dostępu do pliku (filtrowany przez umaskę oczywiście). Zwraca deskryptor pliku.

`oflagi' mogą być następujące:

o_rdonly
tryb odczytu
o_wronly
tryb zapisu
o_rdwr
tryb dwukierunkowy. Nigdy(!) nie należy łączyć tych flag ze względu na odpowiadające im wartości 0, 1 i 2 (wynika to z drobnego błędu w opracowaniu, ale być może zrobiono to specjalnie z jakiegoś powodu; zwłaszcza że wśrod flag jest też coś takiego jak o_accmode o wartości 3, ale nie wiem do czego służy).
o_creat
tworzy taki plik, jeśli nie istnieje
o_excl
razem z o_creat, wymaga, by plik nie istniał (uwaga: na systemach plików podłączonych przez NFS nie będzie działać prawidłowo)
o_noctty
jeśli urządzenie jest terminalem, nie stanie się terminalem kontrolującym procesu (nawet gdy proces takiego nie ma)
o_trunc
jeśli plik istnieje, zostanie obcięty (tzn. zniszczony)
o_append
przed każdym zapisem do pliku wskaźnik ustawia się na koniec
o_ndelay
operacje na tym pliku (z open włącznie) nie będą blokować procesu. Jeśli wtedy zechce się wykonać operację na deskryptorze, który nie jest jeszcze w pełni gotowy do użytku, albo system będzie potrzebował trochę czasu na jej wykonanie, funkcja taka zwróci -1 i errno == eagain ("operation in progress").
o_sync
zapis do pliku będzie synchronizowany (dlaczego może to być potrzebne, wyjaśnię przy funkcji `write').

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 );

Mała uwaga: będę mówił, że deskryptor opisuje "strumień" a nie "plik". Deskryptor bowiem nie musi wcale opisywać połączenia z plikiem; deskryptor jest takim jakby tylko "końcem odpowiedniej rury", a gdzie jest ta "rura" połączona dalej to już tych funkcji z reguły nie interesuje; może to być również np. gniazdo sieciowe, albo połączenie z innym programem.

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 );

Mała ciekawostka: skąd `l' w nazwie? Jak się niektórzy domyślają chodzi o typ zmiennej `offset'. Pierwotnie na Unixach istniała funkcja `seek', która posiadała zamiast `offset' dwa argumenty typu int; wtedy bowiem jeszcze dodatkowo `int' było synonimem `short int', więc mogło nie starczać zakresu. Później jednak wprowadzono funkcję `lseek' o prostszej konstrukcji, dzięki zastosowaniu argumentu typu long. Oczywiście funkcja `seek' została już dawno usunięta z unistd.

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.

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 );

Ustawia podanemu plikowi podany UID i GID jako numer użytkownika i grupy. Odpowiada to poleceniu powłoki `chown', aczkolwiek tu należy podać konkretne numery. Oczywiście funkcja dostępna jest tylko dla roota :*) [dokładniej dla każdego, kto posiada podniesione uprawnienia administracyjne, przyp.AP.].

int chdir( const char* );

Funkcja odpowiada poleceniu `cd'.

char* getcwd( char* buf, size_t len );

Podobna do polecenia `pwd'; wpisuje do `buf' długości `len' nazwę bieżącego katalogu. Jeśli nie da się tego uzyskać lub podano za mały bufor, zwracane jest zero, w przeciwnym razie `buf'. Tu mała uwaga: jeśli jest to możliwe, dokładnie to samo można osiągnąć przez getenv("pwd").

int rmdir( const char* name );

Funkcja usuwa podany katalog (jeśli jest pusty).

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:

int pause(void);
zatrzymuje proces; z tego zatrzymania może go wyrwać tylko nadejście sygnału. Jest to właściwie jakby podstawa wszystkich funkcji systemowych, gdyż tak robi każda z nich; różnica jest tylko taka, że większość z nich kiedyś kończy działanie :*)
int alarm(int);
nastawia budzik za podaną ilość sekund. To oznacza, że po upływie podanego czasu do procesu zostanie wysłany sigalrm. Jeśli poda się zero, jakiekolwiek wcześniej ustawione budziki są kasowane. Funkcja ta nie zwraca -1 w przypadku błędu (należy wyzerować errno przed jej wywołaniem, jeśli to kogoś interesuje). Czas nadejścia sygnału może oczywiście nie być dokładnie taki sam, jak podany, gdyż o tym już decyduje kolejkowanie procesora.
int sleep(int);
zatrzymuje proces na podaną ilość sekund

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 polecenia1 i deskryptora 0 polecenia2. 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).

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 {
  // sekundy
  int tv_sec;
  // mikrosekundy (`u' naśladuje greckie `µ')
  int tv_usec;
};

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:

  1. select:pause; jeśli nie podano ani lampek ani wymiaru czasu (tzn. w miejsca tych argumentów podano zero). Jest to identyczne z wywołaniem pause.
  2. select:stall; jeśli nie podano zestawu lampek, ale podano strukturę timeout. W takim wypadku select odpowiada wywołaniu sleep, z tym tylko, że można podać czas zatrzymania w mikrosekundach (w efekcie więc można w połączeniu z gettimeofday dokonywać synchronizacji np. co 1/4 sekundy).
  3. select:poll; jeśli podano zestawy lampek. W takiej sytuacji funkcja robi normalną odpytkę (ang. polling) deskryptorów. Cała procedura przebiega w ten sposób:
    1. Musimy określić, które deskryptory chcemy odpytywać; te deskryptory należy w danym zestawie "zapalić", pozostałe należy "zgasić" (nie musimy oczywiście obiegać całej tablicy; można ograniczyć ilość deskryptorów podlegających sprawdzaniu i podać jako pierwszy argument `select' ich maksymalny numer). Gaszenie wszystkich deskryptorów wykonuje się makrem fd_zero, natomiast zapalenie konkretnego deskryptora - fd_set i zgaszenie - fd_clr.
    2. Należy zdecydować, ile się chce czekać na odpowiedź w sprawie deskryptorów i w zależności od tego podać ostatni argument. Jeśli nie chce się w ogóle czekać, należy podać wartość typu `timeval*' wskazujący na strukturę z wyzerowanym tv_sec i tv_usec; ewentualnie należy podać sekundy i mikrosekundy czasu oczekiwania. Jeśli chce się czekać "do skutku", należy w miejsce ostatniego argumentu podać zero.
    3. Wywołać select
    4. I teraz: dla każdego deskryptora, którego lampka pali się w zestawie `in_set', można odbierać dane, a w `out_set' - można wysyłać dane. Jeśli jednak w `exc_set' - deskryptor należy po prosty zamknąć [to jest trochę niejasno napisane: chodzi o to, że gdy pojawi się ustawiona flaga exc_set, wtedy należy zamknąć operacje na strumieniu przyp.AP.].
    5. jeśli używamy `read' dla deskryptora zapalonego w `in_set', funkcja zwróci nam ilość faktycznie wczytanych danych. To, że zwróci nam ilość mniejszą, niż wielkość bufora to jest całkiem normalne (zazwyczaj wczytanie ilości bajtów równej długości bufora jest nienormalne!). Jeśli jednak zwróci zero, oznacza to, że drugi koniec rury został zamknięty (w przypadku pliku oznacza to koniec pliku) i deskryptor należy zamknąć.

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) [chodzi o książkę Arkana: Kominikacja między procesami w Unixie, autorstwa Johna Shapley Gray'a, przyp.AP.]. Pominąłem tutaj również funkcje specyficzne dla BSD, aczkolwiek starałem się zawrzeć większość funkcji zgodnych z Unix98.