Teraz, kiedy umiesz fork
ować, przydałoby się umieć komunikować
z potworkami. Do tego użyteczne są potoki. Takie cuś funkcjonuje jak
(oficjalnie) jednokierunkowy kanał komunikacyjny. Pisanie do i czytanie
z potoku opiera się na standardowych funkcjach write()
i read()
, do potoku odwołujesz się przez normalne deskryptory
plików. Wiesz już, jak tego używać, to przydałoby się umieć toto utworzyć. Do
tego służy funkcja int pipe(int
desk[2])
. Funkcja przyjmuje tablicę dwuelementową, zwraca 0 dla
poprawnej operacji, a w przypadku błędu -1 i odpowiednio ustawia zmienną
errno
.
Notka: W zasadzie jedynym przypadkiem, kiedy pipe()
może zwrócić błąd, jest przekroczenie ograniczenia ilości otwartych
deskryptorów.
Aż dwa inty są potrzebne po to, żeby jeden deskryptor mógł być
otwarty do odczytu, a drugi do zapisu, a dokładnie desk[0]
służy
do odczytu, a desk[1]
do zapisu (porównaj z deskryptorami
0 i 1 odpowiadającymi za stdin
i stdout
). To może
przykładowy kod:
int main(void) { int desk[2]; char bufor[20]; pipe(desk); if (fork()) /* rodzic czyta */ { close(desk[1]); /* nie bedziemy pisac */ read(desk[0], bufor, sizeof(bufor)); puts(bufor); } else /* potworek pisze */ { close(desk[0]); /* bedziemy tylko pisac */ write(desk[1], bufor, sprintf(bufor, "Hell oWorld %d!", getpid()) + 1); puts("Potworek zapisal"); } return 0; } |
Zwróć uwagę na role rodzica i potworka. Gdybym pisał w rodzicu, musiałbym
później zaczekać na potworka, inaczej rodzic mógłby zakończyć działanie zanim
potworek coś zapisze na ekran. W przykładzie takiej synchronizacji dostarcza
sam fakt odczytania czegokolwiek: póki w buforze potoku nic nie będzie, póty
rodzic nie przejdzie dalej. A po co w ogóle synchronizacja? Rodzic zwalnia
terminal powłoce, więc to, co wypisze potworek, może się nałożyć na prompt.
Przy bardziej skomplikowanych programach możesz za wcześnie zwolnić jakiś
zasób, który w tym momencie straci spójność i użyteczność.
Drugą ważną rzeczą w kodzie jest zamykanie niepotrzebnych deskryptorów. Jest
to wbrew pozorom bardzo ważna czynność, co pokaże się
później.
Już wiesz, jak się fork
ować, jak exec
ować procesy
i jak podmieniać standardowe strumienie. Pora na coś ciekawszego: odczyt
standardowego wyjścia zewnętrznego programu. Jak się domyślasz, przebiega to
w czterech etapach: tworzenie potoku, fork
owanie, podmienianie
deskryptorów i wreszcie exec
owanie odpowiedniego procesu.
int main(void) { int potok[2]; char bufor[81]; int wczytane; /* etap 1: tworzenie potoku */ pipe(potok); /* etap 2: forkowanie */ if (fork()) /* rodzic czyta */ { close(potok[1]); while (( wczytane = read(potok[0], bufor, sizeof(bufor) - 1) )) { bufor[wczytane] = 0; /* warto zakonczyc ten string zerem */ printf("Wczytane: '%s'\n", bufor); } } else /* potworek execuje "ps -Af" */ { close(potok[0]); /* etap 3: podmiana deskryptora */ dup2(potok[1], 1); close(potok[1]); /* etap 4: execowanie procesu */ execlp("ps", "ps", "-Af", NULL); perror("Blad uruchamiania ps"); } return 0; } |
Zróbmy coś ciekawszego: wyexec
ujmy ps -Af
przekierowane na grep root. Domyślasz się, że będzie trzeba
podmieniać stdin
dla grepa.
int main(void) { int potok[2]; char bufor[81]; int wczytane; pipe(potok); if (fork()) /* rodzic execuje grep root */ { close(potok[1]); dup2(potok[0], 0); close(potok[0]); execlp("grep", "grep", "root", NULL); perror("Blad uruchamiania grep"); } else /* potworek execuje ps -Af */ { close(potok[0]); dup2(potok[1], 1); close(potok[1]); execlp("ps", "ps", "-Af", NULL); perror("Blad uruchamiania ps"); } /* to sie nie wykona, chyba ze byl gdzies blad */ return 0; } |
Zwróć uwagę na role potworka i rodzica. Jest to analogiczne jak w przypadku pierwszego przykładu. Chociaż tutaj raczej nie zauważysz różnicy w kolejności, bo grep kończy się tak szybko, jak zamyka się mu wejście, a to z kolei zamyka się natychmiast po zakończeniu ps. Ale już chyba widzisz, kiedy różnica będzie widoczna?
int main(void) { int potok[2]; char bufor[81]; int wczytane; pipe(potok); if (fork()) /* rodzic execuje less */ { close(potok[1]); dup2(potok[0], 0); close(potok[0]); execlp("less", "less", NULL); /* fallback do more, ktory jest prawie wszedzie */ execlp("more", "more", NULL); perror("Blad uruchamiania less i more"); } else /* potworek execuje ps -Af */ { close(potok[0]); dup2(potok[1], 1); close(potok[1]); execlp("ps", "ps", "-Af", NULL); perror("Blad uruchamiania ps"); } /* to sie nie wykona, chyba ze byl gdzies blad */ return 0; } |
To się wykona ładnie, ale jak zamienisz miejscami kody rodzica i potworka
(najprościej zamienić linijkę if (fork())
na
if (!fork())
), to się rozsypie. ps jest
uruchomiony jako rodzic, zatem przy jego zakończeniu, które nastąpi wcześniej
od less, terminal zostanie zwrócony powłoce, a less
zostanie bez wejścia z klawiatury. Na to się jeszcze nakłada fakt, że
less zmienia właściwości terminala, które po jego zabiciu nie są
przywracane. Dlatego trzeba dobrze przemyśleć, w którym procesie ma zostać
uruchomiony który program.
Alternatywnie możesz exec
ować się dwa razy, po czym zaczekać na
oba procesy:
int main(void) { int potok[2]; char bufor[81]; int wczytane; pipe(potok); if (!fork()) /* jeden potworek uruchamia ps -Af */ { close(potok[0]); dup2(potok[1], 1); close(potok[1]); execlp("ps", "ps", "-Af", NULL); perror("Blad uruchamiania ps"); return 1; } if (!fork()) /* drugi potworek uruchamia less albo more */ { close(potok[1]); dup2(potok[0], 0); close(potok[0]); execlp("less", "less", NULL); /* fallback */ execlp("more", "more", NULL); perror("Blad uruchamiania less i more"); return 1; } /* bardzo wazne! */ close(potok[0]); close(potok[1]); wait(NULL); wait(NULL); puts("\nKoniec obu potworkow"); return 0; } |
Gdyby nie ostatnie dwa close
'y, mogłoby być źle. less
czeka, aż nie zostanie zamknięte jego standardowe wejście, a to nie jest
zamknięte bez close()
w rodzicu. Bardziej widoczne to jest
w przypadku grepa:
int main(void) { int potok[2]; char bufor[81]; int wczytane; pipe(potok); if (!fork()) /* jeden potworek uruchamia ps -Af */ { close(potok[0]); dup2(potok[1], 1); close(potok[1]); execlp("ps", "ps", "-Af", NULL); perror("Blad uruchamiania ps"); return 1; } if (!fork()) /* drugi potworek uruchamia grep root */ { close(potok[1]); dup2(potok[0], 0); close(potok[0]); execlp("grep", "grep", "root", NULL); perror("Blad uruchamiania grep"); return 1; } /* bardzo wazne! */ close(potok[0]); close(potok[1]); wait(NULL); wait(NULL); puts("\nKoniec obu potworkow"); return 0; } |
Bez oznaczonych close
'ów (zwłaszcza close(potok[1])
)
program się nie zakończy. Sam się parę razy na tym naciąłem :) Oczywiście ten
sam komentarz tyczy się close
owania niepotrzebnych deskryptorów
w obu potworkach.
A jeśli będziesz chciał się komunikować między dwoma programami bez wspólnego
rodzica? Tu już nie daje się zastosować pipe()
. Sorry, ale to
jest troszkę niewykonalne. Do tego będziesz już potrzebował
nazwanych potoków.