SOP322/ćw - temat B

Funkcje systemowe Unixa - powtórka?

Pojęcia Unix-owe, które powinniśmy rozumieć:

1. Wstęp.

Aby proces mógł skorzystać z usług systemu operacyjnego (np odczytać zawartość pliku, lub uruchomić inny proces), musi wywołać funkcję systemową. W systemie operacyjnym UNIX będziemy eksperymentować z funkcjami systemowymi przy pomocy języka "C".  Wywołanie funkcji systemowej w języku "C" nie różni się od wywołania funkcji "nie-systemowej", różnica tkwi w sposobie implementacji tej funkcji.

Informacje na temat funcji systemowych znajdują się w rozdziale drugim manuala UNIX-a:

Informacje na temat większości funcji "nie-systemowych", np printf()  znajdują się w trzecim rozdziale manuala, po tym - w praktyce - można rozróżnić, czy funkcja jest systemowa czy nie. Kompilowanie programów pod UNIX-em: Program można potem uruchamiać następująco: Jeśli to nie zadziała, za to zadziała "./prog01", to oznacza to, że brakuje nam "." w zmiennej PATH; można to naprawić poleceniem:
Aby zatrzymać działający program (pierwszoplanowy) naciśnij Ctrl-C !

Jeśli w trakcie działania funkcji systemowej wystąpi błąd to zwraca ona wartość (-1), ponadto globalna zmienna errno jest ustawiana na odpowiednia wartość (opisującą błąd). Słowne opisy możliwych wartości zmiennej errno można zobaczyć w pliku "/usr/include/errno.h". Opis możliwych wartości znajduje się także na stronach manuala, dotyczących konkretnej funkcji systemowej. Słowny opis błędu można wypisać na ekranie funkcją:  perror("tekst pomocniczy").

1.1. Literatura.

  • Stevens "Programowanie sieciowe w Unix-ie", rozdział 2; jest tam krótki opis najważniejszych funkcji systemowych Unix-a.
  • Haviland, Gray, Salema "Unix, programowanie systemowe".
  • Grey "Komunikacja między procesami w Unix-ie", rozdział 10 o gniazdkach, 11 o wątkach.
  • Gabassi, Dupouy "Przetwarzanie rozproszone w systemie Unix", rozdział 4 o gniazdkach.
  • Bach "Budowa systemu operacyjnego UNIX", jest to opis wewnętrznych algorytmów i struktur danych Unix-a System V Wersja 2.
  • Rago "Unix System V Network Programming".
  • The Linux Programmer's Guide
  • The Linux Programmer's Guide (po polsku)
  •  

    2. Tworzenie procesów, uruchamianie programów; 
    funkcje systemowe: fork(), wait(), exec*(), exit().

    Opis funkcji systemowych:

    int fork ( void );

  • proces, który wywołał fork() "rozdwaja się" - mamy teraz proces macierzysty i proces potomny, który początkowo jest dokładną kopią procesu macierzystego
  • proces potomny ma taką samą tablicę deskryptorów jak proces macierzysty (czyli pliki otwarte w procesie macierzystym pozostają otwarte w procesie potomnym !);
  • proces potomny tak samo obsługuje sygnały, jak proces macierzysty;
  • proces potomny ma takie same zmienne środowiska jak proces macierzysty;
  • void exit(int kod_zakończenia)

  • funkcja "exit()" - powoduje zakończenie procesu potomnego, przekazany parametr może być odczytany przez proces macierzysty.
  • gdy proces się kończy, to niektóre(?) zasoby zarezerwowane przez proces zostają zwolnione; np wszytkie otwarte pliki zostaną zamknięte
  • int wait ( int *status );

  • wait() powoduje, że proces macierzysty czeka na zakończenie (dowolnego) procesu potomnego
  • wartością zwracaną przez wait() jest PID procesu potomnego, który się zakończył
  • w zmiennej "status" jest zwracana informacja o sposobie zakończenia działania potomka;
  • Teraz mały przykład (fork01.cc).
    Ten sam przykład bez komentarzy dotyczących języka "C" (fork01a.cc).
     

    Zadanie 35


    W przykładzie "fork01.cc" zaobserwuj jakie informacje zwraca "wait()", gdy proces potomny kończy się na dwa sposoby:
    1. gdy kończy się z powodu wywołania "exit()"
    2. gdy kończy się z powodu otrzymania sygnału SIGINT (czyli nr 2)
    (Wskazówki dotyczące punktu "2.")
    Zwiększ "czas spania" procesu "pp" do 60 lub 120 sekund, aby zdążyć to wszystko zrobić !
    Jak sprawdzić jakie mamy procesy ?
       ps -O ppid
           PID PPID CMD
           111 222 fork01
           333 111 fork01
    Jak wysłać sygnał SIGINT do procesu "pp" ?
       kill -int 333
    Zadanie 36

    Napisz program podobny do "fork01.cc", który stworzy "drzewko procesów", takie jak na następującym rysunku (obok procesow podane jest ile czasu mają "spać"; procesy macierzyste czekają na procesy potomne; wszystkie procesy powinny wyświetlać napisy informujące o swoich poczynaniach podobnie jak to było w przykładzie "fork01.cc"; użyj komentarzy "// pm", "// pp1", "// pp2" itp aby ułatwić sobie zadanie !).

     
     

    Teraz pokażemy, jak w procesie potomnym uruchomić inny program ...

    int exec*(...)

    int execl (
            const char *path,
            const char *arg,
            ...  );
    int execv (
            const char *path,
            char * const argv[ ] );
    int execle (
            const char *path,
            const char *arg,
            ...
            char * const envp[ ] );
    int execve (
            const char *path,
            char * const argv[ ],
            char * const envp[ ] );
    int execlp (
            const char *file,
            const char *arg,
            ... );
    int execvp (
            const char *file,
            char * const argv[ ] );
  • funkcja "exec()" zastępuje kod bieżącego procesu nowym kodem
  • nowy program dziedziczy po procesie, który wywołał "exec" wiele rzeczy (np może dziedziczyć otwarte deskryptory plików)
  • pierwszy parametr funkcji "exec*()" identyfikuje plik z nowym programem
  • istnieją różne sposoby przekazywania "parametrów" i "środowiska" do nowego programu; literki w nazwie funkcji "exec*()" maja następujące znaczenie :
  •    "v" oznacza, ze parametry są przekazywanie przez tablice wskazań
               char *v[]={"ls", "-l", NULL};
               execv("/bin/ls", v);

       "l" oznacza, ze parametry są przekazywanie przez listę
               execl("/bin/ls", "ls", "-l", NULL);

       "p" nie potrzeba podawać pełnej ścieżki do pliku z programem;
           będzie wykorzystywana zmienna PATH
               execlp("ls", "ls", "-l", NULL);

       "e" oznacza, ze podajemy nowe środowisko za pomocą tablicy wskazań
           do napisów "zmienna=wartosc".
               char *v[]={"zmienna1=wart1", "zmienna2=wart2", NULL};
               execle("./prog", "prog", "par1", "par2", NULL, v);
           UWAGA: jeśli nie ma literki "e" to środowisko pozostaje takie
           jakie było przed wykonaniem exec*() !!!
    Jest tu mowa o "parametrach" i "środowisku" które są dostępne w programie (napisanym w języku "C") przy pomocy parametrów "argv" i "env" w fun. main() :
       main(int argc, char *argv[], char *env[]) 
       { 
          int i; char **c;
          for(i=0; i<argc; i++) printf("%s\n", argv[i]);
            // wyswietlanie argumentow

          c=env; while(*c) printf("%s\n", *c++);
            // wyświetlanie środowiska

          c=environ; while(*c) printf("%s\n", *c++);
            // wyświetlanie środowiska innym sposobem
       }
    Środowisko jest dostępne także przy pomocy zmiennej globalnej "environ". Ponadto proces może odczytywać i modyfikować swoje środowisko przy pomocy funkcji putenv() i getenv().
    UWAGA: putenv() i getenv() NIE SĄ funkcjami systemowymi.
     
    Parametry i zmienne środowiska służą do przekazywania danych z procesu macierzystego do procesu potomnego (w momencie tworzenia procesu potomnego).

    Teraz mały przykład (fork02.cc). W przykładzie tym tworzona jest zmienna środowiska o nazwie QWERTY i wartości 123456; następnie proces tworzy proces potomny który dziedziczy m.in. zmienne środowiska, a więc i zmienną QWERTY. Potem proces potomny uruchamia program "env" wyświetlający zmienne środowiska. Po wywołaniu execl() proces posiada te same zmienne środowiska co przed wywołaniem execl(), dlatego zmienna QWERTY także zostanie wyświetlona.

    Zadanie 37


    W przykładzie (fork02.cc) zamień program "env" na "pr01" wyświetlający parametry i środowisko (uruchamiaj go z parametrami "par1" "par2" "par3"). Program "pr01" należy napisać samodzielnie.

    .........................................................................................

    Nieco wyprzedzając, podamy teraz opisy funkcji służących do czytania/pisania z/do otwartego deskryptora udostępniającego plik. Przypomnijmy że każdy proces ma zaraz po uruchomieniu otwarte deskryptory 0, 1, 2 (stdin, stdout, stderr) pozwalające czytać/pisać z/do terminala.

    int read( int filedes, void *buffer, int nbytes );

  • próbuje wczytać podana liczbę bajtów (nbytes) z pliku o podanym deskryptorze (filedes) do podanego bufora; bieżąca pozycja w pliku przesuwa się o tyle, ile bajtów przeczytano
  • read() zwraca ilość bajtów naprawdę przeczytanych (zawracana wartość może być mniejsza od nbytes !)
  • gdy "bieżąca pozycja" przekroczy koniec pliku, to read() zwraca 0
  •  int write( int filedes, void *buffer, int nbytes );

  • działa podobnie do read(), z ta różnicą, że pisze do pliku zamiast czytać
  • A oto mały przykład, w którym odczytujemy z stdin co najwyżej 100 znaków i przepisujemy je na stdout (przepisujemy dokładnie tyle znaków ile wczytaliśmy):

        char buf[100]; // deklaracja tablicy 100 znaków
        int i,j;
        i=read(0, buf, 100);
        j=write(1, buf, i);
        printf("i=%i, j=%i\n", i, j);

     

    3. Operacje na plikach, przeadresowywanie;
    funkcje systemowe: open(), close(), read(), write(), dup(), dup2().

    Otwarcie pliku, to przygotowanie pliku do przetwarzania.  Po otwarciu pliku otrzymujemy deskryptor, przy pomocy którego można się potem odwoływać do pliku - wykonywać operacje zapisu i odczytu.  Deskryptor to liczba całkowita.

    Pamiętajmy, że w UNIX-ie rozmaite urządzenia (np terminale) są dostępne poprzez pliki - są to tzw pliki specjalne, znajdujące się zwyczajowo w katalogu "/dev".  Przykłady:  dostęp do terminala można uzyskać otwierając plik  "/dev/tty??" i pisząc lub czytając z tego pliku;  "surowy" dostęp do dyskietki (traktowanej jako ciąg bajtów) uzyskujemy otwierając plik "/dev/fd0".

    Istnieją deskryptory o specjalnym znaczeniu: 0, 1, 2 - standardowe wejście, wyjście i wyjście błędów.  Jeśli uruchomimy program z powłoki poleceniem:

    to od samego początku, w programie tym będą otwarte deskryptory 0,1,2 i będą one związane z terminalem.
    Jeśli uruchomimy program inaczej: to deskryptory 0 i 2 będą związane z terminalem, natomiast deskryptor 1 będzie związany z plikiem "plik.txt" (jest to tzw przeadresowanie stdout do pliku)
     

    Oto pojęcia niezbędne aby posługiwać się plikami pod Unix-em:
            -> bieżąca pozycja w otwartym pliku
            -> tablica deskryptorów procesu (deskryptory to indeksy elementów tej tablicy)
            -> tablica plików (tu przechowuje się "bieżącą pozycję")
            -> tablica i-węzłów (każdy element tej tablicy identyfikuje plik)

    Każdy proces ma własną tablicę deskryptorów.
    W danej maszynie istnieje jedna tablica plików i jedna tablica i-węzłów.

    Na poniższym rysunku deskryptory 10,11,12 udostępniają ten sam plik "plik.txt".  Jeśli teraz przesuniemy bieżącą pozycję poprzez desk 10 to ma to wpływ na bieżącą pozycje poprzez desk 11 lecz nie poprzez desk 12.  Gdy otwieramy dwukrotnie ten sam plik funkcją open()  - opisaną poniżej - to otrzymamy różne pozycje w tablicy plików.  Gdy duplikujemy deskryptor funkcją dup()/dup2() lub wykonujemy fork() to otrzymamy tę samą pozycję w tablicy plików.
     


     

    Opis funkcji systemowych związanych z plikami:

    int open( const char *path, int oflag [ , mode_t mode ] );

  • służy do otwierania pliku o nazwie podanej przez "path"; zwraca deskryptor
  • bity parametru "oflag" pozwalają określić następujące rzeczy:
  • jeśli jest tworzony nowy plik (O_CREAT), to używane jest "mode", zawiera ono prawa dla nowo tworzonego pliku; parametr "mode" można podawać tak jak ósemkowy parametr dla polecenia "chmod" (uwaga na "umask")
  • int close( int filedes );

  • zamyka plik o deskryptorze "filedes"
  • int read( int filedes, void *buffer, int nbytes );

  • próbuje wczytać podana liczbę bajtów z pliku o podanym deskryptorze do podanego bufora; bieżąca pozycja w pliku przesuwa się o tyle, ile bajtów przeczytano
  • read() zwraca ilość bajtów naprawdę przeczytanych (zawracana wartość może być mniejsza od nbytes !)
  • gdy "bieżąca pozycja" przekroczy koniec pliku, to read() zwraca 0
  • UWAGA:
    Jeśli czytamy z terminala i akurat nie ma nic do przeczytania, to wtedy funkcja read() będzie blokować, tak długo aż nie pojawią się jakieś znaki do przeczytania.  Podobnie, funkcja wait() blokuje proces macierzysty, gdy oczekuje on na zakończenie swojego procesu potomnego.
    Blokowanie NIE JEST realizowane przez "aktywne czekanie", lecz przez wprowadzenia procesu w "stan uśpienia"; gdy pożądane zdarzenie się zdarzy to proces zostanie "obudzony"; dzięki temu proces zablokowany podczas wykonywania funkcji systemowej nie zużywa czasu procesora; w systemie może istnieć bardzo wiele procesów w stanie uśpienia lecz nie ma to wpływu na szybkość działania pozostałych procesów !.

    int write( int filedes, void *buffer, int nbytes );

  • działa podobnie do read(), z ta różnicą, że pisze do pliku zamiast czytać
  • int dup( int filedes );
    int dup2( int filedes, int new );

  • funkcje "dup()" i "dup2()" służą do powielania deskryptorów (otwartych)
  • funkcja "dup()" zwraca nowy deskryptor, który jest związany z plikiem dostępnym poprzez deskryptor "filedes"; będzie to pierwszy wolny deskryptor w tablicy deskryptorów procesu; ten sam plik będzie dostępny poprzez dwa różne deskryptory; uwaga: będą one używać tej samej pozycji w tablicy plików co oznacza że zawsze będą miały tę samą bieżącą pozycję !
  • funkcja "dup2()" zapewnia, że tym nowym deskryptorem będzie "new"; pozwala ona dokonać tzw przeadresowania w następujący sposób:
  • off_t lseek ( int filedes, off_t offset, int whence );
  • ustawianie "bieżącej pozycji" w pliku
  • parametry:
  •     offset:
            o ile przesunąć bieżącą pozycję
        whence: 
            SEEK_SET - względem początku pliku 
            SEEK_CUR - względem bieżącej pozycji 
            SEEK_END - względem końca pliku
  • jedyna różnica w porównaniu z DOS-em: można ustawiać bieżąca pozycje daleko poza końcem pliku i pisać, "dziura" zostanie wypełniona zerami; lseek() "samo w sobie" nigdy nie zwiększa rozmiaru pliku

  •  

    Zadanie 38


    Przerób przykład fork02.cc tak aby program "env" zamiast na terminal pisał do pliku "plik.txt", czyli dokonaj odpowiedniego przeadresowania przed wywołaniem exec(); przypomnij sobie opis funkcji exec().
    UWAGA: Pozostałe napisy wyświetlane przez fork02.cc NIE maja się znaleźć w tym pliku !.

    Zadanie 39


    W przykładzie  open01.cc (+unix.h) pokazuje się, jaka jest różnica między:
    a) otwarciem pliku i powieleniem deskryptora przy pomocy funkcji "dup()" lub "dup2()";
    b) dwukrotnym otwarciem tego samego pliku funkcją "open()"
    Wyjaśnij działanie tego programu (dlaczego wyświetla takie a nie inne napisy).

     

    4. Sygnały;
    funkcja systemowa: signal().

    Do procesów mogą docierać tzw sygnały.  Mogą one być wysyłane przez system operacyjny lub przez inne procesy.  Sygnały informują o wystąpieniu jakiejś nieprawidłowości (SIG???), lub służą do likwidowania niepotrzebnych procesów (SIGKILL), lub informują o pewnych zdarzeniach (SIGINT informuje o naciśnięciu Ctrl-C).

    Sygnały docierające do procesu mogą być:

    1. przechwytywane
    2. ignorowane
    3. obsługiwane standardowo
    Określamy to przy pomocy fun. sys. "signal()" :

    void (*signal( int sig, void (*function) (int) )) (int);

  • przechwytywanie sygnału (czyli definiowanie procedury obsługi sygnału):
  • ignorowanie sygnału:
  • standardowa obsługa sygnału:
  • UWAGA:
    Funkcja signal() może działać różnie w zależności od wersji Unixa, np należy sprawdzić w manualu (man 2 signal) czy w procedurze obsługującej przerwanie nie powinno się umieścić kodu ponownie instalującego tę procedurę. Nowocześniejsza funkcja do definiowania obsługi sygnałów to sigaction().

    Co się dzieje z obsługą sygnałów po wykonaniu "fork()" ?

    Co się dzieje z obsługą sygnałów po wykonaniu "exec*()" ?

    Zadanie 40


    Napisz mini-powłokę zawierającą następujące elementy:

    1. uruchamianie komend (programów) z parametrami
              ls -l -d kat1
    2. uruchamianie procesów drugoplanowych
              sleep 10 &
    3. przeadresowywanie
              ls -l kat1 >plik.txt
              cat <plik_we.txt >plik_wy.txt
    4. prawidłowe działanie Ctrl+C

    Możesz wykorzystać pliki mini01.cc i mini02.cc (+unix.h) w których jest większość kodu (należy go wyciągnąć z komentarza). Powłoka powinna wyświetlać prompt "$" i w ogóle powinna się zachowywać jak typowa powłoka Unixowa ...
    Wskazówki:
    ad 1: wprowadzając komendę trzeba między wszystkie słowa wstawiać spacje; można zakończyć mini-powłokę klawiszami Ctrl+D;
    ad 2: mini-powłoka powinna czekać na zakończenie procesu pierwszoplanowego, a nie drugoplanowego który się wcześniej skończył (ten element mini-)
    ad 4: Ctrl-C powoduje wysłanie sygnału SIGINT; standardowa obsługa sygnału SIGINT to zakończenie procesu; Ctrl-C powinno kończyć proces pierwszoplanowy, natomiast nie powinno kończyć procesów drugoplanowych oraz samej mini-powłoki.
    Do sprawozdania wstaw funkcję main() oraz wydruki eksperymentów stwierdzające poprawność działania mini-powłoki:

    ls >plik1.txt
    cat <plik1.txt >plik2.txt
    cat <plik2.txt

    sleep 100
    Ctrl+C
    ps
    sleep 50 &
    sleep 100 &
    ps
    sleep 5
    ps

     

    5. Łącza.

    Łącze to kanał komunikacyjny pozwalający procesom komunikować się (wysyłać ciąg bajtów).
  • Każde łącze posiada dwie "końcówki": jedną do zapisu, drugą do odczytu.
  • Końcówki są dostępne poprzez deskryptory; zapis i odczyt wykonuje się przy pomocy funkcji  read() i write(), zupełnie tak samo jak w przypadku zwykłych plików.
  • Łącze posiada bufor; jeśli bufor jest pusty/pełny to proces chcący czytać/pisać musi zostac "powstrzymany" (wtedy funkcje read() lub write() będą blokować).


  •  
     

    5.1. Łącza nienazwane;
    funkcja systemowa: pipe().

    Służą one do komunikacji między spokrewnionymi procesami.   Do tworzenia "łącz nienazwanych" służy fun. sys. pipe() :

    int pipe ( int filedes[2] );
    --> funkcja ta tworzy łącze i przydziela dwa deskryptory, dające dostęp do jego końcówek, które umieszcza w podanej przez parametr tablicy 2 integer-ów:

    --> jesli czytamy funkcją "read()" z łącza, ktorego NIKT nie ma otwartego do zapisu, to "read()" zwraca 0 (zupelnie tak, jakby plik się skonczył !).

    --> jeśli ktoś ma łącze otwarte do zapisu, lecz jest ono PUSTE - to fun. "read()" blokuje, tak długo aż nie pojawią się jakies znaki. Jeśli w pewnym momencie okaże się, ze nie ma procesow, ktore maja to łącze otwarte do zapisu to fun. "read()" przestanie blokować i zwroci 0.

    --> jeśli piszemy funkcją "write()" do łącza, ktorego NIKT nie ma otwartego do czytania, to jest wysylany sygnal SIGPIPE do procesu wywołującego write(), a sama funkcja "write()" kończy dzialanie z błędem.

    --> jesli ktoś ma łącze otwarte do czytania, lecz uległo ono PRZEPEŁNIENIU, to fun. "write()" bedzie blokować, o ile liczba zapisywanych danych jest <= od stałej PIPE_BUF (jeśli jest większa od PIPE_BUF to write() może zapisać mniej niż zażądaliśmy ! [wg książki Bacha, str 127]).  Jesli w pewnym momencie okaże się, że nie ma procesów, ktore maja to łącze otwarte do czytania to fun. "write()" zakonczy się błędem i do procesu wywołującego write() będzie wyslany SIGPIPE.

    Przykład użycia pipe() w programie, w języku C:

      struct {int do_czytania,do_pisania;} L;
        // struktura "L" pozwala na bardziej czytelny dostęp do końcówek łącza
        // niż tablica 2 integer-ów !
      pipe((int *)&L);

      write(L.do_pisania, "ABC", 3);
        // zapis do łącza

      char buf[10]; int i;
      i=read(L.do_czytania, buf, 10);
        // odczyt z łącza


    Zadanie 41


    Napisz program uruchamiający "cat" w procesie potomnym. Proces macierzysty ma się z nim komunikować przy pomocy 2 łączy. Proces macierzysty wysyła napisy do procesu potomnego i otrzymuje od niego "echo" (które wyświetla na terminalu). Gdy proces macierzysty zamknie łącze do zapisu to proces potomny powinien się automatycznie zakończyć. Przypominam że program "cat" przepisuje stdin na stdout.

     

    5.2. Potoki.

    --> co to jest potok ?
    Przez potok rozumiemy kilka procesów, np p1, p2, p3, p4, komunikujących się przez łącza
    miedzy parami procesow: p1->-p2, p2->-p3, p3->-p4. Informacja płynie więc w jedną
    stronę jak woda w potoku.

    Przyklad "potok.cc". Jest to program tworzący potok z komend podanych jako parametry, w którym NIE MOŻNA podawać parametrów komend. Sposób użycia:

       potok ls cat cat cat cat
    Przyklad "potok2.cc". Jest to program tworzący potok z komend podanych jako parametry, w którym MOŻNA podawać parametry komend. Sposób użycia:
       potok2 ls -l \| cat \| cat \| grep txt \| cat
    Zadanie 42

    Odpowiedz na pytania:
    1. jak wygląda "pokrewieństwo" procesów tworzonych przez "potok2.cc" ?
    2. jaką własność powinien mieć program uruchamiany "w środku" potoku  przy pomocy jednego z powyższych programów aby wszystko działało tak jak się tego spodziewamy ? (ma to związek z stdin i stdout czyli deskryptorami 0 i 1)
    3. jeśli wydamy polecenie "potok cat cat cat cat" to wprowadzane linie tekstu będą przepisywane na terminal; jeśli naciśniemy Ctrl-D to 4 procesy "cat" będą się prawidłowo i po kolei kończyć; dlaczego tak się dzieje ? - wyjaśnij to dokładnie
    4. co by się stało i dlaczego gdyby funkcja ZamknijLacza() była "pusta",

    5. po uruchomieniu
           potok2 cat \| cat \| cat
      i po wpisaniu kilku linii tekstu oraz naciśnięciu Ctrl-D ?
       
    "Ctrl-D" powoduje że terminal zachowuje się jak plik który się skończył;
    bardziej precyzyjnie: czytanie z terminala na którym naciśnięto Ctrl-D jest identyczne jak czytanie (sekwencyjne) z pliku zwykłego gdy bieżąca pozycja przekroczy koniec pliku; funkcja read() zwraca wtedy wartość 0
     
    Dlatego w punkcie 3 i 4 powyższego ćwiczenia pierwszy "cat" który czyta dane z terminala na pewno zakończy swoje działanie po naciśnięciu Ctrl-D.

    UWAGA: Ctrl-D nie powoduje wysłania sygnału jak to czyni Ctrl-C czy też Ctrl-Z !.

    Zadanie 43


    Do naszej mini-powłoki dodaj możliwość uruchamiania potoków :
        ls -l -a | cat | grep txt | sort | more
    Propozycja rozwiązania znajduje się w pliku mini03.cc, należy tylko wyciągnąć z komentarza co trzeba. W mini03.cc wykorzystuje się procedurę z programu "potok2.cc".
    Odpowiedz na pytanie: jakie procesy istnieją po wydaniu polecenia "cat | cat | cat" i jakie jest między nimi pokrewieństwo ?
    Do sprawozdania wstaw tylko wyniki eksperymentów:
  • cat | cat | cat
    # wpisz z klawiatury jakiś tekst + enter
    # i wszystkie wydruki generowane przez powłokę umieść w sprawozdaniu

    Ponadto pokaż wszystkie procesy utworzone przez mini-powłokę po uruchomieniu powyższego potoku (polecenie "ps -t terminal" wydane z innego terminala).

    Zadanie 44


    Do mini-powłoki w najprostszej wersji dodaj komendę wbudowaną "export" służącego do tworzenia zmiennych środowiska :

    Wskazówka:  Środowisko uruchamianego programu ma zawierać zmienne środowiska zdefiniowane w mini-powłoce poleceniem "export" ORAZ niektóre przydatne zmienne np HOME i PATH. Należy zadeklarować zmienną zewnętrzną:

        extern char **environ;
    oraz wykorzystać funkcję systemową execvp(), która używa tablicy environ aby utworzyć środowisko potomnego procesu. Element tablicy environ znajdujący się za ostatnim niepustym elementem powinien mieć przypisany NULL. Nie musimy nadmiernie dbać o wykrywanie błędów we wprowadzanych komendach. Wypróbuj działanie mini-powłoki uruchamiając program wyświetlający zmienne środowiska, np env. Do sprawozdania wstaw kod mini-powłoki oraz wyniki eksperymentów dowodzące że komenda export rzeczywiście działa.
     

    ............................................................................


    Zadanie 45 (*)


    "CYKL PROCESÓW"
    Napisać program "cykl", podobny do programów z serii "potok*", pozwalający uruchamiać programy "w cyklu". Sposób uruchomienia :
       cykl ile_proc prog -par1 -par2
  • Parametr "ile_proc" oznacza, że ten sam program "prog", ma działać w "ile_proc" procesach. Parametry "-par1" i "-par2" są przekazywane do wszystkich kopii "prog".
  • Programy mają się komunikować poprzez deskryptory "3" i "4" przeadresowane do odpowiednich łączy ("3" oznacza poprzedni proces, "4" oznacza następny proces w cyklu); patrz obrazek.
  • UWAGA: jedyny dopuszczalny sposób komunikacji między procesami to wysyłanie/odbieranie komunikatów do/od sąsiada (zabrania się używania semaforów itp ...),  jest to tzw "model obliczeń z przesyłaniem komunikatów" który jest przeciwieństwem "modelu obliczeń ze wspólną pamięcią".
  • Zakładamy, że połączenia w cyklu są jednokierunkowe (komunikaty mogą przebiegać tylko w jedną stronę).
  • Jeśli jeden z procesów "prog" się zakończy to wszystkie pozostałe procesy też powinny się zakończyć (należy wykorzystać mechanizmy automatycznego kończenia się potoków).
  • Zauważ, że program "cykl" tworzy jedynie infrastrukturę - cykl procesów, które mogą się komunikować (z bezpośrednimi sąsiadami) poprzez łącza. Procesy będą wykonywać program podany jako drugi parametr polecenia "cykl".
  • Jako przyklad zastosowania zrealizuj algorytm "Leader Election in Rings", opisany w pliku leaderel.ps (który należy czytać przegladarka PS), w punkcie 2.3.1 na stronie 15.  Realizacja tego algorytmu powinna być programem o nazwie "leaderel", który będzie można przetestować następująco:
  •    cykl 20 leaderel
    Każdy proces "leaderel" powinien tuż przed swoim zakończeniem wyświetlać PID oraz informacje czy został liderem czy nie.
    Wskazówki:
  • przy przeadresowywaniu końcówki łącza na 3 lub 4, weź pod uwagę że końcówka innego, wcześniej utworzonego łącza może mieć deskryptor 3 lub 4; ten problem trzeba jakoś rozwiązać !!!
  • przemyśl sposób w jaki program "cykl" kończy działanie; czy na pewno wszystkie tworzone przez niego procesy się kończą ?
  • komunikaty algorytmu wyboru lidera zawieraja binarna reprezentację liczb (identyfikatorow); zazwyczaj typ int jest reprezentowany na 4 bajtach; można przyjąć że takie komunikaty są "niepodzielne" tj jeśli do łącza zapisano 4 bajty wywołując raz funkcję write() to te 4 bajty zostaną odczytane w całości pojedynczym wywołaniem  funkcji read() - (?)
  • komunikaty diagnostyczne wyświetlane przy pomocy printf() kończ zawsze przez "\n", np printf("ok\n"); chodzi o wypróżnienie bufora tworzonego przez język C
  • dobrym zwyczajem jest sprawdzanie czy funkcje systemowe nie kończą się błędem (zwracają (-1) i ustawiają odpowiednio zmienną errno, opis błędu można wyświetlić funkcją perror())


  •   
     

    Różne wnioski:

  • Jak mogliśmy się przekonać, program czytający dane przy pomocy deskryptora "d1" i wyświetlający wyniki działania przy pomocy deskryptora "d2", może bez żadnych zmian wspólpracować z terminalami, z łączami czy też z plikami zwykłymi (dzięki możliwości "przeadresowywania" tych deskryptorów); jest to bardzo eleganckie rozwiązanie, oszczędzające pracę programistów ...
  • Warto pamiętać że "prawa" rządzące łączami rządzą także innymi narzędziami do przesyłania strumienia bajtów (takimi jak np gniazda BSD).
  • Zauważmy że łącze to gotowe rozwiązanie jednej z wersji problemu "producenta & konsumenta" (jednak tutaj jest możliwość produkcji i konsumpcji więcej niż jednego elementu !).

  •  

    Zadanie 46 (*)


    Napisz program "testcyklu", który może być uruchamiany w przy pomocy programu "cykl" z powyższego zadania, a którego celem jest zbadanie czasu przebiegu komunikatu przez cykl. Program "testcyklu" powinien zawierac procedurę JestemLiderem() zwracająca 1 w procesie który został wybrany jako lider, a 0 w pozostałych procesach. Program "testcyklu" powinien najpierw wywołać powyższą procedurę, a następnie lider powinien 50-krotnie wysłać komunikat kontrolny do 4 i odebrac go z 3 [sprawdzając czy nie wystąpił jakiś błąd]. Czas powyższej operacji powinien być mierzony w następujący sposób: Program należy przetestować następująco: Czy czas przebiegu komunikatu zależy liniowo od długości cyklu ?