Programowanie współbieżne i rozproszone/PWR Ćwiczenia 4: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Mengel (dyskusja | edycje)
Nie podano opisu zmian
Mengel (dyskusja | edycje)
Nie podano opisu zmian
Linia 140: Linia 140:
: Jak zrealizować komunikację w dwie strony między procesami? Czy jedno łącze wystarczy?
: Jak zrealizować komunikację w dwie strony między procesami? Czy jedno łącze wystarczy?


Oczywiście do komunikacji w dwie strony potrzebne są dwa łącza. W przeciwnym razie proces mógłby odbierać to co sam zapisze do łącza.  
Oczywiście do komunikacji w dwie strony potrzebne są dwa łącza. W przeciwnym razie proces mógłby odbierać to, co sam zapisze do łącza.  
 
 
 


A oto przykładowy program ilustrujący ten scenariusz. Proces macierzysty:
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "err.h"
char message[] = "Hello from your parent!";
int main()
{
  int pipe_dsc[2];                                       
  char pipe_read_dsc_str[10];
Tworzymy teraz łącze nienazwane. Deskryptory są przekazywane jako pipe_dsc[0] i pipe_dsc[1]. Pierwszy z nich służy do odczytu, drugi do zapisu z łącza:
  '''if''' (pipe (pipe_dsc) == -1) syserr("Error in pipe\n");
Tworzymy nowy proces. Każdy z nich ma swoją kopię zmiennej pipe_dsc oraz otwarte deskryptory o wartościach pamiętanych w tej zmiennej, prowadzące do utworzonego łącza:
  '''switch''' (fork()) {
    '''case''' -1:
      syserr("Error in fork\n");
    '''case''' 0:
Proces potomny zamyka niepotrzebny mu deskryptor do zapisu:
      '''if''' (close (pipe_dsc[1]) == -1) syserr("Error in close (pipe_dsc[1])\n");
Następnie zamienia numer deskryptora na napis:
      sprintf(pipe_read_dsc_str, "%d", pipe_dsc[0]);
i przekazuje go jako parametr wywołania procesu potomnego:
      execl("./child_pipe", "child_pipe", pipe_read_dsc_str, 0);
      syserr("Error in execl\n");
    '''default''':
Proces macierzysty zamyka niepotrzebny mu deskryptor do odczytu:
      '''if''' (close (pipe_dsc[0]) == -1) syserr("Error in close (pipe_dsc[0])\n");
Zapisuje komunikat do łącza za pomocą deskryptora pipe_dsc[1]. Zwróć uwagę na to, że nie korzystamy z żadnych konkretnych wartości. Parametrem funkcji write jest <tt>sizeof(message)</tt>, a nie wartość 14!
      '''if''' (write (pipe_dsc[1], message, sizeof(message)) == -1)
        syserr("Error in close write\n");
Na koniec zamykamy łącze i czekamy na koniec potomka:
      '''if''' (close (pipe_dsc[1]) == -1) syserr("Error in close (pipe_dsc[1])\n");
      '''if''' (wait (0) == -1)
        syserr("Error in wait\n");
      exit (0);
  } /* switch */
}


Proces potomny wykonuje kod zawarty w pliku <tt>child_pipe</tt>:
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include "err.h"
#define BUF_SIZE                1024
int main (int argc, char *argv[])
{
  int read_dsc, buf_len;
  char buf [BUF_SIZE];    /* Zarezerwowany bufor na komunikaty */
Najpierw sprawdzamy, czy program wywołano z jednym argumentem:
  '''if''' (argc != 2)
    fatal("Usage: %s <read_fd>\n", argv[0]);
Przekształcamy pierwszy argument na wartość. Jest to deskyptor do odczytu z łącza. Zauważmy, że gdybyśmy nie otrzymali go jako parametr wywołania, to nie znalibyśmy jego wartości, więc nie moglibyśmy korzystać z łącza:
  read_dsc = atoi(argv[1]);
  printf ("Reading data from descriptor %d\n", read_dsc);
Odczytujemy komunikat sprawdzając poprawność funkcji read. I znów nie umieszczamy konkretnych wartości, lecz posługujemy się stałymi:
  if ((buf_len = read (read_dsc, buf, BUF_SIZE - 1)) == -1)
    syserr("Error in read\n");
Jeśli na końcu odczytanego komunikatu nie było 0, to je dopisujemy:
  buf [buf_len < BUF_SIZE - 1 ? buf_len : BUF_SIZE - 1] = '\0';
Reagujemy na błędy:
  if (buf_len == 0)                          /* To nie powinno się zdarzyć */
    fatal("read: unexpected end-of-file\n");
  else                                      /* Udało się odczytać */
    printf ("Read %d byte(s): \"%s\"\n", buf_len, buf);
Zamykamy niepotrzebne już łącze: 
  if (close (read_dsc)==-1) then
    syserr ("Error in close\n");
  exit (0);
}


=== Przekierowywanie wejścia/wyjścia ===
=== Przekierowywanie wejścia/wyjścia ===

Wersja z 09:23, 13 cze 2006

Tematyka laboratorium

  1. Łącza nienazwane w systemie Unix: deskryptory plików, tworzenie i zamykanie łączy, duplikowanie łączy, zapis i odczyt z łącza
  2. Łącza nienazwane jako realizacja komunikacji asynchronicznej

Literatura uzupełniająca

  1. M. Rochkind, Programowanie w systemie Unix dla zaawansowanych, rozdz. 6.2 i 6.3
  2. W. R. Stevens, Programowanie zastowań sieciowych w systemie UNI, rozdz. 2.3 i 3.4
  3. M. J. Bach, Budowa systemu operacyjnego UNIX, rozdz. 7.1
  4. man do poszczególnych funkcji systemowych

Pliki z programami przykładowymi

  • Makefile
  • err.h plik nagłówkowy biblioteki obsługującej błędy
  • err.c biblioteka do obsługi błędów
  • parent_pipe.c program ilustrujący komunikację za pomocą łącza. Współpracuje z child_pipe
  • child_pipe.c program ilustrujący komunikację za pomocą łącza. Współpracuje z parent_pipe
  • parent_dup.c program ilustrujący duplikację łącza

Scenariusz zajęć

Deskryptory, otwarte pliki

Gdy proces otwiera plik, informacje o tym pliku (m.in.: tryb otwarcia, położenie na dysku, wskaźnik pliku, czyli pozycja w pliku, której będzie dotyczyć kolejna operacja wejścia/wyjścia itp.) są zapisywane w tablicy otwartych plików utrzymywanej przez system operacyjny. W całym systemie jest jedna taka tablica. Z kolei informacja o położeniu danych o pliku w systemowej tablicy plików otwartych (oraz pewne dodatkowe informacje) jest zapamiętywana w tablicy deskryptorów tego procesu, który plik otworzył. Każdy proces utrzymuje tablicę deskryptorów --- jest ona lokalna dla procesu, znajduje się w jego danych systemowych (a więc jest kopiowana przy wykonywaniu funkcji fork()). Indeks w tej tablicy to właśnie deskryptor pliku.

Rysunek

Do otwierania pliku służy funkcja systemowa

open.

W wyniku przekazuje ona deskryptor otwartego pliku lub informacje o błędzie.

Zadanie
Przeczytaj stronę man dotyczącą funkcji open

W wyniku otwarcia pliku, tworzy się nowa pozycja w tablicy otwartych plików. Oznacza to, że jeśli proces dwukrotnie otworzy ten sam plik, to w tablicy otwartych plików będą dwie różne pozycje dotyczące tego pliku. Proces będzie miał również dwa różne deskryptory odnoszące się do tego samego pliku. Ponieważ wskaźnik pliku jest pamiętany w systemowej tablicy otwartych plików, więc operacje wejścia/wyjścia wykonywane za pomocą różnych deskryptorów będą od siebie niezależne (wykonanie operacji odczytu za pośrednictwem jednego deskryptora nie ma wpływu na pozycję, z której odczytamy kolejne dane z pliku za pośrednictwem drugiego deskryptora).

Test wyboru

Przypuśćmy, że proces wykonuje następujący ciąg operacji: Załóżmy, że w pliku test.txt znajdują się po kolei znaki abcd. Zmienne tego procesu mają następujące wartości:

Gdy proces wykonuje funkcję systemową fork() tablica deskryptorów jest kopiowana do procesu potomnego. Zatem potomek "dziedziczy" po procesie macierzystym wszystkie otwarte pliki. Co więcej, deskryptory o tych samych wartościach w procesie potomnym i macierzystym odnoszą się do tej samej pozycji w systemowej tablicy otwartych plików.

Rysunek

Wynika z tego, że rodzic i potomek współdzielą otwarte pliki, tj. każda operacja wejścia/wyjścia w procesie macierzystym przesuwa wskaźnik pliku i powoduje, że kolejna operacja wejścia/wyjścia w procesie potomnym będzie dotyczyć kolejnej porcji danych z pliku.

Test wyboru

Przypuśćmy, że proces wykonuje następujący ciąg operacji: Załóżmy, że w pliku test.txt znajdują się po kolei znaki abcd. Zmienne tego procesu mają następujące wartości:


Takie współdzielenie deskryptorów jest także możliwe w pojedynczym procesie. Aby stworzyć kopię deskryptora pliku używamy funkcji:

int dup(int oldfd);
int dup2(int oldfd, int newfd);

Funkcja dup tworzy kopię deskryptora, która odnosi się do tego samego miejsca w tablicy otwartych plików, co oryginał. Jest to zatem zupełnie inna sytuacja niż przy wielokrotnym otwarciu tego samego pliku. Funkcja dup duplikuje deskryptor oldfd i przekazuje wartość nowego deskryptora. Wiadomo przy tym, że przekazywany deskryptor jest deskryptorem o najmniejszej wartości spośród aktualnie dostępnych. Funkcja dup2 zamyka deskryptor newfd jeśli był on otwarty i duplikuje oldfd na tę właśnie wartość. Wynikiem dup2 jest numer nowego deskryptora (czyli newfd). W wypadku błędu obie funkcje dają w wyniku wartość -1.

Rysunek
Test wyboru

Przypuśćmy, że proces wykonuje następujący ciąg operacji: Załóżmy, że w pliku test.txt znajdują się po kolei znaki abcd. Zmienne tego procesu mają następujące wartości:

Zadanie
Przeczytaj man dup

Wykonanie funkcji systemowej exec() nie ma wpływu na postać tablicy deskryptorów.Po wykonaniu funkcji exec() proces zachowuje otwarte pliki (choć zapewne nie zna już wartości ich deskryptorów, bo zmienne, które przechowywały te wartości przestały istnieć w chwili wykonania funkcji exec()).

Do czytania z pliku służy funkcja systemowa read, a do zapisu funkcja systemowa write. Funkcje te są niepodzielne. Oznacza to, że operacje read i/lub write wykonywane na tym samym pliku "jednocześnie" nie będą się przeplatać: druga rozpocznie się po zakończeniu pierwszej.

Zadanie
Przeczytaj man read i man 2 write

Po uruchomieniu każdego procesu rezerwowane są dla niego trzy specjalne deskryptory o numerach 0, 1 i 2. Są to deskryptory standardowego wejścia, standardowego wyjścia i standardowego wyjścia błędów procesu. Domyślnie są one związane z konsolą, czyli odczyt po standardowym deskryptorze wejściowym powoduje czytania klawiatury, zapis po standardowym deskryptorze wyjściowym i diagnostycznym powoduje zapis na ekran. Wejście, wyjście i wyjście błędów można przekierowywać z poziomu interpretera poleceń za pomocą operatorów < > >>, 2> oraz &>. Na przykład:

cat plik > kopia.pliku "wyświetli" plik nie na ekran lecz do pliku kopia.pliku

Łącza nienazwane

Zasada działania

Łącza nienazwane to rodzaj plików istniejących tylko wewnątrz jądra systemu operacyjnego - nie można ich znaleźć na dysku twardym. Służą one do komunikacji między procesami pokrewnymi. Dwa procesy nazwiemy pokrewnymi, jeśli mają wspólnego przodka (ojca, dziadka itd.) lub jeden jest przodkiem drugiego. Do łącza można zapisywać dane i odczytywać je za pomocą tych samych funkcji systemowych co w przypadku plików (read oraz write). W praktyce o łączu nienazwanym można myśleć jak o buforze utrzymywanym przez system operacyjny. Można do niego zapisywać dane i odczytywać, przy czym dane są odczytywane w kolejności ich zapisywania. Taki bufor jest oczywiście skończony, ale nie znamy jego pojemności. Wiadomo jedynie, że zmieści się w nim co najmniej 4KB danych.

W przypadku próby odczytu danych z pustego łącza proces jest (domyślnie) wstrzymywany, choć zachowanie to można zmienić. Łącza zachowują się zatem bardzo podobnie do abstrakcyjnego mechanizmu SendMessage i GetMessage przedstawionego na poprzednich ćwiczeniach. Jest to mechanizm asynchroniczny.

Tworzenie

Do tworzenia łączy nienazwanych służy funkcja systemowa

int pipe(int filedes[2]);

Funkcja ta tworzy nowe łącze nienazwane oraz umieszcza w tablicy podanej jako parametr, wartości dwóch deskryptorów. Pierwszy deskryptor służy do czytania, a drugi do pisania do utworzonego łącza.

Rysunek
Zadanie
Przeczytaj man pipe

Oczywiście informacje o łączu trafiają również do systemowej tablicy otwartych plików, więc rozważania przedstawione we wstępie pozostają w mocy także w odniesieniu do łączy.

Zapis do łącza

Jeśli fd jest deskryptorem służącym do zapisu do pewnego łącza, to funkcja systemowa

write (fd, buf, count)

próbuje zapisuje do łącza o deskryptorze fd count bajtów znajdujących się w tablicy buf. Ale łącza mają ograniczona pojemność. Proces, który próbuje zapisać do łącza, w którym nie ma miejsca na całą zapisywaną porcję jest wstrzymany do czasu, aż z łącza zostanie odczytana taka ilość danych by znalazło się miejsce na wszystkie zapisywane dane. Zwróćmy uwagę: write zapisze wszystko albo nic. Jest jednak wyjątek od tej reguły. Gdy próbujemy na raz zapisać do łącza więcej niż jego pojemność, to proces zapisuje do łącza tyle danych ile może i jest wstrzymywany domomentu aż znowu będzie mógł coś do łącza wpisać. Wynikiem funkcji write jest liczba zapisanych bajtów lub -1, jeśli nastąpił błąd. Jeśli nie próbujemy pisać więcej niż pojemność łącza, to wynikiem powinna być wartość count.

Zapis do łącza jest możliwy tylko wtedy, gdy jest ono otwarte (przez ten sam lub inny proces) do czytania. Jeśli proces spróbuje zapisać coś do łącza, które nie jest przez żaden proces otwarte do czytania, to zostanie przerwany sygnałem SIGPIPE (więcej o sygnałach wkrótce). Ten błąd najczęściej objawia się komunikatem broken pipe z poziomu interpretera poleceń.

Odczyt z łącza

Jeśli fd jest deskryptorem służącym do odczytu z pewnego łącza, to funkcja systemowa:

ssize_t read(int fd, void *buf, size_t count);

odczyta z łącza co najwyżej count bajtów i zapisze je pod adresem <ttbuf>. Jeśli w łączu znajduje się mniej bajtów niż count (ale łącze nie jest puste), to funkcja read odczyta to, co jest w łączu i kończy się pomyślnie. Jej wynikiem jest wówczas liczba faktycznie odczytanych bajtów. Próba odczytu z pustego łącza wstrzymuje proces do czasu pojawienia się w łączu jakichkolwiek danych lub do chwili, gdy łącze nie będzie otwarte do zapisu. Kolejność odczytu jest zgodna z kolejnością zapisu (łącza są kolejkami prostymi).

Jeśli proces próbuje odczytać z łącza, które nie jest przez nikogo otwarte do zapisu, to funkcja read kończy się natychmiast przekazując w wyniku 0. Ta własność jest wykorzystywana do sprawdzania, czy jest coś jeszcze do odczytu.

Zamykanie łącza

Aby zamknąć łącze (lub plik) używamy funkcji systemowej

int close(int fd);

Funkcja przekaże -1 w przypadku błędu.

O zamykanie otwartych, a niepotrzebnych już deskryptorów często się zapomina. Jednak jest to bardzo ważna operacja i to z dwóch powodów:

  1. Zamykając zbędne deskryptory natychmiast, gdy przestają być potrzebne zapobiegamy zbędnemu kopiowaniu deskryptorów (przy fork()) i nadmiernemu wzrostowi tablic deskryptorów. We wczesnych systemach uniksowych można było mieć jednocześnie otwartych tylko 20 deskryptorów.
  2. Zamknięcie wszystkich deskryptorów służących do zapisu do danego łącza jest często jedynym sposobem poinformowania procesu czytającego z tego łącza, że nie ma już więcej danych do odczytu w łączu.

Nie zamykamy sami deskryptorów 0, 1, 2, chyba że jest do tego istotny powód. Zawsze zamykamy to, co sami otworzyliśmy, choć tak naprawdę proces wykonujący funkcję systemową exit zamyka sam wszystkie otwarte łącza.

Zadanie

Przeczytaj man close

Typowy scenariusz

Typowy scenariusz użycia łącza jest zatem następujący. Proces tworzy łącze (funkcją pipe), następnie rozmnaża się (funkcja fork). Proces macierzysty zamyka deskryptor do zapisu a proces potomny zamyka deskryptor do odczytu (odziedziczony po ojcu). Teraz proces macierzysty może wykonywać funkcję read. Będzie ona czekać aż do chwili, gdy proces potomny coś zapisze w łączu. Wtedy przekaże w wyniku wartość niezerową i odczyta z łącza wiadomość. Gdy proces potomny zamknie deskryptor do zapisu, read w procesie macierzystym przekaże wartość zero. W ten sposób proces może wykryć koniec strumienia danych i na przykład zakończyć się.

Ćwiczenie
Co stało by się, gdyby proces potomny nie zamknął (niepotrzebnego mu) deskryptora do odczytu z łącza?
Co stało by się, gdyby proces macierzysty zamknął deskryptor do odczytu zanim jego potomek coś zapisze do łącza?
Czy założenie o niepodzielności read i write jest istotne dla realizacji tego schematu?
Jak zrealizować komunikację w dwie strony między procesami? Czy jedno łącze wystarczy?

Oczywiście do komunikacji w dwie strony potrzebne są dwa łącza. W przeciwnym razie proces mógłby odbierać to, co sam zapisze do łącza.

A oto przykładowy program ilustrujący ten scenariusz. Proces macierzysty:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "err.h"
char message[] = "Hello from your parent!";
int main()
{
  int pipe_dsc[2];                                         
  char pipe_read_dsc_str[10];

Tworzymy teraz łącze nienazwane. Deskryptory są przekazywane jako pipe_dsc[0] i pipe_dsc[1]. Pierwszy z nich służy do odczytu, drugi do zapisu z łącza:

  if (pipe (pipe_dsc) == -1) syserr("Error in pipe\n"); 

Tworzymy nowy proces. Każdy z nich ma swoją kopię zmiennej pipe_dsc oraz otwarte deskryptory o wartościach pamiętanych w tej zmiennej, prowadzące do utworzonego łącza:

  switch (fork()) {
    case -1:
      syserr("Error in fork\n");
    case 0:

Proces potomny zamyka niepotrzebny mu deskryptor do zapisu:

      if (close (pipe_dsc[1]) == -1) syserr("Error in close (pipe_dsc[1])\n");

Następnie zamienia numer deskryptora na napis:

      sprintf(pipe_read_dsc_str, "%d", pipe_dsc[0]);

i przekazuje go jako parametr wywołania procesu potomnego:

      execl("./child_pipe", "child_pipe", pipe_read_dsc_str, 0);
      syserr("Error in execl\n");
   default:

Proces macierzysty zamyka niepotrzebny mu deskryptor do odczytu:

      if (close (pipe_dsc[0]) == -1) syserr("Error in close (pipe_dsc[0])\n");

Zapisuje komunikat do łącza za pomocą deskryptora pipe_dsc[1]. Zwróć uwagę na to, że nie korzystamy z żadnych konkretnych wartości. Parametrem funkcji write jest sizeof(message), a nie wartość 14!

      if (write (pipe_dsc[1], message, sizeof(message)) == -1)
        syserr("Error in close write\n");

Na koniec zamykamy łącze i czekamy na koniec potomka:

      if (close (pipe_dsc[1]) == -1) syserr("Error in close (pipe_dsc[1])\n");
      if (wait (0) == -1)
        syserr("Error in wait\n");
     exit (0);
 } /* switch */

}

Proces potomny wykonuje kod zawarty w pliku child_pipe:

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include "err.h"
#define BUF_SIZE                1024
int main (int argc, char *argv[])
{
  int read_dsc, buf_len;
  char buf [BUF_SIZE];     /* Zarezerwowany bufor na komunikaty */

Najpierw sprawdzamy, czy program wywołano z jednym argumentem:

  if (argc != 2)
    fatal("Usage: %s <read_fd>\n", argv[0]);

Przekształcamy pierwszy argument na wartość. Jest to deskyptor do odczytu z łącza. Zauważmy, że gdybyśmy nie otrzymali go jako parametr wywołania, to nie znalibyśmy jego wartości, więc nie moglibyśmy korzystać z łącza:

  read_dsc = atoi(argv[1]);
  printf ("Reading data from descriptor %d\n", read_dsc);

Odczytujemy komunikat sprawdzając poprawność funkcji read. I znów nie umieszczamy konkretnych wartości, lecz posługujemy się stałymi:

  if ((buf_len = read (read_dsc, buf, BUF_SIZE - 1)) == -1)
    syserr("Error in read\n");

Jeśli na końcu odczytanego komunikatu nie było 0, to je dopisujemy:

 buf [buf_len < BUF_SIZE - 1 ? buf_len : BUF_SIZE - 1] = '\0';

Reagujemy na błędy:

 if (buf_len == 0)                          /* To nie powinno się zdarzyć */
   fatal("read: unexpected end-of-file\n");
 else                                       /* Udało się odczytać */
   printf ("Read %d byte(s): \"%s\"\n", buf_len, buf);

Zamykamy niepotrzebne już łącze:

 if (close (read_dsc)==-1) then 
   syserr ("Error in close\n");
 exit (0);

}

Przekierowywanie wejścia/wyjścia

Implementacja czytelników i pisarzy za pomocą łączy

Zadanie zaliczeniowe

Napisz program pierścień tworzący N procesów, z których każdy działa następująco:

  1. pierwszy z nich w pętli:
    1. odczytuje ze standardowego wejścia komunikaty co najwyżej 100 znakowe zakończone znakiem końca wiersza
    2. przesyła ten komunikat za pomocą standardowego deskryptora wyjściowego do łącza, z którego odczyta go drugi proces
    3. powtarza kroki 1-2 do chwili odczytania pustego komunikatu
  2. wszystkie z wyjątkiem ostatniego:
    1. odbierają z łącza od poprzedniego procesu za pomocą standardowego deskryptora wejściowego komunikat
    2. przesyłają go za pomocą standardowego deskryptora wyjściowego do kolejnego procesu
  3. ostatni proces przesyła odebierane komunikaty do pierwszego, który wypisuje je na ekranie.



 Funkcja ta tworzy nowe łącze nienazwane oraz umieszcza w tablicy podanej
 jako parametr, numery dwóch deskryptorów. Pierwszy deskryptor służy do
 czytania, a drugi do pisania do utworzonego łącza.



  • Przeanalizuj plik źródłowy parent_pipe.c oraz child_pipe.c.
 Zwróć uwagę na to, że proces potomny po wykonaniu funkcji exec()
 ma otwarty deskryptor do łącza, ale nie zna jego numeru (bo
 zmienna przechowująca ten numer przestała istnieć w chwili
 wykonania funkcji exec). Stąd konieczność przekazania tego numeru
 przez argumenty wywołania programu. Nie jest to elegancka technika,
 ładniejszy sposób znajduje się w punkcie 6.


6. Podmiana standardowego wejścia/wyjścia

  --------------------------------------
  Ciekawym sposobem wykorzystania łączy jest podmiana standardowego
  wejścia i wyjścia procesu. Jest to BARDZO WAZNA technika, gdyż
  tak właśnie działa większość programów uniksowych. Scenariusz jest taki:
  Proces tworzy łącze, następnie wykonuje fork().
  Proces macierzysty zamyka niepotrzebne mu deskryptory, po czym
  wykonuje się dalej uzywając otwartego łącza.
  Proces potomny zamyka duplikuje (funkcja dup lub dup2) deskryptory
  łącza na standardowe wejście lub wyjście w zależności od potrzeb,
  a następnie zamyka niepotrzebne deskryptory.
  Teraz wykonuje exec(). Zwróćmy uwagę, że na skutek zduplikowania
  standardowe wejście/wyjście zostało teraz przekierowane do utworzonego
  łącza. Zatem tekst programu wywoływanego w funkcji exec piszemy
  "standardowo": czytanie ze standardowego wejścia, zapis na standardowe
  wyjście. Przekierowane dokonane PRZED wywołaniem funkcji exec
  powoduje, że te standardowe deskryptory dotyczyć będą już nie terminala
  lecz uprzednio utworzonego łącza.
 * Przeanalizuj plik źródłowy parent_dup.c
  Klasycznym przykładem użycia komunikacji jednostronnej przy użyciu
  łączy z podmiana wejścia i wyjścia jest operator | w shell-u
  np. cat /etc/passwd | grep Michal | more


ZADANIE 3



-->