Jerzy Pejaś: Systemy operacyjne - Linux
LINUX
Komunikacja między procesami (IPC)
Komunikacja IPC
q Aby dwa procesy komunikowały się ze sobą, muszą obydwa się na to zgodzić, a system operracyjny musi dostraczyć narzędzi przeznaczonych do komunikacji między procesami (ang. Interprocess communication, IPC).
q Komunikacja między procesami nie dotyczy jedynie wymiany informacji pomiędzy procesami w sieci, ale przede wszystkim procesów wykonywanych w jednym systemie w jednym systemie komputerowym (patrz rys.1).
Rys.1 Komunikacja między dwoma procesami w jednym systemie
q Widzimy, że komunikacja między dwoma procesami odbywa się za pośrednictwem jądra. Jest to sytuacja typowa, ale nie jest to wymóg.
q Komunikacja między procesami w tym samym systemie może być realizowana na kilka róznych sposobów:
· Pół-dupleksowe łącza komunikacyjne (ang. half-duplex UNIX pipes),
· Kolejki FIFO (łącza nazwane, ang. named pipes),
· Kolejki komunikatów (ang.SYS V style message queues),
· Zbiory semaforów (ang.SYS V style semaphore sets),
· Pamięć współdzielona (ang.SYS V shared memory segments),
· Pełno-dupleksowe łacza komunikacyjne (amg. Full-duplex pipes, STREAMS pipes).
q Komunikacja między procesami wykonywanymi w różnych systemach przy użyciu jakiejś sieci łączącej systemy może wygldać tak jak na rys.2.
Rys.2 Komunikacja między dwoma procesami w różnych systemach
q Komunikacja między procesami znajdującymi się w różnych systemach realizowana jest za pośrednictwem gniazd (ang networking socets, Berkley style).
q Łącze komunikacyjne jest metodą, która umożliwia połączenie standardowego wyjścia jednego z procesów do standardowego wejścia innego lub tego samego procesu. Łącze komunikacyjne umożliwia przepływ danych tylko w jednym kierunku (stąd nazwa pół-duplex).
Rys.3 Łącze komunikacyjne w jednym procesie
q Schemat zastosowania łącza komunikacyjnego w jednym i tym samym procesie pokazano na rys.3. Zasady czytania danych z łącza, w którym nie ma żadnych danych oraz pisanie do łącza wówczas, gdy jest zapełnione podamy dalej, przy okazji omawiania łączy nazwanych.
Rys.4 Łącze komunikacyjne w jednym procesie bezposrednio po wywołaniu funkcji fork
q Możliwość wymiany przez proces informacji tylko z sobą jest mało interesująca, chociaż czasami może być przydatna, np. w razie konieczności kolejkowania informacji.
q Typowym zsatosowaniem łączy komunikacyjnych jest komunikowanie się dwóch róznych procesów w następujący sposób. Najpierw proces tworzy łacze komunikacyjne, następnie zaś wywołuje funkcje systemową fork, aby utworzyć swoją kopię (patrz rys.4).
Rys.5 Łącze komunikacyjne między dwoma procesami
q Następnie proces macierzysty zamyka np. koniec łącza służący do czytania, a proces potomny zamyka koniec łącza służący do pisania. Powstaje w ten sposób jednokierunkowy przepływ informacji między dwoma procesami (rys.5).
q Gdy użykownik wprowadzi na przykład z poziomu shell’a następujące polecenie:
who | sort | lpr
Wówczas shell utworzy po kolei trzy procesy i dwa łącza pomiędzy nimi. Utworzony w ten sposób tzw. potok (ang. pipeline) przedstawiony jest na rys.6.
Rys.6 Łącza komunikacyjne między dwoma procesami tworza potok
q Wszystkie omawiane łącza były jednokierunkowe, a więc umożliwiały przepływ danych tylko w jedną stronę. Jeśli chcemy uzyskać przepływ danych w obie strony, to musimy stworzyć dwa łącza komunikacyjne skierowane przeciwnie. Trzeba w tym celu wykonać następujące kroki:
· Utwórz łacze 1, utwórz łacze 2,
· Wywołaj funkcję systemową fork,
· Przodek zamyka łącze 1 do czytania,
· Przodek zamyka łącze 2 do pisania,
· Potomek zamyka łącze 1 do pisania,
· Potomek zamyka łącze 2 do czytania.
q Schemat konstrukcji przedstawiono na rys.7. Od tego momentu oba procesy posiadają pseudo pełno-duplexowe łącze komunikacyjne.
Rys.7 Dwa łącza komunikacyjne umożliwiają dwukierunkowy przepływ informacji
q Standardowo łącze komunikacyjne na poziomie języka C tworzy się za pomocą funkcji systemowej pipe. Funkcja pobiera pojedyńczy parametr, bedący wektorm dwóch liczb całkowitych, i zwraca (jeśli wywołanie kończy się pomyślnie) w nich dwa nowe deskryptory wykorzystywane przy konstrukcji potoku.
Wywołanie systemowe: pipe();
Prototyp: int pipe( int fd[2] );
RETURNS: 0 on success
-1 on error: errno = EMFILE (no free descriptors)
EMFILE (system file table is full)
EFAULT (fd array is not valid)
Uwaga: fd[0] jest deskryptorem czytania, fd[1] – deskryptorem pisania
q Szkic programu wywołującego funkcje pipe może mieć postać:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
main()
{
int fd[2];
pipe(fd);
.
}
q Ustanowiwszy łącze komunikacyjne możemy utworzyć nowy proces:
pid_t childpid;
if((childpid = fork()) == -1)
perror("fork");
exit(1);
q Jeśli przodek chce otrzymywać dane od potomka powinien zamknąć deskryptor fd[1], zaś potomek powinien zamknąć fd[0]. Jeśłi z kolei przodek chce przesyłać dane do potomka, wtedy musi zamknąć fd[0], zas potomek powinien zamknąć fd[1]. Jest to istotne z praktycznego punktu widzenia, ponieważ EOF nie będzie nigdy zwrócony jeśli zbędne końce łącza nie zostaną jawnie zamknięte.
/* "Linux Programmer’s Guide - Chapter 6" */
int main(void)
int fd[2], nbytes;
char string[] = "Hello, world!\n";
char readbuffer[80];
if(childpid == 0)
/* Proces potomny zamyka wejściową stronę łącza */
close(fd[0]);
/* Wysyła "string" poprzez wejście do łącza */
write(fd[1], string, strlen(string));
exit(0);
else
/* Proces przodka zamyka wyjąsiową strone łącza */
close(fd[1]);
/* Czyta ‘string’ z łącza */
nbytes = read(fd[0], readbuffer,
sizeof(readbuffer));
printf("Odebrano łańcuch: %s", readbuffer);
return 0;
q Często deskryptory potomka są duplikowane po to, aby wskazywały na standardowe wejście lub wyjście:
Wywołanie systemowe: dup();
Prototyp: int dup( int olffd);
-1 on error: errno = EBADF (oldfd is not a valid descriptor)
EBADF (newfd is out of range)
EMFILE (too many descriptors for the
process)
Uwaga: stary deskryptor nie jest zamknięty; oba mogą być używane zamiennie
q Zwykle po to duplikujemy deskryptory, aby zamknąć najpierw standardowy strumień (wej/wyj). Dzieje się tak dzięki temu, że wywołanie systemowe dup() przydzielając nowy despryptor wykorzystuje nieużywany (wolny) deskryptor o najniższym numerze. Rozważmy następujący fragment:
childpid = fork();
/* Zamknij standardowe wejście procesu potomnego */
close(0);
/* Duplikuj wejściową stronę łącza i przydziel ją do
stdin */
dup(fd[0]);
execlp("sort", "sort", NULL);
q Procedura execlp uruchamia nowy proces sort (standardowe polecenie shell’a). Ponieważ nowo uruchomiony proces dziedziczy standardowy strumień od swego stwórcy, stąd w naszym przypadku odziedziczy wejście do łącza jako swoje standardowe wejście. Od tego momentu każda informacja wysyłąna przez przodka do łącza będzie przekazywana do procesu sortującego.
q Istnieje także inna odmiana procedury dup(), występująca pod nazwą dup2().
Wywołanie systemowe: dup2();
Prototyp: int dup( int olffd, int newfd);
RETURNS: new descriptor on success
Uwaga: stary deskryptor jest zamykany przez dup2()
q Funkcja dup2() jest niepodzielna, tzn. że proces duplikacji oraz zamykania deskryptora nie może być przerwany przez napływające sygnały. W przypadku użycia dup() zachodzi konieczność użycia nastepnie close(). Pomiędzy tymi dwoma wywołaniami może minąć troche czasu i jeśli w tym czasie nadejdzie sygnał proces duplikowania może zakończyć się błędem (deskryptor 0 może zająć inny proces).
...
wojtas1400