SOP322/ćw - temat C

Klasyczne IPC.

IPC: literatura.

Literatura na temat IPC (=Inter Process Communication):
  • Mechanizmy IPC są opisane w książce Bacha, rozdział 11, strony 388-412 (literatura); są tam także uwagi na temat sposobu realizacji tych mechanizmów.
  • Jest także dostępny opis: 6.4 "System V IPC".
  • Oczywiście, można także korzystać z manuala: (man msgget, man msqid_ds, itp).
  • The Linux Programmer's Guide (po polsku)

  • IPC: kolejki komunikatów; msg*().

    Można utworzyć kolejkę komunikatów, identyfikowaną przy pomocy klucza (jest to liczba całkowita), dostępną w obrębie pojedynczej maszyny.

    Jeśli podasz klucz, to fukcja msgget() utworzy nową kolejkę, lub znajdzie istniejącą (o podanym kluczu). Zwracany przez tę funkcję id_kolejki to identyfikator, którego używamy podczas dalszych odwołań do kolejki.
    Analogia z systemem plików:

    Procesy mogą wstawiać komunikaty do kolejki, oraz je odczytywać przy pomocy funkcji msgsnd() i msgrcv(). Każdy komunikat posiada typ (jest to liczba całkowita). Podczas odczytywania komunikatów z kolejki można zażyczyć sobie komunikatów określonego typu.

    Istnieją prawa dostępu do kolejki, takie same jak prawa do plików.  Z każdą kolejką są związane 2 osoby: twórca kolejki i użytkownik kolejki, a także ich grupy.  Standardowo twórca i użytkownik to ta sama osoba (można to zmienić przy pomocy msgctl()).  Spośród 3 praw unixowych, liczą się tylko "r" i "w", którch interpretacja jest oczywista. Niektóre operacje (te wykonywane funkcją msgctl()) może wykonywać tylko twórca.

    Usuwanie kolejki, odczytywanie rozmaitych informacji dotyczących kolejki, a także zmianę pewnych parametrów kolejki, wykonuje się przy pomocy funkcji msgctl().

    UWAGA: Obiekty IPC (kolejki komunikatów i inne) mogą istnieć także wtedy gdy nie używa ich żaden proces !. Dlatego należy je starannie usuwać gdy nie są już potrzebne. Istnieje polecenia pozwalające zobaczyć istniejące obiekty, i usunąć je:

         ipcs
         ipcrm
    Funkcje systemowe do obslugi kolejek komunikatow:
     
    int id_kolejki = msgget (key_t klucz, int opcje);
    <-- tworzenie lub otwieranie kolejki

    Przykłady:
    Otwieranie istniejącej kolejki o podanym kluczu:

         #define KLUCZ 12345
         int id_kolejki= msgget(KLUCZ, 0);
           // jeśli nie ma takiej kolejki to wystąpi błąd !
    Otwieranie istniejącej kolejki o podanym kluczu, lub tworzenie nowej z prawami dostępu 0600:
         #define KLUCZ 12345
         int id_kolejki= msgget(KLUCZ, IPC_CREAT|0600);
           // jeśli robią to równocześnie 2 procesy, to jeden
           // z nich utworzy kolejkę, a drugi tylko ją otworzy !
    Tworzenie nowej kolejki z prawami dostępu 0600:
         #define KLUCZ 12345
         int id_kolejki= msgget(KLUCZ, IPC_CREAT|IPC_EXCL|0600);
           // jeśli jest kolejka o podanym kluczu to wystąpi błąd !
    int msgsnd (int id_kolejki, void* wskNaBuf, size_t rozmiarDanych, int opcje);
    <-- wysyłanie komunikatów

    Przykłady:

         struct {
           long mtype; // typ komunikatu
           char mtext[50]; // dane komunikatu (dowolna długość i interpretacja)
         } buf;

         buf.mtype=123;
         sprintf(buf.mtext,"Tra la la !!!");

         j=msgsnd(id_kolejki, &buf, 50, 0);
            /*
               ostatni parametr to "opcje";
               gdyby podac IPC_NOWAIT, to w razie gdyby wysłanie
               komunikatu było niemożliwe, funkcje nie będzie blokować
               tylko od razu zwróci błąd
            */
         if(j==-1) perror("msgsnd");
    int msgrcv (int id_kolejki, void* wskNaBuf, size_t rozmiarDanych, long oczekiwanyTyp, int opcje);
    <-- odbieranie komunikatów

    Interpretacja parametru oczekiwanyTyp:

  • oczekiwanyTyp==0; wybierz pierwszy komunikat z kolejki
  • oczekiwanyTyp>0; wybierz komunikat podanego typu
  • oczekiwanyTyp<0; wybierz komunikat z najmniejszym typem, który musi być <= |oczekiwanyTyp|.
  • Przykłady:

         struct {
           long mtype; // typ komunikatu
           char mtext[50]; // dane komunikatu
         } buf;

         j=msgrcv(id_kolejki, &buf, 50, 123, 0);
            /*
               oczekujemy na komunikat typu 123;        
               trzeci parametr to rozmiar bufora NIE LICZĄC zmiennej mtype !;
               ostatni parametr to "opcje":
                 gdyby podac IPC_NOWAIT, to w razie gdyby odebranie
                 komunikatu było niemożliwe, funkcje nie będzie blokować
                 tylko od razu zwróci błąd
            */
         if(j==-1) perror("msgrcv");

         sprintf("otrzymalem komunikat: %s\n", buf.mtext);
    int msgctl (int id_kolejki, int komenda, struct msqid_ds* buf)
    <-- kontrola nad kolejka

    Przykłady:

         j=msgctl(id_kolejki, IPC_RMID, NULL);
            //
            // usuwanie kolejki
            //

         if(j==-1) perror("msgctl");

    Prosty przykład znajduje się w pliku msg01.cc (+unix.h).

    Drugi przykład to dwa programy msg02k.cc, msg02p.cc (+unix.h).
    Tutaj kolejka jest używana przez procesy niespokrewnione. Są to: producent i konsument. Można zaobserwować, że jeśli uruchomimy tylko jeden z programów, to zasoby kolejki prędko zostaną wyczerpane (lub kolejka będzie pusta). W obu przypadkach proces wykonujący operacje na kolejce zostanie uśpiony. Jądro obudzi go dopiero gdy stanie się możliwe kontynuowanie pracy z kolejką. W przykładzie tym jest dodatkowa trudność: klient musi wiedzieć, że producent zakończył definitywnie pracę, stąd potrzeba specjalnego "protokołu zakończenia" !.


    Zadanie 51
    Zaprogramuj serwery usług używające (pojedynczej) kolejki komunikatów do komunikacji z klientami.
  • Klient wstawia do kolejki komunikat zawierający: nr usługi jako typ komunikatu oraz pid klienta umieszczony w danych komunikatu.
  • Serwer odsyła klientowi komunikat, poprzez tą samą kolejkę. Dane komunikatu to odpowiedź dla klienta. Jako typ komunikatu używa się pid-a klienta. W ten sposób każdy klient może się wybierać z kolejki komunikaty dla niego przeznaczone (numery usług to 1,2,3 dlatego nie ma konfliktu z PID-ami klientów).
  • W danej chwili może być kilka usług, używających tej samej kolejki komunikatów. Każda usługa jest realizowana poprzez osobny proces (osobny program). Może też być kilku klientów, używających wszystkich usług w dowolnej kolejności. Klienci powinii informować o wszystkich akcjach jakie podejmują, np:
  •    klient; pid=123; usługa 1; wysyłam sfdgsfg
       klient; pid=123; usługa 1; odebrałem s.f.d.g.s.f.g.
       klient; pid=123; usługa 2; wysyłam sfdgsfg
       klient; pid=123; usługa 2; odebrałem SFDGSFG
  • Usługi: (nr 1) wstawianie kropki miedzy literami komunikatu; (nr 2) zamiana liter na duże.
  • Uruchamianie:
  •    serwer1 &
       serwer2 &
       klient &
       klient &
       klient &
     
     

     

    IPC: pamięc dzielona; shm*().

    Przy pomocy funkcji shmget() uzyskuje się dostęp do segmentu dzielonej pamięci. Obowiązują takie same zasady jak w przypadku kolejek komunikatów. Segment jest identyfikowany poprzez KLUCZ. Jeśli segment pamięci ma być tworzony to podaje się jego rozmiar:
        #define KLUCZ 12345
        int id= shmget(KLUCZ, 100, IPC_CREAT|0600);
          // tworzenie segmentu pamięci o rozmiarze 100 bajtów
        if(id==-1) perror("shmget");
    Jeśli "otwieramy" istniejący segment pamięci to wystarczy:
        int id= shmget(KLUCZ, 0, 0);
        if(id==-1) perror("shmget");
    Segment dołącza się do wirtualnej przestrzeni adresowej procesu, przy pomocy funkcji shmat():
        char *c= (char*)shmat(id, 0, 0);
        if(c==(char*)-1) perror("shmat");
    Ten sam segment może być dołączony do różnych procesów jak i kilka razy do tego samego procesu !.

    Segment odłącza się przy pomocy funkcji shmdt():

        i= shmdt(c); // UWAGA: podaje się "adres" a nie "id" !!!
        if(i==-1) perror("shmdt");
    Po odłączeniu segmentu (nawet od wszystkich procesów), segment ten NADAL istnieje !. Musi być jawnie usunięty funkcją shmctl():
        i=shmctl(id, IPC_RMID, NULL);
        if(i==-1) perror("shmctl");
    Prosty przykład znajduje się w pliku "shm01.cc"(+"unix.h").

    Drugi przykład znajduje się w pliku "shm02.cc"(+"unix.h"). Jest to znany przykład z dwoma kontami, konto1 i konto2, między którymi procesy dokonują losowych przelewów. Suma obu kont się zmienia (co świadczy o błędzie), gdyż nie używamy tutaj żadnych mechanizmów do synchronizacji procesów, takich jak semafory.
     


    IPC: semafory; sem*().

    Mamy tu do czynienia nie z pojedynczymi semaforami, lecz ze zbiorami semaforów. Można wykonywać "zbiorowe" operacje na dowolnym podzbiorze zbioru semaforów. Podczas wykonywania operacji zbiorowej, jeśli choć jedna operacja wymaga blokowania (uśpienia procesu), to cała operacja zbiorowa będzie blokować. Zbiorowe modyfikacje wartości semaforów są wykonywane albo dla wszystkich semaforów albo dla żadnego.

    Zbiór semaforów tworzymy przy pomocy funkcji semget(). KLUCZ identyfikuje zbiór semaforów.

        #define KLUCZ 12345
        int id_sem= semget(KLUCZ, 5, IPC_CREAT|IPC_EXCL|0666);
           // drugi parametr to liczba semaforów w zbiorze.
        if(id_sem==-1) perror("semget");

    Istniejący zbiór semaforów "otwieramy" tak:

        int id_sem= semget(KLUCZ, 0, 0);

    Wykonywanie operacji na semaforach (takich jak podnoszenie i opuszczanie) :

        struct sembuf buf;
        buf.sem_num=0;
           // nr semafora w zbiorze, na którym chcemy wykonać operację
        buf.sem_op=-1;
           // opuszczanie semafora (wait)
        buf.sem_flg=0;
           /* można podać flagi IPC_NOWAIT, SEM_UNDO;
              ta ostatnia powoduje, że przed "błędnym"(?) zakończeniem procesu
              zostaną anulowane modyfikacje semafora
           */
        i=semop(id_sem, &buf, 1);
           // ostatni parametr to rozmiar tablicy struktur "sembuf";
           // można wykonywać kilka operacji na różnych semaforach
           // jednocześnie (tzw "operacja zbiorowa")
        if(i==-1) perror("semop(-1)");

        buf.sem_num=0;
        buf.sem_op=+1;
           // podnoszenie semafora (signal)
        buf.sem_flg=0;
        i=semop(id_sem, &buf, 1);
        if(i==-1) perror("semop(+1)");
    Inicjowanie semafora przy pomocy semctl():
        union semun { 
    // UWAGA: te unię trzeba zdefiniować samodzielnie (?!?!)
    // to dotyczy tylko niektórych wersji Linuxa ...
          int val;
          struct semid_ds *buf;
          unsigned short int *array;
          struct seminfo *__buf;
        };
        union semun arg;
        arg.val=0;
           // początkowa wartość semafora
        j=semctl(id_sem, 0, SETVAL, arg);
           // drugi argument to nr semafora, który chcemy zainicjować
        if(j==-1) perror("semctl(SETVAL)");
    Usuwanie zbioru semaforów przy pomocy semctl():
        i=semctl(id_sem, 0, IPC_RMID, NULL);
        if(i==-1) perror("semctl(IPC_RMID)");
    Odczytywanie ilości procesów oczekujących na podniesienie semafora:
        i=semctl(id_sem, 0, GETNCNT, NULL);
        if(i==-1) perror("semctl(GETNCNT)");
        printf("ilości procesów = %i\n", i);
    UWAGA: Oprócz podnoszenia i opuszczania, na semaforach można wykonywać także operacje: Dokumentacja z manuala: "semget.txt", "semop.txt", "semctl.txt", "semid_ds.txt".

    W pliku "sem01.cc"(+"unix.h") znajduje sie to samo co w pliku "shm02.cc" (przelewy między kontami), jednak tym razem operacje przelewu są chronione semaforami.

    Zadanie 54
    Zaimplementuj łącza przy pomocy wspólnej pamięci (shm*) oraz semaforów (sem*).
    Wskazówki:
  • Zauważ, że tego samego klucza można używać do identyfikowania zbioru semaforów i segmentu pamięci dzielonej, tak więc łącze będzie identyfikowane przez pojedyńczy klucz.  Zaimplementuj następujące funkcje:
  • W klasycznym problemie producenta/konsumenta, wszystkie procesy wstawiają/pobierają po jednym elemencie. Zauważ, że w tym zadaniu jest inna sytuacja: proces może zażądać określonej (>=1)  liczby elementów, a także może zapisywać większą od 1 liczbę elementów. (Być może rozsądnie byłoby najpierw rozwiązać ten problem w języku BACI ?).  Zwróć też uwagę, że semafory Unixa są nieco ogólniejsze od tych z języka BACI !.
  • Możesz przyjąć zasadę, nieco odmienną od występującej w prawdziwych łączach, że podczas czytania z łącza proces jest blokowany tak długo, aż nie otrzyma żądanej liczby elementów.
  • Nieco bardziej ambitne byłoby dokładne naśladowanie łączy Unix-owych: podczas czytania z łącza, funkcja zwraca liczbę dostępnych elementów (być może mniejszą niż zażądaliśmy)
  •  

     

    Odwzorowywanie pliku w pamięć; mmap().

    Przypuśćmy, że chcemy wykonać złożoną operacje na fragmencie pliku. Możemy odwzorować ten fragment pliku w pamięć, a następnie wykonywać operacje na pamięci.

    Dodatkowo, jeśli użyliśmy flagi MAP_SHARED, to modyfikacje są natychmiast(?) widoczne dla innych procesów, które otwarły ten plik (i być możę odwzorowały go w pamięć).

    UWAGA: Ten mechanizm może być używany do komunikacji między procesami. Rolę "kluczy" pełnią w nim pliki. Jest to nawet bardziej zgodne z filozofią UNIX-a, w której "wszystko jest plikiem" !.

    void *mmap (void *addr, size_t len, int prot, int flags, int filedes, off_t off);
    <-- odwzorowywanie pliku w pamięć

    Przykłady:

        int d=open("plik.txt", O_RDWR); // otwieramy plik
        if(d==-1) perror("open");

        struct DanePersonalne *c=mmap(0, sizeof(struct DanePersonalne),
            PROT_READ|PROT_WRITE, MAP_FILE|MAP_SHARED, d, 0);
              /* -> funkcja mmap() dokonuje "odwzorowania" fragmentu pliku w pamięć
                 -> wartość 0 pierwszego parametru oznacza, że system ma sam wybrać 
                    adres tworzonego segmentu pamięci (?)
                 -> drugi parametr to długość segmentu pamięci i fragmentu pliku
                 -> ostatni parametr to "offset" w pliku
              */
        if(c==(char *)-1) perror("mmap");

        close(d);

        // możemy modyfikować fragment pliku
        // za pośrednictwem wskazania "c"
        //
        // inne procesy będą widziały nasze modyfikacje
        // (dzięki MAP_FILE|MAP_SHARED !)
        //
        sprintf(c->Imie, "Jan");
        sprintf(c->Nazwisko, "Kowalski");
        printf("Adres=%s\n", c->Adres);
    Dokumentacja z manuala: "mmap.txt". Zobacz także:
       man munmap
       man msync
    UWAGA: funkcja mmap() Linuxa jest nieco uboższa od opisanej w pliku "mmap.txt" !.

    Prosty przykład znajduje się w pliku mmap01.cc(+unix.h). PRZED jego uruchomieniem utwórz plik "mmap01.txt" przy pomocy polecenia:

         echo "0123456789" > mmap01.txt
    W przykładzie tym jeden proces zapisuje fragment pliku odwzorowany w pamięć, a drugi go odczytuje (oba procesy modyfikują pamięć - nie wykonują żadnych operacji na plikach !).
     

    Zadanie 57 (*)
    Napisz program "leaderel2" implementujący bardziej wyrafinowany [używający O(n log n) komunikatów] algorytm wyboru lidera w cyklu (patrz zadanie 45 oraz artykuł leaderel.ps, str 16, pkt 2.3.2). Porównaj liczbę komunikatów używanych przez programy "leaderel" z zadania 14 i "leaderel2". W tym celu musisz zaprogramowac specjalną wersję programu "cykl" o nazwie "cykl2" która podaje łączną liczbę komunikatów wysłanych w czasie działania algorytmu (a dokładniej łączną długość wysłanych komunikatów).
    Uwaga: identyfikatory procesów powinny być nadawane losowo i przekazywane przez parametr; nie należy używać PID-ów gdyż wtedy kolejne procesy w cyklu mają jako identyfikatory kolejne liczby i ma to wpływ na działanie algorytmów ...
     
    Zadanie 58 (*)
    Zaimplementuj mechanizm blokowania fragmentu pliku używając mechanizmów niskiego poziomu.
    (NIE WOLNO używać fun sys Unixa które służą do tego celu !).
    Należy dostarczyć własne wersje funkcji read() i write() oraz funkcje do zakładania blokady i jej zdejmowania. Powinny być 2 sposoby blokowania: "wyłączny" i "tylko do odczytu" (jeśli zablokujemy fragment pliku to sami mamy do niego pełny dostęp, natomiast inne procesy nie mają dostępu lub mogą tylko czytać). Informacje o pliku można uzyskać przy pomocy funkcji systemowej stat(); można m.in. odczytać nr i-węzła pliku, który jednoznacznie identyfikuje plik.