Rozdział 6
Podstawy programowania
Przedstawiony teraz zostanie opis wybranych funkcji systemowych Unixa. Omawiane funkcje są
dostępne dla programisty jako wywołania funkcji w języku C i tak też będą prezentowane.
Większość funkcji zwraca pewną wartość. Do sygnalizacji błędu używana jest wartość róż-
na od 0, najczęściej -1. Nie należy zaniedbywać sprawdzania wartości, którą zwróciła funkcja
systemowa, gdyż błędy powstałe przy operacjach wejścia-wyjścia nie należą do rzadkości.
Przyczyn wystąpienia błędu może być bardzo dużo, globalna zmienna errno zawiera kod,
który można odczytać po stwierdzeniu, że funkcja systemowa zwróciła błąd. Proszę pamiętać, że
należy zawsze sprawdzić wcześniej wartość zwróconą przez funkcję systemową, ponieważ wartość
zmiennej errno ma znaczenie tylko przy wystąpieniu błędu.
6.1 Programowanie w języku C pod UNIX
Kod programu w języku C standardowo posiada rozszerzenie .c Kompilowanie kodu odbywa się
za pomocą polecenia cc (gcc w wersji GNU).
cc przyklad.c
wynik poprawnej kompilacji zostanie zapisany do pliku a.out. Kompilowanie kodu do pliku o
wskazanej nazwie uzyskuje się za pomocą opcji -o kompilatora.
cc przyklad.c -o przyklad
Kompilowanie kodu programu składającego się z wielu plików:
cc przyklad.c inne_funkcje.c pomocnicze.c -o przyklad
Tworzenie plików wstępnie skompilowanych (obj):
cc -c inne_funkcje.c
wynik zostanie zapisany do pliku inne_funkcje.o. Pliki te można następnie wielokrotnie używać
w innych programach, bez potrzeby ich wstępnego przetwarzanie przez kompilator:
cc przyklad.c inne_funkcje.o pomocnicze.o -o przyklad
28 pazdziernika 2001 roku wersja 0.2 89
Dodatkowe użyteczne polecenia:
program linkujący ld;
debuger db;
program umożliwiający łączenie bibliotek ar;
program ułatwiający i przyśpieszający kompilowanie dużych programów, składających się
z różnych bibliotek make.
Oczywiście lista ta nie wyczerpuje przydatnych dla programistów poleceń i programów, przed-
stawia jedynie najważniejsze.
6.2 Funkcje systemowe operujące na plikach
Wszyscy, którzy pisali kiedykolwiek programy w języku C, znają zapewne funkcje fprintf,
fscanf, fopen czy fclose. Są to standardowe funkcje biblioteczne, umożliwiające zapisywanie
i odczyt danych z pliku. Są to funkcje wysokiego poziomu, które wykonują dość złożone ope-
racje, takie jak np. czytanie liczb w postaci znakowej i zamiana na reprezentacje wewnętrzną,
pozwalają czytać zarówno pojedynczy znak (bajt), jak i wielobajtowy blok. Czasem zachodzi
jednak potrzeba, by skorzystać z funkcji niższego poziomu jest tak zwykle wtedy, gdy zależy
nam na efektywności programu. Poniżej opisany został podstawowy zestaw funkcji systemowych
działających na plikach: creat, open, write, read, lseek, close, unlink, link i chdir.
Ważnym pojęciem, jest deskryptor pliku (ang. file descriptor). Deskryptor ten to nie-ujemna
liczba całkowita, jednoznacznie związana z każdym wykorzystywanym przez dany proces plikiem.
Każdy proces unixa ma do swej dyspozycji 20 deskryptorów, numerowanych od 0 do 19, są
one lokalne dla danego procesu. Użycie jakiegokolwiek pliku wymaga najpierw jego otwarcia,
wówczas system wiąże jeden z deskryptorów z plikiem o podanej nazwie. Każda wykonywana
na pliku operacja wymaga podania tego właśnie deskryptora. Na koniec plik zostaje zamknięty
także za pomocą deskryptora. W ogólności, deskryptory nie muszą być związane z plikami
dyskowymi, mogą to być także strumienie związane z urządzeniem (np. drukarką) 1ub potokiem.
Mimo to tradycyjnie mówi się o deskryptorach plikowych.
Deskryptory plikowe będą zwykle oznaczane jako fd, choć oczywiście pisząc program w C
można używać dowolnej nazwy. Proszę zwrócić uwagę, że deskryptor plikowy nie jest tym sa-
mym, co wskaznik typu FILE* używany w procedurach z serii fprintf, fscanf itd. Wskaznik
ten, oznaczany często przez fp, jest konstrukcją wyższego poziomu, implementowaną przez stan-
dardową bibliotekę języka C w kategoriach deskryptorów fd i operacji korzystających z fd.
28 pazdziernika 2001 roku wersja 0.2 90
Deskryptor jest otwarty , gdy został już związany z pewnym plikiem lub strumieniem po-
przez operację otwarcia, w takiej sytuacji o pliku także mówi się, że jest otwarty . Każdy
proces, gdy zostaje uruchomiony, otrzymuje od systemu (a ściślej, od swego rodzica) trzy już
otwarte deskryptory. Są to: deskryptor 0, związany ze standardowym strumieniem wejściowym;
deskryptor 1, związany ze standardowym strumieniem wyjściowym, oraz deskryptor 2, związany
ze standardowym strumieniem diagnostycznym. Stwierdzenie, że np. program pisze na standar-
dowe wyjście, oznacza, że wykonuje on operację zapisu z użyciem deskryptora 1.
Z każdym otwartym plikiem związany jest wskaznik bieżącej pozycji. Określa on miejsce, skąd
pobrany zostanie kolejny bajt przy najbliższej operacji odczytu, lub gdzie zostanie zapisany przy
najbliższym zapisie. Każda operacja zapisu lub odczytu przesuwa wskaznik o odpowiednią liczbę
bajtów do przodu .
Procedura creat (od ang. create) służy do tworzenia pliku. Jej nagłówek w języku C wygląda
następująco:
int creat(char *sciezka, int prawa);
/* Zwraca deskryptor pliku lub -1 w razie bledu */
Parametr sciezka określa nazwę pliku wraz ze ścieżką, który ma zostać utworzony, przy jego po-
dawaniu obowiązują takie same reguły, jak przy podawaniu nazw plików w dowolnym poleceniu
unixowym. Parametr prawa określa, jakie prawa mają być nadane plikowi. Wykorzystywanych
jest dziewięć najmniej znaczących bitów, ich znaczenie jest takie, jak w poleceniu chmod rwx
dla właściciela, grupy i dla pozostałych. Funkcja creat tworzy plik o długości zero i zostawia go
w stanie otwartym do zapisu, zwracając jego deskryptor. W przypadku wystąpienia błędu funk-
cja zwraca wartość -1. Do poprawnego wykonania funkcji creat należy posiadać prawo zapisu
w katalogu, w którym plik jest tworzony.
Może się zdarzyć, że plik o podanej nazwie już istnieje. W takiej sytuacji zostanie on obcięty
do zerowej długości i otwarty do zapisu. W tym przypadku nie jest wymagane prawo w do kata-
logu, w którym plik się znajduje, lecz prawo x do katalogu i prawo w do samego pliku. Wartość
parametru prawa jest ignorowana.
Istnieją dwa warianty funkcji systemowej open. Poniżej przedstawiony został starszy wariant,
dwu-argumentowy:
int open(char *sciezka, int tryb);
/* Zwraca deskryptor pliku lub -1 w razie bledu. */
28 pazdziernika 2001 roku wersja 0.2 91
Funkcja ta otwiera plik określony przez parametr sciezka do odczytu lub zapisu, w zależności od
wartości parametru tryb, wartość 0 oznacza otwarcie do odczytu, 1 do zapisu, 2 do odczytu,
i do zapisu. Otwierany plik musi już istnieć. Konieczne jest posiadanie odpowiednich przywilejów
(r lub w). Wskaznik bieżącej pozycji otwieranego pliku jest ustawiany na jego pierwszy bajt.
Nowszy wariant procedury open wymaga trzech argumentów i wykonuje te zadania, co creat,
pierwsza wersja open, a także pewne funkcje dodatkowe.
int open(char *sciezka, int tryb, int prawa);
/* Zwraca deskryptor pliku lub -1 w razie bledu. */
Dla wartości parametru tryb równych 0, 1 lub 2 zachowanie trójargumentowego open jest takie
samo, jak wersji dwu-argumentowej. Parametr prawa jest ignorowany. Mogą jednak pojawić się
inne wartości, zdefiniowane jako stałe symboliczne w pliku fcntl.h (plik ten należy dołączyć
do swojego programu dyrektywą #include):
O_CREAT powoduje utworzenie pliku, jeśli wcześniej nie istniał. Jest to jedyny przypadek,
gdy wykorzystywany jest trzeci parametr prawa.
O_TRUNC ucina plik do zerowej długości (niezależnie od tego, czy plik jest otwierany do
zapisu, czy do odczytu).
O_EXCL nie pozwala na otwarcie pliku, jeśli istniał on już wcześniej. Używane łącznie z
O_CREAT.
O_APPEND otwiera plik do dopisywania, gwarantując, że każda operacja zapisu będzie wy-
konywana na końcu pliku.
Powyższe stałe można łączyć za pomocą alternatywy bitowej (operator |). Dla jednorodności
zapisu zdefiniowano także stałe O_RDONLY, O_WRONLY i O_RDWR, odpowiadające wartościom 0, 1
i 2, czyli zgodne ze starą wersją procedury open. Jak widać, stałych O_RDONLY i O_WRONLY nie
można złączyć przy użyciu alternatywy, zamiast tego trzeba stosować O_RDWR.
Tradycyjne wywołanie creat odpowiada wywołaniu open postaci:
open(sciezka, O_WRONLY|O_CREAT|O_TRUNC, prawa);
Ciekawe jest zachowanie się pliku, gdy użyjemy trybu O_APPEND. Otóż gwarantuje on, że każda
operacja zapisu najpierw przesuwa wskaznik pozycji na koniec pliku, co więcej, operacja ta jest
niepodzielna. Unika się tym sposobem problemów, jakie mogłyby wyniknąć przy dopisywaniu
danych do pliku przez kilka procesów jednocześnie, gdyby korzystały one z oddzielnych operacji
28 pazdziernika 2001 roku wersja 0.2 92
przesuwania wskaznika na koniec pliku i zapisu (każdy proces ma swój własny wskaznik pozycji).
Wyobrazmy sobie taki scenariusz: pewien proces ustawia swój wskaznik na koniec pliku i zanim
zdąży on dokonać zapisu danych, drugi proces ustawia swój wskaznik na koniec, (czyli w to samo
miejsce, co pierwszy proces) i zapisuje dane, nieświadomy tego faktu proces pierwszy zamazuje
swoimi danymi dane z procesu drugiego. Przy zastosowaniu O_APPEND system gwarantuje nam
atomiczność operacji przesunięcia wskaznika wraz z zapisem danych.
Do zapisania danych na plik służy procedura write:
int write(int fd, char *bufor, unsigned wielkosc);
/* Zwraca liczbe faktycznie zapisanych bajtow, */
/* lub -1 w razie bledu. */
Aby zapisać blok danych na plik, identyfikowany przez deskryptor pliku fd, wskaznik do bufo-
ra, gdzie znajdują się dane oraz liczbę bajtów do zapisania. Parametr bufor jest wskaznikiem
znakowym (char *), lecz nie istnieją jakiekolwiek ograniczenia co do typu zapisywanych danych
zapisywany jest blok o długości wielkosc bajtów, o adresie początkowym określonym przez
wskaznik bufor. Funkcja write zapisuje blok i przesuwa wskaznik bieżącej pozycji pliku o liczbę
zapisanych bajtów. Zwraca liczbę faktycznie zapisanych bajtów lub -1 w razie błędu. Teoretycz-
nie może się zdarzyć, że wartość zwrócona przez write będzie mniejsza niż wielkosc\verb.
W praktyce, przy zapisywaniu danych na zwykły plik taka sytuacja nie występuje, pojawić się
natomiast może przy korzystaniu z plików specjalnych. Nie jest to wtedy błąd, lecz przejaw
niemożności zapisania całego bloku w danej chwili.
Mówiąc o funkcji write, warto wspomnieć o dwóch sprawach związanych z realizacją operacji
dyskowych przez Unixa. Po pierwsze, zapis na dysku wykonuje się blokami jest to bezpośred-
nia konsekwencja budowy tego urządzenia. Jeśli więc użytkownik będzie żądał zapisania np.
jednego bajtu, to system i tak musi zgromadzić najpierw odpowiednią porcję danych w swoim
buforze (chyba, że zamkniemy plik, wymuszając tym samym zapisanie tego, co jest). Wywołania
funkcji systemowych są bardzo czasochłonne w porównaniu z wywołaniami zwykłych funkcji w C
i dlatego o wiele bardziej efektywne jest np. jednokrotne wywołanie write z żądaniem zapisania
stu bajtów niż sto wywołań, z których każde zapisuje po jednym bajcie. Wynika stąd, że często
dobrze jest mieć swój bufor wstępny , w którym zgromadzimy najpierw większą porcję danych
do zapisu. Idea buforowania wstępnego jest wykorzystywana przez funkcje z serii fprintf, putc
itd., niejednokrotnie więc ich użycie może być bardziej efektywne niż nieprzemyślane zastoso-
wanie funkcji systemowych z rodziny write.
28 pazdziernika 2001 roku wersja 0.2 93
Dane z pliku odczytujemy przy użyciu funkcji read:
int read(int fd, char *bufor, unsigned wielkosc);
/* Zwraca liczbe odczytanych bajtow, */
/* 0 przy dojsciu do konca pliku */
/* lub -1 w razie bledu. */
Procedura czyta wielkosc bajtów z pliku wskazanego przez deskryptor fd i umieszcza je pod
adresem wskazanym przez parametr bufor. Po odczycie wskaznik bieżącej pozycji w pliku jest
odpowiednio przesuwany. Przy wykonywaniu operacji read częsta jest sytuacja, że odczytano
mniej bajtów, niż wskazuje wartość parametru wielkosc. Jest tak zwykle przy końcu pliku, gdy
liczba bajtów, jakie jeszcze pozostały, jest mniejsza niż wielkosc. W takim wypadku następne
read zwróciłoby zero czyli nie ma już nic do przeczytania . Faktyczny błąd jest sygnalizo-
wany wartością -1.
Gdy zachodzi potrzeba przesunięcia wskaznika bieżącej pozycji w pliku, stosuje się funkcję
lseek:
long lseek(int fd, long n, int interpr);
/* Zwraca wartość wskaznika bieżącej pozycji */
/* lub -1 w razie błędu. */
Funkcja lseek przesuwa wskaznik pozycji na trzy różne sposoby, w zależności od parametru
interpr. Dla interpr równego 0, lseek ustawia pozycję w pliku na n-ty bajt licząc od początku
pliku (aby ustawić plik w pozycji początkowej, trzeba podać zerową wartość n). Dla interpr
równego 1, wskaznik pozycji jest przesuwany o n bajtów do przodu (n dodatnie) lub do tyłu (n
ujemne). Wreszcie dla interpr równego 2, przesunięcie następuje względem końca pliku, przy
czym dodatnia wartość n oznacza przejście za koniec pliku, a wartość ujemna cofnięcie się
o wskazaną liczbę bajtów. Funkcja lseek zwraca wartość wskaznika bieżącej pozycji w pliku w
rozumieniu bezwzględnym: pozycja początkowa to 0, za pierwszym bajtem - 1 itd.).
Nie jest błędem przesunięcie wskaznika pozycji za koniec pliku. Jeśli to zostanie zrobione, a
następnie zapisane zostanie coś przy użyciu write, plik zostanie rozciągnięty do wymaganej
długości. Utworzony w ten sposób odstęp zostanie wypełniony przypadkowymi bajtami.
Oprócz normalnego użycia do odnalezienia konkretnej pozycji w pliku, funkcji lseek używa
się bardzo często do odczytania wartości wskaznika bieżącej pozycji. W tym celu należy zażądać
zerowego przesunięcia względem bieżącej pozycji i wykorzystać zwróconą przez lseek wartość:
28 pazdziernika 2001 roku wersja 0.2 94
x = lseek(fd, 0L, 1);
Należy zwrócić uwagę na zapis zera (0L) jest to konieczne, gdyż drugi parametr ma być typu
long. Inne typowe zastosowanie lseek to przejście na koniec pliku:
x = lseek(fd, 0L, 2);
Kończąc pracę z danym plikiem wywołujemy funkcję close:
int close(int fd);
/* Zwraca 0 lub -1 w razie błędu. */
Funkcja ta zwalnia deskryptor pliku fd, pozwalając na jego ponowne użycie dla innego pliku.
Faktyczne zamknięcie pliku nastąpi dopiero w momencie zakończenia procesu (dokonałoby się
ono i tak, nawet jeśli nie wywołanoby close). W praktyce można więc właściwie poniechać
używania close, jeśli tylko nie potrzebujemy zwolnić deskryptora fd. Z drugiej strony, użycie
close jest dobrym zwyczajem, ułatwiającym ewentualne modyfikacje programu.
Procedura unlink jest odpowiednikiem polecenia rm w powłoce.
int unlink(char *sciezka);
/* Zwraca 0 lub -1 w razie błędu. */
Dla plików z pojedynczym dowiązaniem jest to usunięcie wskazanego przez parametr sciezka
pliku. Funkcja usuwa wskazane dowiązanie, a więc zmniejsza liczbę dowiązań o jeden. Usunięcie
ostatniego dowiązania powoduje fizyczne usunięcie pliku. Niemniej, jeśli wywołanie unlink spo-
woduje zniszczenie ostatniego dowiązania, lecz istnieje proces, który danego pliku używa (tzn.
ma go otwartego), to faktyczne usunięcie pliku zostanie wstrzymane do momentu zamknięcia
pliku przez proces. Ta specyficzna cecha unlink jest wykorzystywana przy zakładaniu plików
tymczasowych, np.:
fd = creat("plik_tymcz", 0600);
unlink("plik_tymcz");
Proces używający pliku plik_tymcz nie musi się troszczyć o jego zamknięcie i usunięcie, nastąpi
to automatycznie z chwilą zakończenia procesu. Przy takim rozwiązaniu nie jest konieczne prze-
widywanie wszystkich możliwych wyjść z programu i umieszczanie tam poleceń usuwających plik.
28 pazdziernika 2001 roku wersja 0.2 95
Funkcja systemowa link to odpowiednik polecenia ln:
int link(char *sciezka1, char *sciezka2);
/* Zwraca 0 lub -1 w razie bledu. */
Działanie tej funkcji jest takie samo, jak polecenia ln z parametrami sciezka1 i sciezka2.
Dla pliku wskazanego przez parametr sciezka1 tworzone jest nowe dowiązanie, określone przez
sciezka2.
Funkcja chdir zmienia katalog bieżący, dokładnie tak, jak polecenie cd:
int chdir(char *sciezka);
/* Zwraca 0 lub -1 w razie bledu. */
Aby wykonać chdir, trzeba posiadać prawo x do katalogu, do którego zamierza się przejść, nie
jest natomiast konieczne posiadanie prawa r ani w.
6.3 Aplikacje składające się z wielu procesów
6.3.1 Wprowadzenie
Zanim przedstawione zostaną funkcje systemowe służące do tworzenia nowych procesów, przy-
pomnieć należy pojęcia programu i procesu.
Program to zapisany na pliku (zwykle dyskowym) kod wykonywalny.
Proces to tenże kod, załadowany do pamięci, z własnym obszarem danych, z własnym środowi-
skiem i z własną tożsamością w postaci numeru pid.
Środowisko to specyficzny zestaw danych, odziedziczony po procesie-rodzicu, zapisanych jako se-
ria przypisań typu nazwa=wartość. Kolejne przypisania oddzielone są znakiem o kodzie zero
(konwencja języka C).
Nowy proces otrzymuje od swojego rodzica dwa zestawy danych: środowisko oraz argumenty.
Dla programu w C argumenty są dostępne poprzez argumenty procedury main, oznaczane zwy-
kle jako argc i argv. Pierwszy to liczba przekazanych argumentów, drugi kolejne wartości w
konwencji języka C. Środowisko jest dostępne poprzez wskaznik zewnętrzny o nazwie environ.
Treść kolejnych przypisań można odczytać jako environ[0], environ[1], ..., koniec można
rozpoznać po wartości NULL (jeśli np. environ[8] jest ostatnim przypisaniem w środowisku, to
environ[9] ma wartość NULL). Poniższy program w C wypisuje wszystkie otrzymane argumenty
28 pazdziernika 2001 roku wersja 0.2 96
i całe swoje środowisko.
extern char **environ;
main(int argc, char **argv)
{
int i;
printf("Argumenty:\n");
for (i = 0; i < argc; ++i)
printf("%s\n", argv[i]);
printf("Srodowisko:\n");
for (i = 0; environ[i] != NULL; ++i)
printf("%s\n" , environ[i]);
}
Proces może modyfikować swoje środowisko, tylko w obrębie jego dotychczasowego obszaru.
Komunikacja przez środowisko jest bowiem jednokierunkowa nie ma możliwości, by zmienić
środowisko procesu-rodzica.
Proces może odczytać wartość dowolnej środowiska za pomocą funkcji getenv.
/* wyświetla podana jako parametr zmienna środowiska np a.out PATH */
#include
main(int argc, char *argv[]) {
char *variable;
/* sprawdzenie liczby parametrów */
if (argc < 2)
{
printf("Wywołanie: %s nazwa_zmiennej_srodowiska\n", argv[0]);
exit(1);
}
28 pazdziernika 2001 roku wersja 0.2 97
/* przeszukiwanie środowiska */
if ((variable = getenv(argv[1])) == NULL)
printf("%s: nie zdefiniowana zmienna środowiska %s\n", argv[0], argv[1]);
else
printf("%s = %s\n", argv[1], variable);
exit(0);
}
6.3.2 Funkcje systemowe rodziny exec
Podstawową funkcją z rodziny funkcji exec jest execl:
int execl(char *sciezka, char *arg0, char *argl, char *arg2, ...);
/* Normalnie funkcja nie wraca */
/* w razie bledu zwraca -1. */
Działanie execl tak jak i pozostałych funkcji z rodziny exec polega na zastąpieniu kodu,
stanowiącego treść bieżącego procesu, kodem nowego programu, zapisanego na pliku wskazanym
przez pierwszy parametr. Jeśli wywołanie exec powiedzie się, funkcja nie wraca , wykonywany
jest już bowiem nowy program. Niemniej, tożsamość procesu pozostaje pid nie ulega zmianie.
Powrót z funkcji exec może nastąpić tylko w razie błędu (np. wskazany plik nie istnieje lub nie
zawiera kodu wykonywalnego). Parametry arg0, arg1, arg2, ... są przekazywane ładowanemu
programowi, jako ostatnią należy wpisać wartość NULL. Parametry te należy rozumieć jako łań-
cuchy znaków według reguł języka C, czyli wskazniki do ich początkowych znaków, łańcuchy te
muszą być zakończone znakami zerowymi.
Poszczególne wersje funkcji exec różnią się między sobą sposobem przekazania argumentów,
sposobem poszukiwania pliku z programem do wykonania oraz sposobem przekazania środowi-
ska. W wywołaniu execv, zamiast wyszczególniać wskazniki do kolejnych argumentów, podaje-
my wskaznik do tablicy takich wskazników jest to format danych analogiczny do argv bądz
environ. Ostatni wskaznik w tablicy musi być równy NULL. Funkcje execle oraz execve umoż-
liwiają jawne przekazanie środowiska dodatkowy argument (char **envp) musi wskazywać
na zestaw przypisań zmiennych środowiskowych, w formacie opisywanym poprzednio. Funkcje
execlp oraz execvp poszukują pliku do wykonania w katalogach określonych przez zmienną
środowiskową PATH, a więc używają mechanizmu podobnego jak powłoka (wersje exec bez p na
końcu traktują swój pierwszy parametr jako zwykłe względne lub bezwzględne określenie
nazwy pliku, tak jak np. polecenie cp. Wersje execlp i execvp przeszukują katalogi określone
28 pazdziernika 2001 roku wersja 0.2 98
przez zmienną PATH, o ile podana nazwa nie zawiera żadnego znaku /). Tabela 6.1 podsumowuje
własności funkcji systemowych z rodziny exec.
Tabela 6.1. Funkcje systemowe rodziny exec
Funkcja Argumenty Środowisko Przeszukiwanie
dla programu PATH
excel lista dziedziczone nie
execv tablica dziedziczone nie
execle lista jawne nie
execve tablica jawne nie
execlp lista dziedziczone tak
execvp tablica dziedziczone tak
Należy zwrócić uwagę, że nie ma funkcji exec, które przeszukiwałyby ścieżkę PATH i jednocze-
śnie pozwalały na jawne przekazanie środowiska, innymi słowy, nie istnieją funkcje execlep i
execvep. Poniżej przedstawiono skrótowo nagłówki wszystkich funkcji z rodziny exec:
int execl(char *sciezka, char *arg0, char *arg1, ...);
int execv(char *sciezka, char **argv);
int execle(char *sciezka, char *arg0, char *arg1, ..., char **envp);
int execvp(char *sciezka, char **argv, char **envp);
int execlp(char *plik, char *arg0, char *arg1, ...);
int execvp(char *plik, char **argv);
Banalny przykład programu, w którym użyto exec:
#include
void syserr(char *kom) /* wypisz komunikat o błędzie i zakończ prace */
{
extern int errno, sys_nerr;
extern char*sys_errlist[];
fprintf(stderr,"ERROR;%s(%d",kom,errno);
28 pazdziernika 2001 roku wersja 0.2 99
if (errno > 0 && errno < sys_nerr)
fprintf(stderr,";%s)\n",sys_errlist[errno]);
else
fprintf(stderr,")\n");
exit(1);
}
main()
{
/* setbuf(stdout, NULL); */
printf("Rudy lis przeskoczyl przez ");
/* fflush(stdout); */
execl("/bin/echo", "echo", "leniwe", "psy.", NULL);
syserr("execl");
}
Wynik: leniwe psy. dlaczego?
Standardowa biblioteka wejścia-wyjścia, z której pochodzi również printf stosuje buforowa-
nie danych zapisywanych do pliku lub łącza. Ponieważ proces nie zakończył się przed wywołaniem
execl, a bufor z danymi został zamazany zanim jego zawartość mogła zostać wypisana. Roz-
wiązanie tego problemu może polegać na wymuszeniu braku buforowania danych wejściowych,
co uzyskiwane jest poprzez funkcję setbuf(stdout, NULL) albo wypisanie zawartości bufora
przed wywołaniem exec za pomocą funkcji fflush(stdout).
6.3.3 Rozwidlenie procesów
Podstawową operacją przy tworzeniu nowego procesu jest rozwidlenie:
int fork();
/ * Zwraca pid procesu potomnego (dla rodzica), */
/* 0 (dla potoraka) */
/* lub -1 w razie bledu */
Wywołanie bezargumentowej funkcji fork powoduje utworzenie kopii wywołującego ją proce-
su. Kopia ta zawiera taki sam kod i takie same dane, różnica polega jedynie w tożsamości :
proces, który wywołał fork czyli rodzic zachowa swój pid, natomiast proces powstały
po rozwidleniu czyli potomek będzie miał nowy pid otrzymany od systemu. Rozwidlenie
28 pazdziernika 2001 roku wersja 0.2 100
przebiega w ten sposób, że jeden proces wywołuje funkcję fork, lecz powrót następuje już do
dwóch procesów. Każdy z nich otrzymuje od fork inną wartość. Rodzic otrzymuje pid potomka,
a zatem wartość dodatnią. Potomek otrzymuje zero. Jeśli rozwidlenie nie powiedzie się (np. z
powodu braku miejsca w pamięci lub na dysku), niedoszły rodzic otrzymuje wartość -1. Różne
wartości zwrócone przez fork umożliwiają rozróżnienie obydwu procesów i podjęcie przez nie
różnych zadań. Zwykle rodzic czeka na potomka (tak jak np. powłoka czeka na zakończenie
polecenia), albo kontynuuje pracę niezależnie (jak powłoka po zleceniu wykonania polecenia w
tle). Ten aspekt zostanie przedstawiony wraz z funkcją wait. Proces potomny zazwyczaj od razu
wywołuje procedurę exec, która ładuje do pamięci nowy kod programu.
Potomek dziedziczy po rodzicu wszystkie otwarte deskryptory plikowe. Każdy z otwartych
plików znajduje się w tej samej pozycji, albowiem wskaznik bieżącej pozycji jest wspólny dla ro-
dzica i potomka. Niemniej jednak, sam deskryptor jest już oddzielny; potomek może go zamknąć
i ponownie wykorzystać nie kolidując z działaniami rodzica.
Ilustracja najprostszego z możliwych użycia funkcji fork. Efektem wykonania tego programu
jest powstanie dwóch procesów o różnych numerach pid.
main()
{
int pid;
pid = fork();
printf("Rezultat fork = %d\n", pid);
}
Wynikiem uruchomienia powyższego programu będą dwa komunikaty, przykładowo:
Rezultat fork = 0
Rezultat fork = 1234
Komunikat zawierający zero będzie pochodził od procesu potomnego, komunikat z inną
liczbą od rodzica. Kolejny przykład, niewiele bardziej złożony, uwidacznia typowy sposób
rozdzielenia akcji rodzica i potomka w zależności od wartości zwróconej przez fork. Program
wykorzystuje bezargumentową funkcję getpid, która podaje pid bieżącego procesu.
main()
{
28 pazdziernika 2001 roku wersja 0.2 101
int id;
printf("Proces pierwszy. Moj pid = %d\n", getpid());
id = fork();
switch(id) {
case -1:
printf("Proces pierwszy. Blad fork.\n");
exit(1);
case 0:
printf("Proces drugi. Moj pid = %d\n", getpid());
exit(0);
default:
printf("Proces pierwszy. Moj pid to wciaz = %d\n", getpid());
printf("Pid procesu potomnego = %d\n", id);
}
}
Jeśli wartość zwrócona przez fork wynosi -1, oznacza to, że nowy proces nie został stworzony. W
takiej sytuacji kończymy program wywołaniem exit(1), sygnalizując przy okazji błąd niezero-
wym kodem wyjścia. Wartość zero otrzymuje proces potomny. Wyświetla on swój pid i kończy
pracę przy pomocy exit(0). Gałąz default wykonuje proces-rodzic, ponownie wypisując swój
pid oraz pid potomka, otrzymany od funkcji fork.
Możemy już podać przykład najprostszego rozwidlenia, połączonego z załadowaniem innego
programu. Uruchomiony proces wykona operację fork, natomiast operację exec wykonay-
wana już nowy proces, załaduje on program o nazwie nowy.prog (bez przekazywania mu para-
metrów).
main()
{
switch(fork()){
case -1:
printf("Blad procedury fork.\n");
exit(1);
28 pazdziernika 2001 roku wersja 0.2 102
case 0: /* To juz jest proces potomny */
execlp("nowy.prog", NULL);
printf("Blad procedury exec.\n");
exit(1);
default: /* To jest proces-rodzic */
printf("Rodzic.\n");
}
}
Sposób wykorzystania funkcji fork nie odbiega tu od poprzedniego przykładu. W gałęzi case 0,
do której wchodzi proces potomny, wywołano exec. Jeśli nie nastąpi żaden błąd, procedura ta
nie wróci . Jeśli exec wróci , to musiał nastąpić błąd nie potrzeba nawet sprawdzać zwró-
conej wartości, gdyż jest ona na pewno równa -l. Wypisywany więc jest komunikat i kończy się
proces za pomocą exit(1). Proszę zwrócić uwagę, że po błędzie exec w dalszym ciągu istnie-
ją dwa procesy (chyba, że rodzic już wcześniej zdążył zakończyć swe działanie). Dopiero exit
kończy wykonywanie procesu potomnego.
W przedstawionym przykładzie nie ma niczego, co nakazywałoby procesowi-rodzicowi cze-
kać na zakończenie procesu potomnego. Od momentu rozwidlenia za pomocą fork, istnieją dwa
niezależne od siebie procesy. Często istnieje potrzeba, by rodzic poczekał, aż potomek zakończy
swe działanie. Takie zachowanie rodzica można wymusić przy pomocy funkcji wait.
6.3.4 Inne funkcje systemowe używane przy tworzeniu procesów
int wait(int *kod);
/* Zwraca pid lub -1 w razie bledu. */
/* Przez parametr kod zwraca kod wyjscia procesu. */
Wywołanie wait powoduje zawieszenie wołającego procesu do chwili, gdy dowolny z jego potom-
ków zakończy działanie. Nie ma możliwości określenia, na którego potomka proces ma czekać.
Potomek, który jako pierwszy zakończy działanie, powoduje odwieszenie rodzica, który wywo-
łał wait. Funkcja zwraca pid potomka. Jeśli proces wywołał wait nie mając potomków, funkcja
zwraca -1. O ile wait zostanie wywołane z parametrem kod różnym od NULL, to pod zmienną
wskazywaną przez kod będzie podstawiony kod wyjścia potomka.
28 pazdziernika 2001 roku wersja 0.2 103
Kod wyjścia jest zgłaszany przez proces w wywołaniu funkcji exit. Ta funkcja systemowa
była prezentowana już w przedstawianych przykładach, kończy ona wykonywanie procesu, do-
konując rozmaitych czynności porządkowych. Zakończenie programu w języku C bez wywołania
exit jest równoważne wykonaniu exit(0). Procedura exit nigdy nie wraca .
void exit(int kod);
/* Funkcja nigdy nie wraca. */
Można teraz zaprezentować przykład wykonania programu z oczekiwaniem na jego zakoń-
czenie tak, jak czyni to zwykle powłoka. W przykładzie poniższym ignorowane są wartości
zwracane przez wait.
main()
{
switch(fork()){
case -1:
printf("Blad procedury fork.\n");
exit(1);
case 0: /* To juz jest proces potomny */
execlp("nowy.prog", NULL);
printf("Blad procedury exec.\n");
exit(1);
default: /* To jest proces-rodzic */
printf("Czekam na zakonczenie procesu potomnego.\n");
wait(NULL);
printf("Proces potomny zakonczyl działanie.\n");
}
}
Powstaje pytanie: co zwróci funkcja wait, jeśli potomek zakończy wykonanie zanim rodzic
zdąży wywołać wait? Otóż jeśli potomek kończy wykonanie w chwili, gdy nikt na niego (jesz-
cze) nie czeka, ślad po nim nie ginie. Informacja o nim jest przechowywana przez jądro systemu.
Dzięki temu rodzic, który spóznił się z wywołaniem wait, otrzyma informację o potomku. Po-
wrót z wartością -1 następuje wtedy, gdy rodzic wywoła wait nie mając w ogóle potomków
28 pazdziernika 2001 roku wersja 0.2 104
dlatego, że nigdy ich nie miał bądz dlatego, że poprzednie operacje wait już poinformowały o
ich zakończeniu.
Proste przykłady wywołania funkcji fork i wait://
#include
main() {
int pid = fork();
switch (pid) {
case -1 :
printf("proces pierwszy: BAD FORK\n");
exit(1);
case 0:
printf("Proces drugi: moj pid = %d\n", getpid());
printf("Proces drugi: pid rodzica = %d\n", getppid());
exit(0);
default:
printf("Proces pierwszy: pid rodzica = %d\n", getpid());
printf("Proces pierwszy: pid potomka = %d\n", pid);
printf("Proces pierwszy: czekam na zakończenie procesu potomnego\n");
wait(NULL);
printf("Proces pierwszy: proces potomny zakonczony\n");
exit(0);
}
}
#include
main()
{
int pid = fork();
switch (pid) {
case -1 :
printf("proces pierwszy: BAD FORK\n");
28 pazdziernika 2001 roku wersja 0.2 105
exit(1);
case 0:
execl("/bin/ls", "ls", NULL);
printf("Proces drugi: BAD EXEC\n");
exit(1);
default:
printf("Proces pierwszy: pid rodzica = %d\n", getpid());
printf("Proces pierwszy: pid potomka = %d\n", pid);
printf("Proces pierwszy: czekam na zakończenie procesu potomnego\n");
wait(NULL);
printf("Proces pierwszy: proces potomny zakonczony\n");
}
}
Funkcja getpid podaje pid bieżącego procesu. Istnieje także funkcja getppid, podająca pid
rodzica bieżącego procesu.
int getpid();
/* Zwraca pid biezacego procesu */
int getppid();
/* Zwraca pid rodzica biezacego procesu */
Funkcji getpid używa się np. do utworzenia unikalnej nazwy pliku tymczasowego, wkompono-
wując pid w jego nazwę podobnie jak robi się to za pomocą notacji $$ w powłoce:
char nazwa[10];
sprintf(nazwa, "tmp%d", getpid());
6.4 Tworzenie potoków
Potok (ang. pipe) był już opisywany w kontekście poleceń powłoki. Teraz przedstawione zostanie
tworzeniem potoków przy użyciu funkcji systemowych. Na początek opisane zostały funkcje
pipe, fcntl i dup, tworząc potoki wykorzystywane przez jeden proces. Następnie wyjaśnione
zostaną mechanizmy łączenia potokiem dwóch procesów (producenta i konsumenta), tak jak się
to dzieje np. po wydaniu powłoce polecenia ls | more.
28 pazdziernika 2001 roku wersja 0.2 106
Potok widziany z poziomu języka C reprezentują dwa deskryptory plikowe. Jeden z
nich to wejście potoku, drugi to wyjście . Posługując się operacjami write i read, można
wpisać dane na wejście i odczytać je z wyjścia. Oczywiście, konsument danych może przeczytać
tylko tyle, ile wpisał producent. Na producenta także nałożone jest ograniczenie, ponieważ dane
wpisywane do potoku gromadzone są przez system w buforze o pewnej pojemności (co najmniej
4096 bajtów), producent musi zaczekać, jeśli konsument nie zdąży odebrać danych wystarczająco
szybko. Synchronizacja następuje poprzez wstrzymanie powrotu z wywołania funkcji read lub
write do momentu, gdy nadejdą dane od producenta lub gdy konsument pobierze porcję danych,
zwalniając tym samym miejsce w buforze.
Podstawową funkcją systemową jest tu pipe:
int pipe(int pfd[2]);
/* Zwraca 0, lub -1 w razie bledu. */
Procedura ta tworzy potok, którego wejściem jest deskryptor pfd[0], a wyjściem deskryptor
pfd[1]. Zapis danych do potoku wykonuje się procedurą write. Jeśli w systemowym buforze
potoku nie ma wystarczająco dużo miejsca na zapisanie żądanej liczby bajtów, operacja jest
wstrzymywana do momentu, gdy operacja read na wyjściu pobierze odpowiednio dużą por-
cję danych. Odczyt za pomocą read jest ściśle sekwencyjny, dane raz przeczytane nie mogą
być zwrócone do potoku. Jeśli bufor potoku jest pusty, read zostanie wstrzymane do chwili
pojawienia się przynajmniej jednego bajtu. Procedura read czyta z potoku tyle bąjtów, ile w
nim się znajduje, nawet jeśli zażądano więcej (wartość zwrócona przez read informuje o liczbie
faktycznie pobranych bajtów). Nigdy, rzecz jasna, nie jest pobierane więcej niż żądano. Dane
pozostawione przez jedno wywołanie read można odczytać w następnym wywołaniu.
Przesyłanie danych przez potok powinno się zakończyć wykonaniem funkcji close na wejściu
potoku. Dla czytającego dane z wyjścia będzie to oznaczało koniec pliku . Po pobraniu wszyst-
kich znajdujących się jeszcze w buforze danych, następna operacja read zwróci zero (co dla
zwykłych plików jest znakiem końca pliku). Jeśli close zostałoby wykonane na wyjściu potoku,
najbliższa operacja write zasygnalizuje błąd.
W odniesieniu do potoków nie używa się funkcji creat, open ani lseek. Przydatna jest na-
tomiast funkcja systemowa fcntl. W ogólności służy ona do odczytania lub zmiany parametrów
otwartego pliku lub potoku:
int fantl(int fd, int polec, int arg);
/* Zwraca parametr, o ktory pytano, */
/* lub -1 w razie bledu. */
28 pazdziernika 2001 roku wersja 0.2 107
Pierwszym parametrem jest deskryptor pliku, którego własności należy zmienić. Drugi parametr
to polecenie, interesujące są dwie możliwości:
F_GETFL sprawia, że funkcja fcntl zwraca liczbę określającą tryb otwarcia deskryptora.
Jest to wartość, która została podana jako trzeci argument funkcji open. Dzięki niej można
się zorientować, czy plik jest otwarty do zapisu, czy do odczytu itp.
F_SETFL pozwala ustawić dwa spośród znaczników trybu: O_APPEND i O_NDELAY.
Pierwszy z powyższych znaczników trybu był już opisywany przy omawianiu trójargumentowej
funkcji open. Drugi nie ma znaczenia dla zwykłych plików, lecz ważny jest właśnie w przypadku
potoków. Jego ustawienie powoduje, że operacje write i read nigdy nie są wstrzymywane.
Zamiast tego zwracają zero. W przypadku braku wystarczającej ilości miejsca w buforze potoku
procedura write nie wykonuje zapisu nawet części danych, lecz wraca od razu. Funkcja read,
napotkawszy pusty bufor, także wraca od razu. Należy zwrócić uwagę na występującą w tym
przypadku niejednoznaczność: zwrócona wartość zero oznacza zarówno (chwilowy) brak danych
w potoku, jak i zamknięcie potoku na wejściu.
Wywołanie funkcji fcntl jest jedynym sposobem ustawienia trybu O_NDELAY dla potoku
nie da się tego zrobić podczas tworzenia go procedurą pipe. Ustawiając znaczniki trybu
przy pomocy fcntl należy najpierw odczytać cały ich zestaw, dopisać nowy znacznik przez
alternatywę bitową, a następnie ustawić znaczniki. Poniższy przykład przedstawia ideę takiej
operacji, pomijając jednakże badanie poprawności wykonanych operacji (zwrócenie -1).
tryb = fcntl(fd, F_GETFL, 0);
tryb |= O_NDELAY;
fcntl(fd, F_SETFL, tryb);
Zmienna tryb powinna być typu int. Jeśli chcielibyśmy wyłączyć pewien znacznik, należałoby
np. napisać:
tryb = fcntl(fd, F_GETFL, 0);
tryb &= O_NDELAY;
fcntl(fd, F_SETFL, tryb);
Nagłówek funkcji fcntl oraz definicje stałych F_GETFL i F_SETFL znajdują się w pliku fcntl.h.
Potok jest użyteczny dopiero wtedy, gdy możemy za jego pomocą połączyć dwa procesy. Nie
jest niestety możliwe połączenie dwóch już istniejących procesów, bowiem każdy z nich ma swo-
je lokalne deskryptory plikowe. Nie istnieje taki sposób wywołania funkcji pipe, by dwa końce
28 pazdziernika 2001 roku wersja 0.2 108
potoku znalazły się w dwóch różnych procesach. Jedyną metodą jest otwarcie potoku, a na-
stępnie rozwidlenie procesu. Potomek dziedziczy wszystkie deskryptory plikowe rodzica, a więc
może korzystać z otwartego przezeń potoku. Tym sposobem można utworzyć potok pomiędzy
rodzicem a potomkiem lub między dwoma procesami potomnymi. Dodatkowo należy przekazać
potomkowi numer deskryptora związanego z jednym z końców potoku. Można tego dokonać np.
poprzez argument programu. Poniższy przykład ilustruje tę metodę. Rodzic wpisuje na wejście
potoku tekst powitalny, a następnie zamyka potok. Potomek odczytuje przesłany tekst i wy-
świetla go na ekranie. Dla większej czytelności przykładu wykorzystano w nim niewielką funkcję
blad, której zadaniem jest wypisanie komunikatu o błędzie i zakończenie procesu. Funkcja ta
będzie wykorzystywana także w następnych przykładach.
void blad(char *kom)
{
printf("Blad %s.\n", kom);
exit(1);
}
/* Rodzic - tworca potoku */
main()
{
int pfd[2];
char fd_napis[5];
char s = "Dzien dobry, dziecko!";
if (pipe(pfd) == -1) blad("pipe");
switch (fork()){
case -1:
blad("fork");
case 0: /* Proces potomny */
if (close(pfd[1]) == -1) blad("close");
sprintf(fd_napis, "%d", pfd[0]);
execlp("dziecko", "dziecko", fd_napis, NULL);
blad("exec");
}
28 pazdziernika 2001 roku wersja 0.2 109
/* Proces-rodzic */
if (close(pfd[0]) == -1) blad("close");
if (write(pfd[1], s, strlen(s)+1) == -1) blad("write");
if (close(pfd[1]) == -1) blad("close");
}
/* Potomek -- odbiorca danych: */
main(int argc, char **argv)
{
int fd;
char s[100];
fd = atoi(argv[1]);
if (read(fd, s, sizeof(s)) <= 0) blad("read");
printf("%s\n", s);
}
Należy zwrócić uwagę na zamknięcie tych deskryptorów, które potomkowi i rodzicowi nie będą
potrzebne (odpowiednio pfd[1] i pfd[0]), nie jest to konieczność, lecz dobry obyczaj. Testując
poprawność operacji read, jako błąd klasyfikowana jest również sytuacja nieotrzymania ani
jednego bajtu danych nie jest to reguła, lecz dokonane w tym przykładzie uproszczenie.
Podsumowując sposób, w jaki można połączyć potokiem dwa procesy należy:
1. Wywołać funkcję pipe().
2. Wywołać fork().
3. W procesie potomnym wykonać niezbędne przygotowania i załadować program potomka
poprzez funkcję exec.
4. W procesie-rodzicu wykorzystując drugi koniec potoku.
Jeśli zamierzeniem jest połączenie potokiem dwóch procesów potomnych, to zamiast punktu (4)
należy ponownie wywołać fork i załadować program dla drugiego potomka przez exec.
Powyższy sposób przekazania procesowi potomnemu numeru deskryptora plikowego związa-
nego z wyjściem potoku jest niewątpliwie sztuczny wymaga specjalnej, świadomej akcji ze
strony potomka. W praktyce używany jest mechanizm, który pozwala związać z deskryptory
potoku zdeskryptorami standardowego strumienia wejściowego lub standardowego wyjścia. Jeśli
28 pazdziernika 2001 roku wersja 0.2 110
wtedy wykonana zostanie funkcja fork i exec, to załadowany program będzie miał standardowe
wejście lub wyjście przyłączone do potoku. Takie właśnie działanie podejmuje powłoka, gdy wy-
dana zostanie polecenie z użyciem znaku |. Do zrealizowania tych zamierzeń należy wykorzystać
funkcję dup:
int dup(int fd);
/* Zwraca nowy deskryptor lub -1 w razie bledu. */
Funkcja powiela otwarty deskryptor pliky. Nowy deskryptor jest związany z tym samym plikiem
i może być używany zamiennie, na zupełnie tych samych prawach co deskryptor oryginalny.
Funkcja dup gwarantuje, że nowy deskryptor będzie miał najniższy numer z możliwych, innymi
słowy, wykorzystany zostanie pierwszy deskryptor, który nie był dotychczas otwarty. To własność
decyduje o użyteczności tej funkcji. Jeśli np. wykonano close(0) i zaraz po tym dup(fd), to
dup na pewno powieli deskryptor fd na deskryptor 0 (zaraz po wykonaniu close(0) będzie
on najniższym wolnym deskryptorem).
Taki zabieg można zastosować do związania np. wyjścia potoku ze standardowym wejściem
procesu. Podobnie jak w poprzednim przykładzie, potok otwiera proces-rodzic. Następnie zamy-
ka deskryptor 0 i duplikuje wyjście potoku. Załadowany program potomka będzie miał dzięki
temu standardowe wejście przyłączone do wyjścia potoku. Co ważne, odbędzie się to bez przeka-
zywania jakichkolwiek parametrów i bez żadnej akcji ze strony potomka. W analogiczny sposób
można przyłączyć standardowe wejście potomka do otwartego przez rodzica pliku. Program w
poniższym przykładzie zachowuje się tak, jak polecenie dziecko < plik.we wydane powłoce.
main()
{
int fd;
if ((fd = open("plik.we", O_RDONLY, 0)) == -1) blad("open");
switch (fork()) {
case -1:
blad("fork");
case 0: /* Proces potomny */
if (close(0) == -1) blad("close");
if (dup(fd) != 0) blad("dup");
if (close(fd) == -1 ) blad("close");
28 pazdziernika 2001 roku wersja 0.2 111
execlp("dziecko", "dziecko", NULL);
blad("exec");
}
/* Proces-rodzic */
if (close(fd) == -1) blad("close");
}
Najpierw otwarty do odczytu zostaje plik plik.we. Po wykonaniu rozwidlenia, deskryptor pliku
jest powielany na deskryptor 0, a oryginalny deskryptor jest zamykany. Załadowany następ-
nie program dziecko ma już standardowe wejście (deskryptor 0) związane z plikiem plik.we.
Proces-rodzic zamyka niepotrzebny mu deskryptor otwartego pliku. Uwaga powielenie fd na
deskryptor 0 odbylo się już po rozwidleniu, w procesie potomnym, a więc deskryptor 0 rodzica
nie uległ zniszczeniu.
W zupełnie analogiczny sposób można wykorzystać funkcję dup do związania pliku lub po-
toku z deskryptorem o numerze 1 (standardowe wyjście). Na mocy konwencji, deskryptory 0,
1 i 2 są na początku procesu zawsze otwarte. Jeśli więc wykonane zostanie close(1), można
mieć pewność, że pierwszym wolnym deskryptorem będzie l, a nie 0. Zatem sekwencja operacji
close(1) i dup(fd) powieli fd na deskryptor numer l. Stworzenie przez powlokę prawdziwe-
go potoku, takiego jak np. ls|more, odbywa się właśnie poprzez wywołanie pipe i dwukrotne
rozwidlenie. Zanim każdy z procesów potomnych zostanie załadowany przez exec, wykonywane
jest powielenie deskryptora potoku: dla jednego potomka wejścia, dla drugiego wyjścia.
Zilustrowano to poniższym przykładem:
/* Stworzenie potoku ls | more. */
main()
{
int pfd[2];
if (pipe(pfd) == -1) blad("pipe");
switch(fork()){
case -1:
blad("fork");
case 0: /* Proces potomny nr 1 */
if (close(1) == -1) blad("close");
if (dup(pfd[1]) != 1) blad("dup");
if (close(pfd[0]) == -1 ) blad("close");
28 pazdziernika 2001 roku wersja 0.2 112
if (close(pfd[1]) == -1 ) blad("close");
execlp("ls", "ls", NULL);
blad("exec");
}
switch(fork()){
case -1:
blad("fork");
case 0: /* Proces potomny nr 2 */
if (close(0) == -1) blad("close");
if (dup(pfd[0]) != 0) blad("dup");
if (close(pfd[0]) == -1) blad("close");
if (close(pfd[1]) == -1) blad("close");
execlp("more", "more", NULL);
blad("exec");
}
/* Proces-rodzic */
if (close(pfd[0]) == -1) blad("close");
if (close(pfd[1]) == -1) blad("close");
}
Zamiast używać funkcji dup, można także wykorzystywać funkcję fcntl z drugim para-
metrem podanym jako F_DUPFD. Powiela ona wskazany deskryptor (pierwszy parametr) w taki
sposób, że nowy deskryptor ma numer nie mniejszy niż trzeci parametr wywołania fcntl. Wywo-
łanie dup(fd) jest równoważne wywołaniu fcntl(fd, F_DUPFD, 0). Jak widać, funkcja fcntl
jest ogólniejsza niż dup. Pozwala na łatwe powielenie deskryptora na dowolny inny bez zastana-
wiania się, czy wszystkie deskryptory o niższych numerach są zajęte (chcąc wykonać to zadanie
przy użyciu dup, należałoby powtarzać wywołania dup do momentu, osiągnięcia pożądanego
numeru deskryptora). Niemniej, w praktyce najczęściej interesujące są deskryptory 0 i 1, któ-
re i tak nie przedstawiają tego rodzaju problemów. Poniższa sekwencja obrazuje zastosowanie
funkcji fcntl do powielenia wskazanego deskryptora fd na deskryptor o numerze 13.
close(13);
nowy_fd = fcntl(fd, F_DUPFD, 13);
28 pazdziernika 2001 roku wersja 0.2 113
Wyszukiwarka
Podobne podstrony:
UNIX omowienie 1
UNIX omowienie 2
UNIX omowienie 4
UNIX omowienie 5
Active Directory omówienie domyślnych jednostek organizacyjnych
Unix lab 9
materialy pomocnicze unix
Kasy fiskalne 2014 z omowieniem ekspertow CMS Cameron McKenna
Berkeley Unix Summary
Omówienie metodyki prowadzenia poszczególnych analiz problemowych na
Unix Wprowadzenie Internet i inne sieci
Ćwiczenia Active Directory omówienie jednostek organizacyjnych
Instalacja pracowni omówienie
Systemy Operacyjne Unix Linux solarka2
OMÓWIENIE INTERFEJSÓW I KLAS ABSTRAKCYJNYCH W JĘZYKU JAVA
2 Omowienie pakietu MS Office i Open Office Ogolne wlasciwosci?ytora tekstu
więcej podobnych podstron