@Sztakler # SO Lista 7 ## Zadanie 1 ![](https://i.imgur.com/acvBaPz.png) ### Definicje ![](https://i.imgur.com/ARZhAeo.png) **Odwzorowanie plików w pamięć** (ang. *memory-mapped file*) -- mapowanie fragmentów pliku lub zasobu plikopodobnego między pamięcią fizyczną a wirtualną w sposób bezpośredni, bajt po bajcie. Strony są ładowane z pliku w sposób leniwy, gdy zajdzie taka potrzeba ze strony programu. **Odwzorowanie pamięci anonimowej** (ang. *anonymous mapping*) -- mapowanie adresów fizycznych na wirtualne, któremu nie odpowiada żaden plik. Strony są inicjalizowane zerami. ![](https://i.imgur.com/G49zSeD.png) **Odwzorowanie prywatne** (ang. *MAP_PRIVATE*) -- wszelkie modyfikacje zmapowanej pamięci są lokalne, widoczne jedynie dla danego procesu, a zmiany nie są zapisywane w pliku. Pozostałe procesy, które odnoszą się do danego zasobu nie widzą tych zmian. Odwzorowanie prywatne jest wykonywane przez **kopiowanie przy zapisie** (ang. *Copy On Write -- COW*). Przy próbie modyfikacji zawartości strony przez pewien proces jądro systemu tworzy kopię strony przypisaną temu procesowi, a wszelkie modyfikacje są wprowadzane tylko na tej kopii. Rozwiązanie to eliminuje problem występujący w sytuacji, gdy kilka procesów próbuje modyfikować zawartość jednego fragmentu pamięci. **Odwzorowanie dzielone** (ang. *MAP_SHARED*) -- modyfikacje wprowadzane na odwzorowanym fragmencie pamięci są widoczne dla wszystkich dzielących go procesów i zapisywane do pliku. ### Jaką wartością jest wypełniana pamięć wirtualna należąca do tych odwzorowań Jest wypełniana zerami. ### Czym różni się odwzorowanie prywatne od dzielonego Odwzorowanie prywatne przypisuje nową kopię fragmentu pamięci dla danego procesu. Wszelkie modyfikacje tego fragmentu są widoczne jedynie dla tego procesu i nie są zapisywane na oryginalnym pliku. W odwzorowaniu dzielonym wszystkie procesy pracują na orginalnym fragmencie i wprowadzone przez nie modyfikacje są zapisywane do pliku. ![](https://i.imgur.com/n1GOgOF.png) ### Czy pamięć obiektów odwzorowanych prywatnie może być współdzielona Zanim proces, który posiada odwzorowanie prywatne, spróbuje wykonać modyfikację danej strony, współdzieli ją z innymi procesami. Dopiero po próbie zapisu jądro dokonuje przypisania mu kopii danej strony. Rodzic może dzielić obiekt odwzorowany prywatnie ze swoim dzieckiem (utworzonym przez `fork`). ![](https://i.imgur.com/ntR5yfo.png) ### Czemu można tworzyć odwzorowania plików urządzeń blokowych w pamięć, ale plików znakowych już nie Urządzenia blokowe są przeszukiwalne, tzn. możemy ustalić pozycję kursora w pliku, iterować się po nim. Urządzenia znakowe nie pozwalają na to, można je czytać tylko sekwencyjnie. ## Zadanie 2 ![](https://i.imgur.com/kvMLntQ.png) [Rozdz. 49] https://mwatler.github.io/unx511/The%20Linux%20Programming%20Interface%20-%20A%20Linux%20and%20UNIX%20System%20Programming%20Handbook.pdf#G40.1106170 ### Scenariusze użycia prywatnych i dzielonych odzworowań plików w pamięć albo do pamięci anonimowej ![](https://i.imgur.com/1NtNmxq.png) * *Private file mapping* -- inicjalizuje mapowany region zawartością pliku. Na początku różne procesy mapujące ten sam plik dzielą ze sobą fizyczne strony w pamięci, ale przy próbie zapisu mechanizm *COW (copy on write)* przydziela zapisującemu procesowi kopię, niewidoczną dla innych procesów. Ta forma mapowania jest używana do inicjalizacji regionu z pamięci za pomocą zawartości pliku, np. przy inicjalizacji segmentów *text* i *data* danego pliku wykonywalnego albo biblioteki współdzielonej. * *Private anonymous mapping* -- każde wywołanie `mmap()` tworzy nowe mapowanie, nie dzielące fizycznych stron z innymi anonimowymi mapowaniami procesu (niekoniecznie tego samego). Chociaż dziecko utworzone przez `fork()` dzieli ze swoim rodzicem mapowania, to mechanizm *COW* zapewnia, że nie będą one widziały nawzajem zmian wprowadzonych w pliku przez drugi proces. Główną rolą tego mapowania jest alokowanie nowej (wypełnionej zerami) przestrzeni w pamięci dla danego procesu, np. podczas użycia `malloc()` przy alokowaniu dużych bloków pamięci. * *Shader file mapping* -- wszystkie procesy mapujące ten sam obszar w pliku współdzielą fizyczne strony pamięci, inicjalizowane zawartością pliku. Wszelkie modyfikacje pliku są w nim zapisywane. Pozwala to na *memory-mapped I/O*, tzn. plik jest załadowany do pamięci wirtualnej procesu, a wszelkie zmiany w pliku są w nim natychmiastowo zapisywane. Można użyć tych własności do implementacji alternatywnych operacji I/O na pliku względem standardowych `read()` i `write()`. Innym użyciem tej metody mapowania jest umożliwienie niespokrewnionym ze sobą procesom dzielenia fragmentu pamięci w celu wykonywania bardzo szybkich operacji IPC. * *Shared anonymous mapping* -- każde wywołanie `mmap()` tworzy nowe mapowanie, które nie dzieli stron pamięci z żadnym innym mapowaniem. W odróżnieniu od prywatnego mapowania anonimowego strony nie są kopiowane przy zapisie, co sprawia, że kiedy dziecko dziedziczy mapowanie przy `fork()`, zaczyna dzielić pewne strony pamięci ze swoim rodzicem i zmiany wprowadzone przez jeden z tych procesów są widczne dla drugiego. Ta metoda mapowania pozwala na operacje IPC, ale wyłącznie między spokrewnionymi procesami. ### Jak utworzyć za pomocą `mmap(2)` dane odwzorowanie Używamy procedury `mmap()`. ```c= #include <sys/mman.h> void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); ``` * `addr` -- adres w pamieci wirtualnej, pod którym ma zostać umieszczone mapowanie (jeśli ustawione jako NULL, wtedy jądro samo wybiera odpowiedni adres; jest to preferowane rozwiązanie), * `length` -- określa długość mapowania w bajtach. Nie musi być wielokrotnością rozmiaru strony, ale jądro i tak zaokrągli tę wartość w górę do najbliższej wielokrotności. * `prot` -- maska przechowująca uprawnienia dla mapowania. Jest to albo `PROT_NONE` albo alternatywa bitowa `PROT_READ`, `PROT_WRITE` i `PROT_EXEC`. * `flags` -- maska bitowa, ustalająca rodzaj mapowania. Może to być `MAP_PRIVATE` lub `MAP_SHARED` dla mapowania prywatnego lub dzielonego. Może być również alternatywą powyższych i innych flag. * `fd` -- deksryptor mapowanego pliku. Ignorowany dla mapowania anonimowego. * `offset` -- ustala pozycję w pliku, od którego zaczyna sie mapowanie. Jeśli ustawiony na 0, a `length` jest równy rozmiarowi pliku, wtedy zmapujemy cały plik. Ignorowany dla mapowania anonimowego. ### Co się dzieje z odwzorowaniem po forku Dziecko dziedziczy mapowanie po swoim rodzicu. W przypadku mapowania prywatnego mechanizm *COW* zapewnia, że oba procesy nie będą widziały wzajemnie zmian wprowadzanych na swoich kopiach mapowania. Przy mapowaniu dzielonym procesy współdzielą zmapowane strony pamięci. ### Jakiego typu odwzorowania tworzy execve https://stackoverflow.com/questions/64048588/why-execve-function-map-private-areas `execve()` tworzy odwzorowania prywatne. Ma to uchronić system przed sytuacją, gdzie dwa osobne programy (rodzic i dziecko po `execve()`) mają dostęp do tej samej pamięci. ### Jak jądro automatycznie zwiększa rozmiar stosu do ustalonego limitu? ![](https://i.imgur.com/VHAe3KX.png) https://unix.stackexchange.com/questions/63742/what-is-automatic-stack-expansion Jeśli proces odwoła się do pamięci stosu, która nie jest zaalokowana, wywoła `pagefault`. Jądro spróbuje rozszerzyć stos tak, żeby następna próba dostępu zakończyła się sukcesem. Jeśli przy rozszerzaniu jądro dotrze do górnego limitu stosu, wtedy nie jest w stanie go rozszerzyć. ![](https://i.imgur.com/h72B06P.png) ![](https://i.imgur.com/sJrihQr.png) Prawdopodobnie za pomocą wywołania systemowego `setrlimit`. https://linux.die.net/man/2/setrlimit ```c= int getrlimit(int resource, struct rlimit *rlim); int setrlimit(int resource, const struct rlimit *rlim); ``` Jeśli wywołanie to przyjmie `resouce = RLIMIT_STACK`, wtedy ustawi `rlimit` jako limit pamięci dla stosu. ![](https://i.imgur.com/mz2g6gm.png) ![](https://i.imgur.com/OvLvJo5.png) ### Kiedy jądro wysyła sygnał `SIGBUS` do procesu https://www.man7.org/linux/man-pages/man2/mmap.2.html ![](https://i.imgur.com/ocE3TXK.png) Jądro wysyła do procesu posiadającego mapowanie w pamięć sygnał `SIGBUS`, kiedy zażąda on dostępu do strony w buforze, która leży za końcem zmapowanego pliku. Jądro może zareagować na ten sygnał, np. przez próbę rozszerzenia pliku. https://mwatler.github.io/unx511/The%20Linux%20Programming%20Interface%20-%20A%20Linux%20and%20UNIX%20System%20Programming%20Handbook.pdf#G40.1106170 ![](https://i.imgur.com/mLByj06.png) ## Zadanie 3 ![](https://i.imgur.com/3aUCykg.png) ### Działanie polecenia ``` $ cat /proc/$(pgrep Xorg)/status | egrep 'Vm|RSS' VmPeak: 1309784 kB VmSize: 1196492 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 208272 kB VmRSS: 131208 kB VmData: 115876 kB VmStk: 132 kB VmExe: 1852 kB VmLib: 112492 kB VmPTE: 720 kB VmSwap: 20744 kB ``` ### Znaczenie poszczególnych pól * *VmPeak* -- największy rozmiar pamięci wirtualnej, * *VmSize* -- obecny rozmiar pamięci wirtualnej, * *VmLck* -- rozmiar pamięci zablokowanej, ![](https://i.imgur.com/35jiJAy.png) * *VmPin* -- rozmiar przypiętej pamięci (strony, które nie mogą zostać przeniesione, ponieważ coś wymaga bezpośredniego dostępu do fizycznej pamięci, na którą wskazują), * *VmHWM* -- maksymalny rozmiar zbioru rezydentnego, * *VmRSS* -- rozmiar zbioru rezydentnego, jest sumą: * RssAnon -- odwzorowana anonimowo, * RssFile -- odwzorowana z pliku, * RssShmem -- dzielona część zbioru rezydentnego, dokładniej dzielone odwzorowania anonimowe, tmpfs (temporary file system) i pamięci dzielonej System V, * *VmData* -- rozmiar danych (nie są to dokładne dane -- lepiej /proc/[pid]/statm), * *VmStk* -- rozmiar stosu (nie są to dokładne dane -- lepiej /proc/[pid]/statm), * *VmExe* -- rozmiar segmentu `text` (nie są to dokładne dane -- lepiej /proc/[pid]/statm), * *VmLib* -- rozmiar kodu biblioteki współdzielonej, * *VmPTE* -- rozmiar tablicy stron, * *VmSwap* -- rozmiar pamięci wirtualnej zeswapowanej przez anonimowe strony prywatne (nie są to dokładne dane -- lepiej /proc/[pid]/statm). ![](https://i.imgur.com/C0x0HHO.png) ### Zbiór roboczy i rezydentny **Zbiór rezydentny** -- strony, które dany proces przechowuje w pamięci operacyjnej. Można uzyskać do nich dostęp bez wywoływania `page fault`. **Zbiór roboczy** -- podzbiór zbioru rezydentnego, potrzebny do wykonywania procesu. Rozmiar zbioru roboczego jest trudny do dokładnego ustalenia. System przybliża jego rozmiar rozmiarem zbioru rezydentnego. ### Łączny rozmiar VmRSS i VmSize ```python= from glob import glob from re import findall path = "/proc/*" directories = glob(path) VmRSS_total_size = 0 VmSize_total_size = 0 for directory in directories: # print(directory) try: file = open(f"{directory}/status", "r") file_content = file.read() except: # print(f"read: {directory}: is not a directory.") continue for line in file_content.split("\n"): if "VmRSS" in line: VmRSS_total_size += int(findall(r'\d+', line)[0]) if "VmSize" in line: VmSize_total_size += int(findall(r'\d+', line)[0]) print(f"Total VmSize: {VmSize_total_size} kB\nTotal VmRSS: {VmRSS_total_size} kB") ``` ``` Wydruk programu: Total VmSize: 879348788 kB Total VmRSS: 8118176 kB Wydruk vmstat -s Pamięć razem: 8058112 K Pamięć użyta: 6087952 K Pamięć aktywna: 5615216 K Pamięć nieaktywna: 1495460 K Pamięć wolna: 310872 K Pamięć buforów: 49048 K Pam. podr. obsz. wymiany: 1610240 K Obszar wymiany razem: 1818096 K Obszar wymiany użyty: 659200 K Obszar wymiany wolny: 1158896 K Cykli CPU użytk. zwykłych: 1801052 Cykli CPU użytk. z nice: 5045 Cykli CPU systemowych: 633648 Cykli CPU bezczynności: 19300685 Cykli CPU oczek. na we/wy: 6364 Cykli CPU w IRQ: 0 Cykli CPU w softirq: 215982 Cykli CPU skradzionych: 0 Stron wczytanych z dysku: 5756720 Stron usuniętych z pam.: 7091221 Stron z pamięci wymiany: 8770 Stron do pamięci wymiany: 172912 Przerwań: 71134535 Przełączeń kontekstu CPU: 388083889 Czas rozruchu: 1637572395 Odgałęzień procesów: 48065 ``` Różnice między napisanym programem, a wynikiem `vmstat -s` wynikają z faktu, że zbiory rezydentne procesu nie zawsze są rozłączne. Część pamięci jest dzielona między procesami i w takim wypadku program w pythonie policzy ją wielokrotnie. ### Zadanie 4 ![](https://i.imgur.com/ao7WcY3.png) ### Jak jądro obsługuje błąd stronicowania ![](https://i.imgur.com/KbdU3Ww.png) Błąd obsługi stronicowania występuje w trzech sytuacjach: * próbujemy dostać się do nieistniejącej strony (segfault), * próbujemy dostać się do strony, która nie istnieje w RAMie (page fault), * próbujemy dostać się do strony, do której nie mamy uprawnień (segfault). Algorytm sprawdza, który z powyższych przypadków ma miejsce. Jeśli jest to pierwszy z nich, wtedy jądro nie jest w stanie naprawić tego błędu i zabija proces sygnałem `SIGSEGV`. W drugim scenariuszu jądro wykrywa `pagefault` i próbuje wydobyć z dysku do RAMu odpowiednią stronę. Postępuje w sposób omawiany na wcześniejszych ćwiczeniach. Ostatni scenariusz również nie może zostać naprawiony. Proces zostaje zabity przez `SIGSEGV`. `SIGSEGV` występuje w dwóch smakach: ![](https://i.imgur.com/2UwQfJo.png) https://elixir.bootlin.com/linux/latest/source/include/uapi/asm-generic/siginfo.h#L224 ![](https://i.imgur.com/kEPOPRn.png) ### Jakie dane procesor musi dostarczyć, żeby umożliwić obsługę błąd stronicowania Procesor przekaże jądru cztery wartości: ``` `fault_addr` -- adres strony, którą próbowaliśmy odczytać, `fault_pc` -- adres instrukcji, która wywołała błąd, `fault_size` -- ile bajtów próbowano odczytać, `_prot` -- maska zawierająca uprawnienia, z którymi próbowano dostępu. ``` ### Do czego służą struktury `mm_struct::pgd` i `mm_struct::mmap` z `include/linux/mm_types.h` https://elixir.bootlin.com/linux/latest/source/include/linux/mm_types.h `mmap` -- zawiera listę wszystkich segmentów odwzorowanych w przestrzeń adresową procesu (vm_area_struct). Każda struktura w tej liście zawiera adres początku i końca zmapowanego fragmentu, jego flagi, uprawnienia, itd. `pgd` -- wskaźnić na tablicę stron procesu. ### Kiedy jądro wyśle procesowi sygnał `SIGSEGV` z kodem `SEGV_MAPPER`, a kiedy z kodem `SEGV_ACCERR` ![](https://i.imgur.com/2UwQfJo.png) ``` There are two common kinds of SEGV, which is an error that results from an invalid memory access: A page was accessed which had the wrong permissions. E.g., it was read-only but your code tried to write to it. This will be reported as SEGV_ACCERR. A page was accessed that is not even mapped into the address space of the application at all. This will often result from dereferencing a null pointer or a pointer that was corrupted with a small integer value. This is reported as SEGV_MAPERR. ``` ### Kiedy występują pomniejsze usterki strony (ang. *minor page fault*), a kiedy poważne usterki strony (ang. *major page fault*) Poważna usterka występuje w sytuacji, gdy strona, do której usiłujemy uzyskać dostęp nie znajduje się w pamięci. Jądro musi znaleźć wolne miejsce w pamięci (lub je zwolnić), a następnie wczytać stronę do pamięci, dodać dotyczące jej wpis w MMU i oznaczyć ją jako wczytaną. Pomniejsze usterki pojawiają sie, gdy strona jest już załadowana do pamięci w momencie pojawienia się błędu, ale nie została jeszcze oznaczona w MMU (Memory Management Unit) jako wczytana. Jądro naprawia tę usterkę poprzez dodanie wpisu w MMU o tej stronie i oznaczenie jej jako wczytaną. Nie musi wczytywać strony do pamięci. ### Rola bufora stron w systemie (ang. *page cache*) Kąkuter nie lubi marnować zasobów, dlatego woli przeznaczyć nieużywany RAM na bufor stron. Dyski są względnie powolne, operacje odczytu i zapisu trwają nieporównywalnie dłużej niż w przypadku RAMu, stąd pomysł na przechowywanie części już wczytanych danych z dysku w pamięci. By przyspieszyć odczyt i zapis danych na dysku część stron jest przechowywanych we fragmencie RAMu -- buforze stron. Wszystkie zapisy są zapisywane tylko w buforze, a informacja o zmodyfikowane strony są oznaczone bitem `dirty`. Zapis na dysk następuje przy `sync()` lub `fsync()`, *brudne* strony są do niego przepisywane. Takie rozwiązanie przyspiesza odczyty i zapisy z danej strony, ponieważ nie ma już konieczności wydobywania jej za każdym razem z dysku, co jest bardzo powolną operacją, ale z RAMu -- znacznie szybszej pamięci. ![Uploading file..._e14w5lxxg]() ## Zadanie 5 ![](https://i.imgur.com/uQNibxH.png) ### Co jądro przechowuje w strukturze `vm_area_struct` ![](https://i.imgur.com/AEKDDES.png) https://linux-kernel-labs.github.io/refs/heads/master/labs/memory_mapping.html * vm_start, vm_end - the beginning and the end of the memory area, respectively (these fields also appear in /proc/<pid/maps); * vm_file - the pointer to the associated file structure (if any); * vm_pgoff - the offset of the area within the file; * vm_flags - a set of flags `unsigned long vm_flags; /* Flags, see mm.h. */`; * vm_ops - a set of working functions for this area * vm_next, vm_prev - the areas of the same process are chained by a list structure * `pgprot_t vm_page_prot; /* Access permissions of this VMA. */` protection flags for this mapping ### Jak jądro zmodyfikuje `mm_struct` w trakcie pierwszego odczytu ze strony `p` należącej do `D`, a jak w trakcie póżniejszego pierwszego zapisu do `p`. Strony są odczytywywane leniwie, więc jeżeli dana strona nie występuje w RAMie, wtedy wystąpi `pagefault` i jądro spróbuje wydobyć stronę z dysku. Pierwszy zapis zakończy się `pagefaultem`, ponieważ nowy proces posiada tylko uprawnienia `read only`. Jądro następnie musi sprawdzić, czy dany segment na dysku ma uprawnienia `read` i `write` oraz ustawioną flagę `copy on write`. Jeśli tak, wtedy tworzy kopię strony i przypisuje ją do procesu. Wykonanie zapisu do kopii zakończy się powodzeniem, ponieważ będzie miała prawidłowe uprawnienia -- `read` i `write`. ### Co jądro musi zrobić z tablicą stron procesu podczas `fork()` Wszystkie strony tablicy stron rodzica zostają przepisane do dziecka, ale z uprawnieniami `read only`. Oba procesy ustawiają na stronach flagę `copy on write`. ### Czemu jądro nie musi kopiować tablicy stron z rodzica do dziecka Skoro mamy odwzorowanie prywatne, zatem procesy mogą początkowo współdzielić tablice stron odwzorowania. Zostaną one zamienione na nowe dla danego procesu dopiero, gdy spróbuje on wprowadzić zmiany w danej stronie (*copy on write*). ![](https://i.imgur.com/o90d021.png) ## Zadanie 6 ![](https://i.imgur.com/I0AmmFZ.png) ### Definicje **Stronicowanie na żądanie** (ang. *demang paging*) -- strony są ładowane z dysku do RAMu dopiero, gdy dany proces spróbuje odczytać lub zapisać dane do tej strony. ### Czy mamy gwarancję, że program nie zobaczy modyfikacji zawartości pliku dokonanych po utworzeniu prywatnego odwzorowania pliku w pamięć Nie mamy takiej gwarancji. Pewien fragment mapowanego pliku może nie zostać od razu wciągnięty do pamięci. Jeśli w tym czasie wprowadzimy w nim zmiany, a następnie spróbujemy sprowadzić z dysku, wtedy program zobaczy jego zmodyfikowaną wersję. ### Co złego mogłoby się stać, gdyby system operacyjny pozwolił modyfikować plik wykonywalny, który jest uruchomiony Wprowadzanie zmian w, np. sekcji `.text` pliku wykonywalnego (np. w opisanym wyżej scenariuszu) może skutkować nieoczekiwanymi, trudnymi do zlokalizowania błędami. Co więcej, takie błędy mogą pojawić się dopiero, gdy program załaduje do pamięci zmodyfikowany fragment z dysku, co w niektórych przypadkach może stać się długo po wprowadzeniu zmian w pliku. Prawdopodobnie napotkamy standardowy zestaw problemów, tzn: * crashowanie programu, * nieoczekiwane zachowanie programu, * podatność na błędy i ataki ze strony użyszkodników. Tu jest coś napisane: https://lwn.net/Articles/866493/ https://unix.stackexchange.com/questions/187931/modifying-binary-during-execution https://unix.stackexchange.com/questions/138214/how-is-it-possible-to-do-a-live-update-while-a-program-is-running https://unix.stackexchange.com/questions/88487/what-happens-if-you-edit-a-script-during-execution ## Zadanie 7 ![](https://i.imgur.com/VkamQ8c.png) ### Zmodyfikowany kod z tworzeniem podprocesów ```c= #include "csapp.h" static void InsertionSort(long table[], size_t left, size_t right) { for (size_t pivot = left + 1; pivot <= right; pivot++) { size_t insert = left; while ((insert < pivot) && (table[insert] < table[pivot])) insert++; if (insert < pivot) { size_t n = pivot - insert; long tmp = table[pivot]; memmove(&table[insert + 1], &table[insert], n * sizeof(long)); table[insert] = tmp; } } } static void SwapElem(long table[], size_t i, size_t j) { long tmp = table[i]; table[i] = table[j]; table[j] = tmp; } static size_t Partition(long table[], size_t begin, size_t end, long pivot) { size_t left = begin; size_t right = end; while (left < right) { while ((left < right) && (table[left] < pivot)) left++; while ((left < right) && (table[right] >= pivot)) right--; if (left < right) SwapElem(table, left, right); } return left; } #define INSERTSORT_MAX 32 #define FORKSORT_MIN (1L << 18) static int QuickSort(long table[], size_t left, size_t right) { pid_t pid_left = -1, pid_right = -1, pid = -1; /* TODO: If there is more to sort than FORKSORT_MIN start a subprocess. */ if (right - left >= FORKSORT_MIN) { pid = Fork(); if (pid > 0) return pid; } if (left < right) { if (right - left <= INSERTSORT_MAX) { InsertionSort(table, left, right); } else { size_t pivot = left + random() % (right - left + 1); size_t split = Partition(table, left, right, table[pivot]); if (left == split) { SwapElem(table, left, pivot); split++; } else { pid_left = QuickSort(table, left, split - 1); } pid_right = QuickSort(table, split, right); /* TODO: Wait for possible children and exit if created a subprocess. */ if (pid_left != -1) // has left child Waitpid(pid_left, NULL, 0); if (pid_right != -1) // has right child Waitpid(pid_right, NULL, 0); if (!pid) // created a subprocess exit(0); } } return pid; } #define NELEMS (1L << 26) int main(void) { /* Table size in bytes must be divisible by page size. */ size_t size = NELEMS * sizeof(long); assert((size & (getpagesize() - 1)) == 0); /* Initialize PRNG seed. */ struct timeval tv; gettimeofday(&tv, NULL); srandom(tv.tv_usec); /* TODO: Allocate table... */ /* Creating a table using mmap with permission to read and * write, shared between processes and located in anonymous * memory */ long* table = Mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0); /* ... and fill it in with random elements. */ for (size_t i = 0; i < NELEMS; i++) table[i] = random(); /* Sort it using hybrid algorithm... */ if (QuickSort(table, 0, NELEMS - 1) > 0) Wait(NULL); /* ... and verify if the table is actually sorted. */ long prev = LONG_MIN; for (size_t i = 0; i < NELEMS; i++) { assert(prev <= table[i]); prev = table[i]; } return 0; } ``` ### Czasy dla wersji bez podprocesów ``` $ /usr/bin/time -p ./forksort real 20.55 user 20.07 sys 0.48 ``` ### Czasy dla wersji z podprocesami ``` $ /usr/bin/time -p ./forksort real 6.26 user 27.01 sys 1.39 ``` ### Prawo Amdahla ![](https://i.imgur.com/uTJxmch.png) Prawo Amdahla określa granicę wzrostu prędkości przy wprowadzeniu implementacji równoległej algorytmu, względem implementacji szeregowej. Stosując powyższy wzór zauważamy, że dla P (procent programu, który może zostać zrównoleglony) i przy użyciu N procesorów możemy zauważyć wzrost: Zauważmy, że dla P różnego od 1 (pewna część programu nie może zostać zrównoleglona) wyrażenie będzie dążyło do wartości $\frac{1}{1-P}$ przy wzroście liczby procesorów. Istnieje więc pewna granica przyspieszenia, której nie jesteśmy w stanie przekroczyć i wynika ona z faktu, że pewna część programu nie daje się zrównoleglić. Z tego powodu w naszym eksperymencie uzyskaliśmy przyspieszenie nieproporcjonalne do liczby zastosowanych podprocesów. ### Których podprocedur nie da się zrównoleglić Procedura `Partition` oraz `InsertSort` nie nadają się do zrównoleglenia, ze względu na swój charakter (muszą operować na wszystkich elementach podtablicy jednocześnie). ## Zadanie 8 ![](https://i.imgur.com/Yp4wfwF.png) ```c= #include "csapp.h" #define ENT_LENGTH 48 #define DB_MODE (S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH) #define DB_PROT (PROT_READ | PROT_WRITE) typedef enum { DB_QUERY, DB_INSERT } op_t; typedef char entry_t[ENT_LENGTH]; typedef struct db { entry_t *entry; ssize_t size; char *name; } db_t; static bool db_action(db_t *db, const char *str, op_t op) { /* Do not insert empty strings. Database doesn't store them. */ if (*str == '\0') return op == DB_INSERT; size_t len = strnlen(str, ENT_LENGTH); int chain_len = max((db->size >> 14) - 1, 3); uint32_t chain = jenkins_hash(str, len, HASHINIT); for (int c = 0; c < chain_len; c++) { size_t idx = (chain + c) % db->size; if (!db->entry[idx][0] && op == DB_INSERT) { strncpy(db->entry[idx], str, ENT_LENGTH); return true; } if (strncmp(str, db->entry[idx], ENT_LENGTH) == 0) return true; } return false; } #define db_query(db, key) db_action(db, key, DB_QUERY) #define db_maybe_insert(db, key) db_action(db, key, DB_INSERT) /* Open (`size` = 0) or create (`size` > 0) database from `name` file. */ static void db_open(db_t *db, const char *name, size_t size) { assert(powerof2(size)); int fd = Open(name, O_RDWR | O_CREAT | (size ? O_EXCL : 0), DB_MODE); if (size == 0) { struct stat sb; Fstat(fd, &sb); size = sb.st_size / sizeof(entry_t); if (size == 0) size = 1; } /* TODO: Setup DB structure, set file size and map the file into memory. Inform OS that we're going to read DB in random order. */ size_t db_byte_size = size * sizeof(entry_t); Ftruncate(fd, db_byte_size); db->name = strdup(name); db->size = size; db->entry = Mmap(NULL, db_byte_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); // https://man7.org/linux/man-pages/man2/madvise.2.html // Informujemy jądro, że będziemy odwoływać się w sposób losowy do pamięci // o rozmiarze db->size * sizeof(entry_t) rozpoczynającą się pod adresem db->entry Madvise(db->entry, db_byte_size, MADV_RANDOM); Close(fd); } /* Remove DB memory mapping and release associated memory. */ static void db_close(db_t *db) { Munmap(db->entry, db->size * sizeof(entry_t)); free(db->name); } /* Attempt to increase size of database. */ static bool db_rehash(db_t *db, size_t new_size) { assert(powerof2(new_size)); /* Create new database. */ db_t new[1]; char *name = alloca(strlen(db->name) + sizeof(".reshash") + 1); strcpy(name, db->name); strcat(name, ".rehash"); db_open(new, name, new_size); /* Copy everything from old database to new database. */ /* TODO: Inform OS that we're going to read DB sequentially. */ size_t db_byte_size = db->size * sizeof(entry_t); Madvise(db->entry, db_byte_size, MADV_SEQUENTIAL); for (size_t i = 0; i < db->size; i++) { if (!db_maybe_insert(new, db->entry[i])) { /* Oops... rehashing failed. Need to increase db size and try again. */ /* TODO: Remove new database, since rehashing failed. */ db_close(new); Unlink(name); return false; } } /* TODO Replace old database with new one, remove old database. */ Rename(new->name, db->name); // zastępuje stary plik Munmap(db->entry, db_byte_size); db->entry = new->entry; db->size = new->size; free(new->name); return true; } /* Insert key into database and possibly increase DB size. */ static void db_insert(db_t *db, const char *str) { while (!db_maybe_insert(db, str)) { size_t size = db->size; do { size *= 2; fprintf(stderr, "db_rehash: %ld -> %ld\n", db->size, size); } while (!db_rehash(db, size)); } } /* Read a key from buffer and perform `mode` operation of the database. */ static char *consume_line(char *buf, db_t *db, op_t mode) { /* Terminate string at new line character. */ char *end = strchr(buf, '\n'); if (end) { if (buf < end && end[-1] == '\r') end[-1] = '\0'; *end++ = '\0'; } /* Skip if line is empty. */ if (*buf) { if (mode == DB_INSERT) db_insert(db, buf); else if (mode == DB_QUERY) puts(db_query(db, buf) ? "YES" : "NO"); } /* Return pointer to next line or NULL. */ return end; } static void doit(const char *path, op_t mode) { db_t db; db_open(&db, path, 0); /* If input file is a terminal device then use standard reading technique. */ /* TODO: Use fstat instead to handle pipes correctly. */ struct stat st; Fstat(STDIN_FILENO, &st); // zwróci informacje o pliku do struktury buf int is_regular_file = S_ISREG(st.st_mode); int is_block_device = S_ISBLK(st.st_mode); // if (isatty(STDIN_FILENO)) { if (!(is_regular_file || is_block_device)) { char buf[ENT_LENGTH + 1]; while (fgets(buf, ENT_LENGTH + 1, stdin)) consume_line(buf, &db, mode); } else { /* TODO: Map stdin into memory, and read it line by line. */ char* buf = Mmap(NULL, st.st_size, PROT_READ | PROT_WRITE, MAP_PRIVATE, STDIN_FILENO, 0); char* next = consume_line(buf, &db, mode); while (next != NULL) next = consume_line(next, &db, mode); Munmap(buf, st.st_size); } db_close(&db); } static noreturn void usage(int argc, char **argv) { app_error("Usage: %s [-i|-q] [FILE]", argv[0]); } int main(int argc, char **argv) { op_t mode = -1; int opt; while ((opt = getopt(argc, argv, "iq")) != -1) { if (opt == 'i') mode = DB_INSERT; else if (opt == 'q') mode = DB_QUERY; else usage(argc, argv); } if (optind >= argc || mode == -1) usage(argc, argv); doit(argv[optind], mode); return 0; } ```