Systemy operacyjne (wiosna 2014) Laboratorium 11 – tutorial Komunikacja procesów dr inż. Wojciech Bieniecki Instytut Nauk Ekonomicznych i Informatyki http://wbieniec.kis.p.lodz.pl/pwsz
Metody komunikacji międzyprocesowej Komunikacja międzyprocesowa (ang. Inter-Process Communication — IPC) – sposoby komunikacji pomiędzy procesami systemu operacyjnego. Pojęcie IPC może odnosić się do wymiany informacji w systemach rozproszonych (klastrów, systemów odległych połączonych siecią). Mechanizmy IPC opierają się na budowaniu w pamięci lub na dysku dynamicznych struktur, używanych do transmisji komunikatów pomiędzy procesami Lista metod IPC ta obejmuje: pliki i blokady – najprostsza i najstarsza forma IPC sygnały (ang. signals) – czasami znane jako przerwania programowe semafory (ang. semaphores) łącza nienazwane (ang. pipes) – znane też jako łącza komunikacyjne łącza nazwane (ang. named pipes) – znane też jako nazwane łącza komunikacyjne kolejki komunikatów (ang. message queues) pamięć dzielona (ang. shared memory) gniazda Uniksa (ang. Unix domain sockets) gniazda (ang. sockets) RPC (ang. Remote Procedure Call) – zdalne wywoływanie procedur.
Łącza komunikacyjne Łącza w UNIX są plikami specjalnymi. Są podobne do plików zwykłych – posiadają swój i-węzeł, posiadają bloki z danymi, na otwartych łączach można wykonywać operacje zapisu i odczytu. Czym różnią się od plików zwykłych • ograniczona liczba bloków – łącza mają rozmiar 4KB – 8KB w zależności od konkretnego systemu, • dostęp sekwencyjny – na łączach można wykonywać tylko operacje zapisu i odczytu, ale nie można wykonywać funkcji lseek) • sposób wykonywania operacji zapisu i odczytu – dane odczytywane z łącza są zarazem usuwane (nie można ich odczytać ponownie) • proces jest blokowany w funkcji read na pustym łączu i w funkcji write, jeśli w łączu nie ma wystarczającej ilości wolnego miejsca, żeby zmieścić zapisywany blok
Łącza komunikacyjne – cechy wspólne i różnice Łącze nazwane (tzw kolejki FIFO). Łącze nienazwane (tzw. potok) Posiada dowiązanie w systemie plików (istnieje jako plik w jakimś katalogu) nie ma dowiązania w systemie plików – istnieje tylko tak długo, jak jest otwarte Może być identyfikowane przez nazwę Jest identyfikowane tylko przez deskryptory Po zamknięciu pozostaje przydzielony i-węzeł, ale wszystkie jego bloki na dysku są zwalniane. Po zamknięciu wszystkich jego deskryptorów przestaje istnieć (zwalniany jest jego i-węzeł i wszystkie bloki) Procesy, które chcą komunikować się za pomocą łącza nienazwanego, muszą znać jego deskryptor. Sposobem przekazania deskryptora łącza innemu procesowi jest utworzenie go jako proces potomny – tablica otwartych plików jest dziedziczona. Można w procesie macierzystym otworzyć łącze a następnie utworzyć dwa procesy potomne, które będą komunikować się ze sobą.
Operacje na łączach w UNIX Łącza nazwane mkfifo - funkcja systemowa tworząca łącze nazwane (działa podobnie jak creat dla plików zwykłych, ale w przeciwieństwie do creat nie otwiera łącza) int mkfifo (char* path, mode_t mode) Argumenty funkcji: path - nazwa ścieżkowa pliku specjalnego będącego kolejką FIFO mode - prawa dostępu do łącza Wartości zwracane: poprawne wykonanie funkcji: 0 zakończenie błędne: -1 W przypadku błędnego zakończenie funkcji, kod błędu możemy odczytać w errno.
Operacje na łączach w UNIX Łącza nazwane open – otwiera łącze podobnie jak plik. Uwaga – aby łącze działało musi być otwarte do odczytu i zapisu (np. jeden proces pisze drugi czyta) int open (char* path, int flags) Wartości zwracane: poprawne wykonanie funkcji: deskryptor kolejki FIFO zakończenie błędne: -1 Argumenty funkcji: path - nazwa ścieżkowa pliku specjalnego będącego kolejką fifo mode - prawa dostępu do łącza flags - określenie trybu w jakim jest otwierana kolejka: O_RDONLY - tryb tylko do odczytu O_WRONLY- tryb tylko do zapis
Przykład użycia łącza nazwanego Utworzone metodą mkfifo łącze musi zostać otwarte przez użycie funkcji open. Funkcja ta musi zostać wywołana przynajmniej przez dwa procesy w sposób komplementarny, tzn. jeden z nich musi otworzyć łącze do zapisu, a drugi do odczytu. Odczyt i zapis danych z łącza nazwanego odbywa się za pomocą funkcji read i write mkfifo("kolejka",0666); desc = open("kolejka" , O_RDONLY); desc = open("kolejka" , O_WRONLY); read(desc); write(desc); close(desc); close(desc); unlink("kolejka");
Przykład użycia łącza nazwanego #include <fcntl.h> main(){ int fd; if (mkfifo("/tmp/fifo", 0600) == -1) exit(1); //Błąd tworzenia kolejki switch(fork()){ case -1: // blad w tworzeniu procesu exit(1); case 0: //dotyczy procesu potomnego fd = open("/tmp/fifo", O_WRONLY); if (fd == -1) exit(1); //Błąd otwarcia kolejki do zapisu if (write(fd, "Witaj!", 7) == -1) exit(1); //Błąd zapisu do kolejki exit(0); //normalne zakończenie procesu potomnego default: // dotyczy procesu macierzystego char buf[10]; fd = open("/tmp/fifo", O_RDONLY); if (read(fd, buf, 10) == -1) printf("Odczytano z potoku: %s\n", buf); }
Użycie łączy nienazwanych
Funkcja pipe int pipe(int fd[2]) Funkcja tworzy parę sprzężonych deskryptorów pliku, wskazujących na inode potoku i umieszcza je w tablicy fd. fd[0] – deskryptor potoku do odczytu fd[1] – deskryptor potoku do zapisu Proces, który utworzył potok może się przez niego komunikować tylko ze swoimi potomkami lub przekazać im deskryptory, umożliwiając w ten sposób wzajemną komunikację. Wartości zwracane: poprawne wykonanie funkcji: 0 zakończenie błędne: -1
Przykład komunikacji przodek-potomek main() { int fd[2]; if (pipe(fd) == -1) exit(1); // błąd tworzenia potoku switch(fork()){ // rozwidlamy procesy case -1: // blad w tworzeniu procesu exit(1); case 0: // proces potomny if (write(fd[1], "Witaj!", 7) == -1) exit(1); // błąd zapisu do potoku exit(0); // kończymy proces potomny default: // proces macierzysty char buf[10]; if (read(fd[0], buf, 10) == -1) exit(1); // błąd odczytu z potoku printf("Odczytano z potoku: %s\n", buf); }
Przykład użycia potoku Działanie funkcji read w przypadku pustego potoku main() { int fd[2]; pipe(fd); if (fork() == 0){ // proces potomny write(fd[1], "witaj!", 7); exit(0); } else { // proces macierzysty char buf[10]; read(fd[0], buf, 10); read(fd[0], buf, 10); //Uwaga tutaj!!! printf("Odczytano z potoku: %s\n", buf); Drugi odczyt spowoduje zawieszenie procesu, gdyż potok jest pusty, a proces potomny ma otwarty deskryptor do zapisu.
Przykład użycia potoku Niskopoziomowa realizacja ls|tr a-z A-Z z użyciem łączy nienazwanych #define MAX 512 //będziemy przetwarzać po 512 znaków main(int argc, char* argv[]) { int fd[2]; pipe(fd) == -1); //Tworzenie potoku if(fork() == 0) {// proces potomny dup2(fd[1], 1); // tworzymy drugie dowiązanie do stdout execvp("ls", argv); // Uruchomienie programu ls } else { // proces macierzysty char buf[MAX]; int lb, i; close(fd[1]); // zamykamy możliwość zapisu do łącza while ((lb=read(fd[0], buf, MAX)) > 0){ //czytamy for(i=0; i<lb; i++) buf[i] = toupper(buf[i]); //konwertujemy znaki w pętli write(1, buf,lb); // zapisujemy znaki na wyjście
Przykład użycia potoku wysokopoziomowa realizacja ls|tr a-z A-Z z użyciem łączy nienazwanych main(int argc, char* argv[]) { int fd[2]; pipe(fd); if(fork() == 0) {// proces potomny dup2(fd[1], 1); execvp("ls", argv); } else { // proces macierzysty close(fd[1]); dup2(fd[0], 0); execlp("tr", "tr", "a-z", "A-Z", 0); //Uruchomienie tr W przykładzie tym i poprzednim pominięto obsługę błędów.
Zadanie producenta-konsumenta Jeden proces-producent generuje produkuje dane (zapisuje do łącza) a drugi proces-konsument pobiera je (czyta z potoku). Potok jest wspólną zmienną która jest ograniczonym buforem. Może pojawić się problem przepełnienia łącza lub gdy łącze jest puste.