Wprowadzenie do systemów operacyjnych
Cześć, Habr! Chcę przedstawić Wam serię artykułów-przekładów jednej ciekawej literatury — OSTEP. W tym materiale omawiane są dość dogłębnie zasady działania systemów operacyjnych typu unix, a mianowicie — praca z procesami, różnymi planistami, pamięcią i innymi podobnymi komponentami, które składają się na nowoczesny system operacyjny. Oryginał wszystkich materiałów można zobaczyć tutaj . Proszę pamiętać, że tłumaczenie zostało wykonane w sposób nieprofesjonalny (dosyć luźno), ale mam nadzieję, że ogólny sens zachowałem.
Laboratoria dotyczące tego tematu można znaleźć tutaj:
Inne części:
Możecie również zajrzeć na mój kanał w =)
Alarm! do tej lekcji jest laboratorium! zobacz
API Procesu
Rozważmy przykład tworzenia procesu w systemie UNIX. Odbywa się to poprzez dwa wywołania systemowe. fork() i exec().
Wywołanie fork()

Rozważmy program, który wykonuje wywołanie fork(). Wynik jego wykonania będzie następujący.

Po pierwsze, wchodzimy do funkcji main() i wykonujemy wyświetlenie ciągu na ekranie. Ciąg zawiera identyfikator procesu, który w oryginale nazywa się PID lub identyfikator procesu. Ten identyfikator jest wykorzystywany w UNIX do odniesienia się do procesu. Następnym poleceniem będzie wywołanie fork(). W tym momencie tworzy się praktycznie dokładna kopia procesu. Dla systemu operacyjnego wygląda to tak, jakby w systemie uruchomione były dwie kopie tej samej programu, które następnie wyjdą z wykonania funkcji fork(). Nowo utworzony proces-potomka (w odniesieniu do procesu-rodzica, który go stworzył) nie będzie już wykonywany, zaczynając od funkcji main(). Należy pamiętać, że proces-potomka nie jest dokładną kopią procesu-rodzica, w szczególności ma swoją własną przestrzeń adresową, własne rejestry, wskaźnik na instrukcje wykonawcze i tym podobne. Tak więc, wartość zwracana przez wywołanie funkcji fork() będzie różna. W szczególności proces-rodzic otrzyma jako zwrot wartość PID procesu dziecka, a dziecko otrzyma wartość równą 0. Na podstawie tych kodów zwrotu można już oddzielić procesy i zmusić każdy z nich do wykonania swojej pracy. Przy tym wykonanie tego programu nie jest ściśle określone. Po podziale na dwa procesy, system operacyjny zaczyna monitorować je, a także planować ich pracę. W przypadku wykonania na procesorze jednordzeniowym, pracę kontynuuje jeden z procesów, w tym przypadku — rodzic, a następnie kontrolę uzyska proces-potomka. Przy ponownym uruchomieniu sytuacja może się zmienić.
Wywołanie wait()

Rozważmy następujący program. W tym programie dzięki obecności wywołania wait() proces-rodzic zawsze będzie czekał na zakończenie pracy procesu-potomka. W takim przypadku otrzymamy ściśle określony wynik tekstu na ekranie.

Wywołanie exec()

Rozważmy wywołanie exec(). To wywołanie systemowe jest przydatne, gdy chcemy uruchomić zupełnie inny program. Tutaj wywołamy execvp() Aby uruchomić program wc, który jest programem do liczenia słów. Co się dzieje, gdy wywołujemy exec()? W tym wywołaniu przekazywane są jako argumenty nazwa pliku wykonywalnego i pewne parametry. Następnie następuje załadowanie kodu i statycznych danych z tego pliku wykonywalnego oraz nadpisanie własnego segmentu z kodem. Pozostałe obszary pamięci, takie jak stos i sterta, są ponownie inicjalizowane. Po czym system operacyjny po prostu wykonuje program, przekazując mu zestaw argumentów. Tak więc, nie utworzyliśmy nowego procesu, po prostu przekształciliśmy bieżąco uruchomiony program w inny uruchomiony program. Po wykonaniu wywołania exec() w procesie potomnym odnosi się wrażenie, że pierwotny program w zasadzie nie został uruchomiony.
To skomplikowanie uruchamiania jest całkowicie normalne dla powłoki Unix, i pozwala tej powłoce wykonywać kod po wywołaniu fork(), ale przed wywołaniem exec(). Przykładem takiego kodu może być dostosowanie środowiska powłoki do potrzeb uruchamianego programu przed jego bezpośrednim uruchomieniem.
Powłoka — to zaledwie program użytkownika. Pokazuje ci linię poleceń i czeka, aż coś tam wpiszesz. W większości przypadków, jeśli wpiszesz tam nazwę programu, powłoka znajdzie jego lokalizację, wywoła metodę fork(), a następnie, aby utworzyć nowy proces, wywoła jeden z typów exec() i poczeka na jego wykonanie za pomocą wywołania wait(). Gdy proces potomny zakończy się, powłoka wróci z wywołania wait() i ponownie wyświetli linię poleceń, czekając na wpisanie następnego polecenia.
Rozdzielenie fork() i exec() pozwala powłoce robić następujące rzeczy, na przykład:
wc file > new_file.
W tym przykładzie wyjście programu wc jest przekierowane do pliku. Sposób, w jaki powłoka to osiąga, jest dość prosty — przy tworzeniu procesu potomnego przed wywołaniem exec(), powłoka zamyka standardowy strumień wyjściowy i otwiera plik new_file, tak więc całe wyjście z później uruchamianego programu wc zostanie przekierowane do pliku zamiast na ekran.
Rury Unix są realizowane w podobny sposób, z różnicą, że wykorzystują wywołanie pipe(). W tym przypadku strumień wyjściowy procesu jest podłączany do kolejki rury, znajdującej się w jądrze, do której również będzie podłączony strumień wejściowy innego procesu.
Źródło: habr.com
