# SO Lista 10 ![](https://i.imgur.com/ikig5WH.png) 2 Advanced Programming in the UNIX Environment https://raw.githubusercontent.com/zwan074/technical-books/master/Advanced.Programming.in.the.UNIX.Environment.3rd.Edition.0321637739.pdf 5 OPERATING SYSTEMS THREE EASY PIECES https://techiefood4u.files.wordpress.com/2020/02/operating_systems_three_easy_pieces.pdf - [x] Zadanie 1 ### Zadanie 1 ![](https://i.imgur.com/yS6vcVe.png) #### Czym różni się przetwarzanie równoległe (ang. parallel) od przetwarzania współbieżnego(ang. concurrent)? :::spoiler Krócej **Przetwarzanie równoległe (ang. parallel)** - polega na jednoczesnym przetwarzaniu wielu różnych zadań, czy obliczeń (np. przez 2 lub więcej procesory). **Przetwarzanie współbieżne (ang. concurrent)** - polega na współistnieniu wielu wątków lub procesów. Uruchomione wątki na tym samym procesorze są wykonywane naprzemiennie, sprawiając wrażenie, że wykonują się równolegle. **Różnica** - Te dwie formy przetwarzania różni sposób, w jaki wykonują zadania/obliczenia. Przy przetwarzaniu współbieżnym w miejscu linii przerywanych należy pamiętać o tym, że zmieniany jest kontekst (przez co zmiana między zadaniami nie jest natychmiastowa). ![](https://i.imgur.com/UDTuKc4.png) ::: :::spoiler Dłużej **Przetwarzanie równoległe (ang. parallel)** - polega na wykonywaniu wielu zadań jednocześnie, przez co proces ten jest szybszy niż przetwarzanie sekwencyjne, gdzie zadania są wykonywane po kolei. Można to osiągnąć poprzez użycie wielu procesorów lub wirtualnych procesorów, takich jak wątki, które są używane do przetwarzania równoległego w systemach jednoprocesorowych. Przetwarzanie równoległe jest często używane do zadań, które mogą być rozdzielone na mniejsze części i wykonywane niezależnie, takie jak obliczenia matematyczne lub przetwarzanie dużych ilości danych. **Przetwarzanie współbieżne (ang. concurrent)** oznacza wykonywanie wielu zadań jednocześnie, ale w przeciwieństwie do przetwarzania równoległego, zadania te nie są wykonywane na różnych procesorach lub wątkach. Zamiast tego są one wykonywane na jednym procesorze, ale są one zarządzane tak, aby wymieniać się między nimi, tak aby każde zadanie mogło być wykonane w częściach. Przetwarzanie współbieżne jest często używane do zadań, które wymagają wielu operacji wejścia/wyjścia, takich jak komunikacja z bazami danych lub sieciami, ponieważ pozwala na zarządzanie wieloma zadaniami bez całkowitego zatrzymania wykonywania każdego z nich. **Różnice** - Główną różnicą między przetwarzaniem równoległym a przetwarzaniem współbieżnym jest to, że przetwarzanie równoległe polega na wykonywaniu wielu zadań jednocześnie na różnych procesorach lub wirtualnych procesorach, natomiast przetwarzanie współbieżne polega na wykonywaniu wielu zadań jednocześnie na jednym procesorze, ale zarządzaniu nimi tak, aby wymieniać się między nimi. Oba te podejścia są stosowane w celu zwiększenia wydajności systemu poprzez wykonywanie wielu zadań jednocześnie, ale są one używane w różnych sytuacjach i mogą mieć różne skutki uboczne, takie jak konieczność zarządzania synchronizacją dostępu do zasobów w przypadku przetwarzania równoległego lub konieczność zarządzania zmianami kontekstu w przypadku przetwarzania współbieżnego. ::: #### Czym charakteryzują się procedury wielobieżne (ang. reentrant)? **Procedury wielobieżne (ang. reentrant)** - to procedury, które mogą być wykonywane wielokrotnie przez różne wątki jednocześnie, bez konieczności synchronizacji dostępu do ich kodu lub danych wejściowych. W przeciwieństwie do zwykłych procedur, które mogą być wykonywane tylko przez jeden wątek na raz, procedury wielobieżne mogą być wielokrotnie wywoływane przez różne wątki bez ryzyka utraty danych lub błędów spowodowanych konfliktem dostępu. #### Podaj przykład procedury w języku C (a) wielobieżnej, ale nie wielowątkowo-bezpiecznej (ang. MT-safe) (b) na odwrót. :::spoiler **a)** ```C #include <stdio.h> int counter = 0; void increment_counter() { counter++; } int main() { increment_counter(); printf("Counter: %d\n", counter); return 0; } ``` **increment_counter** jest wielobieżna, ponieważ może być wielokrotnie wywoływana przez różne wątki jednocześnie, ale nie jest wielowątkowo-bezpieczna, ponieważ nie została zabezpieczona przed konfliktem dostępu do zmiennej counter. Jeśli ta procedura zostanie wywołana przez kilka wątków jednocześnie, istnieje ryzyko, że wynik będzie niepoprawny z powodu konfliktu dostępu do tej zmiennej. ::: :::spoiler **b)** ```C #include <stdio.h> #include <pthread.h> int counter = 0; pthread_mutex_t mutex; void increment_counter() { pthread_mutex_lock(&mutex); counter++; pthread_mutex_unlock(&mutex); } int main() { pthread_t thread1, thread2; pthread_mutex_init(&mutex, NULL); pthread_create(&thread1, NULL, increment_counter, NULL); pthread_create(&thread2, NULL, increment_counter, NULL); pthread_join(thread1, NULL); pthread_join(thread2, NULL); printf("Counter: %d\n", counter); pthread_mutex_destroy(&mutex); return 0; } ``` W przypadku tej procedury, każdy wątek wywołuje procedurę increment_counter po kolei, a nie jednocześnie. W związku z tym nie ma konfliktu dostępu do zmiennej counter, ale procedura nie jest wielobieżna, ponieważ każdy wątek musi czekać na swoją kolej, aby wywołać procedurę. ::: ::: spoiler **a) prościej** ```c int c; void incr(){ c++; } /* jest wielobieżna, bo wywołanie jej jeszcze raz nie zrobi nic złego, ale MT-safe nie jest, bo race do zmiennej */ ``` ::: :::spoiler **b) prosciej** ```c printf() //używa locków ``` ::: #### Kiedy w jednowątkowym procesie uniksowym może wystąpić współbieżność? W jednowątkowym procesie uniksowym może wystąpić współbieżność, jeśli proces wywołuje funkcje systemowe lub biblioteczne, które mogą być wykonywane w tle przez system operacyjny. Na przykład, jeśli proces wywołuje funkcję read() w celu odczytania danych z pliku, system operacyjny może przełączyć kontekst procesu, aby wykonać inne zadania, a następnie powrócić do procesu, gdy dane są dostępne do odczytu. W takim przypadku może wystąpić współbieżność, ponieważ proces jest wielobieżny i może być wielokrotnie przełączany między różnymi zadaniami przez system operacyjny. - [x] Zadanie 2 ### Zadanie 2 ![](https://i.imgur.com/nqRdPpM.png) **Zakleszczenie (ang. deadlock)** to sytuacja, w której dwa lub więcej wątków są zablokowane na stałe, ponieważ każdy z nich chce uzyskać dostęp do zasobu, który jest już zajęty przez drugi wątek. W takiej sytuacji wątki mogą zostać zablokowane na zawsze, ponieważ nie są w stanie uzyskać dostępu do zasobów, które są im potrzebne. <br> :::spoiler **przykład deadlock** **Scenariusz** - dwa wątki (A i B) konkurują o dostęp do dwóch zasobów (X i Y). Wątek A chce najpierw uzyskać dostęp do zasobu X, a następnie do zasobu Y. Wątek B chce najpierw uzyskać dostęp do zasobu Y, a następnie do zasobu X. Oba wątki są zablokowane, ponieważ każdy z nich chce uzyskać dostęp do zasobu, który jest już zajęty przez drugi wątek. ::: <br> **Uwięzienie (ang. livelock)** to sytuacja, w której jeden lub więcej wątków są zablokowane, ale stale wykonują pewne akcje, aby uniknąć konfliktów z innymi wątkami. W takiej sytuacji wątki stale się przełączają, ale żaden z nich nie jest w stanie uzyskać dostępu do zasobów, które są mu potrzebne. Uwięzienie może prowadzić do opóźnień i problemów z wydajnością, ponieważ wątki stale pracują, ale nie są w stanie zakończyć swoich zadań. <br> :::spoiler **przykład livelock** Na przykład, jeśli dwa wątki (A i B) konkurują o dostęp do zasobu X, a wątek A uzyska dostęp do tego zasobu, a następnie wątek B uzyska dostęp do tego zasobu, a następnie wątek A ponownie uzyska dostęp do tego zasobu, a następnie wątek B ponownie uzyska dostęp do tego zasobu itd., to wszystkie wątki mogą być uwięzione w tym wzajemnym przełączaniu się, ale żaden z nich nie jest w stanie zakończyć swoich zadań, ponieważ zasób jest stale zajęty przez inny wątek. W takiej sytuacji mówimy o uwięzieniu. ::: <br> **Głodzenie (ang. starvation)** to sytuacja, w której jeden lub więcej wątków są zablokowane na dłuższy czas i nie mogą uzyskać dostępu do zasobów, które są im potrzebne, ponieważ inne wątki stale konkurują o te zasoby. W takiej sytuacji wątek głodujący może być blokowany przez długi czas, co może prowadzić do opóźnień i problemów z wydajnością. <br> :::spoiler **przykład starvation** :::danger Mamy trzy wątki (A, B i C) i trzy zasoby (X, Y i Z). Wątek A chce uzyskać dostęp do zasobu X, B chce uzyskać dostęp do Y, a C chce uzyskać dostęp do Z. Wątek A ma najwyższy priorytet i zawsze ma pierwszeństwo dostępu do zasobów. Wątek B ma średni priorytet, a C najniższy. W takiej sytuacji, nawet jeśli wątek B lub C jest gotowy do pracy, nie będą mogli uzyskać dostępu do swoich zasobów, ponieważ wątek A zawsze będzie miał pierwszeństwo. W takiej sytuacji wątki B i C mogą być głodzone, ponieważ nie będą mogli uzyskać dostępu do zasobów, które są im potrzebne. :::success A, B -> wątki X -> zasób A pracuje na X w inf loopie (np. nasz echo server) B nie ma dostępu do X, bo jest zajęty przez A, który ma większy pryjo ::: <br> - [x] Zadanie 3 ### Zadanie 3 ![](https://i.imgur.com/3BkxfUx.png) #### W poniższym programie występuje sytuacja wyścigu (ang. race condition) dotycząca dostępów do współdzielonej zmiennej «tally». Wyznacz jej najmniejszą i największą możliwą wartość. ![](https://i.imgur.com/3z1gn0D.png) **<2, 2n>** :::spoiler **Dlaczego 2** 2 ::: :::spoiler **Dlaczego 2n** Bo essa masz 2 wątki każdy se robi i zwiększa. Nie ma wyścigów bo po co to komu to nie pracownia z SO. Masz 2 wątki 1 n essa suma bum magia 2n. ::: #### Jak zmieni się przedział możliwych wartości zmiennej «tally», gdy wystartujemyk procesów zamiast dwóch? Odpowiedź uzasadnij pokazując przeplot, który prowadzi do określonego wyniku. :::danger No imo <2, k*n> no ale ni wim ::: - [x] Zadanie 4 ### Zadanie 4 ![](https://i.imgur.com/DV6K27f.png) #### Podaj procedury wątków POSIX, które pełnią podobną funkcję co fork(2), exit(3), waitpid(2), atexit(3) oraz abort(3) na procesach. ![](https://i.imgur.com/dzdibVS.png) ##### fork 1. Fork działa w odrębie całego programu, gdzie pthread_create w jednej funkcji. 2. Fork tworzy proces, pthread operuje na istniejącym ##### exit 1. Exit kończy proces, a pthread_exit() wątek 2. Exit wywołuje funkcje atexit(), a drugi te z pthread_cleanup_push Kiedy wątek wywoła pthread_exit() wszystkie sharowane zasoby nie są zwalniane ##### waitpid 1. Waitpid musi być zawołany, aby zwolnić pamięć w kernelu Pthread_join wołamy, aby zwolnić zasoby w procesie (dopóki nie odczytamy to wisiało gdzieśtam) 2. Waitpid może być zawołany przez rodzica, gdzie drugi może wołać każdy wątek i czekać na inne ##### atexit 1. Atexit nie możemy przekazać argumentów, a w drugim możemy 2. Funkcje są wywoływane przy exit() i return w przypadku atexit(), a w wątkowym wariancie tylko przy pthread_exit() ##### abort możemy abortować inne wątki w wątkowym wariancie, a w procesowym tylko siebie #### Porównaj zachowanie wątków złączalnych (ang. joinable) i odczepionych (ang. detached). **wątek złączalny** - domyślny mode wątku, join potrzebny, aby uwolnić zasoby itd. **wątek odczepiony** - wątek po zakończeniu działania od razu ma zwolnione zasoby, raczej nie obchodzi nas co zwraca funkcja #### Kto odpowiada za usunięcie segmentu stosu z przestrzeni użytkownika, gdy wątek złączalny albo odczepiony zakończy pracę? Jeśli wątek jest <b>odczepiony</b> to w `pthread_exit()` jego stack jest zwalniany (plik pthread_create.c 165 linijka) Inaczej jego stack jest czyszczony w `pthread_join()` - [x] Zadanie 5 ### Zadanie 5 ![](https://i.imgur.com/siHTTUZ.png) #### Jeden z wątków zawoła funkcję fork(2) lub execve(2) lub exit_group(2)? **fork:** - Skopiuje address space i dziecko odziedziczy stan wszystkich mutexów, locków i cond. variable. W dziecku będzie jeden wątek. Dziecko nie wie które locki trzeba odblokować, żeby się nie zablokować. Można to naprawić stosując pthread_atfork() **execve** - replace całego addres space’u. Niszczymy też wszystkie wątki. Jedyny problem to może być to, że nie wiemy tak naprawdę kiedy nasze wątki przestaną istnieć, bo nie zaczekamy na ten co robi execve, to potencjalnie deskryptor pliku trafi do nowego programu **exit_group()** :::danger - Nie wiadomo?? ::: #### Proces zadeklarował procedurę obsługi sygnału «SIGINT», sterownik terminala wysyła do procesu «SIGINT» – w kontekście którego wątek zostanie obsłużony sygnał? - Z APUE: Zostanie wybrany arbitralny wątek, który dostanie sygnał -> randomowo #### Określono domyślną dyspozycję sygnału «SIGPIPE», a jeden z wątków spróbuje zapisać do rury pipe(2), której drugi koniec został zamknięty? - SIGPIPE i wszystkie dead xx #### czytamy w wielu wątkach ze pliku zwykłego korzystając z tego samego deskryptora pliku? - **(APUE 496 pdf)**: Jako, że mamy jeden kursor to musimy używać lseek(), ale wtedy załóżmy, że A robi lseek(); read() i B robi to samo. Załóżmy, że A wykona lseek(), a potem będzie contex switch na B i ten wykona lseek() i read(), potem znów switch na A i ten wykona read(). Tym sposobem przeczytaliśmy ten sam fragment pliku - [ ] Zadanie 6 ### Zadanie 6 ![](https://i.imgur.com/qZ8Ct4Q.png) :::danger Od nich Niby spoko ale duzo tego ja nie deklaruje ### Jaka motywacja za wprowadzeniem poll? Gdy program operuje na dużej liczbie deskryptorów (wiele połączeń przychodzących/wychodzących), nie chcemy zgadywać narażać się na długość blokowanie na jednym z nich (read()/write()), tylko chcemy mieć pewność, że coś jest do zrobienia na jednym lub więcej z nich i przetworzyć to. Istnieją inne opcje do tego problemu, ale nie są tak efektywne lub są skomplikowane ### Czemu lepiej konstruować oprogramowanie w oparciu o poll? Odpytywanie deskryptorów zużywa dużo cykli procesora, ponieważ odpytujemy po kolei nieblokującym `readem` każdy z nich, jeśli są dane to git, jeśli nie to idź dalej. Zazwyczaj tych danych nie będzie, a syscalle są kosztowne Używanie sygnałów po pierwsze jest nieustandaryzowane do końca. Poza tym działają tylko fd terminala i sieciowe. Jeśli dostaniemy sygnał to nawet nie wiemy, który fd ma coś dla nas tylko, że któryś ma ### Na jakich fd działa poll? Regularne pliki, terminale, rury, FIFO, sockety (APUE mówi, że każde działa, ale stosuje się do tych) ### Czemu fd muszą być nieblokujące? (z pollem) 1. Jak poll wróci to mamy pewność, że fd X może być przeczytany <b>raz</b> bez blokowania. To znaczy, że na każdy read przypada 2 polle. Jeśli mamy nieblokujące fd to czytamy ile wlezie i ignorujemy EWOULDBLOCK na końcowym `readzie` 2. Jest jeszcze problem taki, że poll wróci, bo mamy klienta do zaakceptowania. Wtedy klient się rozłącza, a my blokujemy się w accept() ### Jak zapewnić, żeby wywołania nie blokowały się w jądrze? fcntl ustawić flagę O_NONBLOCK na jakimś fd używanym w tych funkcjach ### Co musi być w revents, żeby po powrocie z poll nie było errora o blokowaniu? ![](https://i.imgur.com/GprHQrk.png) ::: - [x] Zadanie 7 ### Zadanie 7 ![](https://i.imgur.com/DEJJ5Sk.png) ```c /* TODO: Start threads and wait for them to finish. */ pthread_t tid[nthreads]; for (int i = 0; i < nthreads; i++) Pthread_create(&tid[i],NULL,thread,NULL); for (int i = 0; i < nthreads; i++) Pthread_join(tid[i],NULL); ``` - [ ] Zadanie 8 ### Zadanie 8 ![](https://i.imgur.com/wV7w823.png) ```c /* TODO: If listening descriptor ready, add new client to the pool. */ if(fds[0].revents & POLLIN){ char host[MAXLINE],port[MAXLINE]; socklen_t clientlen = sizeof(struct sockaddr_storage); struct sockaddr_storage clientaddr; int connfd; connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); Getnameinfo((SA *)&clientaddr, clientlen, host, MAXLINE, port, MAXLINE, 0); addclient(connfd,host,port); nready--; } /* TODO: Echo a text line from each ready connected descriptor. * Delete a client when end-of-file condition was detected on socket. */ int i = 1; while (nready > 0) { if(fds[i].revents & POLLIN){ int n; n = clientread(i); if(n == 0){ delclient(i); i--; } nready--; } i++; } ```