osdev.labedz.org

Czym wogóle będziemy się zajmować?

Przed rozpoczęciem procesu implementacji systemu operacyjnego należy podjąć szereg decyzji, które rzutują na architekturę projektowanego systemu, a więc mają znaczny wpływ na realizację podjętych wcześniej założeń. Ponieważ głównym celem pracy jest implementacja systemu, który ma przybliżyć zainteresowanym proces tworzenia prostego systemu operacyjnego 'od zera', tworzony program ma być jak najprostszy w budowie, a jednocześnie przedstawiać sposób oprogramowania procesora oraz układów z nim współpracujących, jak również zawierać podstawowe mechanizmy i struktury danych występujące w typowych, istniejących już systemach operacyjnych.

Architektura sprzętowa

Pierwszym zagadnieniem jaki należy rozważyć przed rozpoczęciem projektowania systemu operacyjnego jest wybór architektury sprzętowej na jakiej ma on być uruchamiany. Głównym czynnikiem rzutującym na tą decyzję jest jak największa dostępność na rynku określonej platformy sprzętowej - z powodu przeznaczenia, implementowany system powinien sprawiać jak najmniej problemów podczas uruchamiania, w tym również problemów związanych ze znalezieniem komputera na którym mógłby pracować. Ponieważ w obecnym czasie architekturą dominującą na rynku komputerów osobistych jest komputer zgodny z IBM-PC/AT oparty na procesorze z rodziny IA-32, to właściwą decyzją wydaje się wybranie właśnie tej architektury, jako platformy sprzętowej implementowanego systemu operacyjnego.

Wybierając architekturę sprzętową warto jest się zastanowić, czy zależy nam na przenośności systemu na inne architektury sprzętowe. W celu umożliwienia kompilowania systemu operacyjnego na innych rodzinach komputerów, należy go rozbudować o dodatkowe procedury niskiego poziomu (dla każdego obsługiwanego procesora oddzielne), mające praktycznie tą samą funkcjonalność, oraz dołączyć dodatkowe warunki kompilacji wraz ze skryptem konfiguracyjnym. Taki dodatkowy narzut kodu w edukacyjnym systemie operacyjnym jest, na tym etapie, całkowicie zbędny, gdyż nie wnosi praktycznie żadnej nowej wiedzy na temat implementowania systemów operacyjnych od podstaw. Oczywiście przenośność systemu, jeżeli zajdzie taka potrzeba, może być zaimplementowana w przyszłości.

Język programowania

Po wyborze odpowiedniej architektury sprzętowej należy zdecydować się na język programowania w którym będzie implementowany system operacyjny. Ponieważ wiele operacji systemu należy przeprowadzać na niskim poziomie, w którym istnieje bezpośredni dostęp do wszystkich rejestrów procesora, czy instrukcji sterujących kontrolą nad wykonywanym programem, użycie języka assemblera procesora jest niezbędne i nie do uniknięcia. Jednakże dla ułatwienia implementacji, i w późniejszym czasie interpretacji kodu należy zastosować również dodatkowo języka wyższego poziomu. Do tego celu nadaje się wiele współczesnych języków programowania (oczywiście nie wliczając w to typowych 'kombajnów' stosowanych współcześnie do programowania aplikacji użytkownika pod systemy graficzne typu C#, Borland Builder czy Microsoft Visual Studio) takich jak C/C++, Modula, Turbo Pascal czy Ada.

Jako język programowania do implementacji systemu operacyjnego został użyty język C. Za wyborem tym przemawiało wiele czynników - przede wszystkim język ten jest powszechny, bardzo dobrze udokumentowany (norma ISO), posiada kod sformalizowany i zwarty (łatwy do zrozumienia). Kolejną ważną zaletą zastosowania języka C do implementacji systemu operacyjnego, jest stosunkowo prosty sposób łączenia ze sobą procedur napisanych na różnych poziomach, co znacznie ułatwia programowanie systemu.

Do implementacji systemu operacyjnego został użyty kompilator GCC, oraz assembler GNU NASM. Oba te narzędzia są w pełni profesjonalne i całkowicie darmowe.

Wielozadaniowość

W obecnie projektowanych systemach operacyjnych możliwość współbieżnego wykonywania procesów jest standardem - systemy jednozadaniowe są używane najczęściej do nadzoru prostych układów sterujących. Ponieważ implementowany system ma służyć celom edukacyjnym, powinien on przedstawiać prosty mechanizm wielozadaniowości.

Wraz z wyborem wielozadaniowości należy się też zastanowić nad sposobem kontroli czasu procesora przydzielonym dla procesu. Właściwie są tu do wyboru dwie opcje: model procesu wielozadaniowego z wywłaszczaniem procesów (ang. preemptive multitasking) oraz bez wywłaszczania (ang. cooperative multitasking). Oba te modele posiadają podobną złożoność kodu, jednakże wnioskując z tego, że większość współczesnych systemów operacyjnych pracuje z wywłaszczeniem procesów (zwłaszcza systemy czasu rzeczywistego), taki wybór jest najbardziej zasadny.

Tryb pracy procesora

Decydując się na architekturę sprzętową kompatybilną z IBM-PC/AT, należy również wybrać tryb pracy procesora. Układ Intel386 udostępnia dwa podstawowe tryby pracy: rzeczywisty oraz chroniony. Ten pierwszy jest teraz praktycznie utrzymywany jedynie w celu zachowania zgodności programowej ze starszą rodziną układów firmy Intel. Tryb chroniony jest natomiast domyślnym trybem pracy - układ osiąga maksimum swojej wydajności, oraz udostępnia wszystkie zawarte w nim mechanizmy.

Wybór trybu chronionego procesora pozwala również na wykorzystanie zawartych w nim mechanizmów ochrony zasobów. Dzięki temu istnieje możliwość sprzętowej separacji jądra systemu od procesów aplikacji, oraz procesów aplikacji między sobą.

Mechanizm zarządzania pamięcią

Procesor rodziny IA-32 pracujący w trybie chronionym może korzystać z wbudowanych mechanizmów zarządzania pamięcią: segmentacji oraz stronicowania.

Segmentacja jest w zupełności wystarczającym mechanizmem do zarządzania pamięcią, gdyż za jej pomocą istnieje możliwość implementacji zarówno ochrony pamięci jak i mechanizmu pamięci wirtualnej. Jest to jednak model pamięci stosunkowo trudny do oprogramowania. Dużo większą funkcjonalność daje zastosowanie mechanizmu stronicowania - jest on prosty w oprogramowaniu, i nie posiada wielu ograniczeń mechanizmu segmentacji.

Najlepszym wyborem wydaje się być zastosowanie prostego modelu pamięci (wszystkie segmenty takie same, obejmujące cały obszar pamięci), wraz ze stronicowaniem i odwzorowaniem adresów liniowych bezpośrednio na odpowiadające im adresy fizyczne. Takie rozwiązanie znacznie upraszcza niskopoziomowy dostęp do pamięci, nie ujmując nic z funkcjonalności ochrony zasobów czy wielkości obszaru adresowego.

Mechanizm przełączania procesów

Procesory z rodziny IA-32 mają zaimplementowany sprzętowy mechanizm przełączania procesów, jednakże od czasu jego wprowadzenia praktycznie nie jest on przez producenta rozwijany, co powoduje, że jest zdecydowanie wolniejszy od przełączania programowego. Poza wydajnością posiada również szereg innych wad. Przede wszystkim sposób przełączenia procesów jest zdefiniowany na stałe - projektant systemu chcący dołączyć dodatkowe elementy jest zmuszony i tak pewną część mechanizmu przełączania procesów zaimplementować w sposób programowy. Kolejnym problemem jest to, że mechanizm przełączania w takiej postaci występuje jedynie w procesorach IA-32, co ogranicza w pewien sposób możliwość prezentacji ogólnej idei przełączania procesów.

Przełączanie w sposób programowym jest rozwiązaniem znacznie szybszym od poprzednio przedstawionego, Jest też sposobem bardziej ogólnym - w podobnej postaci prezentuje się na praktycznie każdej platformie sprzętowej.

Implementowany system operacyjny korzysta z programowego przełączania procesów 'na stosie'.

Zgodność ze standardami

Przed implementacją systemu operacyjnego warto zastanowić się nad ewentualną jego zgodnością z ogólnie przyjętymi standardami interfejsów poziomu jądra i aplikacji. Zgodność z którymś z nich znacznie ułatwi implementację programów aplikacji, oraz umożliwi przenośność źródeł programów.

Obecnie najbardziej rozpowszechnionym standardem jest POSIX (ang. portable operating system interface). Implementowany system operacyjny powinien być w znacznej mierze zgodny z tym standardem.

Program ładujący system operacyjny

Wiele z obecnych systemów operacyjnych do inicjacji środowiska pracy wykorzystuje własne programy ładujące. Główną zaletą takiego rozwiązania jest ścisłe powiązanie programu ładującego z samym systemem operacyjnym, dzięki czemu jest on ładowany szybko i bez potrzeby dołączania dodatkowych plików konfiguracyjnych ze strony użytkownika. Problem często stanowi jednak fakt, iż dedykowane programy ładujące najczęściej potrafią obsługiwać tylko jeden system operacyjny. W przypadku kiedy potrzebujemy zainstalować na komputerze dwa różne systemy, istnieje potrzeba zastosowania innych rozwiązań.

W chwili obecnej dostępnych jest szereg programów ładujących współpracujących z wieloma rodzinami systemów operacyjnych. Wśród darmowych warto wymienić takie jak: LILO - dość prosty loader dedykowany systemowi Linux (jednakże potrafiący współpracować również z innymi systemami), oraz GRUB - bardzo mocno rozbudowany loader, pierwszy z przedstawicieli bootloader'ów wspierających system Multiboot - standard interfejsu między programem ładującym a systemem operacyjnym.

Obecnie najbardziej rozpowszechnionym standardem jest POSIX (ang. portable operating system interface). Implementowany system operacyjny powinien być w znacznej mierze zgodny z tym standardem.

Ponieważ pisanie własnego programu ładującego wykracza poza ramy tematyczne pracy, jako bootloader został użyty dostępny na licencji GNU program GRUB. Implementowany system jest zgodny z standardem Multiboot, dzięki czemu programem ładującym może być praktycznie dowolny bootloader obsługujący ten interfejs.

Model jądra systemu

Na późniejszym etapie implementacji jądra systemu należy zdecydować się na określony model jądra systemowego. Ponieważ projekt i implementacja mikrojądra jest prostszy od jądra monolitycznego, system w przyszłości będzie rozwijany właśnie według tego modelu.