MINIX jest systemem o architekturze mikrojądra. Oznacza to, że jądro zapewnia najbardziej podstawową funkcjonalność: jest odpowiedzialne za obsługę przerwań, umożliwia zarządzania procesami, implementuje obsługę komunikacji międzyprocesowej oraz odpowiada za przełączanie procesów. Wszystkie pozostałe elementy takie jak obsługa systemów plików, sieci, sterowniki urządzeń itp. są uruchomione jako serwery i działają poza przestrzenią jądra. Większość wywołań systemowych obsługiwanych jest przez serwery. Samo jądro obsługuje niewielką grupę wywołań systemowych, zwanych zadaniami. Więcej o architekturze systemu i o serwerach dowiemy się na kolejnych zajęciach. Dzisiaj skupimy się na tym, jak dodać nowe wywołanie systemowe.
Podamy teraz przepis na dodanie nowego wywołania systemowego do MINIX-a.
Do obsługi naszego wywołania wybieramy serwer ipc
.
Zgodnie z zasadami sztuki dodanie nowego wywołania systemowego
składa się z dwóch etapów:
Funkcja biblioteczna ma zadanie ukryć szczegóły techniczne przed użytkownikiem i ułatwić korzystanie z danego wywołania systemowego.
message
Struktura message
jest specjalną strukturą, która zawiera
informację o źródle wywołującym funkcję systemową a także
argumenty przekazane do funkcji, o ile takie są. Argumenty muszą
zostać umieszczone w postaci jednego z dostępnych formatów. Cała struktura
i wszystkie dostępne formaty są zdefiniowane w pliku
/usr/include/minix/ipc.h
Funkcja systemowa może przekazać wynik do źródła również poprzez odpowiednią modyfikację
zawartości struktury message
.
Obejrzyj dostępne formaty struktury message
w pliku /usr/include/minix/ipc.h
.
Za spakowanie parametrów do jednego z formatów struktury message
,
przekazanie jej do funkcji obsługującej wywołanie systemowe i odczytanie wyników
jest odpowiedzialna funkcja biblioteczna. Pełni ona rolę opakowania (ang. wrapper)
właściwego wywołania systemowego. Możemy założyć, że każde
wywołanie funkcji systemowej nastąpi za pośrednictwem biblioteki.
Pliki źródłowe znajdują się w katalogu /usr/src/minix
. Natomiast pliki
nagłówkowe w katalogach /usr/include
oraz /usr/include/minix
.
Należy zwrócić uwagę, że te same pliki nagłówkowe są też odpowiednio w katalogach
/usr/src/include
oraz /usr/src/minix/include/minix
.
Kompilator clang
szuka plików nagłówkowych w /usr/include
,
ponieważ tak ma ustawioną ścieżkę. Natomiast przy wykonywaniu make install
pliki z tego katalogu mogą zostać nadpisane przez pliki z katalogu /usr/src
.
Jeżeli dokonujemy zmian w plikach nagłówkowych, to najbezpieczniej jest zmieniać
je w obydwu miejscach.
Tradycyjnie, nasze pierwsze wywołanie systemowe wypisze na ekran „Hello world!”
Każde wywołanie systemowe ma unikalny numer.
W pierwszej kolejności musimy wybrać numer dla naszego nowego wywołania systemowego.
Ponieważ będzie ono umieszczone w serwerze ipc
, warto zadbać o to, aby number
wywołania należał do grupy związanej z ipc
.
Numery poszczególnych wywołań systemowych związanych z ipc
znajdują się w
plikach:
/usr/src/minix/include/minix/com.h
/usr/include/minix/com.h
W sekcji dotyczącej ipc
(wyszukaj IPC_BASE
) należy dopisać wiersz
#define IPC_PRINTMESSAGE (IPC_BASE+8)
Magiczna liczba 8 jest po prostu kolejnym wolnym numerem w tablicy wywołań systemowych – poprzednie są zajęte na wywołania związane z semaforami i segmentami pamięci dzielonej.
Następnie trzeba dodać kod funkcji obsługującej to wywołanie systemowe oraz zarejestrować nowe wywołanie systemowe.
W kodzie serwera ipc
, którego pliki źródłowe znajdują się w katalogu
/usr/src/minix/servers/ipc
, należy zmodyfikować lub stworzyć następujące pliki:
inc.h
printmessage.c
main.c
Makefile
W pliku inc.h
dodajemy na końcu nagłówek naszej funkcji obsługującej wywołanie
systemowe.
int do_printmessage(message *);
W pliku printmessage.c
umieszczamy kod naszej funkcji obsługującej wywołanie
systemowe:
#include "inc.h" int do_printmessage(message *m) { printf("Hello world!\n"); return OK; }
W pliku main.c
rejestrujemy naszą funkcję pod numerem IPC_PRINTMESSAGE
.
// (...) } ipc_calls[] = { // (...) { IPC_SEMOP, do_semop, 1 }, { IPC_PRINTMESSAGE, do_printmessage, 0 }, };
Trzeci element tablicy informuje o tym, czy funkcja ma być blokująca. Wartość 0 oznacza, że nie, a 1, że tak. Przyjrzymy się temu bliżej na kolejnych zajęciach.
W pliku Makefile
dodajemy informację o nowym pliku do skompilowania.
SRCS= main.c utility.c shm.c sem.c printmessage.c
Po wykonaniu tych kroków należy skompilować i zainstalować serwer ipc
.
W katalogu /usr/src/minix/servers/ipc
wykonujemy
# make # make install # reboot
Teraz możemy już korzystać z naszego nowego wywołania systemowego. Sprawdźmy to przed napisaniem funkcji bibliotecznej.
Do wywołania funkcji systemowej służy funkcja
_syscall()
zdefiniowana w pliku /usr/include/lib.h
:
int _syscall(endpoint_t who, int syscallnr, message *msgptr);
Parametr who
oznacza adresata funkcji systemowej (w naszym wypadku będzie to
serwer ipc
), syscallnr
jest numerem wywołania funkcji systemowej
(czyli IPC_PRINTMESSAGE
), a msgptr
jest wskaźnikiem na odpowiednią strukturę message
, która zostanie przekazana
funkcji obsługującej wywołanie systemowe. Aby otrzymać identyfikator
adresata, można skorzystać z funkcji minix_rs_lookup()
, zdefiniowanej w pliku
/usr/include/minix/rs.h
:
int minix_rs_lookup(const char *name, endpoint_t *value)
Funkcja przekazuje 0 w przypadku sukcesu i −1 w przypadku
porażki. Parametr name
oznacza nazwę szukanego adresata ("ipc"
),
a w zmiennej, na którą wskazuje value
, zostanie zapisany
identyfikator odbiorcy.
Następujący program (test-direct-printmessage.c
) wywoła nową funkcję systemową.
#include <lib.h> #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <minix/rs.h> int main(int argc, char** argv) { message m; endpoint_t ipc_ep; minix_rs_lookup("ipc", &ipc_ep); _syscall(ipc_ep, IPC_PRINTMESSAGE, &m); }
Program możesz skompilować poleceniem make
i nie musisz pisać wcześniej pliku
Makefile
(zalety NetBSD).
# make test-direct-printmessage
Wykonaj wszystkie wymienione kroki i sprawdź, czy po wywołaniu test-direct-printmessage
zobaczysz napis „Hello world!” na konsoli a na końcu pliku /var/log/messages
wpis
<aktualna data i czas> kernel: Hello world!
Funkcja biblioteczna jest odpowiedzialna za stworzenie i wypełnienie struktury
message
, a następnie przekazanie jej do wywołania systemowego.
Naszą przykładową funkcję biblioteczną dodamy do biblioteki libc
.
Funkcja musi być widoczna w programach użytkowych, więc należy ją zadeklarować
w jednym z plików nagłówkowych biblioteki. Wybieramy do tego unistd.h
.
Nagłówek naszej funkcji dodamy w plikach /usr/include/unistd.h
i /usr/src/include/unistd.h
.
int printmessage(void);
W katalogu /usr/src/lib/libc/misc
umieszczamy plik printmessage.c
z implementacją wywołania printmessage()
:
#include <lib.h> #include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <minix/rs.h> int get_ipc_endpt(endpoint_t *pt) { return minix_rs_lookup("ipc", pt); } int printmessage(void) { endpoint_t ipc_pt; message m; if (get_ipc_endpt(&ipc_pt) != 0) { errno = ENOSYS; return -1; } return (_syscall(ipc_pt, IPC_PRINTMESSAGE, &m)); }
Dodajemy także informację o naszej nowej funkcji do pliku Makefile.inc
:
SRCS+= stack_protector.c printmessage.c
Aby zbudować nową wersję biblioteki, należy po wejściu do katalogu /usr/src/lib/libc
wykonać polecenia
# make # make install
Po zainstalowaniu nowej biblioteki powinien się skompilować przykładowy program test-printmessage.c
:
#include <unistd.h> int main(int argc, char** argv) { printmessage(); return 0; }
a następnie po jego uruchomieniu na konsoli i w pliku
/var/log/messages
powinniśmy zobaczyć „Hello world! ”.
Sprawdź działanie funkcji bibliotecznej printmessage()
.
Zaimplementuj jednoelementowy schowek, dostępny przez wywołanie systemowe
obsługiwane przez serwer ipc
. Nagłówek funkcji bibliotecznej
powinien być następujący:
int storage(int number);
Początkowo w schowku znajduje się liczba 0. Po wywołaniu funkcji w schowku
ma znaleźć się liczba przekazana jako parametr number
. Wywołanie systemowe
przekazuje jako wynik poprzednią wartość przechowywaną w schowku.
Wywołanie systemowe ma być dostępne po dołączeniu nagłówka
unistd.h
.
Przykłady użycia:
int main() { int x = storage(1); // x == 0; int y = storage(2); // y == 1; return 0; }
Kolejne wywołanie:
int main() { int x = storage(3); // x == 2; int y = storage(0); // y == 3; return 0; }
W pliku test-storage.c
znajduje się program testujący poprawność implementacji
funkcji storage()
.
Schemat dodawania nowych wywołań do innych serwerów jest taki sam, ale niektóre
szczegóły różnią się. Na przykład jeśli będziemy chcieli dodać funkcję do serwera pm
,
to konieczna będzie zmiana następujących plików z katalogu /usr/src/minix
:
include/minix/callnr.h
— dodanie numeru nowej funkcji (patrz komentarz na początku pliku include/minix/com.h
)
servers/pm/proto.h
— dodanie nagłówka nowej funkcji
servers/pm/<nowa-funkcja.c>
— implementacja nowej funkcji
servers/pm/table.c
— rejestracja nowej funkcji
servers/pm/Makefile
W przypadku niektórych serwerów nie wystarczy wykonać
make && make install
.
Na przykład w przypadku pm
konieczna będzie także rekompilacja jądra systemu, ponieważ
część funkcjonalności serwera pm
wchodzi w skład jądra.
W katalogu /usr/src/releasetools
należy wykonać make hdboot
, a po restarcie systemu wybrać najnowszą wersję jądra.
Dodaj wywołanie systemowe wypisujące „Hello world from server PM!” obsługiwane
przez serwer pm
. Napisz także odpowiednią funkcję biblioteczną.