Mikrojądro

Autorzy
W kolejności występowania na scenie:

Spis treści

  1. Wstęp
  2. Troszkę teorii
    1. Mikrojądro: a cóż to jest?
    2. Monolit - twój wróg?
    3. Different systems for different purposes
      1. Minix
      2. QNX
      3. Hurd
    4. A co na to MS?
    5. Monolit vs Mikrojądro
  3. Jak działają systemy z mikrojądrem?
    1. Architektura Windows NT
    2. QNX (Neutrino)
    3. Minix
    4. Hurd (Mach)
  4. Testy wydajności
  5. Screenshoty

Wstęp

Początkowo jądra systemów były "niewielkich" rozmiarów. Było to spowodowane ograniczonością architektur (pamieć), ale także małymi wymaganiami użytkowników czy też małą różnorodnością/ilocią sprzętu który winien być obsługiwany przez system operacyjny.

Jednakże wraz ze wzrostem możliwości komputerów, wraz z dodawaną funkcjonalnością, jądra systemów rosły i stawały się coraz to bardziej skomplikowane. Przejscie z systemow 16 na 32 bitowe tylko spotęgowało efekt rozrastania się kodu źródłowego, ktorego ilość w jądrach monolitowych możemy liczyć w milionach.

Spowodowało to powstawanie wielu błędów, które, ze względu na ogrom kodu, niemożliwy do ogarnięcia przez pojedyńczą osobę, były i ciągle są trudne do wytropienia i zlikwidowania.

Szczególnie niekorzystny jest to fakt dla monolitów dla których niewielki błąd zawarty w peryferyjnym sterowniku może spowodować crash całego systemu.

Inną konsekwencją wielu linii kodu źródłowego jest bardzo pracochłonna i trudna pielęgnacja. Aby zapobiec tym skutkom zaczęto rozwijać ideę systemów opartych o mikrojądra, które minimalizują wielkosć kodu źródłowego serca SO.

Troszkę teorii

Mikrojądro a cóż to jest?

Mikrojądro jak nazwa wskazuje jest to minimalne jądro, gdzie przez minimalność rozumiemy ilość udostępnionych mechanizmów koniecznych do prawidłowego funkcjonowania systemu.
Takim minimalnym zestawem jest:

obecny w Nucleusie - jądrze systemu RC 4000 Multiprogramming System.

minimality principle

Jeśli zastanawiamy się co jest potrzebne w jądrze możemy posłużyć się zasadą minimalności:

A concept is tolerated inside the microkernel only if moving it outside the kernel would prevent the implementation of the system's required functionality

Ale czy to dobra droga?
I w ogólności co możemy usunąć z jądra systemu by ten ciągle działał??

Wiemy zatem czym jest mikrojądro, ale nie zostało (explicite) powiedziane w jaki sposób możemy zachować sprawność systemu wyrzucająć z jądra prawie cały kod.
A co ważniejsze dlaczego chcielibyśmy chcieć to zrobić ?


Skoro nie mamy żadnej funkcjonalności jądra w jądrze to musimy się posiłkować aplikacjami wykonywanymi w trybie użytkownika - serwerami.
Serwery są to zwykłe aplikacje, demony, ktore dodatkowo mogą posiadać prawo do bezpośredniego dostepu do sprzętu, np. do pamięci fizycznej, a wszelkie usługi oferowane przez serwery są udostępniane innym programom poprzez komunikaty IPC. Przykładami serwerów są:

Innymi słowy wszystkie funkcje jądra są udostępniane jako oddzielne procesy w trybie użytkownika.

Monolit twoj wróg?

Zanim możemy mówić o zaletach albo wadach mikrojądra powinniśmy poznać jego najważniejszego oponenta jądro monolitowe
Jądro monolitowe jest to jądro z jakim codziennie spotykamy się pożytkując nasz czas na naukę SO.
Przykładem jest oczywiście Linux.
Specyfikę jądra monolitowego poznajemy systematycznie na wykładzie, zatem pozwolę sobie jedynie zamieścić obrazek podsumowujący cechy systemu opartego na jądrze monolitowym.


Different systems for different purposes

MINIX

Minix - system stworzony przez A. Tanenbauma , opublikowany w 1987r.
Został stworzony do celów edukacyjnych i rozpowszechniany razem z książką "Operating Systems: Design and Implementation" autorstwa A.T i A.W.
Jest to pierwszy klon Uniksa z dostępnym źródłem.

W roku 2005 zostały ogłoszone prace nad trzecią wersją Miniksa, pisaną w zasadzie od podstaw.
Która w zaożeniach miała być "a serious system on resource-limited and embedded computers and for applications requiring high reliabity"
Jej najnowsza wersja to 3.1.2 z 8.05.2006

Cechy miniksa to:

  1. niewielki rozmiar jądra systemu (ok. 4k linii kodu źródłowego)
  2. wysoka niezawodność, elastyczność i bezpieczeństwo
  3. self-healing
  4. wielozadaniowość
  5. niewielkie wymagania sprzętowe
Jądro miniksa składa się z:
  1. obsługi przerwań
  2. schedulera
  3. IPC
Trusted computing base

Jest to jądro oraz zespół serwerów których załamanie powoduje śmierć całego systemu.
W skład wchodzą: file, reincarnation, process server oraz jądro systemu.
Załamanie jakiegokolwiek innego elementu miniksa nie powinien mieć znaczącego wpływu na kondycję systemu.
Cały TCB to około 28k lini kodu.

Cel do którego dążą tworcy miniksa:

(...)Minix 3 (...) is not about microkernels. It is about bulding highly reliable, self-healing, operating system.
I will consider the job finished when no manufacturer anywhere makes PC with reset button

QNX

QNX jest to system czasu rzeczywistego, ktorego początki sięgają 1980 roku.
System czasu rzeczywistego, w tym kontekscie, oznacza, że system jest zobowiazany aby każde zadanie było wykonane w ściśle określonym terminie. Jest to przykład systemu komercyjnego opartego na mikrojądrze.
Jego targetem są głównie systemy wbudowane, wymagające niezawodności i dotrzymywania terminów.
QNX jest wykorzystywany w takich urządzeniach jak:

Jądro QNXa zawiera:
  1. IPC
  2. CPU scheduler
  3. Obsługę przerwań
  4. Liczniki czasu

Hurd

Oczywiście ostatnia data to tylko żart. Prawdą jest jednak, że Hurd nie doczekał się ani jednej wersji stabilnej, oraz w obecnym stanie nie obsługuje kontrolerów SATA, w związku z czym jest nie do zastosowania w komputerach domowych.

A co na to MS?

Microsoft, który wie jak trudno pielęgnować kod systemu monolitycznego, także próbował stworzyć własny system oparty na mikrojądrze. Miał nim być WinNT a dokładniej wersja 3.1, jednakże zamysł koncepcyjny spowodował powstanie tzw. jądra hybrydowego, a pomysł utworzenia Windowsa opartego na mikrojądrze został chwilowo zarzucony.
Pojawił się ponownie w projekcie o nazwie Singularity.
Obecnie jest dostępna wersja 2.0 wydana w listopadzie 2008 roku.
Singularity Research Development Kit jest dostepny jako Shared Source, co pozwala na niekomercyjne, naukowe wykorzystanie żródła.

Monolit vs mikrojądro

Po takiej dawce informacji możemy udzielić odpowiedzi na zadane wcześniej pytania._

Zatem co wygrywa w pojedynku mono vs. mikro

Porównanie koncepcji
MikrojądroJądro monolitowe
  • mały rozmiar kodu źródłowego
  • łatwiejsze wykrywanie błędów
  • błędy w sterownikach nie powodują "crash'u" całego systemu
  • duży narzut systemowy na wykonanie aplikacji
  • ogromna ilość komunikatów synchronizujących
  • duża ilość komunikacji międzyprocesowej, wąskie gardło systemu
  • brak potrzeby bardzo szybkiej komunikacji międzyprocesowej
  • mniejszy narzut systemowy na sys_call
  • większy kod jądra - więcej miejsca na błędy
  • istnieje standardowy zestaw sterowników w jądrze
  • dużo łatwiejsze w implementacji

Jak działają systemy z mikrojądrem?

Architektura Windows NT


Modularna budowa - podzielone na składowe odpowiadające za udostępnianie określonych funkcji.
Dwie główne części: podsystemy pracujące w trybie użytkownika i elementy trybu jądra.
Tryb użytkownika:
Tryb jądra:

Transparent Distributed Processing (QNX)

TDP - ( protokół / moduł ) łączący jądra systemów w sieci, powodujący że wszystkie usługi systemowe dostępne są przez ten sam mechanizm, niezależnie od tego z jakiej jednostki (maszyny) są wywoływane.

Możliwości jakie to stwarza: są imponujące. Dodatkowo środowisko graficzne photon pozwala na przeciąganie aplikacji między ekranami różnych komputerów ;)

Przepływ komunikatów (Minix) - na przykładzie wywołania systemowego read

Programista wywołuje read

Od strony programisty kod wywołania wygląda następująco
	count = read(fd, buffer, nbytes); 

Przełożenie na język komunikatu

Funkcja read składa się z jednej linii
	  return(callm1(FS, READ, fd, nbytes, 0, buffer, NIL_PTR, NIL_PTR));

Skonstruowanie komunikatu

callm1 - zrób wywołanie systemowe używając komunikatu typu 1.
PUBLIC int callm1(proc, syscallnr, int1, int2, int3, ptr1, ptr2, ptr3)
int proc;			/* FS or MM */
int syscallnr;			/* which system call */
int int1;			/* first integer parameter */
int int2;			/* second integer parameter */
int int3;			/* third integer parameter */
char *ptr1;			/* pointer parameter */
char *ptr2;			/* pointer parameter */
char *ptr3;			/* pointer parameter */
{
/* Send a message and get the response.  The 'M.m_type' field of the
 * reply contains a value (>= 0) or an error code (<0). Use message format m1.
 */
  _M.m1_i1 = int1;
  _M.m1_i2 = int2;
  _M.m1_i3 = int3;
  _M.m1_p1 = ptr1;
  _M.m1_p2 = ptr2;
  _M.m1_p3 = ptr3;
  return callx(proc, syscallnr);
}
Struktura _M jest de facto zmienną globalną
	message _M = {0};

Wysłanie komunikatu

Funkcja callx wywołuje funkcję sendrec, natomiast ta jest napisana w assemblerze
SEND = 1
RECEIVE = 2
BOTH = 3
SYSVEC = 32

|*========================================================================*
|                           send and receive                              *
|*========================================================================*
| send(), receive(), sendrec() all save bp, but destroy ax, bx, and cx.
.globl _send, _receive, _sendrec
_send:	mov cx,*SEND		| send(dest, ptr)
	jmp L0

_receive:
	mov cx,*RECEIVE		| receive(src, ptr)
	jmp L0

_sendrec:
	mov cx,*BOTH		| sendrec(srcdest, ptr)
	jmp L0

  L0:	push bp			| save bp
	mov bp,sp		| can't index off sp
	mov ax,4(bp)		| ax = dest-src
	mov bx,6(bp)		| bx = message pointer
	int SYSVEC		| trap to the kernel
	pop bp			| restore bp
	ret			| return

Odbiór komunikatu przez jądro - weryfikacja i przesłanie dalej

Komunikat po stronie jądra odbiera funkcja s_call ładowana w main():
  set_vec(SYS_VECTOR, s_call, base_click);
Funkcja s_call (w assemblerze) zachowuje stan procesora, po czym wywoluje sys_call
 07477	/*===========================================================================*
 07478	 *                              sys_call                                     * 
 07479	 *===========================================================================*/
 07480	PUBLIC int sys_call(call_nr, src_dst, m_ptr)
 07481	int call_nr;                    /* system call number and flags */
 07482	int src_dst;                    /* src to receive from or dst to send to */
 07483	message *m_ptr;                 /* pointer to message in the caller's space */
 07484	{
 07489	  register struct proc *caller_ptr = proc_ptr;  /* get pointer to caller */
 07490	  int function = call_nr & SYSCALL_FUNC;        /* get system call function */
 07491	  unsigned flags = call_nr & SYSCALL_FLAGS;     /* get flags */
 07492	  int mask_entry;                               /* bit to check in send mask */
 07493	  int result;                                   /* the system call's result */
 07494	  vir_clicks vlo, vhi;          /* virtual clicks containing message to send */

 07550	  /* Now check if the call is known and try to perform the request. The only
 07551	   * system calls that exist in MINIX are sending and receiving messages.
 07552	   *   - SENDREC:  combines SEND and RECEIVE in a single system call
 07553	   *   - SEND:     sender blocks until its message has been delivered
 07554	   *   - RECEIVE:  receiver blocks until an acceptable message has arrived
 07555	   *   - NOTIFY:   nonblocking call; deliver notification or mark pending
 07556	   *   - ECHO:     nonblocking call; directly echo back the message 
 07557	   */
 07558	  switch(function) {
 07559	  case SENDREC: 
 07560	      /* A flag is set so that notifications cannot interrupt SENDREC. */
 07561	      priv(caller_ptr)->s_flags |= SENDREC_BUSY;
 07562	      /* fall through */
 07563	  case SEND:                     
 07564	      result = mini_send(caller_ptr, src_dst, m_ptr, flags);
 07565	      if (function == SEND || result != OK) {   
 07566	          break;                                /* done, or SEND failed */
 07567	      }                                         /* fall through for SENDREC */
 07568	  case RECEIVE:                  
 07569	      if (function == RECEIVE)
 07570	          priv(caller_ptr)->s_flags >= ~SENDREC_BUSY;
 07571	      result = mini_receive(caller_ptr, src_dst, m_ptr, flags);
 07572	      break;
 07573	  case NOTIFY: 
 07574	      result = mini_notify(caller_ptr, src_dst);
 07575	      break;
 07576	  case ECHO: 
 07577	      CopyMess(caller_ptr->p_nr, caller_ptr, m_ptr, caller_ptr, m_ptr);
 07578	      result = OK;
 07579	      break;
 07580	  default: 
 07581	      result = EBADCALL;                        /* illegal system call */
 07582	  }
 07583	
 07584	  /* Now, return the result of the system call to the caller. */
 07585	  return(result);
 07586	}
Funkcja generalnie upewnia się, że komunikat jest poprawny i przekazuje go dalej przez sendrec.

Odbiór komunikatu przez serwer systemu plików

Komunikat ten odbierany jest przez obsługę systemów plików (która decyduje jaką funkcję należy wykonać po otrzymaniu jakiego komunikatu)
 24075	                if (call_nr < 0 || call_nr >= NCALLS) { 
 24076	                        error = ENOSYS;
 24077	                        printf("FS, warning illegal %d system call by %d\n", call_n
 24078	                } else if (fp->fp_pid == PID_FREE) {
 24079	                        error = ENOSYS;
 24080	                        printf("FS, bad process, who = %d, call_nr = %d, slot1 = %d
 24081	                                 who, call_nr, m_in.slot1);
 24082	                } else {
 24083	                     
I w przypadku read() wywoła się do_read()
PUBLIC int do_read()
{
  return(read_write(READING));
}

Wykonanie

A funkcja read_write robi już to co trzeba.

System plików - translatory (QNX/Hurd)

Linux

Na linuxie dostępny jest system montowania katalogów. Jeżeli jądro obsługuje system plików XXX to możemy wykonać
	mount -t XXX /path/device /another/path
Aby sprawić by w katalogu /another/path dostępna była zawartość urządzenia /path/device zinterpretowana w odpowiedni (XXX) sposób.

Hurd

W mikrojądrach obsługa systemu plików odbywa się przez program zewnętrzny, który decyduje należy zrobić z wywołaniami systemowymi dotyczącymi systemu plików. Może (jak w wywyższym przykładzie wywołania read() w minixie) sam zająć się jego realizacją, lub zlecić ją komuś innemu.

W systemach takich jak QNX/Hurd użytkownik może (analogicznie do mount-owania) zdefiniować jaki program (translator) ma obsługiwać jakie odwołania. Przykładowo mount -t iso9660 /dev/cdrom /mnt przetłumaczone na język Hurda brzmi
settrans -a /cdrom /hurd/iso9660fs /dev/hd1
Gdzie /hurd/iso9660fs jest programem do obsługi tego systemu plików. Niewątpliwie idea translatorów wydaje się ogólniejsza i bardziej praktyczna - dzięki niej, możemy na przykład łatwo stworzyć translator obsługujący kopiowanie za pomocą scp, tak by dla użytkownika wyglądało jak operowanie na lokalnym systemie plików.

FUSE

Odpowiedzią na tę potrzebę w Linuxie jest FUSE - Filesystem in USErspace, który pokrywa wszystkie możliwości translatorów Hurda :)

Możemy zdefiniować kilka funkcji m.in.
static int hello_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
                         off_t offset, struct fuse_file_info *fi)
{
    (void) offset;
    (void) fi;

    if(strcmp(path, "/") != 0)
        return -ENOENT;

    filler(buf, ".", NULL, 0);
    filler(buf, "..", NULL, 0);
    filler(buf, hello_path + 1, NULL, 0);

    return 0;
}
static struct fuse_operations hello_oper = {
    .getattr	= hello_getattr,
    .readdir	= hello_readdir,
    .open	= hello_open,
    .read	= hello_read,
};

int main(int argc, char *argv[])
{
    return fuse_main(argc, argv, &hello_oper);
}
I cieszyć się rezultatem:
~/fuse/example$ mkdir /tmp/fuse
~/fuse/example$ ./hello /tmp/fuse
~/fuse/example$ ls -l /tmp/fuse
total 0
-r--r--r--  1 root root 13 Jan  1  1970 hello
~/fuse/example$

QNX

W QNXie jest lepiej, ze względu na TDP. Ponieważ nie ma tam rozróżnienia na komunikaty przechodzące tylko w obrębie systemu i komunikaty wychodzące do innej maszyny, wywołania systemowe mogą zostać obsłużone przez inne komputery.

Dlatego ścieżka pliku ma w QNXie postać //n/[katalog]/plik, gdzie n jest numerem maszyny. Jeżeli //n się pominie, to ścieżka będzie odnosić się do pliku na maszynie lokalnej.

W QNXie nic nie stoi na przeszkodzie, by stworzyć link symboliczny do pliku na innym komputerze.

Mountowanie w QNXie zostało przystosowane do unixowego (linuxowego) sposobu używania. Polecenie mount -t iso9660 /dev/cd0 /cd0 wykonuje: mount_iso9660 /dev/cd0 /cd0

Metody komunikacji

Spotkania (QNX)

Wiadomości w tym schemacie przenoszone są bezpośrednio z pamięci jednego procesu do pamięci drugiego (ewentualnie przez sieć, jeśli procesy znajdują się na różnych maszynach).

Depozyty (QNX)

System depozytów służący do informowania procesów o zaistniałych zdarzeniach zewnętrznych.

Sygnały (QNX)

Sygnały - działające prawie dokładnie jak w Linuxie.

Obsługa przerwań (QNX)

Jądra QNXa zajmuje się obsługą przerwań, ale nie wyklucza ono obsługi przerwać przez serwery.

Funkcja InterruptAttach pozwala na zdefiniowanie funkcji obsługi przerwania.
W L4 przekazywaine informacji o przerwaniu odbywa się przez IPC

QNX vs minix

Operacja Send jest blokująca, co może doprowadzić do niemiłych sytuacji zakleszczenia. Złośliwy klient usługi mógłby z łatwością przeprowadzić atak DoS polegający na zakleszczeniu serwera. QNX i minix proponują dwa rozwiązania tego problemu:

Adaptive Partition Schedulers (QNX)

RTOS - Real-Time Operating System (system, którego poprawność zależy nie tylko od wyniku, ale od momentu w którym ten wynik zostanie dostarczony).
Opóźnienie obsługi przerwania w QNXie, przy dosyć wolnej maszynie jest rzędu 10 mikrosekund.
Przy obciążeniu systemu ważne jest, żeby krytyczne procesy dostawały co pewien czas czas procesora, pomimo tego, że mają niższy priorytet niż inne procesy. AP - powoduje, że jest zarezerwowany czas procesora dla procesów krytycznych, jednocześnie nie blokując na stałe określonego procentu czasu CPU.

Testy wydajności

W jądrze monolitycznym usługa przez pojedyncze wywołanie systemowe, wymagające dwukrotnego przełączenia kontekstu. W architekturze mikrojądrowej usługa jest osiągana poprzez wysłanie do serwera komunikatu i odbiór wyniku w innym komunikacie. Może to prowadzić do zwiększenia kosztu wykonania operacji o dodatkowe przełączanie kontekstu oraz kopiowanie danych pomiędzy przestrzeniami adresowymi procesów. Wczesne systemy operacyjne operacyjne oparte na mikrojądrze miały bardzo słabą wydajność, MkLinux oparty na jądrze Mach był średnio o ok. 50% wolniejszy od monolitycznego linuxa.
Problemy z wydajnością były jednak spowodowane kiepską architekturą i implementacją. Poprzez staranną implementację jądro L4 miało o rząd wielkości szybszą komunikację międzyprocesową, dzięki czemu uzyskiwało wyniki tylko o kilka procent gorsze od jąder monolitycznych. Platforma testowa : notebook z procesorem AMD Turion X2 1.8 Ghz, 1GB RAM Testowane systemy: