Wprowadzenie do systemów operacyjnych
Cześć, Habr! Chciałbym zaprezentować Wam serię artykułów-przekładów jednej ciekawiej według mnie literatury — OSTEP. W tym materiale omawiane są dość szczegółowo działania systemów operacyjnych podobnych do Unix, a mianowicie — praca z procesami, różnymi planistami, pamięcią i innymi podobnymi komponentami, które tworzą nowoczesny system operacyjny. Oryginał wszystkich materiałów można zobaczyć tutaj . Proszę mieć na uwadze, że tłumaczenie zostało wykonane nieprofesjonalnie (dość dowolnie), ale mam nadzieję, że ogólny sens został zachowany.
Laboratoria z tego przedmiotu można znaleźć tutaj:
- oryginał:
- oryginał:
- moja osobista adaptacja:
Inne części:
Możecie również zaglądać do mnie na kanał w =)
Rozważmy najbardziej fundamentalną abstrakcję, którą system operacyjny zapewnia użytkownikom: proces. Definicja procesu jest dość prosta — to działająca program. Program sam w sobie jest martwą rzeczą, znajdującą się na dysku — jest to zbiór instrukcji i być może jakichś statycznych danych, czekających na moment uruchomienia. To właśnie system operacyjny bierze te bajty i uruchamia je, przekształcając program w coś użytecznego.
Najczęściej użytkownicy chcą uruchamiać więcej niż jeden program jednocześnie, na przykład można uruchomić na laptopie przeglądarkę, grę, odtwarzacz multimediów, edytor tekstu i tym podobne. Faktycznie typowy system może uruchamiać dziesiątki, a nawet setki procesów jednocześnie. Ten fakt sprawia, że system jest prostszy w użyciu, nigdy nie trzeba martwić się o to, czy CPU jest dostępne, po prostu uruchamiasz programy.
Stąd wynika problem: jak zapewnić iluzję wielu CPU? Jak system operacyjny może stworzyć iluzję praktycznie nieskończonej liczby CPU, nawet jeśli masz tylko jeden fizyczny CPU?
System operacyjny tworzy tę iluzję poprzez wirtualizację CPU. Uruchamiając jeden proces, potem go zatrzymując, uruchamiając drugi proces itd., system operacyjny może utrzymywać iluzję istnienia wielu wirtualnych CPU, chociaż w rzeczywistości może to być jeden lub kilka fizycznych procesorów. Taka technika nazywa się podziałem zasobów CPU w czasieTa technika pozwala użytkownikom uruchamiać tyle równoczesnych procesów, ile tylko zechcą. Kosztem tego rozwiązania jest wydajność – ponieważ jeśli CPU dzieli kilka procesów, każdy proces będzie przetwarzany wolniej.
Aby wprowadzić wirtualizację CPU, a zwłaszcza, aby to zrobić dobrze, system operacyjny potrzebuje zarówno niskopoziomowego, jak i wysokopoziomowego wsparcia. Wsparcie niskopoziomowe nazywa się mechanizmami — to niskopoziomowe metody lub protokoły, które realizują potrzebną część funkcjonalności. Przykładem takiej funkcjonalności jest zmiana kontekstu, która umożliwia systemowi operacyjnemu zatrzymywanie jednego programu i uruchamianie na procesorze innego. Takie podzielenie czasu jest realizowane we wszystkich nowoczesnych systemach operacyjnych.
Na szczycie tych mechanizmów znajduje się pewna logika zakodowana w systemie operacyjnym w formie "polityk". Polityka — to pewien algorytm podejmowania decyzji przez system operacyjny. Takie polityki, na przykład, decydują, który program należy uruchomić (z listy poleceń) w pierwszej kolejności. Tak więc, na przykład, to zadanie rozwiąże polityka zwana planistą (scheduling policy) i przy wyborze decyzji będzie kierować się takimi danymi jak: historia uruchamiania (jaki program był uruchamiany najdłużej w ostatniej minucie), jakie obciążenie generuje dany proces (jakie rodzaje programów były uruchamiane), metryki wydajności (czy system jest zoptymalizowany do interaktywnej obsługi czy do przepustowości) i tak dalej.
Abstrakcja: proces
Abstrakcja działającego programu, wykonywana przez system operacyjny to to, co nazywamy proces. Jak już wcześniej powiedziano, proces to po prostu działający program w każdym danym momencie. Program, za pomocą którego możemy uzyskać zbiorcze informacje z różnych zasobów systemu, do których ta program ma dostęp lub które są dotykane przez ten program w trakcie jego wykonywania.
Aby zrozumieć składniki procesu, należy zrozumieć stany systemu: co program może czytać lub zmieniać podczas swojej pracy. W każdej chwili trzeba rozumieć, które elementy systemu są ważne dla wykonania programu.
Jednym z oczywistych elementów stanu systemu, które obejmuje proces, jest pamięć. Instrukcje są umieszczane w pamięci. Dane, które program odczytuje lub zapisuje, również znajdują się w pamięci. W ten sposób pamięć, którą proces może adresować (tak zwana przestrzeń adresowa), jest częścią procesu.
Również częścią stanu systemu są rejestry. Wiele instrukcji ma na celu zmianę wartości rejestrów lub odczytanie ich wartości, dlatego rejestry również stają się istotną częścią działania procesu.
Należy zauważyć, że stan maszyny składa się także z kilku specjalnych rejestrów. Na przykład, IP — wskaźnik instrukcji — wskazuje na instrukcję, którą program wykonuje w danym momencie. Jest jeszcze wskaźnik stosu oraz związany z nim wskaźnik ramki, które są wykorzystywane do zarządzania: parametrami funkcji, zmiennymi lokalnymi i adresami powrotu.
Wreszcie, programy często zwracają się do ROM (pamięci stałej). Tego rodzaju informacje o 'I/O' (wejściu/wyjściu) powinny obejmować listę plików otwartych przez proces w danym momencie.
API Procesu
Aby lepiej zrozumieć działanie procesu, przestudiujmy przykłady wywołań systemowych, które powinny być zawarte w każdym interfejsie systemu operacyjnego. Te API w tej czy innej formie są dostępne w każdym systemie operacyjnym.
● Utwórz (tworzenie): W systemie operacyjnym musi istnieć metoda pozwalająca na tworzenie nowych procesów. Kiedy wpisujesz polecenie w terminalu lub uruchamiasz aplikację poprzez podwójne kliknięcie na ikonie, wysyłane jest zapytanie do systemu operacyjnego o utworzenie nowego procesu i uruchomienie wskazanego programu.
● Usunięcie: Skoro istnieje interfejs do tworzenia procesu, system operacyjny musi również umożliwiać wymuszone usunięcie procesu. Większość programów naturalnie będzie uruchamiana i kończona sama, w miarę ich wykonania. W przeciwnym razie użytkownik chciałby mieć możliwość ich zakończenia, a więc interfejs do zatrzymywania procesu byłby przydatny.
● Oczekiwanie (oczekiwanie): Czasami warto poczekać na zakończenie procesu, dlatego zapewniono pewne interfejsy umożliwiające oczekiwanie.
● Kontrola dodatkowa (różne zarządzanie): Poza zbijaniem i czekaniem na proces istnieją inne różnorodne metody kontroli. Na przykład większość systemów operacyjnych oferuje możliwość zamrożenia procesu (zatrzymanie jego wykonania na pewien czas) oraz późniejsze wznowienie (kontynuacja wykonania)
● Status (stan): Istnieją różne interfejsy do uzyskiwania informacji o statusie procesu, takie jak czas jego działania lub w jakim stanie obecnie się znajduje.

Tworzenie procesu: szczegóły
Jedną z interesujących rzeczy jest, jak programy przekształcają się w procesy. Zwłaszcza, jak system operacyjny uruchamia i aktywuje program. Jak konkretnie tworzy się proces.
Na początku system operacyjny musi załadować kod programu i dane statyczne do pamięci (do przestrzeni adresowej procesu). Programy są zazwyczaj przechowywane na dysku lub solid-state drive w pewnym formacie wykonywalnym. Dlatego proces ładowania programu i danych statycznych do pamięci wymaga od systemu operacyjnego możliwości odczytu tych bajtów z dysku i umiejscowienia ich gdzieś w pamięci.
Wczesne systemy operacyjne ładowały proces w sposób niecierpliwy (eagerly), co oznacza, że kod był ładowany do pamięci w całości zanim program został uruchomiony. Nowoczesne systemy operacyjne robią to leniwie (lazily), czyli ładując kawałki kodu lub danych jedynie wtedy, gdy są one potrzebne programowi podczas jego wykonywania.
Po tym, jak kod i dane statyczne zostały załadowane do pamięci, system operacyjny musi wykonać jeszcze kilka rzeczy przed uruchomieniem procesu. Należy przydzielić pewną ilość pamięci na stos. Programy używają stosu do zmiennych lokalnych, parametrów funkcji i adresów powrotu. System operacyjny przydziela tę pamięć i oddaje ją procesowi. Stos może być także przydzielany z pewnymi argumentami, zwłaszcza wypełnia parametry funkcji main(), na przykład tablicą argc i argv.
System operacyjny może również przydzielić pewną ilość pamięci na stertę (heap) programu. Sterta jest używana przez programy do jawnie żądanych, dynamicznie przydzielonych danych.. Programy żądają tej przestrzeni, wywołując funkcję malloc() i jawnie oczyszczają, wywołując funkcję free(). Stos heap jest potrzebny dla takich struktur danych jak: powiązane listy, tablice haszujące, drzewa i inne. Na początku przydzielana jest mała ilość pamięci, ale z biegiem czasu, w trakcie działania programu, heap może poprosić o większą ilość pamięci przez wywołanie API biblioteki malloc(). System operacyjny jest zaangażowany w proces przydzielania większej ilości pamięci, aby pomóc w zaspokajaniu tych żądań.
System operacyjny będzie także wykonywał zadania inicjalizacji, w szczególności te związane z wejściem-wyjściem. Na przykład w systemach UNIX każdy proces domyślnie ma 3 otwarte deskryptory plików dla standardowego strumienia wejścia, wyjścia i błędów. Te deskryptory umożliwiają programom odczyt danych z terminala oraz wyświetlanie informacji na ekranie.
W ten sposób, ładując kod i dane statyczne do pamięci, tworząc i inicjalizując stos oraz wykonując inne prace związane z realizacją zadań wejścia-wyjścia, system operacyjny przygotowuje platformę do wykonania procesu. Ostatecznie pozostaje ostatnie zadanie: uruchomić program przez jego punkt wejścia, nazywany funkcją main(). Przechodząc do wykonania funkcji main(), system operacyjny przekazuje sterowanie procesorowi do nowo utworzonego procesu, w ten sposób program zaczyna być wykonywany.
Stan procesu
Teraz, gdy mamy pewne zrozumienie, czym jest proces i jak jest tworzony, wymieńmy stany procesu, w jakich może się znajdować. W najprostszej formie proces może znajdować się w jednym z tych stanów:
● Running. W stanie uruchomionym proces jest wykonywany na procesorze. Oznacza to, że dochodzi do wykonywania instrukcji.
● Gotowy. W stanie gotowości proces jest gotów do uruchomienia, ale z jakichś powodów system operacyjny nie wykonuje go w danym momencie.
● Zablokowany. W stanie zablokowanym proces wykonuje jakieś operacje, które uniemożliwiają mu bycie gotowym do wykonania, dopóki nie wystąpi jakieś zdarzenie. Typowym przykładem jest sytuacja, kiedy proces inicjuje operację wejścia-wyjścia, staje się zablokowany, a w ten sposób inny proces może używać procesora.

Można wyobrazić sobie te stany jako graf. Jak możemy zobaczyć na obrazku, stan procesu może zmieniać się między RUNNING a READY w zależności od systemu operacyjnego. Kiedy stan procesu zmienia się z READY na RUNNING, oznacza to, że proces został zaplanowany. W przeciwnym przypadku — usunięty z planowania. W momencie, gdy proces staje się BLOCKED, na przykład podczas inicjowania operacji wejścia/wyjścia, system operacyjny będzie utrzymywał go w tym stanie do momentu wystąpienia jakiegoś zdarzenia, na przykład zakończenia operacji IO. W tym momencie następuje przejście do stanu READY, a jeśli system operacyjny zdecyduje, to być może natychmiast do stanu RUNNING.
Przyjrzyjmy się przykładom, jak dwa procesy przechodzą przez te stany. Na początek wyobraźmy sobie, że oba procesy są uruchomione i każdy z nich zużywa tylko CPU. W takim przypadku ich stany będą wyglądały w następujący sposób.

W następnym przykładzie pierwszy proces po pewnym czasie pracy żąda IO i przechodzi w stan BLOCKED, dając drugiemu procesowi możliwość uruchomienia (rysunek 1.4). System operacyjny widzi, że proces 0 nie używa CPU i uruchamia proces 1. W czasie wykonywania procesu 1 — operacja IO kończy się, a status procesu 0 zmienia się na READY. Na koniec proces 1 kończy się, a po jego zakończeniu proces 0 jest uruchamiany, wykonywany i kończy swoją pracę.

Struktura danych
System operacyjny sam jest programem i, tak jak każdy inny program, ma kluczowe struktury danych, które śledzą różne istotne informacje. Aby śledzić stan każdego procesu, system operacyjny będzie utrzymywał pewną listę procesów dla wszystkich procesów w stanie READY oraz dodatkowe informacje do śledzenia procesów, które są obecnie wykonywane. Oprócz tego system operacyjny musi również monitorować i zablokowane procesy. Po zakończeniu operacji IO, system operacyjny ma obowiązek obudzić odpowiedni proces i przenieść go w stan gotowości do uruchomienia.
Na przykład, system operacyjny musi zachować stan rejestrów procesora. W chwili zatrzymania procesu stan rejestrów jest zapisywany w przestrzeni adresowej procesu, a przy wznowieniu jego pracy - wartości rejestrów są przywracane, dzięki czemu można kontynuować wykonywanie tego procesu.
Oprócz stanów ready, blocked, running istnieją też inne stany. Czasami w momencie tworzenia proces może mieć stan INIT. Wreszcie proces może zostać umieszczony w stanie FINAL, gdy już się zakończył, ale informacje o nim nie zostały jeszcze usunięte. W systemach UNIX to gorny stan nazywa się procesem-zombie. Stan ten jest przydatny, gdy proces rodzicielski chce poznać kod zwrotny potomka. Zwykle 0 oznacza pomyślne zakończenie, a 1 błąd, jednak programiści mogą stosować dodatkowe kody wyjścia, sygnalizując różne problemy. Po zakończeniu, proces rodzicielski wykonuje ostatnie wywołanie systemowe, na przykład wait(), aby poczekać na zakończenie pracy procesu potomnego i poinformować system operacyjny, że można usunąć wszelkie dane związane z zakończonym procesem.

Kluczowe punkty wykładu:
● Proces — główna abstrakcja działającego programu w systemie operacyjnym. W każdej chwili proces może być opisany przez jego stan: zawartość pamięci w jego przestrzeni adresowej, zawartość rejestrów procesora, w tym wskaźnik instrukcji i wskaźnik stosu oraz informacje o IO, na przykład otwarte pliki, które są odczytywane lub zapisywane.
● API Procesu składa się z wywołań, które programy mogą wykonywać w odniesieniu do procesów. Zazwyczaj są to wywołania tworzenia, usuwania lub inne.
● Proces znajduje się w jednym z wielu stanów, w tym running, ready, blocked. Różne zdarzenia, takie jak planowanie, wykluczenie z planowania lub oczekiwanie mogą zmieniać stan procesu z jednego w drugi.
● Lista procesów zawiera informacje o wszystkich procesach w systemie. Każdy wpis w niej nazywa się blokiem kontrolnym procesu, który w rzeczywistości jest strukturą zawierającą wszystkie niezbędne informacje o danym procesie.
Źródło: habr.com
