Wykład 4 - Instytut Informatyki Teoretycznej i Stosowanej
Transkrypt
Wykład 4 - Instytut Informatyki Teoretycznej i Stosowanej
Oprogramowanie systemów równoległych i rozproszonych Wykład 4 Dr inż. Tomasz Olas [email protected] Instytut Informatyki Teoretycznej i Stosowanej Politechnika Cz˛estochowska Wykład 4 – p. 1/7 Proces i pamie˛ ć systemowa W systemie Unix procesem staje sie˛ program wykonywalny, który zostaje wczytany do pamieci ˛ systemowej przez jadro, ˛ a nastepnie ˛ uruchomiony. Pamieć ˛ systemowa może zostać podzielona na: obszar użytkowy - w którym uruchomione sa˛ procesy użytkowników, obszar jadra ˛ - pozostaje do dyspozycji jadra ˛ i jego procesów. Procesy użytkowników moga˛ uzyskiwać dostep ˛ do jadra ˛ wyłacznie ˛ za pośrednictwem wywołań systemowych. Wykład 4 – p. 2/7 Reprezentacja w systemie operacyjnym W systemie operacyjnym proces reprezentowany jest poprzez strukture˛ Proces Control Block (Blok kontrolny procesu). kod programu w pamieci ˛ (obszar programu i danych), otwarte pliki (identyfikowane przez deskryptory), środowisko (zestaw zmiennych i przypisanych im wartości), funkcja main() wywołana przez procedure˛ startowa˛ z podanymi argumentami (argv[]) i zmiennymi środowiskowymi (getenv(), putenv(), extern char **environ) zakończenie procesu unixowego może być: normalne, funkcje: return, exit(atexit), _exit anormalne, funkcja abort lub otrzymanie odpowiedniego sygnału Wykład 4 – p. 3/7 Przestrzeń adresowa procesu Pamieć ˛ procesu jest podzielona na kilka bloków: code segment (segment kodu) Obszar w którym znajduja˛ sie ˛ wykonywalne instrukcje programu data segment (segment danych) initialized data (dane zainicjowane) Dane które sa˛ przechowywane w pliku wykonywalnym w sekcji „Data” BSS (block started by symbol) Obszar danych domyślnie inicjowanych wartościa˛ 0 (nie sa˛ przechowywane w pliku wykonywalnym) heap (sterta) Pamieć ˛ zarezerwowana np. przez malloc, calloc, new stack segment (stos) Lokalne zmienne, parametry funkcji. Wykład 4 – p. 4/7 Parametry procesu W bloku kontrolnym procesu zapisane sa˛ miedzy ˛ innymi nastepuj ˛ ace ˛ parametry charakteryzujace ˛ proces: PID Process ID Identyfikator Procesu UID User ID Identyfikator Użytkownika Wywołujacego ˛ Proces GID Group ID Identyfikator Grupy do jakiej należy Proces SID Session ID Identyfikator Sesji (zwiazany ˛ z terminalem sterujacym) ˛ Wykład 4 – p. 5/7 Środowisko przetwarzania getpid - umożliwia uzyskanie identyfikatora procesu, std::cout << "moj PID: " << getpid() << std::endl; identyfikator procesu macierzystego - getppid(): std::cout << "identyfikator przodka: " << getppid() << std::endl; identyfikator użytkownika - getuid(), efektywny identyfikator użytkownika - geteuid(), identyfikator grupy - getgid(), efektywny identyfikator grupy - getegid(). Wykład 4 – p. 6/7 Stany procesu wykonywalny (runnable) - proces w kolejce procesów gotowych do wykonania, uśpiony (sleeping) - proces czeka na wystapienie ˛ określonego „zdarzenia” (np na dane do przeczytania, na sygnał, na dostep ˛ do zasobu lub dobrowolnie „śpi” na określony okres), zatrzymany (stopped) - proces wykonywany lecz zatrzymany na skutek otrzymania sygnału SIGSTOP lub SIGTSTP, wymieciony (swapped out - proces usuniety ˛ okresowo z kolejki procesów gotowych do wykonywania wskutek działania algorytmu obsługi pamieci ˛ wirtualnej, zombie - proces czeka na zakończenie. Wykład 4 – p. 7/7 Zmienne środowiskowe Każdy program otrzymuje dostep ˛ do zmiennych środowiskowych poprzez tablice˛ wskaźników extern char **environ Przykładowe funkcje: char *getenv(const char *name); pobiera wartość zmiennej int setenv(const char *name, const char *value, int rewrite); ustawia wartość zmiennej, rewrite określa czy funkcja ma nadpisać istniejac ˛ a˛ zmienna˛ Wykład 4 – p. 8/7 Obsługa błedów ˛ W wiekszości ˛ przypadków funkcje systemowe, które kończa˛ sie˛ błedem ˛ zwracaja˛ wartość -1 i przypisuja˛ zmiennej errno wartość wskazujac ˛ a˛ rodzaj błedu. ˛ Kody błedów ˛ - plik nagłówkowy <sys/errno.h> Odpowiedni komunikat można uzyskać przy pomocy funkcji perror: #include <stdio.h> void perror(const char *s); const char *sys_errlist[]; int sys_nerr; int errno; lub poprzez wywołanie funkcji: char *strerror(int errnum); int strerror_r(int errnum, char *buf, size_t n); Wykład 4 – p. 9/7 Obsługa błedów ˛ - przykład #include <iostream> #include <errno.h> int main(int argc, char** argv) { char buffer[255]; int nchar = read(158, buffer, 255); if (nchar == -1) { std::cerr << "Wystapil blad - [" << strerror(errno) << "] o numerze " << errno << std::endl; perror(argv[0]); exit(1); } } Wynik: Wystapil blad - [Bad file descriptor] o numerze 9 Numer bledu: 9 ./error: Bad file descriptor Wykład 4 – p. 10/7 Argumenty wywołania programu Do obsługi argumentów podawanych przy wywołaniu programu została utworzona funkcja getopt: #include <stdlib.h> int getopt(int argc, char** argv, char* optstring); extern char* optarg; extern int optind, opterr; argc - liczba argumentów (liczba łańcuchów wystepuj ˛ aca ˛ w tablicy optstring), argv - tablica z przekazanymi argumentami, optstring - opis opcji (przełaczników ˛ programu), jeżeli opcja posiada dodatkowy argument, to za litera˛ powinien pojawić sie˛ znak dwukropka. Wykład 4 – p. 11/7 getopt - wynik działania Funkcja getopt zwraca jedna˛ z trzech wartości całkowitych: Liczbe˛ -1, która oznacza, że wszystkie opcje zostały już odczytane albo że natrafiono na pierwszy argument nie bed ˛ acy ˛ opcja. ˛ Ekwiwalent znaku „?”, który oznacza, że została odczytana litera opcji nie majaca ˛ odpowiednika w zmiennej optstring, Kolejna˛ litere˛ opcji z tablicy argv, która odpowiada literze ze zmiennej optstring. Jeżeli po literze dopasowanej do optstring znajduje sie˛ dwukropek, to wtedy zewnetrzny ˛ wskaźnik na znak optrarg bedzie ˛ odsyłał do wartości argumentu. Wykład 4 – p. 12/7 getopt - przykład (I) void PrintUsage(std::ostream &os) { os << "opt:\n" << " Uzycie: opt [-o output_file] [-hv] input_file\n" << " OPCJE\n" << " -o output_file: zapisuje wyniki do pliku o nazwie output_file\ << " -v: wypisuje dodatkowe informacje o wykonywaniu programu\n" << " -h: wypisuje powyzsza informacje\n" << " input_file: plik wejściowy\n" << std::flush; } int main(int argc, char** argv) { bool verbose = false; // dodatkowe informacje o wykonywaniu programu bool usage = false; // informacje o opcjach programu std::string outFileName; extern extern extern static opterr int c; char* optarg; int optind; int opterr; char optstring[] = "vho:"; = 0; Wykład 4 – p. 13/7 getopt - przykład (II) while ((c = getopt(argc, argv, optstring)) != -1) { switch (c) { case ’v’: verbose = true; break; case ’h’: usage = true; PrintUsage(std::cerr); return 0; case ’o’: outFileName = std::string(optarg); break; case ’?’: std::cerr << "Bledna opcja" << std::endl; exit(1); break; } } Wykład 4 – p. 14/7 getopt - przykład (III) if (verbose) std::cout << "output file name: " << outFileName << std::endl; std::string inputFileName = argv[optind]; std::cout << "input file name: " << inputFileName << std::endl; return 0; } Wykład 4 – p. 15/7 Tworzenie procesu Do klonowania procesów służy funkcja fork: #include <sys/types.h> #include <unistd.h> pid_t fork(void); W wyniku działania funkcji fork jest utworzenie procesu potomka, który jest prawie dokładna˛ kopia˛ rodzica (zasoby pamieci ˛ sa˛ duplikowane, różni sie˛ od procesu macierzystego identyfikatorem procesu). Proces macierzysty, który wywołał funkcje fork dostaje identyfikator dziecka. Proces potomny dostaje 0. Jeżeli działanie funkcji zakończyło sie˛ błedem ˛ zwracana jest wartość -1. Wykład 4 – p. 16/7 Kończenie pracy procesu W chwili śmierci procesu potomnego jego rodzic otrzymuje sygnał SIGCLD (domyślnie ignorowany). Standardowo proces rodzica powinien odczytać kod zakończenia potomka wywołujac ˛ funkcje˛ systemowa˛ wait(): pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options); Gdy proces nadrz˛edny żyje w chwili zakończenia pracy potomka, i nie wywołuje funkcji wait(), to potomek zostaje w stanie zombie. Jeżeli proces ustawi sygnał SIGCHLD na SIG_IGN oznacza to, proces macierzysty nie jest „zainteresowany” stanem końcowym swoich procesów potomnych. W takim wypadku jadro ˛ bedzie ˛ wykonywać funkcje wait() dla wszystkich jego procesów potomnych. Wykład 4 – p. 17/7 fork - przykład (I) #include <sys/types.h> #include <unistd.h> #include <iostream> int main() { std::cout << "* Kod wykonywany przez proces macierzysty\n" << "* przed wykonaniem funkcji fork" << std::endl; pid_t pid = fork(); if (pid != 0) { std::cout << "* Kod wykonywany przez proces macierzysty po\n" << "* wykonaniu funkcji fork" << std::endl << "* PID: " << getpid() << std::endl << "* PID procesu potomka: " << pid << std::endl; int status; wait(&status); } else std::cout << "# Kod wykonywany przez proces potomka po\n" << "# wykonaniu funkcji fork" << std::endl << "# PID: " << getpid() << std::endl; return 0; } Wykład 4 – p. 18/7 fork - przykład (II) Przykładowy rezultat działania programu: * * * * * * # # # Kod wykonywany przez proces macierzysty przed wykonaniem funkcji fork Kod wykonywany przez proces macierzysty po wykonaniu funkcji fork PID: 1375 PID procesu potomka: 1376 Kod wykonywany przez proces potomka po wykonaniu funkcji fork PID: 1376 Wykład 4 – p. 19/7 Funkcje exec Funkcja exec umożliwia uruchomienie innego programu. Kiedy proces uruchamia funkcje exec, zostaje on przykryty nowym kodem programu. Segmenty tekstu, danych i stosu zapełniane sa˛ nowymi danymi. #include <unistd.h> extern char **environ; int execl(const char *path, const char *arg0, ... /*, (char *)0 */); int execv(const char *path, char *const argv[]); int execle(const char *path, const char *arg0, ... /*, (char *)0, char *const envp[]*/); int execve(const char *path, char *const argv[], char *const envp[]); int execlp(const char *file, const char *arg0, ... /*, (char *)0 */); int execvp(const char *file, char *const argv[]); Wykład 4 – p. 20/7 exec - Przykład #include <qapplication.h> #include "getfile.h" int main(int argc, char** argv) { QApplication app(argc, argv); GetFile getFile; getFile.resize(210, 70); app.setMainWidget(&getFile); getFile.show(); app.exec(); if (getFile.IsOk()) execlp("acroread", "acroread", "/tmp/slides.pdf", NULL); return 0; } Wykład 4 – p. 21/7 Komunikacja miedzyprocesowa ˛ W systemie UNIX można wyróżnić nastepuj ˛ ace ˛ mechanizmy komunikacji pomiedzy ˛ procesami: pliki, sygnały, łacza ˛ komunikacyjne mechanizmy synchronizacji IPC Systemu V: semafory, kolejki komunikatów, pamieć ˛ dzielona, gniazda. Wykład 4 – p. 22/7 Procesy - współdzielenie informacji Wykład 4 – p. 23/7 Pliki blokujace ˛ Pliki blokujace ˛ sa˛ jednym z najprostszych sposobów komunikacji pomiedzy ˛ procesami. Stosujac ˛ uzgodniona, ˛ wspólna˛ konwencje˛ nazywania plików, określony proces sprawdza istnienie w danym miejscu pliku blokujacego. ˛ Jeżeli plik jest obecny proce wykonuje określone działanie, w przeciwnym wypadku jest wykonywana inna czynność. Tego typu rozwiazanie ˛ wiaże ˛ sie˛ jednak z wystepowaniem ˛ wielu problemów. Rozwiazaniem ˛ cz˛eści problemów mechanizmu plików blokujacych ˛ może być zrealizowane poprzez wykorzystaniu mechanizmu blokowania plików. Wykład 4 – p. 24/7 Blokady typu POSIX Funkcja fcntl umożliwia wykonanie wielu operacji na deskryptorze otwartego pliku, w tym miedzy ˛ innymi operacji dostepu ˛ do flag pliku i zarzadzaniem ˛ blokadami typu POSIX: int fcntl(unsigned int fd, unsigned int cmd, unsigned long arg); fd - deskryptor pliku, cmd - komenda (w przypadku blokad plików sa˛ to: F_GETLK - pobiera blokady założone na plik, F_SETLK - ustawia blokade˛ na plik, F_SETLKW - ustawia blokade˛ na plik jeśli to możliwe), arg - argument zależny od wykonywanej komendy. W przypadku blokad pliku argument arg jest traktowany jako wskaźnik na strukture˛ typu struct flock: struct short short off_t off_t pid_t flock { l_type; l_whence; l_start; l_len; l_pid; /* /* /* /* /* typ blokady */ tryb obliczania przesuniecia w rekordzie */ poczatek obszaru */ przesuniecie w bajtach */ wlasciciel blokady zwracany przez F_GETLK */} Typ blokady może przyjmować wartości: F_RDLCK - blokada odczytu, F_WRLCK blokada zapisu, F_UNLCK - usuwanie blokady. Wykład 4 – p. 25/7 Blokady typu flock Funkcja flock() udostepnia ˛ programiście dostep ˛ do drugiego rodzaju blokady pliku: int flock(unsigned int fd, unsigned int cmd); fd - deskryptor pliku, cmd - komenda: LOCK_SH - blokada dzielona, LOCK_EX - blokada wyłaczna, ˛ LOCK_NB - nie usypiaj w czasie blokowania, LOCK_UN - usuwanie blokady. Na plik nie można jednocześnie zakładać blokad typu dzielonego i wyłacznego. ˛ Blokada wyłaczna ˛ może być tylko jedna na jednym pliku, natomiast blokad dzielonych może być wiecej. ˛ Wywołanie funkcji flock może blokować proces w przypadku, gdy blokada została już założona przez inny proces. W celu wywołania nieblokujacego ˛ należy wywołać funkcje flock z ustawiona˛ flaga˛ LOCK_NB Wykład 4 – p. 26/7 Funkcje dup i dup2 Funkcje dup i dup2 służa˛ do powielania istniejacych ˛ deskryptorów plików: int dup(int oldfd); int dup2(int oldfd, int newfd); funkcja dup tworzy nowy deskryptor, który jest kopia˛ deskryptora pliku oldfd podanego jako argumentem wywołania funkcji, funkcja dup2 tworzy deskryptor pliku newfd jako kopie˛ deskryptora oldfd. W przypadku, gdy deskryptor newfd jest otwarty, to zostanie on zamkniety. ˛ Wykład 4 – p. 27/7 Sygnały Sygnały służa˛ do powiadamiania procesów o wystepuj ˛ acych ˛ zdarzeniach. Sygnały pojawiaja˛ sie˛ asynchronicznie (w nieustalonych chwilach i bez przewidywanej kolejności). Moga˛ być wysyłane przez procesy i przez jadro. ˛ Sygnał jest generowany w chwili wystapienia ˛ zdarzenia i uznawany za dostarczony w chwili, gdy proces podejmie w odpowiedzi na nie stosowne działanie. Wykład 4 – p. 28/7 Ignorowanie i przechwytywanie sygnałów W przypadku otrzymania sygnału proces może zareagować na trzy sposoby: wykonać operacje˛ domyślna: ˛ Exit Core Stop Ignore zignorować sygnał, przechwycić sygnał. Wykład 4 – p. 29/7 Przykładowe sygnały Nazwa symboliczna Wartość Sygnalizowane zdarzenie SIGABRT 6 Przerwanie (abort) SIGALRM 14 Budzik (alarm clock) SIGHUP 1 Zawieszenie (hangup) SIGILL 4 Nielegalna instrukcja SIGINT 2 Przerwanie (interrupt) SIGKILL 9 Zabicie (kill) SIGQUIT 3 Koniec (quit) SIGUSR1 16 Sygnał użytkownika nr 1 SIGUSR2 17 Sygnał użytkownika nr 2 Wykład 4 – p. 30/7 Generowanie sygnału #include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig); pid - oznacza proces lub grupe˛ procesów do których wysłany zostanie sygnał, Wartość pid Procesy, które odbieraja˛ sygnał >0 Procesy o identyfikatorach równych pid 0 Procesy należace ˛ do tej samej grupy co proces wysyłajacy ˛ sygnał -1 W przypadku procesu superużytkownika - wszystkie procesy oprócz specjalnych, dla pozostałych procesów - wszystkie procesy, których rzeczywisty identyfikator jest równy efektywnemu identyfikatorowi procesu wysyłajacego ˛ sygnał < -1 Procesy, których grupa ma identyfikator -pid. Wykład 4 – p. 31/7 Funkcje obsługujace ˛ sygnały Ignorowanie i przechwytywanie sygnału wymagaja˛ skojarzenia sygnału z funkcja˛ przechwytujac ˛ a. ˛ #include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); signum - sygnał, który zostanie skojarzony z nowa˛ funkcja, ˛ SIG_ING lub SIG_DFL, handler - funkcja przechwytujaca ˛ sygnał, w przypadku niepowodzenia funkcja zwraca SIG_ERR. Wykład 4 – p. 32/7 Sygnały - przykład #include <iostream> #include <signal.h> void obsluga_sygnalu(int _signal) { std::cout << "otrzymano sygnal: " << _signal << std::endl; } int main() { if (signal(SIGINT, obsluga_sygnalu) == SIG_ERR) { perror("Problem z ustawieniem sygnalu SIGINT"); exit(SIGINT); } for (int i = 0; ; i++) { std::cout << "." << std::flush; sleep(1); } } Wykład 4 – p. 33/7 Maska sygnałów Do manipulacja˛ maska˛ sygnałów można wykorzystać funkcje Systemu V: #include <signal.h> int int int int sighold(int sig); sigignore(int sig); sigpause(int sig); sigrelse(int sig); Wykład 4 – p. 34/7 POSIX - maska sygnałów #include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); Wykład 4 – p. 35/7 Łacza ˛ Łacza ˛ (potoki) umożliwiaja˛ komunikacje˛ pomiedzy ˛ procesami zgodnie z zasada˛ kolejki FIFO (First In First Out), jak również synchronizacje˛ procesów. Komunikacja może odbywać sie˛ również wtedy, gdy procesy nie znaja˛ procesów znajdujacych ˛ sie˛ po drugiej stronie łacza. ˛ Dzieli sie˛ je na dwa rodzaje: łacza ˛ nienazwane, łacza ˛ nazwane, Różnia˛ sie˛ sposobem w jaki procesy zaczynaja˛ z nich korzystać, później do obsługi używa sie˛ tych samych funkcji. Ponadto komunikacja pomiedzy ˛ łaczami ˛ nienazwanymi może odbywać sie˛ tylko pomiedzy ˛ procesem potomnym, a procesem macierzystym. Wykład 4 – p. 36/7 Łacza ˛ nienazwane Do otwarcia nienazwanego łacza ˛ służy funkcja systemowa pipe #include <unistd.h> int pipe(int filedes[2]); W przypadku bezbłednego ˛ działania funkcja zwraca dwa deskryptory plików filedes[0] i filedes[1], które odsyłaja˛ do dwóch strumieni danych, filedes[1] służy do zapisu, filedes[1] wykorzystywany jest do odczytu. Wykład 4 – p. 37/7 read/write ssize_t read(int fd, void *buf, size_t count) ssize_t write(int fd, const void *buf, size_t count) fd - deskryptor buff - adres danych do odczytania/zapisania, length - rozmiar danych do odczytania/zapisania, wynik - liczba odczytanych/zapisanych danych. Wykład 4 – p. 38/7 Zamykanie deskryptorów łacza ˛ W przypadku komunikacji jednostronnej każdy z procesów powinien zamknać ˛ nieużywany deskryptor łacza ˛ za pomoca˛ funkcji close: int close(int fd); Zamykanie nieużywanego pliku nie jest konieczne, ale wśród programistów jest uważane za dobry zwyczaj programistyczny. Wykład 4 – p. 39/7 Łacza ˛ nienazwane - przykład I #include #include #include #include <unistd.h> <iostream> <sys/types.h> <sys/wait.h> const int BUFFSIZE = 255; int main(int argc, char** argv) { int pipes[2]; char buffer[255]; int result = pipe(pipes); if (result == -1) { perror("pipe"); exit(1); } Wykład 4 – p. 40/7 Łacza ˛ nienazwane - przykład II switch (fork()) { case -1: perror("fork"); exit(2); case 0: // proces potomka close(pipes[1]); if (read(pipes[0], buffer, BUFSIZE) != -1) std::cout << "otrzymano: " << buffer << std::endl; else { perror("read"); exit(3); } break; default: close(pipes[0]); if (write(pipes[1], argv[1], strlen(argv[1])) != -1) sts::cout << "wyslano: " << argv[1] << std::endl; else { perror("write"); exit(4); } int status; wait(&status); } Wykład 4 – p. 41/7 Łacza ˛ nazwane Łacza ˛ nazwane sa˛ podobne koncepcyjnie do potoków nienazwanych. Potoki nazwane tworzone sa˛ w postaci fizycznych plików. Moga˛ być tworzone z poziomu powłoki (polecenie mknod) lub z poziomu programu: int mknod(const char* path, mode_t mode, dev_t dev); int mkfifo(const char* path, mode_t mode); Stała symboliczna Plik S_IFIFO potok FIFO S_IFCHR specjalny znakowy S_IFDIR katalog S_IFBLK specjalny blokowy S_IFREG zwykły plik Wykład 4 – p. 42/7 Łacza ˛ nazwane - przykład I #include #include #include #include #include <iostream> <stdlib.h> <sys/stat.h> <unistd.h> <linux/stat.h> int main() { FILE* file; char buffer[255]; mknod("my_fifo", S_IFIFO | 0666, 0); while (true) { file = fopen("my_fifo", "r"); fgets(buffer, 255, file); std::cout << "odebrano: " << buffer << std::endl; fclose(file); } return 0; } Wykład 4 – p. 43/7 Łacza ˛ nazwane - przykład II #include <iostream> #include <stdlib.h> int main(int argc, char** argv) { if (argc < 2) { std::cerr << "Program wymaga argumentu" << std::endl; exit(1); } FILE* file; file = fopen("my_fifo", "w"); if (file == NULL) { perror("fopen"); exit(2); } fputs(argv[1], file); fclose(file); return(0); } Wykład 4 – p. 44/7 Mechanizmy IPC Systemu V Na mechanizmy IPC (interprocess communications) składaja˛ sie: ˛ kolejki komunikatów, semafory, pamieć ˛ wspólna (dzielona). Wykład 4 – p. 45/7 Cechy IPC Nie sa˛ deskryptorami plików i nie wykonuje sie˛ na nich operacji I/O standardowymi funkcjami read/write, lecz każde urzadzenie ˛ ma swój specyficzny zbiór operacji I/O. Po utworzeniu danego urzadzenia ˛ jest ono od razu gotowe do pracy, nie jest konieczne jego otwieranie przez każdy proces pragnacy ˛ sie˛ komunikować. (Z wyjatkiem ˛ obszarów pamieci ˛ wspólnej, które każdy proces musi jeszcze odwzorować na swoja˛ przestrzeń adresowa.) ˛ Identyfikatory urzadze ˛ ń System V IPC (typu int) sa˛ globalne w systemie, odmiennie niż deskryptory plików (także nie sa˛ kolejno generowanymi małymi liczbami). Oznacza to, że jeden proces może użyć identyfikatora utworzonego przez inny proces. Wynika stad ˛ możliwość, celowego lub nie, wkleszczania sie˛ procesu w komunikacje˛ prowadzona˛ przez inne procesy. Konieczne jest jawne kasowanie urzadze ˛ ń funkcjami kontrolnymi. Urzadzenia ˛ komunikacyjne System V IPC istnieja˛ trwale w pamieci ˛ systemu, ale nie sa˛ przechowywane na dysku. Oznacza to, że istnieja˛ nadal po zakończeniu procesu, który je utworzył, ale znikaja˛ bezpowrotnie przy restarcie systemu. Wykład 4 – p. 46/7 Obiekty IPC Każdy obiekt IPC musi zostać utworzony zanim bedzie ˛ można z niego korzystać. Obiekty IPC posiadaja˛ twórce, ˛ właściciela oraz przypisane prawa dostepu, ˛ które można modyfikować. Na poziomie systemowym dane znajdujace ˛ sie˛ w obiekcie pobiera sie˛ za pomoca˛ programu ipcs: ------ Shared Memory Segments -------key shmid owner perms 0x00000000 18219008 olas 600 bytes 393216 ------ Semaphore Arrays -------key semid owner perms nsems ------ Message Queues key msqid 0x4204bd0b 327680 0x4204bd0d 360449 used-bytes 0 0 -------owner olas olas perms 700 700 nattch 2 status dest messages 0 0 Do usuniecia ˛ obiektu IPC można użyć programu ipcrm (potrzebne sa˛ do tego odpowiednie uprawnienia - właściciel, root): ipcrm [ -M key | -m id | -Q key | -q id | -S key | -s id ] ... Wykład 4 – p. 47/7 Zestawienie funkcji IPC Działanie Kolejka komunikatów Semafory Pamieć ˛ wspólna <sys/msg.h> <sys/sem.h> <sys/shm.h> Rezerwacja obiektu IPC, uzyskanie do niego dostepu ˛ msgget semget shmget Sterowanie obiektem IPC msgctl semctl shmctl Operacje specyficzne msgsnd semop shmmat Plik nagłówkowy msgrcv shmdt Wykład 4 – p. 48/7 Klucze key_t i funkcja ftok Komunikacja miedzyprocesowa ˛ Systemu V używa jako swoich nazw wartości typu key_t. Cz˛esto wykorzystywanym sposobem przypisywania wartości tego typu danym jest wykorzystanie funkcji ftok: # include <sys/types.h> # include <sys/ipc.h> key_t ftok(const char *pathname, int proj_id); Funkcja łaczy ˛ otrzymane argumenty, tj. nazwe˛ ścieżki (argument pathname) i 8-bitowy identyfikator całkowitoliczbowy (argument proj_id) i tworzy klucz IPC w postaci liczby całkowitej. Wykład 4 – p. 49/7 ftok - przykład #include <iostream> # include <sys/types.h> # include <sys/ipc.h> int main(int argc, char** argv) { if (argc != 2) std::cerr << "Usage: ftok pathname" << std::endl; std::cout << "wartosc klucza key_t: " << ftok(argv[1], 1) << std::endl; } Przykładowy wynik działania: $ ./ftok . wartosc klucza key_t: 17078610 $ ./ftok .. wartosc klucza key_t: 17042617 $ ./ftok /tmp wartosc klucza key_t: 16982853 Wykład 4 – p. 50/7 Struktura IPC_PERM Jadro ˛ systemu przechowuje na temat każdego obiektu IPC strukture˛ informacyjna˛ o postaci podobnej do przechowywanej informacji o plikach: struct ipc_perm { key_t key; // klucz uid_t uid; // identyfikator użytkownika właściciela gid_t gid; // identyfikator grupy właściciela uid_t cuid; // identyfikator użytkownika twórcy gid_t cgid; // identyfikator grupy twórcy mode_t mode; // tryby dost˛ epu unsigned short seq; // numer kolejny }; Wykład 4 – p. 51/7 Kolejki komunikatów Kolejki komunikatów (Message Queue) to kolejka, na która˛ można odkładać komunikaty. Kolejki moga˛ być publiczne - każdy proces może odłożyć komunikat na kolejce i prywatne - tylko proces rodzica i ewentualnie dziecko może odłożyć komunikat. Komunikaty moga˛ być odczytywane i zapisywane w tym samym czasie - nie ma ograniczenia jak w przypadku łacz, ˛ że sa˛ typu FIFO. Wykład 4 – p. 52/7 Tworzenie kolejki Utworzenie nowej kolejki komunikatów lub otwarcie dostepu ˛ do już istniejacej ˛ umożliwia funkcja systemowa msgget(): #include <sys/types.h> #include <sys/ipc.h> #include <sys/msg.h> int msgget (key_t key, int msgflg); gdzie: key - klucz, msgflg - flagi: IPC_CREAT - zostanie utworzona kolejka (o ile nie istnieje), uprawnienia do kolejki określaja˛ 9 najmniej znaczacych ˛ bitów parametru msgflag, IPC_EXCL - powoduje, że zwracany jest bład ˛ w przypadku, gdy kolejka już istnieje. Wykład 4 – p. 53/7 Bufor komunikatu Operacje przesyłania komunikatów wymagaja˛ posłużenia sie˛ buforem komunikatu zdefiniowanym w nastepuj ˛ acy ˛ sposób: struct msgbuf { long mtype; # typ komunikatu (wartość > 0) char mtext[1]; # tekst komunikatu }; Pole mtype określa typ komunikatu w postaci dodatniej liczby całkowitej. Tablica mtext[] przechowuje treść komunikatu, która˛ moga˛ stanowić dowolne dane. Rozmiar tablicy podany w definicji nie stanowi rzeczywistego ograniczenia, ponieważ bufor komunikatu można dowolnie przedefiniować w programie pod warunkiem zachowania typu na poczatku. ˛ Wykład 4 – p. 54/7 Przesyłanie komunikatu Funkcja msgsnd() umożliwia przesłanie komunikatu do kolejki: int msgsnd(int msqid, struct msgbuf *msgp, int msgsz, int msgflg); gdzie: msqid - identyfikator kolejki komunikatów, msgp - wskaźnik do bufora zawierajacego ˛ komunikat do wysłania, msgsz - rozmiar bufora komunikatu z wyłaczeniem ˛ typu (rozmiar treści komunikatu), msgflg - flagi. Wykład 4 – p. 55/7 Pobranie komunikatu Funkcja msgrcv() pobiera z kolejki jeden komunikat wskazanego typu. Pobrany komunikat jest usuwaney z kolejki. int msgrcv(int msqid, struct msgbuf *msgp, int msgsz, long msgtype, int msgflg); gdzie: msqid - identyfikator kolejki komunikatów, msgp - wskaźnik do bufora, do którego ma być zapisany komunikat, pobrany z kolejki, msgsz - rozmiar bufora komunikatu z wyłaczeniem ˛ typu (rozmiar treści komunikatu), msgtype - typ komunikatu do pobrania z kolejki: msgtype > 0 - pobiera najstarszy komunikat danego typu, msgtype == 0 - pobiera najstarszy komunikat w kolejce. msgflg - flagi. Wykład 4 – p. 56/7 Kolejki - przykład I struct my_msg { long mtype; char mtext[255]; }; int main(int argc, char** argv) { if (argc != 2) std::cerr << "Uzycie: sender komunikat" << std::endl; key_t klucz = ftok("sender.cpp", 1); int msgid = msgget(klucz, IPC_CREAT | 0666); std::cout << "msgid: " << msgid << std::endl; my_msg buf; buf.mtype = 1; strcpy(buf.mtext, argv[1]); if (msgsnd(msgid, (struct msgbuf *)&buf, sizeof(buf), 0) == -1) perror("msgsnd"); } Wykład 4 – p. 57/7 Kojeki - przykład II struct my_msg { long mtype; char mtext[255]; }; int main(int argc, char** argv) { key_t klucz = ftok("sender.cpp", 1); int msgid = msgget(klucz, IPC_CREAT | 0666); my_msg buf; if (msgrcv(msgid, (struct msgbuf *)&buf, sizeof(buf), 1, 0) == -1) perror("msgsnd"); std::cout << "id: " << buf.mtype << "\ntext: " << buf.mtext << std::end if (msgctl(msgid, IPC_RMID, NULL) == -1) perror("msgctl"); } Wykład 4 – p. 58/7 Kolejki komunikatów - zarzadzanie ˛ Do wykonywania operacji kontrolnych na kolejce komunikatów służy funkcja msgctl: int msgctl(int msqid, int cmd, struct msqid_ds *buf); msgid - identyfikator kolejki, cmd - rodzaj operacji wykonywanych na kolejce: IPC_RMID - usuniecie ˛ kolejki, IPC_STAT - pobranie struktury msqid_ds i zapisanie jej w parametrze buf (zawiera prawa dostepu ˛ do kolejki, informacje dotyczace ˛ ostatniego wysłania i odebrania komunikatu, aktualna˛ liczbe˛ danych znajdujacych ˛ sie˛ w kolejce, jej pojemność), IPC_SET - zmiana praw dostepu ˛ i pojemności kolejki poprzez przekazana˛ strukture˛ msqid_ds w parametrze buf. Wykład 4 – p. 59/7 Semafory I W teorii semafor jest nieujemna˛ zmienna˛ (sem), domyślnie kontrolujac ˛ a˛ przydział pewnego zasobu. Wartość zmiennej sem oznacza liczbe˛ dostepnych ˛ jednostek zasobu. Określone sa˛ nastepuj ˛ ace ˛ operacje na semaforze: P(sem) - oznacza zajecie ˛ zasobu sygnalizowane zmniejszeniem wartości semafora o 1, a jeżeli jego aktualna wartość jest 0 to oczekiwanie na jej zwiekszenie, ˛ V(sem) - oznacza zwolnienie zasobu sygnalizowane zwiekszeniem ˛ wartości semafora o 1, a jeżeli istnieje(a) ˛ proces(y) oczekujacy(e) ˛ na semaforze to, zamiast zwiekszać ˛ wartość semafora, wznawiany jest jeden z tych procesów. Istotna jest niepodzielna realizacja każdej z tych operacji, tzn. każda z operacji P, V może albo zostać wykonana w całości, albo w ogóle nie zostać wykonana. Z tego powodu niemożliwa jest prywatna implementacja operacji semaforowych przy użyciu zmiennej globalnej przez proces. Wykład 4 – p. 60/7 Semafory II Możemy wyróżnić: semafor binarny : semafor, którego wartościa˛ jest 0 lub 1. semafor zliczajacy ˛ : semafor, którego wartość leży w zakresie od 0 do określonego limitu. W Systemie V wprowadzono zbiór semaforów zliczajacych, ˛ tj. zbiór składajacy ˛ sie˛ z co najmniej jednego semafora, a każdy semafor jest zliczajacy. ˛ Wykład 4 – p. 61/7 Utworzenie semafora Utworzenie nowego zestawu semaforów lub dostep ˛ do już istniejacej ˛ zapewnia funkcja systemowa semget(): int semget(key_t key, int nsems, int semflg); gdzie: key - klucz, nsems - liczba semaforów w zbiorze, semflg - flagi. Wykład 4 – p. 62/7 Operacje na semaforze (I) Operacje na semaforach umożliwia funkcja semop(): int semop(int semid, struct sembuf *sops, unsigned nsops); gdzie: semid - identyfikator zbioru semaforów, sops - wskaźnik do tablicy, której elementami jest struktura danych: struct sembuf { short sem_num; short sem_op; short sem_flg; }; // indeks semafora w tablicy: 0, 1, 2, ..., nse // operacja na semaforze // sygnalizatory operacji: 0, IPC_NOWAIT, SEM_U Każdy element tej tablicy określa operacje˛ wykonywana˛ na jednym konkretnym semaforze w zbiorze. nsops - liczba elementów struktury sembuf Wykład 4 – p. 63/7 Operacje na semaforze (II) Dla sem_op < 0 podana wartość bezwzgledna ˛ zostanie odjeta ˛ od wartości semafora. Ponieważ wartość semafora nie może być ujemna, to proces może zostać zablokowany (uśpiony) do momentu uzyskania przez semafor odpowiedniej wartości, która umożliwi wykonanie operacji. Odpowiada to zajeciu ˛ zasobu. Dla sem_op > 0 podana wartość zostanie dodana do wartości semafora. T˛e operacje˛ można zawsze wykonać. Odpowiada to zwolnieniu zasobu. Przy sem_op = 0 proces zostanie uśpiony do momentu, gdy semafor osiagnie ˛ wartość zerowa. ˛ Wykład 4 – p. 64/7 Pamie˛ ć współdzielona Wszystkie dotychczasowe metody komunikacji powstały m.in. po to, aby współdzielić dane. Jedyna ich niedogodność to sekwencyjność. Pamieć ˛ współdzielona rozwiazuje ˛ problem sekwencyjności dostep ˛ do zasobów jest swobodny. Z pomoca˛ przychodzi Unixowy sposób przydzielania każdemu z procesów wirtualnej przestrzeni adresowej, co implikuje duża˛ stabilność. Wykład 4 – p. 65/7 Pamie˛ ć współdzielona - idea Deklarujemy sekcje˛ w pamieci, ˛ z której procesy bed ˛ a˛ korzystały jednocześnie. Oznacza to, iż dane w tej sekcji pamieci ˛ (segmencie) bed ˛ a˛ widziane przez różne procesy. Wiaże ˛ sie˛ z tym możliwość jednoczesnej modyfikacji zmiennej, co wymusza użycie narz˛edzi synchronizujacych. ˛ Wykład 4 – p. 66/7 Tworzenie segmentu pamieci ˛ Utworzenie nowego segmentu pamieci ˛ dzielonej lub uzyskanie dostepu ˛ do już istniejacego ˛ umożliwia funkcja systemowa shmget(). int shmget(key_t key, int size, int shmflg); gdzie: key - klucz, size - rozmiar segmentu pamieci, ˛ shmflg - flagi. Funkcja zwraca identyfikator segmentu pamieci ˛ zwiazanego ˛ z podana˛ wartościa˛ klucza key. Wykład 4 – p. 67/7 Przyłaczanie ˛ segmentu pamieci ˛ W wynika wywołania funkcji shmget() proces uzyskuje identyfikator segmentu pamieci ˛ dzielonej. Aby można było z niego korzystać, segment musi zostać jeszcze przyłaczony ˛ do wirtualnej przestrzeni adresowej procesu za pomoca˛ funkcji shmat(): void *shmat(int shmid, const void *shmaddr, int shmflg); gdzie: shmid - identyfikator segmentu pamieci ˛ dzielonej, shmaddr - adres w przestrzeni adresowej procesu, od którego ma być dołaczony ˛ segment, shmflg - flagi. Funkcja zwraca adres poczatkowy ˛ dołaczonego ˛ segmentu w wirtualnej przestrzeni adresowej procesu. Adres ten może być wyspecyfikowany w argumencie shmaddr. Jadro ˛ systemu próbuje wtedy dołaczyć ˛ segment od podanego adresu pod warunkiem, że jest on wielokrotnościa˛ rozmiaru strony pamieci. ˛ Zaokraglanie ˛ adresu w dół do granicy strony może być dokonane przez jadro, ˛ jeżeli ustawiona jest flaga SHM_RND. Zalecane jest jednak ustawienie shmaddr = 0 w wywołaniu funkcji, aby pozwolić na wybór adresu przez jadro. ˛ Wykład 4 – p. 68/7 Odłaczanie ˛ segmentu pamieci ˛ Po zakończeniu korzystania z segmentu pamieci ˛ dzielonej, proces powinien odłaczyć ˛ go za pomoca˛ funkcji systemowej shmdt(). int shmdt(const void *shmaddr); gdzie: shmaddr - adres poczatkowy ˛ segmentu w przestrzeni adresowej procesu. Wykład 4 – p. 69/7 Usuniecie ˛ segmentu pamieci ˛ Odłaczenie ˛ segmentu nie oznacza automatycznie usuniecia ˛ z jadra ˛ systemu. Segment pozostaje w pamieci ˛ i może być ponownie dołaczany ˛ przez procesy. W celu usuniecia ˛ segmentu trzeba posłużyć sie˛ funkcja˛ systemowa˛ shmctl(). int shmctl(int shmid, int cmd, struct shmid_ds *buf); gdzie: shmid - identyfikator segmentu, cmd - operacja sterujaca, ˛ buf - wskaźnik do bufora przeznaczonego na strukture˛ shmid_ds segmentu. Wykład 4 – p. 70/7