z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Rodzina systemów POSIX zaopatrzona została w mechanizm tworzenie międzyprocesowych łączy komunikacyjnych pomiędzy procesami, zwanych potokami: nienazwanymi a więc istniejącymi wyłącznie w pamięci jądra obiektów tymczasowych, tworzonymi obok otwieranych w momencie inicjowania procesu strumieni standard input, output i error; <unistd.h> <stdio.h> <iostram> input STDIN_FILENO FILE *stdin std::cin output STDOUT_FILENO FILE *stdout std::cout error STDERR_FILENO FILE *stderr std::cerr std::clog nazwanymi, czyli posiadających dowiązanie w systemie plików, do których odwołania następując explicite przez nazwę a czas ich istnienia nie jest ograniczony czasem wykonania procesu. Każdemu z tych strumieni system operacyjny przypisuje deskryptor pliku, który stanowi unikalna liczbą całkowitą 0, 1, 2, 3, itd choć trzy pierwsze są przypisane standardowym strumieniom wejściowemu, wyjściowemu i błędu. Służą one procesowi identyfikacji i odwołaniom, przy czym procesy potomne dziedziczą te deskryptory po procesach macierzystych. Liczba możliwych do wykorzystania przez proces deskryptorów, a więc i ilość otwartych plików jest ograniczona. 1
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. W gruncie rzeczy można byłoby się zawahać na ile w przypadku problematyki współbieżności, czy rozpatrywanej tu wieloprocesowości zasadne są takie rozwiązania. W końcu, każdy proces potomny stanowi dokładną kopię procesu macierzystego, czy w takim razie nie łatwiej uzyskać komunikację między procesową poprzez zdefiniowane już w obrębie kodu zmienne (chociażby globalne). Okazuje się jednak że całość nie przedstawia się aż tak prosto, co pokazuje wprost kolejny przykład. Rozważmy hipotetyczną sytuację, że w procesie głównym z pewnego powodu zaistniała potrzeba obliczania wartość całek funkcji jednej zmiennej. W rozpatrywanym konkretnie przypadku jest to całka 1 I = sin 2 x e x dx 0 której wartość, obliczona na drodze analitycznej wynosi I =2 1 e 1 1 4 2 = 0.09811971024 Równocześnie może pojawić się sytuacja, że wyrażenie podcałkowe stanowić będą także i inna funkcja. W zawiązku z czym, logicznym rozwiązaniem byłoby wprowadzenie dodatkowej funkcji, która umożliwi obliczenie wartości całki drogą kwadratury numerycznej. 2
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Postać jej deklaracji nagłówkowej można by przyjąć następująco double quad( unsigned int n,double a,double b, double (*fun)(double) ); gdzie: unsigned int n ilość węzłów kwadratury double a, a b granice całkowania double fun() wskazanie do funkcji stanowiącej wyrażenie podcałkowe Istnieje wiele skutecznych metod obliczania kwadratur numerycznych, gdyby przyjąć tutaj przykładowo metodę trapezów, to b a gdzie: f x dx b a 2n f x 0 2 f x 1 2 f x k 2 f x n 1 f x n x 0 =a ; x n =b; x k =a b a k, k =0,1,2,,n n 3
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Samą funkcję kwadratury numerycznej możemy więc zdefiniować jak niżej. double quad( unsigned int n, double a, double b, double (*fun)(double) ) { unsigned int k; double xk, sum; sum = fun( a ) + fun( b ); for( k=1;k<n;k++ ) { xk = a+ (b-a)*k/n; sum += 2.0*fun( xk ); return ( (b-a)/(2.0*n)*sum ); Ponieważ proces macierzysty ma również i wiele innych zadań do wykonania, obliczenia wartości będą przekazane do utworzonego w tym celu procesu potomnego. 4
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Kod źródłowy może przedstawiać się następująco. #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <math.h> int main( void ) { int pid,status; double sine( double ); double quad( unsigned int,double,double,double (*)(double)); unsigned int n; double a,b,i; Inicjujemy wartości początkowe zmiennych a=0.0; b=1.0; I=0.0; i jeszcze ilość węzłów kwadratury n=3200; 5
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Wywołaniem funkcji fork() duplikujemy bieżący proces i różnicujemy kod. switch( (int)fork() ) { case -1: perror( "<!> błąd inicjacji potomka" ); exit( 1 ); break; case 0: Obliczamy całkę, ale już w potomku I = quad( n,a,b,sine ); Dla pewności wyprowadzamy informację o tym co wyliczyliśmy printf( "[%d] wartość całki\t%16.6f\n",(int)getpid(),i );... i kończymy działanie potomka exit( 0 ); Teraz kod dla procesu nadrzędnego default: Powiedzmy, że coś tutaj ważnego się dzieje printf( "[%d] wykonuje ważne rzeczy...\n",(int)getpid() ); Oczekiwanie na wynik z potomka pid = (int)wait( &status );... i mamy gotowy rezultat printf( "[%d] zakończył z kodem %d\n",pid,status ); printf( "[%d] otrzymał wartość\t%16.6f\n",(int)getpid(),i ); Na tym program kończy działanie return 0; 6
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Oczywiście w kodzie musimy uzupełnić jeszcze definicję funkcji podcałkowej przedstawia się ona bardzo elementarnie double sine( double x ){ return sin( 2*M_PI*x )*exp(-x); Kompilacja i konsolidacja $ gcc -Wall integra.c -o integra -lm zwróćmy uwagę na dyrektywę -lm nakazującą konsolidację z biblioteką matematyczną libm.so a efekt wykonania [12401] wartość całki 0.098120 [12400] wykonuje, ważne rzeczy... [12401] zakończył z kodem 0 [12400] otrzymał wartość 0.000000 a więc nieco zaskakujący. Mimo, że utworzony proces potomny wywiązał się z zadania dobrze uzyskując bardzo dobre przybliżenie 0.098120 wobec wartości dokładnej (wyliczonej na drodze analitycznej) 0.09811971024 jednak proces nadrzędny tej wartości nie otrzymał, w jego przypadku wartość zmiennej I, pozostała równa wartości inicjującej. Stało się tak, ponieważ każdy proces posiadał własną, prywatną, kopię zmiennej, stąd i ich wartości końcowe są różne. Natomiast sama komunikacja międzyprocesowa nie przedstawia się aż tak elementarnie a wymaga specjalnych mechanizmów. 7
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Wróćmy w takim razie do kwestii użycia łączy na początek nienazwanych które mogą stanowić użyteczne narzędzia rozwiązania problemu komunikacji międzyprocesowej. Umożliwiają one asynchroniczną wymianę danych między pokrewnymi procesami a więc mającymi wspólnego przodka, bądź między bezpośrednio miedzy przodkiem a potomkiem (i odwrotnie). Jak wspomniano to już na wstępie, są realizowane jako obiekty tymczasowe w obszarze pamięci jądra, w ten sposób że dodawane są nowe pozycje do tablicy deskryptorów otwartych plików. OBSZAR PAMIĘCI JĄDRA proces łącze proces W obrębie danego łącza przepływ odbywa się zawsze w jednym kierunku (half duplex), w konsekwencji proces piszący musi na wstępie zamknąć odczyt a czytający zapis. Jeżeli komunikacja międzyprocesowa wymaga także kierunku zwrotnego, to konieczne jest otwarcie dwóch łączy. Łącza nienazwane nie są objęte standardem ISO/ANSI C lecz POSIX. De facto łącza nienazwane pipe stanowią wczesną postać UNIX System IPC (omawianego na późniejszych zajęciach). 8
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Komunikacja międzyprocesowa za pośrednictwem potoków może być realizowana zarówno z poziomu kodu źródłowego jak i poziomu instrukcji systemowej, a obecne było w systemach UNIX'opodobnych praktycznie od momentu ich powstania. Realizowane jest za pośrednictwem operatora przetwarzania potokowego, który łączy standardowe wyjście jednego procesu ze standardowym wejściem innego. Przykładowo $ ls -l /usr/bin/ cat -n tail -n 5 2386 -rwxr-xr-x 1 root root 3652 lis 25 2006 znew 2387 lrwxrwxrwx 1 root root 8 kwi 6 2007 zsh -> /bin/zsh 2388 -rwxr-xr-x 1 root root 27920 kwi 18 2007 zsoelim 2389 -rwxr-xr-x 1 root root 10904 lis 27 2006 zvbi-chains 2390 -rwxr-xr-x 1 root root 396688 lip 4 2008 zypper Użyto tutaj trzech instrukcji systemowych ls -l /usr/bin listing katalogu /usr/bin w formacie long cat -n wyświetla zawartość wejście dodając numerację linii tail -n 5 wyświetla 5 ostatnich linii ze strumienia na wejściu ustanawiające między ich standardowymi wyjściami a wejściami dwa łącza nienazwane (symbol ' '). 9
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Korzystając z API systemowego POSIX otwarcie łącza następuje wywołaniem funkcji pipe() zaś zamknięcie close(). Synopsis #include <unistd.h> int pipe( int fd[2] ); int fd[0] zawiera numer deskryptora do odczytu int fd[1] zawiera numer deskryptora do zapisu int close( int fd ); int fd numer deskryptora do zamknięcia Return 0 operacja zakończona powodzeniem Errors -1 Odczyt/zapis z/do potoku odbywa się za pośrednictwem pary funkcji read() i write(). Synopsis #include <unistd.h> ssize_t read( int fd, void *buf, size_t count ); ssize_t write( int fd, const void *buf, size_t count ); void* buf obszar pamięcie do odczytu/zapisu size_t count ilość bajtów do odczytu/zapisu Return ilość bajtów przeczytana / zapisana Errors -1 10
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Jako elementarną ilustrację przygotujemy program, który utworzy proces potomny, wysyłający do macierzystego zwrotną wiadomość. #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <linux/limits.h> int main( void ) { int fd[2],n; PIPE_BUF jest predefiniowaną stała dla bufora systemowego pipe() char line[pipe_buf]; Próbujemy ustanowić połączenie (nienazwane) if( pipe( fd )< 0 ) { printf( "...błąd otwarcia łącza\n" ); exit( 1 ); Tworzymy proces potomny switch( fork() ) { Na wypadek ewentualnego niepowodzenia case -1: perror( "<!> błąd inicjacji potomka" ); exit( 1 ); break; 11
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Jeżeli fork() zakończył się sukcesem, to dla procesu potomnego case 0: Na początek zamykamy kanał od strony odczytu close( fd[0] ); bowiem będziemy do niego pisać write( fd[1],"\n\t[pozdrowienia od potomka]\n\n",29 ); W zasadzie to zamknięcie jest zbędne close( fd[1] ); ponieważ po wykonaniu exit() system i tak zamknie kanał od strony potomka exit( 0 ); Natomiast kod dla procesu nadrzędnego default: Tu natomiast zamykamy zapis close( fd[1] ); Czytamy teraz to co wcześniej wysłał potomek n = read( fd[0],(void*)line,pipe_buf ); //close( fd[0] ); Wypisujemy co otrzymaliśmy od potomka na STDOT write( STDOUT_FILENO,line,n ); return 0; 12
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Zauważmy, że celem wyprowadzenia informacji wykorzystano tutaj jeden ze standardowo predefinowanych deskryptorów #define STDOUT_FILENO 1 /* Standard output. */ #define STDERR_FILENO 2 /* Standard error output. */ które otwierane są dla każdego procesu, w momencie jego tworzenia, przez system. Definicje znajdują się w pliku unistd.h. Rozmiar bufora zdefiniowano za pomocą stałej PIPE_BUF zdefiniowanej /linux/limits.h Jej wartość określa maksymalny rozmiar dla przesyłanej porcji danych potokiem nienazwanym, tak aby operacja ta była atomowa a więc niepodzielna. Zgodnie z definicją #define PIPE_BUF 4096 Obok niej istnieje także definicja stałej POSIX (posix1_lih.h), o identycznym sensie #define _POSIX_PIPE_BUF 512 choć innej (znacznie mniejszej) wartości. Inaczej niż zwykle, w kodzie procesu macierzystego nie użyto funkcji wait(). Było to możliwe ponieważ wywołanie read() ma charakter blokujący. Domniemanie to można zmienić wykorzystując funkcję fcntl() i ustawiając znacznik O_NONBLOC dla uzyskanego wcześniej deskryptora. Wymagałoby to jednak użycia funkcji wait(). 13
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Wracając teraz do problemu przedstawionego na wstępie potomka który wykonywał na rzecz procesu nadrzędnego obliczenia kod programu powinien się przedstawiać jak w listingu. #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> #include <math.h> int main( void ) { int pid,status; double sine( double ); double quad( unsigned int, double, double,double (*)(double) ); unsigned int n; double a,b,i; int fd[2]; a=0.0,b=1.0,i=0.0; n=3200; if(pipe(fd)<0){ printf("...błąd otwarcia łącza\n"); exit(1); 14
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Teraz dopiero tworzymy proces potomny. switch( fork() ) { case -1: perror( "<!> błąd inicjacji potomka" ); exit( 1 ); break; case 0: close( fd[0] ); I = quad( n,a,b,sine ); printf( "[%d] wartość całki %19.6f\n",(int)getpid(),I ); write( fd[1],(void*)&i,sizeof( double ) ); exit( 0 ); default: close( fd[1] ); printf( "[%d] wykonuje, ważne rzeczy...\n",(int)getpid() ); read( fd[0],(void*)&i,sizeof( double ) ); printf( "[%d] zakończył z kodem %d\n",pid,status ); printf( "[%d] otrzymał wartość %16.6f\n",(int)getpid(),I ); return 0; Oczywiście funkcje sine() i quad() nie wymagają żadnych zmian. 15
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Efekt wykonania będzie następujący $ integral [4225] wartość całki 0.098120 [4224] wykonuje, ważne rzeczy... [4225] zakończył z kodem 0 [4224] otrzymał wartość 0.098120 a więc komunikacja między procesami przebiegła bez zakłóceń a wynik otrzymany przez proces nadrzędny poprawny. 16
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Ponieważ proces może utworzyć wiele potoków w postaci łączy nienazwanych, a z reguły (w większości implementacji) jak zaznaczono to na wstępie, są one jednokierunkowe, bardzo użytecznymi mogą okazać się funkcje dup() i dup2(). Synopsis #include <unistd.h> int dup( int old ); int dup2( int old, int new ); int old istniejący, utworzony uprzednio wywołaniem pipe(),, deskryptor int new nowy deskryptor na który zostanie skopiowany (skojarzony) old Return nowy deskryptor Errors -1 Tak więc efektem wywołanie obu funkcji będzie utworzenie kopii deskryptora, z tą różnicą że w przypadku dup2() mamy możliwość wskazania w sposób jawny na co skopiować. Funkcja dup() zwraca natomiast najniższy, pierwszy wolny. 17
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. W kolejnym przykładzie z procesu macierzystego wyślemy kilka linii tekstu, które w procesie potomnym zostaną posortowane za pomocą programu sort (systemowy). Przy okazji użyjemy tu w relacji do łącza nienazwanego funkcji przeznaczonych do działania na strumieniu skojarzonym z plikiem. #include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h> int main () { int fd[2]; int pid,status; Ta zmienna zostanie w przyszłości skojarzona z potokiem FILE* stream; if( pipe( fd )< 0 ) { printf( "...błąd otwarcia łącza\n" ); exit( 1 ); 18
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. W takim razie tworzymy proces potomny switch( (pid=(int)fork()) ) { case -1: perror( "<!> błąd inicjacji potomka" ); exit( 1 ); break; Kod dla potomka case 0: Na początek powitanie printf( "<!>\tpotomek [%d] startuje\n",(int)getpid() ); Zamykamy fd[1] bo potomek nie będzie pisał do potoku close( fd[1] ); Kopjujemy potomkowi fd[0] potoku na jego stdin dup2( fd[0],stdin_fileno ); Zamykamy fd[0], bo już niepotrzebne skopiowaliśmy na stdin close(fd[0]); Teraz pozostaje już tylko wywołać program sort printf("------------------------------------------\n" ); execl( "/usr/bin/sort","sort","--reverse",(char*)null ); Zauważmy że program uruchomiony przez execl() odziedziczył wejście stdin po potomku, a więc przekierowane fd[0]. 19
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Teraz kod dla procesu macierzystego. default: Zamykamy od tej strony kanału odczyt, bo będziemy pisać. close( fd[0] ); Przypisanie strumienia (plikowego) istniejącemu deskryptorowi. stream = fdopen( fd[1], "w" ); Piszemy do kanału, na końcu którego jest potomek (właściwie to sort). fprintf( stream, "\taaaaa\n" ); fprintf( stream, "\tbbbbb\n" ); fprintf( stream, "\tccccc\n" ); fprintf( stream, "\tddddd\n" ); Na wszelki wypadek opróżniamy bufor plikowy fflush( stream ); Zamykamy deskryptor, ponieważ nie jest dłużej potrzebny close( fd[1] ); i czekamy na potomka, aż skończy wait( &status ); Po zakończeniu wyświetlamy komunikat pintf("------------------------------------------\n" ); printf("<!>\tpotomek [%d] zakończył działanie i zwrócił [%d]\n",pid,status ); return 0; 20
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Efekt wykonania programu przedstawia się następująco: $./duplicate <!> potomek [8269] startuje ----------------------------------------------------------- Ddddd Ccccc Bbbbb Aaaaa ----------------------------------------------------------- <!> potomek [8269] zakończył działanie i zwrócił [0] $ Użyto tutaj celem skojarzenia deskryptora ze strumieniem - dodatkowo funkcji fdopen() i fflush(). #include <stdio.h> int fflush( FILE *stream ); FILE *fdopen( int fildes, const char *mode ); Tryb mode może być określony jako r (odczyt), r+ w+ (odczyt lub zapis), a (rozszerzenie, czyli pisanie na końcu pliku), a+ (rozszerzenie lub czytanie). 21
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Mechanizm łączy nienazwanych można z powodzeniem wykorzystać do komunikacji dwukierunkowej, między procesem nadrzędnym a potomnym. Kolejny przykład pokazuje tego rodzaju wariant komunikacji międzyprocesowej. Złóżmy, że proces nadrzędny prześle do procesu potomnego pewną wartość x oczekując na wykonanie na niej pewnej operacji f(x)=y a proces potomny zwróci do procesu nadrzędnego wynik tej operacji, czyli y. #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> int main( void ) { int pid,status; int one[2],two[2]; double x=1.0,y=1.0; printf( "\t[%d] nadrzędny, start\n\n",(int)getpid() ); Oczywiście, w przypadku komunikacji dwukierunkowej, konieczne są dwa kanały if( pipe( one )< 0 pipe( two )<0 ) { printf( "<!> błąd otwarcia łączy\n" ); exit( 1 ); 22
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. switch( pid=(int)fork() ) { Gdyby coś poszło nie tak case -1: perror( "<!> błąd inicjacji potomka" ); exit( 1 ); break; Teraz kod dla potomka case 0: Powitanie printf( "\t[%d] potomek, start\n\n",(int)getpid() ); Zamykamy niepotrzebne deskryptory, odpowiednio do kierunku przesyłu close( one[1] ); close( two[0] ); Czytamy to co nadrzędny ma nam do powiedzenia read( one[0],(void*)&x,sizeof( double ) ); printf( "\t[%d] otrzymał x=%f\n",(int)getpid(),x ); Wykonujemy właściwe operacje, na rzecz nadrzędnego y = x*m_pi; printf( "\t[%d] wykonał f(x)=y, wysyła y=%f\n",(int)getpid(),y ); no i w końcu wysyłamy wynik końcowy do nadrzędnego write( two[1],(void*)&y,sizeof( double ) ); printf( "\t[%d] potomek, stop\n\n",(int)getpid() ); exit( 0 ); 23
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. A teraz kod dla procesu nadrzędnego default: Oczywiście zamykamy to co nie jest nam potrzebne close( one[0] ); close( two[1] ); Wysyłamy dane do potomka printf( "\t[%d] wysyła do potomka [%d] x=%f\n\n",(int)getpid(),pid,x ); write( one[1],(void*)&x,sizeof( double ) ); no i czekamy na wynik do potomka read( two[0],(void*)&y,sizeof( double ) ); wait( &status ); printf( "\t[%d] potomek, zwrócił sterowanie, kod powrotu [%d]\n",pid,status ); informacja diagnostyczna printf( "\t[%d] otrzymał y=%f\n",(int)getpid(),y ); printf( "\n\t[%d] nadrzędny, stop\n",(int)getpid() ); Proces nadrzędny kończy ostatecznie działanie return 0; 24
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Efekt wykonanie programu przedstawia się następująco $ bidir [7810] nadrzędny, start [7811] potomek, start [7810] wysyła do potomka [7811] x=1.000000 [7811] otrzymał x=1.000000 [7811] wykonał f(x)=y, wysyła y=3.141593 [7811] potomek, stop [7811] potomek, zwrócił sterowanie, kod powrotu [0] [7810] otrzymał y=3.141593 [7810] nadrzędny, stop [7810] nadrzędny, stop $ 25
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Prócz ewidentnych zalet i korzyści wynikających z użycia w komunikacji międzyprocesowej łączy nienazwanych, mimo wszystko nie zawsze warto je stosować. Zasadniczym utrudnieniem w przypadku łączy nienazwanych jest kwestia współużytkowania takiego łącza przez procesy niespokrewnione. W takiej sytuacji zwykle lepszym wyborem będą łącza w postaci potoku nazwanego (named pipe). W odróżnieniu od łącze nienazwane: jest identyfikowane przez nazwę i może z niego korzystać (o ile ma odpowiednie prawa dostępu) wiele procesów (nawet nie spokrewnionych); posiada organizację FIFO, czyli First In First Out, stąd i ich skrótowa nazwa; posiada dowiązanie w systemie plików (jako plik specjalny urządzenia) aż do momentu ich jawnego usunięcia; mimo iż zachowuje cechy pliku de facto jak wyjaśnia to dokumentacja systemowa LINUX: is a window into the kernel memory that "looks" like a file. Podobnie jak i łącza nienazwane mogą być tworzone w dwojaki sposób: z poziomu systemowego interface'u użytkownika albo wywołaniem funkcji API z procesu (czy wątku). 26
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Z poziomu interface systemowego łącza nazwane FIFO tworzone są komendą $ mkfifo -m mode name mode maska praw dostępu, czyli symbolicznie dla u (user), g (group), o (other), a (all) dodaje (+),, ujmuje (-) od istniejących lub ustawia (=) prawo r (read), w (zapis), x (execute) name nazwa pliku specjalnego FIFO, ewentualnie wraz ze ścieżką zaś jego usunięcie odbywa się w identyczny sposób jak każdego pliku dyskowego, a więc $ rm name Utwórzmy w takim razie przykładowe łącze FIFO $ mkfifo a=rw /tmp/km-fifo Jeżeli wykonamy teraz $ ls -l /tmp/km-* prw-r--r-- 1 kmirota users 0 maj 22 12:09 /tmp/km-fifo Zwróćmy uwagę na opis dowiązania, zgodnie ze specyfikacją dla ls - Regular file l Symbolic link b Block special file n Network file c Character special file p FIFO d Directory s Socket czyli powstało FIFO pipe. 27
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Takie cechy "pliku" km-fifo potwierdza także, w szczegółach, komenda stat $ stat /tmp/km-fifo File: `/tmp/km-fifo` Size: 0 Blocks: 0 IO Block: 4096 potok Device: 806h/2054d Inode: 591930 Links: 1 Access: (0644/prw-r--r--) Uid:(1000/kmirota) Gid:(100/users) Access: 2003-05-22 12:35:52.000000000 +0200 Modify: 2003-05-22 12:35:51.000000000 +0200 Change: 2003-05-22 12:35:51.000000000 +0200 Otwórzmy teraz dwie sesje terminala, w oknie pierwszego pierwszego wpisujemy $ cat < /tmp/km-fifo czyli przekierujemy zawartość km-fifo na wejście komendy systemowej cat. Ta, jak wiadomo, wyświetla na swoim wyjściu (czyli tutaj w oknie terminala pierwszego), to co otrzyma na wejściu. Teraz przełączamy się na drugie okno terminala, i wykonujemy $ cat > /tmp/km-fifo czyli w przeciwnym kierunku, zatem wejście drugiego terminala zostało przekierowane na plik km-fifo. Efekt będzie taki, że cokolwiek wprowadzimy w oknie terminala drugiego, natychmiast zobaczymy w oknie pierwszego. Zauważmy że plik tem będzie miał przez cały czas rozmiar zerowy. Całość można zakończyć przesyłając do cat kod EOF (End Of File), czyli z klawiatury <Ctrl>-d. 28
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Jeżeli na pierwszym terminalu ponownie wykonamy przekierowanie $ cat < /tmp/km-fifo zaś na drugim przekierujemy do pliku km-fifo wykonanie jakiejkolwiek komendy, przykładowo $ stat /tmp/km-fifo >/tmp/km-fifo to na pierwszym zobaczymy oczywiście File: `/tmp/km-fifo' Size: 0 Blocks: 0 IO Block: 4096 potok Device: 806h/2054d Inode: 591930 Links: 1 Access: (0644/prw-r r--) Uid: (1000/kmirota) Gid:(100/users) Access: 2003-05-22 13:53:13.000000000 +0200 Modify: 2003-05-22 13:53:13.000000000 +0200 Change: 2003-05-22 13:53:13.000000000 +0200 $ i w tym momencie potok zostanie automatycznie zamknięty, bowiem strumień przepływających danych uległ zakończeniu. Zauważmy że po zakończeniu sesji plik pozostaje w katalogu, przez cały czas posiadając rozmiar zerowy. Usuwamy go jednak jak każdy plik $ rm /tmp/km-fifo 29
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. W systemach rodziny POSIX łącza nazwane tworzone są wywołaniami funkcji mkfifo(). Synopsis #include <sys/types.h> #include <sys/stat.h> int mkfifo( const char *pathname, mode_t mode ); const char *pathname nazwa tworzonego pliku (tu: łącza) mode_t mode maska uprawnień odnośnie odczytu i zapisu Return 0 jeżeli zakończone sukcesem Errors -1 Usuwane natomiast odbywa się przy pomocy funkcji unlink(). Synopsis #include <unistd.h> int unlink( const char *pathname ); const char *pathname nazwa usuwanego pliku (tu: łącza) Return 0 jeżeli zakończone sukcesem Errors -1 30
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Maska uprawnień może być zadana z wykorzystaniem sumy bitowej predefiniowanych stałych w pliku nagłówkowym sys/stat.h: S_IRUSR S_IWUSR S_IXUSR dla właściciela S_IRGRP S_IWGRP S_IXGRP dla grupy, do której należy właściciel S_IROTH S_IWOTH S_IXOTH dla pozostałych lub też korzystając z stałej DEFFILEMODE, dla wszystkich łącznie #define DEFFILEMODE (S_IRUSR S_IWUSR S_IRGRP S_IWGRP S_IROTH S_IWOTH) ustawiając dla wszystkich uprawnienia do odczyty i zapisu (zwróćmy uwagę, że na efektywne uprawnienia mają wpływ stosowane ustalenia odnośnie katalogu nadrzędnego). Ponieważ łącze nazwane posiada dowiązanie do struktury plików operacje na nim przeprowadza się w zwyczajowy sposób, tak jak i w przypadku ogółu, a więc: utworzone łącze trzeba otworzyć, przed użyciem, uzyskując w ten sposób jego deskryptor albo wskazanie do obiektu typu FILE; wykonujemy odczyt lub zapis, odpowiednio do potrzeb; po wykorzystaniu łącza proces powinien zamknąć je, od swojej strony. 31
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Niskopoziomowe operacje otwarcia i zamknięcie wykonujemy przy pomocy open() i close(). Synopsis #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int open( const char *pathname, int flags ); const char *pathname nazwa otwieranego pliku (tu: łącza) int flags maska określająca sposób korzystania: O_RDONLY O_WRONLY O_RDWR tylko odczyt, tylko zapis, odczyt lub zapis O_NONBLOCK otwarcie w trybie nieblokującym Return deskryptor pliku Errors -1 Synopsis #include <unistd.h> int close( int fd ); int fd deskryptor pliku (tu: łącza nazwanego) Return 0 zakończona sukcesem Errors -1 32
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Przydzielając deskryptor łączu za pośrednictwem funkcji open() trzeba pamiętać, że jej wywołanie mają zasadniczo charakter blokujący, tzn. wywołanie fd=open( pathname,o_rdonly ); będzie czekać aż jakikolwiek proces nie otworzy kolejki do zapisu fd=open( pathname,o_wronly ); będzie czekać aż jakikolwiek proces nie otworzy kolejki do odczytu Jeżeli jednak otwarcie nastąpi z użyciem maski O_NONBLOCK wówczas fd=open( pathname,o_rdonly O_NONBLOCK ); zwróci natychmiast sterowanie fd=open( pathname,o_wronly O_NONBLOCK ); także zwróci natychmiast sterowanie, jeżeli jednak nie będzie żadnego procesu, który otworzył tę kolejkę do odczytu to zamiast deskryptora otrzymamy -1 (oraz errno = ENXIO) Dysponując deskryptorem, do odczytu i zapisu, używamy read() i write(). Synopsis #include <unistd.h> ssize_t read( int fd,void *buffer,size_t bytes ); ssize_t write( int fd,const void *buffer,size_t bytes ); int fd deskryptor pliku Return void* buffer bufor do odczytu lub zapisu ilość bajtów przesłanych size_t bytes ilość bajtów do przesłania Errors -1 33
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Na łączu nazwanym możemy także operować w sposób bezpośredni za pomocą wysokopoziomowych funkcji ISO C standard I/O. W pierwszej kolejności należy oczywiście otworzyć łącze wywołaniem fopen(). Synopsis #include <stdio.h> FILE *fopen( const char *pathname,const char *mode ); const char *pathname nazwa otwieranego pliku (tu: łącza) char *mode tryb otwarcia pliku: r (r+ r+) odczyt (i i zapis), w (w+ w+) zapis (i i odczyt), a (a+ a+) rozszerzenie (i i odczyt) Return wskazanie do strumienia plikowego FILE Errors NULL W końcu zaś zamknąć przy pomocy fclose(). Synopsis #include <stdio.h> int fclose( FILE *stream ); FILE *stream wskazanie do strumienia, do zamknięcie Return 0 jeżeli sukces Errors EOF 34
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Celem odczytu i zapis z i do strumienia plikowego stosuje się głównie funkcje formatowanego wejścia/wyjścia fscanf() i fprintf(). Synopsis #include <stdio.h> int fscanf( FILE *stream,const char *format,... ); int fprintf( FILE *stream,const char *format,... ); FILE *stream wskazanie do strumienia otwartego do odczytu const char *format łańcuch formatujący zawierający wzorce konwersji, w szczególności: "%c" znak, "%s" łańcuch, "%d" liczba całkowita, "%f" liczba rzeczywista... lista zmiennych Return ilość zmiennych wczytanych skutecznie, wg listy argumentów Errors EOF Całość operacji plikowych z czego tu przedstawiono tylko drobny wycinek stanowi element normy ISO/IEC 9899:1999 "Programming languages C" 35
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Wejście i wyjście plikowe, z punktu widzenie programisty, przedstawia się zwykle dość korzystnie i atrakcyjnie, jednak może przysporzyć także problemów. Między innymi za sprawą stosowanych tu mechanizmów buforowania. Stąd mogą pojawić się niezgodności tego co zawarte jest w skojarzonym buforze plikowym a wartościami zmiennych, póki strumień nie zostanie zamknięty. Przykładem tego rodzaju sytuacji jest poniższy kod. #include <stdio.h> int main( void ) { printf( "1 sek " ); sleep( 1 ); printf( "2 sek " ); sleep( 1 ); printf( "3 sek " ); sleep( 1 ); printf( "4 sek " ); sleep( 1 ); printf( "5 sek " ); sleep( 1 ); printf( "i koniec\n" ); return 0; 36
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Nie zobaczymy wcale serii komunikatów (choć zależy to od użytego kompilatora), pojawiających się co 1 sekundę, ale wszystkie równocześnie w momencie kiedy napotkany będzie znak końca linii '\n'. 1 sek 2 sek 3 sek 4 sek 5 sek i koniec Aby osiągnąć zamierzony efekt, wyświetlanego komunikatu co sekundę należałoby użyć funkcji fflush() opróżniającej bufor plikowy, tutaj standardowego wyjścia stdout. Synopsis #include <stdio.h> int fflush( FILE *stream ); FILE *steram bufor plikowy do opróżnienia (uprzednio otwarty) Return 0 jeżeli sukces Errors EOF Zatem w przypadku rozpatrywanego kodu konieczne jest jego uzupełnienie o wywołania fflush( stdout ); w sposób ja na załączonym dalej listingu. 37
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. #include <stdio.h> int main( void ) { printf( "1. " ); fflush( stdout); sleep( 1 ); printf( "2. " ); fflush( stdout); sleep( 1 ); printf( "3. " ); fflush( stdout); sleep( 1 ); printf( "4. " ); fflush( stdout); sleep( 1 ); printf( "5. " ); fflush( stdout); sleep( 1 ); printf( "i koniec\n" ); return 0; O samej funkcji warto pamiętać ponieważ problem buforów plikowych w odniesieniu do łączy komunikacyjnych bywa nad wraz uciążliwy, stając się przyczyną wielu zaskakujących wyników i frustracji. Na koniec należy nadmienić że o ile użycie fflush( stdout ) jest zgodne z normą ISO, to w odniesieniu do strumienia wejściowego fflush( stdin ) już nie jest. 38
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Problem buforowania po stronie odczytu można także rozwiązać używając celem wczytywania funkcji "nie zaśmiecającej" tak bufor plikowy jak scanf(). Mogłaby to być w tym przypadku fgets(), która wczytuje łańcuch tekstowy, a dopiero z niego za pomocą sscanf() wczytujemy potrzebne wartości zmiennych. Synopsis #include <stdio.h> char *fgets(char *buffer, int size, FILE *stream); char *buffer bufor odczytu int size ilość znaków do wczytania, pomniejszona o 1,, na ostatniej pozycji dodawane jest '\0' (funkcja( kończy czytanie jeżeli napotka EOF lub '\n') FILE *stream otwarty do odczytu bufor plikowy Return char *buffer jeżeli sukces, to wskazanie do wyniku Errors NULL Deklaracja i użycie funkcji int sscanf( const char *buffer,const char *format,... ); jest właściwie identyczne z fscanf(), w tym że odczyt nie następuje ze strumienia plikowego ale łańcucha const char *buffer bufor w postaci łańcucha tekstowego, z którego zawartość podlega konwersji, zgodnie z łańcuchem format 39
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Przygotujemy teraz dwie aplikacje, które poprzez łącze nazwane pipe będą wzajemnie się komunikować. Pierwsza stanowić będzie serwer nasłuchujący danych napływających łączem, a w przypadku odebrania wyświetli komunikat. #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <time.h> #include <unistd.h> int main( void ) { time_t stamp; pid_t pid; int fd,run; char cmd; printf("\n[%d]*s*e*r*w*e*r*[%d]\n\n",(int)getpid(),(int)getpid()); Na początek próbujemy otworzyć łącze do odczytu if( (fd=open( "pipe",o_rdonly )) >0 ){ run=1; else{ printf( "!.!..nie znaleziono łącza..!.!\n\n" ); run=0; 40
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. W przypadku kiedy udało się uzyskać deskryptor while( run ) { Odczytujemy, od kogo pochodzi wiadomość read( fd,&pid,sizeof( pid_t ) ); następie czytamy komendę read( fd,&cmd,sizeof( char ) ); Wyświetlamy informację, od kogo, co i kiedy otrzymano stamp = time( NULL ); printf( "[%d]\t %c -> %s",(int)pid,cmd,ctime( &stamp ) ); Jeżeli odebrano komendę Q(uit), to kończymy if( cmd=='q' ){ run=0; close( fd ); return 0; Teraz kod klienta, który będzie komunikował się z naszym serwerem przez łącze pipe. 41
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. #include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <ctype.h> int main( void ) { int fd,run; pid_t pid; char cmd; char buffer[256]; Wyświetlamy komunikat diagnostyczny pid = getpid(); printf( "\n[%d]*k*l*i*e*n*t*[%d]\n\n",(int)pid,(int)pid ); Podobnie jak wcześniej sprawdzamy dostępność łącza if( (fd=open( "pipe",o_wronly )) >0 ){ run=1; else{ printf( "!.!..nie znaleziono łącza..!.!\n\n" ); run=0; 42
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Jeżeli udało się uzyskać deskryptor łącza while( run ) { Pobieramy komendę od użytkownika printf( "\t?...\t" ); fgets( buffer,256,stdin ); sscanf( buffer,"%c",&cmd ); cmd = toupper( cmd ); Piszemy do łącza write( fd,&pid,sizeof( pid_t ) ); write( fd,&cmd,sizeof( char ) ); Jeżeli użytkownik podał Q(uit), to kończymy proces klienta if( cmd=='q' ){ run=0; close( fd ); return 0; 43
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Zanim uruchomimy procesy serwer i klienckie, najpierw musimy utworzyć łącze. Zatem $ mkfifo pipe Sprawdźmy jeszcze wynik komendy $ ls -l razem 32 -rwxr-xr-x 1 kmirota users 11667 maj 23 10:36 klient -rw-r--r-- 1 kmirota users 697 maj 23 10:36 klient.c prw-r--r-- 1 kmirota users 0 maj 23 10:37 pipe -rwxr-xr-x 1 kmirota users 11576 maj 23 10:36 serwer -rw-r--r-- 1 kmirota users 682 maj 23 10:36 serwer.c a więc łącze pipe zostało utworzone. Otwieramy teraz dwa terminale w pierwszym uruchamiamy proces serwera a w drugim (i ewentualnie kolejnych) procesy klienckie. $./klient [8053]*K*L*I*E*N*T*[8053]?...??...??...??... q $./serwer [8052] * S * E * R * W * E * R * [8052] [8053]? -> Fri Oct 23 11:07:48 2003 [8053]? -> Fri Oct 23 11:07:50 2003 [8053]? -> Fri Oct 23 11:07:51 2003 [8053] Q -> Fri Oct 23 11:07:54 2003 44
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Ponownie wykorzystując mechanizm łącza nazwanego przygotujemy serwer, który pozostając na końcu łącza zamknie je i będzie odsyłał zwrotnie cokolwiek dostanie. Kod źródłowy serwera przedstawia się jak na załączonym listingu. #include <stdio.h> #include <limits.h> int main( void ) { FILE *stream; char buffer[line_max]; int run; if( (stream = fopen( "channel","r+" ) ) ){ run=1; else{ run=0; perror( "!.!..błąd otwarcia łącza..!.!" ); while( run ) { if( fgets( buffer,256,stream ) ) { fprintf( stream,"%s",buffer ); fflush( stream ); return 0; 45
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Zwróćmy uwagą, że celem wyświetlenia informacji o ewentualnym błędzie otwarcia kanału, użyliśmy funkcji perror(), gdyż tylko ona gwarantuje że nawet w przypadku procesu pracującego w tle zobaczymy komunikat o błędzie. Początkowy fragment kodu klienta niewiele tylko różni się od przedstawionego wcześniej #include <stdio.h> #include <limits.h> #include <string.h> #include <ctype.h> int main( void ) { FILE *stream; char buffer[line_max]; int run; int empty( char* ); if( (stream = fopen( "channel","r+" ) ) ){ run=1; else{ run=0; perror( "!.!..błąd otwarcia łącza..!.!" ); 46
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. W dalszym fragmencie umieszczamy pętle czytającą i piszącą, do momentu kiedy użytkownik wprowadzi linię pustą. while( run ) { bzero( (void*)buffer,line_max ); fgets( buffer,line_max,stdin ); if(!empty( buffer ) ) { fprintf( stream,"%s",buffer ); fflush( stream ); else{ fclose( stream ); break; fgets( buffer,line_max,stream ); if(!empty( buffer ) ) { fprintf( stdout,"%s",buffer ); fflush( stream ); return 0; Pozostaje jeszcze dodać funkcję która będzie sprawdzać czy nie pojawił się łańcuch pusty (w naszym rozumieniu łańcuch nie zawierający ani jednej litery czy też cyfry). int empty( char * string ) { while( *string ){ if( isalnum(*string++) ){ return 0; return 1; 47
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Oczywiście na początek należy utworzyć właściwe łącze nazwane $ mkfifo channel i sprawdźmy $ stat channel File: `channel' Size: 0 Blocks: 0 IO Block: 4096 potok Device: 807h/2055d Inode: 4162321 Links: 1 Access: (0644/prw-r r--) Uid:(1000/kmirota) Gid: (100/users) Access: 2006-12-23 06:29:21.000000000 +0200 Modify: 2006-12-23 06:29:21.000000000 +0200 Change: 2006-12-23 06:29:21.000000000 +0200 Następnie uruchamiamy z bieżącego terminala proces serwera, w tle (zwróćmy uwagę na znak '&' na końcu linii komendy, który wywołuje właśnie takie uaktywnienie procesu przez system). $./serwer & [1] 8603 gdyby teraz sprawdzić listę aktywnych procesów $ ps PID TTY TIME CMD 4311 pts/1 00:00:00 bash 8603 pts/1 00:00:00 server 8607 pts/1 00:00:00 ps to jak proces o identyfikatorze 8603 pracuje nasz serwer. Jego działanie możemy w dowolnej chwili zakończyć, komendą (o ile nie będzie już potrzebny) $ kill -SIGKILL 8603 48
z przedmiotu, prowadzonych na Wydziale BMiI, Akademii Techniczno-Humanistycznej w Bielsku-Białej. Następnie z bieżącego terminala uruchamiamy klienta. Ponieważ serwer pracuje zamyka pętlę zwrotną, czyli odsyła to co otrzymał, więc cokolwiek wprowadzimy zostanie nam natychmiast odesłane przez serwer. $./client pierwszy pierwszy drugi drugi i jeszcze nieco dłuższy łańcuch, 1234567890 i jeszcze nieco dłuższy łańcuch, 1234567890 $ a więc, po wprowadzeniu linii pustej, klient zakończył. Pozostaje nam jeszcze zamknąć serwer. Tak jak sugerowano to wcześniej wykorzystamy w tym celu sygnał SIGKILL, zatem z terminala wykonujemy $ kill -SIGKILL 8603 49