LINUX Komunikacja mi«dzy procesami (IPC).doc

(1930 KB) Pobierz

Jerzy Pejaś:  Systemy operacyjne - Linux

 

 

LINUX

Komunikacja między procesami (IPC)






Komunikacja IPC

Wprowadzenie

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).

Łącza komunikacyjne (ang. pipes)

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

Tworzenie łaczy komunikacyjnych w języku C

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:

#include <stdio.h>

#include <unistd.h>

#include <sys/types.h>

main()

{

  int fd[2];

  pid_t childpid;

 

  pipe(fd);

  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" */

#include <stdio.h>

#include <unistd.h>

#include <sys/types.h>

int main(void)

{

  int fd[2], nbytes;

  pid_t childpid;

  char string[] = "Hello, world!\n";

  char readbuffer[80];

  pipe(fd);

  if((childpid = fork()) == -1)

  {

    perror("fork");

    exit(1);

  }

  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);

RETURNS: 0 on success

-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();

if(childpid == 0)

{

  /* 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

-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 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).

childpid = fork();

...

Zgłoś jeśli naruszono regulamin