Każdy programista un*xowy zna te funkcje. Prawie każdy użytkownik un*xa piszący czasem jakieś programy zna te funkcje. Są to bardzo wygodne funkcje. Opis może się przydać nawet programistom, którzy używają Linuksa (FreeBSD, Solarisa czy co tam jeszcze) do rozwiązywania algorytmów. Do rzeczy.
Jeśli chcesz przekierować standardowe wejście do jakiegoś pliku, możesz
otworzyć plik i albo zapisywać wszystko fprintf
em, albo sprawić,
żeby stdout
było otwartym plikiem. Pierwsze wyjście nie jest zbyt
wygodne w rozbudowanych programach, podczas gdy drugie pociąga za sobą
dopisanie co najwyżej trzech linijek kodu. Wystarczy posłużyć się funkcjami
int dup(int desk)
i int dup2(int stary_desk, int
nowy_desk)
. dup()
zwraca nowy deskryptor, odnoszący się do
tego samego pliku co desk
. Nowy deskryptor ma najniższy możliwy
numer (tzn. jest to najniższa liczba całkowita, która nie była poprawnym
deskryptorem). dup2()
działa ciut inaczej: nowy_desk
zaczyna odnosić się do tego samego pliku, co stary_desk
, przy
czym jeśli poprzednio był prawidłowym deskryptorem, to zostanie zamknięty.
To teraz przydałoby się wiedzieć, które deskryptory odpowiadają którym
strumieniom we/wy. Deskryptor 0 odpowiada za standardowe wejście, w związku
z czym jest otwarty tylko-do-odczytu. Deskryptor 1 to standardowe
wyjście i jest otwarty tylko-do-zapisu. Deskryptor 2 jest standardowym
błędem i jest otwarty do-odczytu-i-zapisu (tzn. można stąd czytać dane
z klawiatury i tu zapisywać dane na monitor).
int main(void) { int plik = open("plik.txt", O_WRONLY | O_CREAT | O_EXCL, 0666); /* kod dający praktycznie ten sam efekt: close(1); dup(plik); */ dup2(plik, 1); close(plik); /* to nie jest potrzebne, ale w dobrym tonie */ puts("Hell oWorld!"); return 0; } |
Funkcja unsigned int sleep(int sek)
usypia proces na sek
sekund. Funkcja int
usleep(unsigned long usek)
usypia proces na
usek
mikrosekund (1 usekunda = 1/1000 ms = 1/1000 000 s). Funkcja
usleep()
na systemach z rodziny *BSD nie zwraca nic (jest typu
void). W zasadzie nigdy nie korzystałem z wartości zwracanych
przez te funkcje, zawsze stosowałem je najprościej możliwie:
int main(void) { puts("Witam na poczatku progsa"); sleep(1); /* spimy jedna sekunde */ puts("Witam po jednej sekundzie"); return 0; } |
Dalej jest funkcyjka void bzero(void *blok,
size_t rozmiar)
, zerująca wielkosc
bajtów
(size_t to tak naprawdę int) zaczynając od adresu
blok
. Przydatne przy dynamicznej alokacji tablic albo struktur.
W manualu stoi, że funkcja jest odradzana, zamiast niej należy używać funkcji
void* memset(void *blok, int
znak, size_t rozmiar)
. memset()
wypełnia
rozmiar
bajtów spod adresu blok
bajtem
znak
, a zwraca dokładnie to, co było w blok
. Czemu
więc int zamiast char? Bo procesor pojedynczą
instrukcją może przetwarzać tylko 32-bitowe liczby (na procesorach
32-bitowych, czyli pochodne po x86), nie mniejsze i nie większe. Zatem
obcinanie zmiennej do ośmiu bitów tylko po to, żeby zaraz z powrotem zamienić
to na 32 bity nie ma sensu.
int main(void) { int *dynamiczna_tablica; dynamiczna_tablica = malloc(200); /* 200 bajtow, czyli 50 intow */ memset(dynamiczna_tablica, 0, 200); /* teraz mamy tablice wypelniona zerami */ /* tu jakis uzyteczny kod */ free(dynamiczna_tablica); return 0; } |
Niezła jest funkcja void perror(char
*komunikat)
. Wypisuje na ekranie (dokładnie na standardowym
wyjściu błędu) bzdury podane przez programistę, po czym wypisuje systemowe
objaśnienie numeru błędu ze zmiennej errno
(-> man
errno). Nie musisz zakładać a priori żadnych możliwych wartości
błędu. Jeśli jakaś funkcja w razie błędu ustawia zmienną errno
,
możesz łatwo dowiedzieć się, z jakiego powodu się wywaliła.
int main(void) { int deskryptor; if ( ( deskryptor = open("nazwapliku.txt", O_RDONLY) ) == -1 ) { /* tu bedzie wiecej informacji, np. "No such file or directory" */ perror("Blad przy otwieraniu pliku"); return 1; } close(deskryptor); return 0; } |
Czasem zdarza się, że chcesz zmierzyć, ile czasu będą się wykonywać pewne
instrukcje. Nienajgorzej do tego się nadaje funkcja time_t
time(time_t *czas)
. Tak na prawdę zwraca int,
a dokładnie ilość sekund, jaka upłynęła od 1 stycznia 1970, od godziny 0:0:0
czasu UTC (GMT, jak kto woli, wyrażenie UTC spotyka się częściej w un*xach).
Jest to tak zwany Epoch (ang. epoka, okres).
Notka: W okolicach początku roku 1970 na uniwersytecie w Berkley
po raz pierwszy uruchomiono pierwszą wersję pierwszego wielozadaniowego
systemu operacyjnego Unix. Stąd czas w systemach uniksowych mierzy się od tego
czasu.
Notka: Wprawdzie liczba typu int jest ograniczona
(32 bity), więc i czas opisywany przez nią nie może być zbyt długi, ale
problematyczne stanie się to dopiero ok. 2030 roku, a do tego czasu komputery
64-bitowe będą powszechne (albo i przestarzałe), a przy 64 bitach następna
problematyczna data będzie obchodzić nasze praprawnuki.
Funkcja time()
przyjmuje jeden parametr, wskaźnik do zmiennej,
w której będzie chciała zapisać dokładnie to samo, co zwraca. W sumie nie
wiem, po co. Zamiast tego wskaźnika można władować tam NULL. Ja
tego używam tak:
int main(void) { int poczatek, koniec; int i; /* * to mozna inaczej: * srand(time(&poczatek)); * srand( (poczatek = time(NULL)) ); */ srand(time(NULL)); /* siejemy ziarenko losowosci, bo tak trzeba */ poczatek = time(NULL); for (i = 0; i < 200000; ++i) /* losujemy 200 tys. razy liczbe od 0 do 27 */ rand() % 28; koniec = time(NULL); printf("Czas wykonania: %d"); return 0; } |
Podobne zastosowanie ma funkcja int
gettimeofday(struct timeval *czas, struct timezone
*strefa_czasowa)
. Ta pobiera adres struktury, w której zapisze
aktualny czas i adres drugiej struktury, w której zapisze strefę czasową.
Każdy z adresów może być równy NULL. Ze strefy czasowej jeszcze
nie zdarzyło mi się skorzystać, wątpię, żeby to było potrzebne szeregowemu
programiście :) Sama struktura timeval wygląda tak (podaję za
odpowiadającym manualem):
struct timeval { long tv_sec; /* sekundy od Epochu */ long tv_usec; /* mikrosekundy (1/1 000 000 sekundy) */ };
Ostatnie dwie funkcje, które na tej stronie opiszę, to int
kill(pid_t pid, int sygnal)
(pid_t, podobnie jak time_t i size_t,
to zatypedef
owany int)
i sighandler_t signal(int sygnal,
sighandler_t funkcja)
. Pierwsza, jak sama nazwa wskazuje,
wysyła sygnał do procesu o odpowiednim pidzie, druga (co również nazwa
wskazuje) pozwala na przechwycenie sygnału. Funkcja kill()
jest
użyteczna od razu, o ile się zna interesujący pid. W zasadzie użycie tego
ustrojstwa jest proste:
int main(void) { pid_t pid; /* pamietajmy, ze pid_t to tak na prawde int */ int sygnal; puts("Podaj pid procesu"); scanf("%d", &pid); puts("Podaj numer sygnalu"); scanf("%d", &sygnal); if ( kill(pid, sygnal) ) perror("Blad wysylania sygnalu"); return 0; } |
Jeśli chodzi o sygnały w un*xie, to jest parę interesujących i przydatnych
rzeczy. Przede wszystkim, nie wolno ci wysłać sygnałów do nie swoich procesów.
Wyjątkiem jest root, ale administrator musi mieć prawo zabijać cokolwiek. To
nie Windows ;) To po pierwsze. Po drugie, sygnałów w Linuksie jest w sumie 63,
ale to jest limit kernela. POSIX gwarantuje istnienie sygnałów od 1 do 31,
sygnały 32 do 63 nie powinny być używane przez użytkownika. Jeśli programista
chce zakillować jakiś proces (nie zabić, ale zasygnalizować mu coś), to od
tego są sygnały SIGUSR1
(pod Linuksem sygnał 10)
i SIGUSR2
(linuksowy numer 12). Te dwa sygnały nie są stałe,
w różnych implementacjach Unixa mogą mieć różne numery. Polecam odwoływać się
do ich #definicji. Domyślną akcją na te sygnały jest zakończenie procesu.
Z innych sygnałów zabójczych (kończących proces) używa się
SIGTERM
(zawsze 15), SIGKILL
(zawsze 9),
SIGINT
(zawsze 2), SIGHUP
(zawsze 1) oraz
SIGQUIT
(zawsze 3). Sygnał SIGINT
jest wysyłany, gdy
użytkownik programu naciśnie ^C ([Ctrl+C]). Sygnał
SIGQUIT
jest wysyłany, gdy użytkownik naciśnie ^\,
a akcją jest zakończenie programu i - w przeciwieństwie do pozostałych
wymienionych - zrzut pamięci (tzw. core dump, użyteczne, pod warunkiem,
że masz sporą wprawę w debuggowaniu programów na podstawie obrazu pamięci).
SIGTERM
to sygnał domyślnie wysyłany przez komendę
kill. SIGHUP
jest z kolei wysyłany do programów,
których terminal kontrolujący zniknął
- dlatego podczas zdalnej pracy nie jest bezpieczne odpalać jakąś dłuższą
kompilację czy coś takiego inaczej, niż poleceniem screen (zerwanie
połączenia nie przerwie pracy takiej kompilacji). Na koniec zostawiłem sobie
najbardziej zabójczy sygnał SIGKILL
. Powoduje bezwarunkowe
zakończenie programu, nie można się przed nim obronić
[*].
Z ciekawszych sygnałów zostały jeszcze SIGFPE
(zawsze 8),
SIGSEGV
(zawsze 11), SIGPIPE
(zawsze 13),
SIGBUS
(w Linuksie 7) oraz SIGTSTP
(w Linuksie 20).
Ostatni sygnał jest wysyłany przy naciśnięciu ^Z i oznacza
zastopowanie programu z klawiatury (tty stop).
Pozostałe to kolejno: floating point exception (np. dzielenie przez 0,
także przy intach), segmentation fault (naruszenie ochrony
pamięci, znaczy program próbuje się odwołać do nie swojej pamięci, częste przy
odczycie tablicy w pętli), oba generujące zrzut pamięci przy kończeniu
procesu, broken pipe (proces próbuje pisać do
potoku albo nazwanego potoku,
podczas gdy nikt nie odczytuje danych), kończące proces bez zrzutu pamięci
oraz bus error (błąd szyny [nie mam pomysłu na tłumaczenie], oznacza
błąd adresowania, na przykład próba odczytu inta spod adresu
nieparzystego, dość rzadki sygnał), generujący zrzut pamięci. Oczywiście tam,
gdzie zaznaczyłem numer sygnału "w Linuksie", numer sygnału zależy od
implementacji, tak więc pod Solarisem czy FreeBSD może to być inny numer.
To teraz parę słów o signal()
. Funkcja pozwala na przechwycenie
praktycznie dowolnego sygnału, oprócz SIGKILL
i SIGSTOP
, które są nieprzechwytywalne. Typ
sighandler_t to tak naprawdę wskaźnik do funkcji (czyli funkcja).
Ta funkcja ma przyjmować jednego inta, a zwracać nie ma nic.
Przykład będzie tu chyba najbardziej obrazowy:
/* nasza funkcja obslugujaca sygnaly */ void sygnalizator(int numer_sygnalu) { printf("Kukuryku! (numer sygnalu: %d)\n", numer_sygnalu); } int main(void) { char znaczek; int i; for (i = 1; i < 32; ++i) signal(i, sygnalizator); /* jakos trzeba program zakonczyc, a kill -9 jest niewygodne */ while ( (znaczek = getchar()) != 'q' ) ; return 0; } |
Teraz po każdym naciśnięciu ^C, ^\, ^Z albo
zakillowaniu wyświetli się numer sygnału. Możesz sprawdzić, że
kill -STOP i kill -KILL nie zostały przechwycone.
Co ciekawe, możesz przywrócić poprzedni handler sygnału.
sighandler_t poprzedni_handler; void handler(int sygnal) { puts("Wywolany handler"); signal(sygnal, poprzedni_handler); } int main(void) { poprzedni_handler = signal(SIGINT, handler); while (1) sleep(1); return 0; } |
Ten program przy pierwszym naciśnięciu ^C wyświetli Wywolany
handler, a przy drugim naciśnięciu wyjdzie. Przy okazji widzisz, jak można
łączyć w sekwencje handlery (wewnątrz handlera wywołanie funkcji
signal()
).
Na koniec, zamiast funkcji handlera możesz podać SIG_IGN
, żeby
sygnał zignorować (zamiast pisać pustą funkcję), albo SIG_DFL
,
żeby przywrócić domyślny handler sygnału (zamiast ręcznie pamiętać, jak
w ostatnim przykładzie).
[*] Czasem system operacyjny potrzebuje
procesu do jakiś dziwnych rzeczy, czasem musi po drodze pozwalniać zasoby
procesu, więc może się zdarzyć proces-duch odporny nawet na
SIGKILL
, ale to są baaardzo rzadkie przypadki.