Mechanizm szeregowania zadań w jądrach z serii 2.6 systemu GNU

Transkrypt

Mechanizm szeregowania zadań w jądrach z serii 2.6 systemu GNU
UNIWERSYTET MIKOŁAJA KOPERNIKA
Wydział Fizyki, Astronomii i Informatyki Stosowanej
Jakub Przybyła
Mechanizm szeregowania zadań
w jądrach z serii 2.6 systemu GNU/Linux
Praca inżynierska wykonana
w Zakładzie Mechaniki Kwantowej
opiekun: dr hab. Jacek Kobus
Toruń 2007
Pracę dedykuję mojej Matce oraz śp. Ojcu
Niniejsza praca to owoc szeroko pojętej fascynacji szczegółami funkcjonowania systemu
operacyjnego Linux. Praca nie rozwinęłaby się w takim stopniu bez udziału mojego promotora dr hab. Jacka Kobusa, któremu pragnę szczerze podziękować za wsparcie teoretyczne
i praktyczne, dzięki któremu wiele napotkanych podczas pisania tej pracy problemów
zostało pomyślnie rozwiązanych i przeanalizowanych.
UMK zastrzega sobie prawo własności niniejszej pracy inżynierskiej w celu udostępnienia
dla potrzeb działalności naukowo-badawczej lub dydaktycznej
Spis treści
1 Wstęp
2 Szeregowanie procesów
2.1 Klasy procesów . . . . . . . . .
2.2 Priorytety i kwanty czasu . . .
2.3 Interaktywność . . . . . . . . .
2.4 Szeregowanie . . . . . . . . . .
2.4.1 Funkcja schedule() . . .
2.4.2 Funkcja scheduler tick()
2.4.3 Funkcja sched yield . . .
5
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
6
7
11
13
16
16
17
21
3 Moduły jądra
3.1 Konstruktor i destruktor modułu . .
3.2 Zależności między modułami . . . . .
3.3 Licznik odwołań . . . . . . . . . . . .
3.4 Parametryzacja modułów . . . . . . .
3.5 Inne informacje o module . . . . . . .
3.6 Wersje modułów i jądra . . . . . . .
3.7 Ładowanie modułów na żądanie . . .
3.8 Dynamiczne łączenie i relokacja kodu
3.9 Budowa pliku ELF . . . . . . . . . .
3.10 Zastosowanie modułów . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
21
21
21
22
23
25
26
26
27
28
29
4 Sterowniki urządzeń
4.1 Rejestrowanie i wyrejestrowywanie urządzeń
4.2 Operacje na pliku urządzenia . . . . . . . .
4.2.1 Funkcja open . . . . . . . . . . . . .
4.2.2 Funkcja release . . . . . . . . . . . .
4.2.3 Funkcja read . . . . . . . . . . . . .
4.2.4 Funkcja write . . . . . . . . . . . . .
4.2.5 Funkcja llseek . . . . . . . . . . . . .
4.2.6 Funkcja ioctl . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
30
33
33
35
35
36
36
37
37
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5 Implementacja modułu kernstat i programu schedstat
38
5.1 Modyfikacja sched.h i sched.c . . . . . . . . . . . . . . . . . . . . . . . . . 39
5.2 Konstruktor modułu kernstat . . . . . . . . . . . . . . . . . . . . . . . . . 41
3
5.3
5.4
5.5
5.6
5.7
5.8
5.9
5.10
5.11
5.12
Destruktor modułu kernstat . . . . . . .
Struktura file operations . . . . . . . . .
Otwarcie pliku urządzenia kernstat . . .
Zamknięcie pliku urządzenia kernstat . .
Czytanie z pliku urządzenia kernstat . .
Funkcja kontroli urządzenia kernstat ioctl
Funkcja export sched stat . . . . . . . . .
Funkcja export sched stat . . . . . . . .
. . . . .
Funkcja sched pio array tasks
Funkcja export task stat . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
6 Działanie planisty wg programu schedstat
6.1 Zadania z tablicy active . . . . . . . . . .
6.2 Zadania z tablicy expired . . . . . . . . . .
6.3 Kolejka zadań gotowych do wykonania . .
6.4 Czasowe statystyki zadania . . . . . . . . .
6.5 Statystyki zadań wg planisty . . . . . . . .
7 Słownik podstawowych pojęć
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
43
44
45
47
48
51
55
58
59
61
.
.
.
.
.
65
66
68
69
70
72
74
Literatura
79
4
1
Wstęp
System operacyjny GNU/Linux, który jest intensywnie rozwijany od 1991 r., osiągnął
taką dojrzałość i jakość, że na jego bazie powstały dwie dystrybucje klasy korporacyjnej:
Red Hat Enterprise oraz Suse Linux Enterprise Server. Jądro systemu GNU/Linux jest
w wysokim stopniu konfigurowalne nie tylko dzięki swojej modułowej budowie, prawie 800
konfigurowalnym parametrom, które można zmieniać w czasie pracy systemu, ale także
dzięki dostępności źródeł, które także można modyfikować. Żeby jednak dostrajać jądro
do konkretnych potrzeb trzeba dobrze poznać jego funkcjonowanie.
Niniejsza praca poświęcona jest budowie narzędzia, które służy do śledzenia pracy planisty, czyli jednego z kluczowych elementów jądra. Składa się ono ze specjalnego modułu
jądra kernstat oraz programu schedstat działającego w przestrzeni użytkownika. Moduł
kernstat służy do przekazywania danych sterujących generowanych przez program schedstat z przestrzeni użytkownika do przestrzeni jądra, odczytywaniu odpowiednich danych
ze wskazanych struktur jądra oraz przekazywanie ich z powrotem do przestrzeni użytkownika. Wymiana danych odbywa się za pośrednictwem pliku urządzenia znakowego
/dev/kernstat.
Moduł kernstat został tak pomyślany i zaimplementowany, aby w przyszłości mógł
zostać rozszerzony o dodatkowe funkcje pobierające z jądra inne parametry jego pracy.
Jeśli zostanie to powiązane z tworzeniem odpowiednich narzędzi działających w przestrzeni użytkownika, narzędzi podobnych do programu schedstat, to uzyskamy nie tylko
cenne narzędzia diagnostyczne, ale także dydaktyczne, które powinny ułatwić zrozumienie
działania jądra systemu GNU/Linux.
Budowa omawianego narzędzia jest utrudniona z dwóch powodów. Po pierwsze, dostęp do zmiennych planisty z poziomu modułów jądra, a w szczególności do kolejki zadań
gotowych do wykonania, jest niemożliwy, ponieważ konsolidator tworzy takie dane w specjalnej sekcji wykonywalnej .data.percpu, do której moduły mają zabroniony dostęp [1].
Po drugie, nie mamy możliwości wykorzystania definicji zmiennych, struktur danych oraz
funkcji planisty, gdyż te definicje nie są dostępne poprzez pliki nagłówkowe.
Taka możliwość istniała jeszcze do wersji 2.6.18, gdyż prawie wszystkie potrzebne
definicje znajdowały się w pliku <include/linux/sched.h>, a nie <kernel/sched.c>. Ta
zmiana podyktowana została względami bezpieczeństwa, ale utrudnia budowę modułów
wykorzystujących te definicje. Poprzednie podejście pozwalało na tworzenie instancji odpowiednich zmiennych. Wówczas dostęp do zmiennych uprzednio zdefiniowanych wymagał
jedynie odczytania adresów z eksportowanych symboli jądra i ich rzutowanie (o tym czy
jakiś symbol jest eksportowany można się przekonać przeglądając plik /boot/System.mapver, gdzie ver jest numerem wersji jądra).
Przy implementacji sterownika urządzenia znakowego kernstat oraz funkcji, które
5
są odpowiedzialne za pobieranie statystyk planisty wykorzystano jądro 2.6.18 ze specjalnie
zmodyfikowaną wersją pliku <kernel/sched.c> oraz <linux/sched.h>. Zmiany polegają
na dodaniu oraz wyeksportowaniu funkcji, która daje nam bezpośredni dostęp do instancji zmiennej typu struct rq, czyli do kolejki zadań gotowych do wykonania. Bywa tak,
że kolejne wersje jądra wprowadzają zmiany w nazewnictwie symboli, tzn. zmiennych,
struktur danych oraz funkcji, a także w położeniu ich definicji. W celu uniknięcia tego
problemu ograniczono rozważania do jednego ściśle określonego jądra, co daje gwarancję,
że tworzony moduł będzie działać poprawnie.
Dzięki zastosowaniu programu schedstat możliwe jest obserwowanie w jaki sposób
system operacyjny realizuje operacje wielozadaniowości, jak dba o to, aby w systemie,
w którym pracuje jednocześnie wiele procesów nie dochodziło do przestojów. Program
schedstat ilustruje mechanizmy wykorzystywane przez planistę w postaci prezentacji różnego rodzaju statystyk oraz warunków determinujących odpowiednie ich traktowanie.
Układ pracy jest następujący. Po wstępie następują trzy rozdziały omawiające zagadnienia związane z działaniem i implementacją planisty oraz zasad działania i tworzenia
sterowników oraz modułów w jądrach systemu GNU/Linux. W rozdziale piątym przedstawiono także szczegóły implementacji modułu kernstat oraz programu schedstat. Kolejny
rozdział zawiera omówienie szeregu przykładowych zastosowań programu schedstat. Pracę
kończy dodatek wyjaśniający znaczenie szeregu terminów przewijających się w pracy.
2
Szeregowanie procesów
Planista (ang. scheduler ) to część jądra systemu operacyjnego, która zajmuje się przydzielaniem czasu procesora procesom. Przydział czasu procesora odbywa się za pomocą
pewnych, ściśle określonych reguł noszących nazwę polityki przydziału. Polityka przydziału obejmuje:
• podział procesów na klasy;
• określenie sposobu szeregowania dla każdej klasy;
• określenie zasad przydziału procesora pomiędzy klasami;
• zarządzanie priorytetami statycznymi i dynamicznymi.
Do realizacji określonej polityki potrzebne są następujące mechanizmy:
• procedura przełączająca kontekst;
• przerwanie zegarowe i inne narzędzia do odmierzania czasu;
6
• kolejki i inne struktury danych opisujące stan procesów.
Procesy można zakwalifikować jako zorientowane obliczeniowo i zorientowane na wejściewyjście. Metoda szeregowania powinna umieć sobie radzić z dwoma przeciwstawnymi
celami tzn: minimalizacją czasu reakcji i maksymalizacją przepustowości. Faworyzowanie
procesów zorientowanych na wejście-wyjście zwykle poprawia pierwszy z tych mierników,
gdyż taki charakter mają procesy interakcyjne. Linux faworyzuje takie procesy, ale nie
zapominając o zadaniach zorientowanych obliczeniowo. Stosowane strategie szeregowania wykorzystują min. priorytety. Procesy o wyższym priorytecie dostają procesor przed
procesami o niższym priorytecie i na dłuższy odcinek czasu. Priorytety mogą zmieniać
się dynamicznie, mianowicie proces może rozpocząć działanie z pewnym priorytetem bazowym, który w trakcie działania może się zmniejszyć bądz też zwiększyć. Począwszy
od jąder z serii 2.5 Linux stosuje nowego planistę, który zapewnia:
• wybór kolejnego procesu do wykonania w stałym czasie;
• sprawiedliwy przydział procesora ;
• dobrą skalowalność dla architektur wieloprocesorowych;
• równoważenie obciążenia dla systemów wieloprocesorowych;
• doskonałe działanie aplikacji interaktywnych, nawet przy dużym obciążeniu systemu;
• możliwość łatwego dostrajanie do specyficznych potrzeb.
2.1
Klasy procesów
Linux rozróżnia następujące klasy procesów, w kolejności malejących priorytetów:
• procesy czasu rzeczywistego: SCHED RR, SCHED FIFO;
• procesy zwykłe: SCHED NORMAL;
• procesy wsadowe: SCHED BATCH ;
• procesy typu idle.
Proces typu idle to proces o numerze 0 tworzony bez udziału wywołania fork, inicjowany makrodefinicją INIT TASK. Cały czas znajduje się w stanie TASK RUNNING
i w przypadku, gdy nie ma innego procesu gotowego do pracy, to on otrzymuje procosor. Jednak gdy tylko pojawi się jakiś inny proces gotowy do wykonania, to idle
jest wywłaszczany.
7
Procesy czasy rzeczywistego mają bezwzględne pierwszeństwo przed zwykłymi procesami:
• jeśli jest gotowy proces klasy RT, to nie może zostać wybrany do wykonania żaden
proces innej klasy;
• jeśli podczas wykonywania zwykłego procesu, proces RT przechodzi do stanu gotowy, to proces wykonywany jest natychmiast wywłaszczany.
W klasie RT procesy mogą być szeregowane na dwa sposoby:
• Z podziałem czasu wg algorytmu karuzelowego SCHED RR. Proces taki przerywa
swoje działanie gdy:
– w kolejce procesów gotowych pojawia się proces o wyższym priorytecie może
to być tylko proces czasu rzeczywistego;
– skończy się przydzielony mu kwant czasu;
– zablokuje się w oczekiwaniu na zasób;
– dobrowolnie zrezygnuje z procesora;
– zakończy się.
• Zgodnie z zasadą kolejki prostej SCHED FIFO. Proces przerywa swoje działanie
gdy:
– w kolejce procesów gotowych pojawia się proces o wyższym priorytecie, może
to być tylko proces czasu rzeczywistego;
– zablokuje się w oczekiwaniu na zasób;
– dobrowolnie zrezygnuje z procesora;
– zakończy się.
Jeśli procesy są typu SCHED NORMAL, to do wykonania zostaje wybrany ten z procesów
gotowych, który ma największą wartość priorytetu dynamicznego. Proces taki przerywa
swoje działanie, jeśli:
• w kolejce procesów gotowych pojawia się proces o wyższym priorytecie;
• skończy się przydzielony mu w kwant czasu;
• zablokuje się w oczekiwaniu na zasób;
• dobrowolnie zrezygnuje z procesora;
8
• zakończy się.
Planista korzysta z następujących pól w strukturach task struct oraz thread info:
unsigned long p o l i c y ;
int s t a t i c p r i o ;
int p r i o ;
unsigned long s l e e p a v g ;
enum s l e e p t y p e s l e e p t y p e ;
unsigned long long timestamp ;
unsigned long policy określa tryb szeregowania procesu, dostępne tryby to: SCHED NORMAL, SCHED FIFO, SCHED RR,SCHED BATCH ;
int static prio określa priorytet statyczny, który użytkownik może zmienić wywołując
funkcję nice(). Priorytet statyczny przyjmuje wartości od 100 do 139 przy czym
priorytety z przedziału od 0 do 99 są przeznaczone dla procesów typu rzeczywistego,
a te z przedziału od 100 do 139 dla procesów zwykłych. Duża liczba to niski priorytet,
a mała liczba to wysoki priorytet. Makro TASK NICE wylicza wartość nice dla
ustalonego static prio, a makro NICE TO PRIO wylicza wartość static prio dla
ustalonego nice;
int prio procesu jest przechowywany w polu task struct->prio. Jest oparty na priorytecie statycznym. Priorytet dynamiczny jest sumą priorytetu statycznego i pewnego
bonusu, który zwiększa priorytet dynamiczny procesom interaktywnym, a zmniejsza
procesom zorientowanym na obliczenia;
unsigned long sleep avg to zmienna reprezentująca stosunek czasu wykonywania do
oczekiwania procesu;
enum sleep type sleep type to zmienna przyjmująca jedną z wartości: SLEEP NORMAL, SLEEP NONINTERACTIVE, SLEEP INTERACTIVE, SLEEP INTERRUPTED;
unsigned long long timestamp określa datę zdarzenia, czyli moment od ktorego rozpoczęło sie wykonywanie procesu lub kiedy proces został dodany do kolejki; zmienna
używana do aktualizowania sleep avg.
Z każdym procesorem związana jest jedna struktura struct rq. W struct rq znajdują się
procesy gotowe do wykonania. Podstawowymi składowymi struktury struct rq są dwie
tablice typu struct prio array. Pierwsza tablica o nazwie active przeznaczona jest dla
9
procesów, które jeszcze nie wykorzystały swojego kwantu czasu, natomiast druga o nazwie
expired przeznaczona jest dla procesów, które wykorzystały już swój kwant czasu.
struct rq {
spinlock t lock ;
/∗
∗ n r r u n n i n g and c p u l o a d s h o u l d be i n t h e same c a c h e l i n e b e c a u s e
∗ remote CPUs use b o t h t h e s e f i e l d s when d o i n g l o a d c a l c u l a t i o n .
∗/
unsigned long n r r u n n i n g ;
unsigned long r a w w e i g h t e d l o a d ;
#i f d e f CONFIG SMP
unsigned long c p u l o a d [ 3 ] ;
#endif
unsigned long long n r s w i t c h e s ;
/∗
∗ This i s p a r t o f a g l o b a l c o u n t e r where o n l y t h e t o t a l sum
∗ o v e r a l l CPUs m a t t e r s . A t a s k can i n c r e a s e t h i s c o u n t e r on
∗ one CPU and i f i t g o t m i g r a t e d a f t e r w a r d s i t may d e c r e a s e
∗ i t on a n o t h e r CPU. Always u p d a t e d under t h e runqueue l o c k :
∗/
unsigned long n r u n i n t e r r u p t i b l e ;
unsigned long e x p i r e d t i m e s t a m p ;
unsigned long long t i m e s t a m p l a s t t i c k ;
struct t a s k s t r u c t ∗ c u r r , ∗ i d l e ;
struct mm struct ∗prev mm ;
struct p r i o a r r a y ∗ a c t i v e , ∗ e x p i r e d , a r r a y s [ 2 ] ;
int b e s t e x p i r e d p r i o ;
atomic t nr iowait ;
#i f d e f CONFIG SMP
struct sched domain ∗ sd ;
/∗ For a c t i v e b a l a n c i n g ∗/
int a c t i v e b a l a n c e ;
int push cpu ;
int cpu ;
/∗ cpu o f t h i s runqueue ∗/
struct t a s k s t r u c t ∗ m i g r a t i o n t h r e a d ;
struct l i s t h e a d m i g r a t i o n q u e u e ;
#endif
#i f d e f CONFIG SCHEDSTATS
/∗ l a t e n c y s t a t s ∗/
10
struct s c h e d i n f o r q s c h e d i n f o ;
/∗ s y s s c h e d y i e l d ( ) s t a t s ∗/
unsigned long y l d e x p e m p t y ;
unsigned long y l d a c t e m p t y ;
unsigned long y l d b o t h e m p t y ;
unsigned long y l d c n t ;
/∗ s c h e d u l e ( )
unsigned long
unsigned long
unsigned long
stats
sched
sched
sched
∗/
switch ;
cnt ;
goidle ;
/∗ t r y t o w a k e u p ( ) s t a t s ∗/
unsigned long t t w u c n t ;
unsigned long t t w u l o c a l ;
#endif
struct l o c k c l a s s k e y r q l o c k k e y ;
};
Struktura prio array jest określona następująco:
struct p r i o a r r a y {
unsigned int n r a c t i v e ;
DECLARE BITMAP( bitmap , MAX PRIO+1); /∗ i n c l u d e 1 b i t f o r d e l i m i t e r ∗/
struct l i s t h e a d queue [ MAX PRIO ] ;
};
nr active liczba procesów w tablicy;
bitmap bitmapa reprezentująca stan tablicy queue. Ponieważ queue ma stałą długość
(MAX PRIO=140), więc bitmapa zajmuje 140 bitów. Stan bitu określa, czy odpowiednia lista jest pusta;
queue tablica list. Pod indeksem P znajduje się lista procesów z priorytetem dynamicznym równym P.
2.2
Priorytety i kwanty czasu
Priorytet statyczny procesu jest przechowywany w polu task struct->static prio. Jest
to liczba z przedziału od 0 do 139, przy czym priorytety z przedziału od 0 do 99 są przeznaczone dla procesów typu rzeczywistego, a te z przedziału od 100 do 139 dla procesów
11
zwykłych. Duża liczba oznacza niski priorytet, a mała wysoki. Priorytet dynamiczny procesu jest przechowywany w polu task struct->prio. Jest on oparty na priorytecie statycznym. Priorytet ten jest używany wewnętrznie przez algorytm planisty, tak aby kolejność
wykonywania procesów uwzględniała ich interaktywność. Priorytet dynamiczny jest sumą
priorytetu statycznego i pewnej premii (ang. bonus), która powoduje zwiększenie priorytetu dynamicznego dla procesów interaktywnym, a zmniejszenie procesom uzależnionym
od CPU. Premia, którą proces może otrzymać, to wartość z przedziału [-5, 5]. Procesy
interaktywne mogą dostać premię równą 5, co oznacza pomniejszenie ich priorytetu o 5,
czyli zwiększenie ich względnej ważności. Priorytet dynamiczny procesu jest pamiętany
w polu prio struktury task struct. Wartość ta obliczana jest za pomocą funkcji effective prio() w następujący sposób [1]:
#define CURRENT BONUS( p ) ( NS TO JIFFIES ( ( p)−> s l e e p a v g ) ∗ \
MAX BONUS / MAX SLEEP AVG)
s t a t i c i nt e f f e c t i v e p r i o ( t a s k t ∗p )
{
int bonus , p r i o ;
i f ( rt task (p))
return p−>p r i o ;
bonus = CURRENT BONUS( p ) − MAX BONUS / 2 ;
p r i o = p−>s t a t i c p r i o − bonus ;
i f ( p r i o < MAX RT PRIO)
p r i o = MAX RT PRIO ;
i f ( p r i o > MAX PRIO−1)
p r i o = MAX PRIO−1;
return p r i o ;
}
Kwant czasu, to okres na jaki proces otrzymuje jednostkę centralną. Minimalny kwant
wynosi 5 msec, domyślnie jest ustalany na poziomie 100 msec, a maksymalnie może wynosić 800 msec. Wiele jąder dystrybucyjnych preferuje różne wartości więc nie powinno być
dla nikogo żadnym zaskoczeniem jeżeli zamiast wartości przedstawionych poniżej znajdzie
zupełnie inne. W jądrze te wartości określane są przez poniższe definicje:
#define MIN TIMESLICE
#define DEF TIMESLICE
#define MAX TIMESLICE
( 1 0 ∗ HZ / 1 0 0 0 )
100 ∗ HZ / 1 0 0 0 )
( 2 0 0 ∗ HZ / 1 0 0 0 )
HZ częstotliwość zagara, tzn. liczba tyknięć na sekundę;
MIN TIMESLICE minimalny kwant jaki może otrzymać proces;
12
MAX TIMESLICE maksymalny kwant czasu jaki może otrzymać proces.
Kwant czasu przydzielony procesowi jest pamiętany w polu time slice struktury task struct
i obliczany jest w następujący sposób:
#define BASE TIMESLICE( p ) \
(MIN TIMESLICE + ( (MAX TIMESLICE − MIN TIMESLICE) ∗ \
(MAX PRIO−1 − ( p)−> s t a t i c p r i o ) / (MAX USER PRIO−1)))
s t a t i c unsigned int t a s k t i m e s l i c e ( t a s k t ∗p )
{
return BASE TIMESLICE( p ) ;
}
2.3
Interaktywność
Wartość priorytetu dynamicznego zależy tylko od wartości zmiennej sleep avg, która jest
miarą interaktywności procesu. Wartość tej zmiennej jest równa stosunkowi długości
dwóch przedziałów czasowych: czasu oczekiwania przez proces na przydział procesora
do czasu wykonywania się procesu. Wartość sleep avg jest zwiększana, gdy proces zostaje uaktywniony, tzn. obudzony bądź przeniesiony z innego procesora. Uaktywnienie
procesu jest realizowane za pomocą funkcji activate task. Wartość sleep avg jest zmniejszana przy przełączaniu kontekstu, co ma miejsce w procedurze schedule. W funkcji activate task wywoływana jest procedura recalc task prio, która wyznacza nowy priorytet
dynamiczny procesu. Procedura recalc task prio aktualizuje pole sleep avg procesu. Zauważmy, że po obudzeniu procesu nieinteraktywnego nie należy zwiększać jego sleep avg
tak samo jak po obudzeniu procesu interaktywnego, gdyż może to doprowadzić do sytuacji, że procesy interaktywne będą musiały czekać, aż proces nieinteraktywny wykorzysta
swój kwant czasu. W strukturze task struct znajduje się pole interactive credit, które odpowiada interaktywności procesu w długiej perspektywie. Wartość tego pola zmienia się
tylko o jeden, a istotne wartości określają następujące makrodefinicje:
#define HIGH CREDIT( p ) ( ( p)−> i n t e r a c t i v e c r e d i t > CREDIT LIMIT)
#define LOW CREDIT( p )
( ( p)−> i n t e r a c t i v e c r e d i t < −CREDIT LIMIT)
#define INTERACTIVE SLEEP( p ) ( JIFFIES TO NS (MAX SLEEP AVG ∗ \
(MAX BONUS / 2 + DELTA( ( p ) ) + 1 ) / MAX BONUS − 1 ) )
Makro INTERACTIVE SLEEP(p) definiuje jak długo proces powinien spać, aby został
uznany za proces interaktywny. Ta arbitralna wartość okresu spania ustalana jest w na13
nosekundach. Jak już wcześniej wspomniano w funkcji recalc task prio obliczana jest wartość pola sleep avg, a także nowy priorytet dynamiczny prio. Algorytm jakim posługuje
się planista przy ustalaniu odpowiednich wartości oraz cech wygląda następująco:
1. Obliczanie sleep time, czyli czasu jaki proces spędził w kolejce:
unsigned long long
s l e e p t i m e = now − p−>timestamp ;
unsigned long s l e e p t i m e ;
i f ( s l e e p t i m e > NS MAX SLEEP AVG)
s l e e p t i m e = NS MAX SLEEP AVG ;
e l s e s l e e p t i m e = ( unsigned long ) s l e e p t i m e ;
2. Sprawdzenie, kiedy proces spełnia warunki interaktywności polega na określeniu,
czy:
• proces nie jest wątkiem jądra;
• proces nie czekał w stanie UNINTERRUPTIBLE;
• proces odpowiednio długo przebywał w kolejce.
i f ( p−>mm && p−>a c t i v a t e d !=−1 && s l e e p t i m e > INTERACTIVE SLEEP( p ) )
są spełnione sleep avg przypisana zostaje prawie maksymalna wartość; w przeciwnym wypadku wartość sleep time zostaje pomnożona przez wartość odpowiadającą
obecnej wartości sleep avg. Dla mniejszej wartości sleep avg czynnik będzie większy.
s l e e p t i m e ∗= (MAX BONUS − CURRENT BONUS( p ) ) ? : 1 ;
Następnie wykonane zostają instrukcje zapobiegające zwiększeniu sleep avg dla niektórych grup procesów. Dla procesów LOW CREDIT(p) wartość sleep time jest tak
ustalana, aby była mniejsza od kwantu, który pozostał procesowi do wykorzystania.
Powoduje to, że procesy nieinteraktywne nie dostaną nagle dużego sleep avg i nie
będą traktowane jak interaktywne.
i f (LOW CREDIT( p)&& s l e e p t i m e > JIFFIES TO NS ( t a s k t i m e s l i c e ( p ) )
s l e e p t i m e = JIFFIES TO NS ( t a s k t i m e s l i c e ( p ) ) ;
3. Sprawdzenie warunków nieinteraktywności obejmuje określenie czy:
• proces czekał w stanie UNINTERRUPTIBLE;
• proces nie ma wysokiego interactive credit;
14
• proces nie jest wątkiem jądra.
Jeżeli powyższe warunki są spełnione proces uznawany jest za CPU-bound i na
sleep avg zostaje przypisana odpowiednio mała wartość, a sleep time zostaje wyzerowane.
i f ( p−>a c t i v a t e d == −1 && ! HIGH CREDIT( p ) && p−>mm)
{
i f ( p−>s l e e p a v g >= INTERACTIVE SLEEP( p ) )
sleep time = 0;
e l s e i f ( p−>s l e e p a v g + s l e e p t i m e >=
INTERACTIVE SLEEP( p ) )
{
p−>s l e e p a v g = INTERACTIVE SLEEP( p ) ;
sleep time = 0;
}
}
4. Ostateczne wyliczenie wartości sleep avg wygląda następująco:
p−>s l e e p a v g += s l e e p t i m e ;
i f ( p−>s l e e p a v g > NS MAX SLEEP AVG)
{
p−>s l e e p a v g = NS MAX SLEEP AVG ;
i f ( ! HIGH CREDIT( p ) )
p−>i n t e r a c t i v e c r e d i t ++;
}
5. Obliczenie nowego priorytetu dynamicznego prio
p−>p r i o = e f f e c t i v e p r i o ( p ) ;
Warto przyjrzeć się warunkowi stwierdzającemu nieinteraktywność procesu w funkcji
recalc task prio
i f ( p−>mm && s l e e p t i m e >
INTERACTIVE SLEEP( p ) && p−>s l e e p a v g < INTERACTIVE SLEEP( p ) )
a dokładniej
s l e e p t i m e > INTERACTIVE SLEEP( p )
Ta nierówność nigdy nie będzie spełniona dla procesów z odpowiednim niskim priorytetem static prio, tzn. [136-139] lub nice 16-19, zatem takie procesy nigdy nie
będą traktowane jak interaktywne.
15
2.4
Szeregowanie
W jądrze 2.4 procedura schedule spełnia dwie podstawowe funkcje: znajduje następny
proces do wykonania oraz po zakończeniu epoki wylicza nowy kwant czasu wszystkim
procesom. Od jąder 2.5 oba te zadania zostały rozdzielone pomiędzy procedurę schedule, która znajduje następny proces do wykonania, oraz procedurę scheduler tick, która
każdemu procesowi wylicza nowy priorytet i nowy kwant czasu.
2.4.1
Funkcja schedule()
W jądrze 2.4 kwant czasu był wyliczany dopiero po zakończeniu epoki. Trzeba to było
robić dopiero po wyczerpaniu przez proces kwantu czasu, gdyż w przeciwnym wypadku
nie dałoby się rozróżnić procesów, które się jeszcze nie wykonywały, od tych, które mają
już nowy kwant czasu. W jądrze 2.6 odseparowano te dwie grupy procesów przechowując
je w dwóch tablicach: active i expired. Linux utrzymuje dwie kolejki procesów gotowych
dla każdego procesora. W kolejce active są przechowywane te procesy, którym pozostał
niezerowy kwant czasu, a w kolejce expired te, które już wykorzystały swój kwant czasu.
Kiedy proces zużyje swój kwant czasu, to jest przenoszony z kolejki active do kolejki
expired. Przy okazji wykonywania tej operacji wyliczany jest dla tego procesu nowy kwant
czasu. Gdy kolejka active jest już pusta, to zamieniamy wskaźniki active na expired :
a r r a y = rq−>a c t i v e ;
i f ( u n l i k e l y ( ! array −>n r a c t i v e ) ) {
rq−>a c t i v e = rq−>e x p i r e d ;
rq−>e x p i r e d = a r r a y ;
a r r a y = rq−>a c t i v e ;
rq−>e x p i r e d t i m e s t a m p = 0 ;
rq−>b e s t e x p i r e d p r i o = MAX PRIO ;
}
Planista wykonując procedurę schedule wybiera proces o najwyższym priorytecie wg następującego algorytmu:
• na podstawie array->bitmap znajdowany jest indeks idx pierwszej niepustej listy
procesów
• na zmienną queue przypisywany jest wskaźnik na listę znalezioną w pierwszym kroku;
• wyznaczany jest następny next proces do wykonania; jest to pierwszy proces na tej
liście.
16
typedef struct t a s k s t r u c t t a s k t ;
void s c h e d u l e ( ) {
...
t a s k t ∗ next ;
p r i o a r r a y t ∗ array ;
struct l i s t h e a d ∗ queue ;
int i d x ;
a r r a y = rq−>a c t i v e ;
i d x = s c h e d f i n d f i r s t b i t ( array −>bitmap ) ;
queue = array −>queue + i d x ;
next = l i s t e n t r y ( queue−>next , t a s k t , r u n l i s t ) ;
...
}
2.4.2
Funkcja scheduler tick()
Procedura scheduler tick() jest wywoływana przy każdym przerwaniu pochodzącym od zegara. Najpierw obsługiwane są procesy czasu rzeczywistego. Procesy szeregowane zgodnie
z zasadą kolejki prostej FIFO nie wymagają żadnej akcji, gdyż nie korzystają z kwantu
czasu. Procesy szeregowane zgodnie ze strategią karuzelową RR po upłynięciu kwantu
czasu idą na koniec kolejki procesów gotowych o tym samym priorytecie, od razu dostają nowy kwant poprzez natychmiastowe wywołanie funkcji task timeslice(). Jeśli proces
zużyje swój kwant czasu, to przelicza się mu na nowo priorytet w funkcji effective prio()
i wylicza nowy kwant czasu w funkcji task time slice(). Proces zasadniczo powinien powędrować teraz do kolejki expired, ale jeśli proces szeregujący stwierdzi, że ma do czynienia
z procesem interakcyjnym poprzez makro TASK INTERACTIVE, to wstawi go z powrotem do kolejki active, upewniwszy się przedtem jednak, że nie spowoduje to zagłodzenia procesów czekających w kolejce expired, czyli nie ma tam procesów, które czekają
już bardzo długo, bo dawno nie przełączano kolejek, sprawdza się to za pomocą makra
EXPIRED STARVING. Jeśli kwant czasu procesu jeszcze się nie skończył, to lepiej się
upewnić, że kwant nie jest nadmiernie duży i że proces nie próbuje zmonopolizować procesora. W takim przypadku dzieli się kwant na mniejsze kawałk czyli po prostu przenosi
proces na koniec kolejki procesów o tym samym priorytecie.
void s c h e d u l e r t i c k ( int u s e r t i c k s , int s y s t i c k s )
{
...
i f ( unlikely ( rt task (p ))) {
i f ( ( p−>p o l i c y == SCHED RR) && !−−p−>t i m e s l i c e )
{
17
p−>t i m e s l i c e = t a s k t i m e s l i c e ( p ) ;
p−> f i r s t t i m e s l i c e = 0 ;
set tsk need resched (p ) ;
d e q u e u e t a s k ( p , rq−>a c t i v e ) ;
e n q u e u e t a s k ( p , rq−>a c t i v e ) ;
}
goto o u t u n l o c k ;
}
...
}
Zauważmy, że jeśli proces ma wysoki priorytet prio, to z chwilą kiedy zostanie przełożony do kolejki expired może nie otrzymać czasu procesora przez względnie długi czas,
czyli do momentu aż wszystkie aktywne procesy wyczerpią swoje kwanty czasu i nastąpi
zamiana tablic. Często takim procesem o wysokim prio jest proces interaktywny i dlatego zastosowano inne rozwiązanie. Mianowicie, jeśli proces interaktywny wyczerpie swój
kwant czasu, to wyliczany jest dla niego nowy kwant czasu oraz nowy priorytet, ale proces nie jest przekładany do tablicy expired, a wraca do tablicy active. Ten powrót jest
tak zorganizowany, żeby nie głodzić innych procesów poprzez upewnienie się czy procesy
w kolejce expired nie czekają za długo na zamianę kolejek. Kwant czasu procesów interaktywnych jest dzielony w taki sposób, żeby procesy nie wykorzystywały całego kwantu od
razu. Do tego celu służą makra: TIMESLICE GRANULARITY, EXPIRED STARVING
oraz TASK INTERACTIVE.
#i f d e f CONFIG SMP
#define TIMESLICE GRANULARITY( p )
(MIN
( 1 << ( ( (MAX BONUS − CURRENT BONUS( p ) ) ?
num online cpus ( ) )
#e l s e
#define TIMESLICE GRANULARITY( p )
(MIN
( 1 << ( ( (MAX BONUS − CURRENT BONUS( p ) ) ?
#endif
TIMESLICE ∗ \
: 1) − 1)) ∗ \
TIMESLICE ∗ \
: 1) − 1 ) ) )
Dla procesu bardziej interaktywnego makro TIMESLICE GRANULARITY zwraca mniejszą wartość. TIMESLICE GRANULARITY(p) określa na jakie porcje należy podzielić
kwant procesu. Makro TASK INTERACTIVE to makro rozstrzygające, czy proces jest
interaktywny:
#define TASK INTERACTIVE( p ) \
( ( p)−> p r i o <= ( p)−> s t a t i c p r i o − DELTA( p ) )
#define DELTA( p ) \
18
(SCALE(TASK NICE( p ) , 4 0 , MAX BONUS) + INTERACTIVE DELTA)
#define SCALE( v1 , v1 max , v2 max ) ( v1 ) ∗ ( v2 max ) / ( v1 max )
#define INTERACTIVE DELTA
2
makro TASK INTERACTIVE(p) jest łatwiejsze do spełnienia dla procesów z wyższym
priorytetem statycznym. Warto zauważyć że makro opiera się na priorytecie dynamicznym, dlatego ten sam proces może być raz uznany za interaktywny, a innym razem za nieinteraktywny. Makro EXPIRED STARVING, to makro rozstrzygające, czy procesy w kolejce expired głodują:
#define EXPIRED STARVING( rq ) \
( ( STARVATION LIMIT && ( ( rq)−>e x p i r e d t i m e s t a m p && \
( j i f f i e s − ( rq)−>e x p i r e d t i m e s t a m p >= \
STARVATION LIMIT ∗ ( ( rq)−>n r r u n n i n g ) + 1 ) ) ) \
( ( rq)−>c u r r −>s t a t i c p r i o > ( rq)−> b e s t e x p i r e d p r i o ) )
STARVATION LIMIT stała;
rq->expired timestamp zmienna, której wartości są interpretowane w dwojaki sposób.
0 oznacza, że do kolejki expired nie został jeszcze włożony żaden proces (ustawiane
na zero przy zamianie wskaźników tablic). Wartość zmiennej większa od 0, to stempel czasowy, określający kiedy do tablicy expired został włożony pierwszy proces;
jiffies to zmienna zwracająca aktualny czas systemowy.
Najistotniejsze warunki marka EXPIRED STARVING, to sprawdzenie, czy najdłużej
przebywający proces w tablicy expired nie przebywa w niej zbyt długo w stosunku do liczby procesów gotowych do wykonania w całej kolejce:
( j i f f i e s − ( rq)−>e x p i r e d t i m e s t a m p >= \
STARVATION LIMIT ∗ ( ( rq)−>n r r u n n i n g ) + 1 ) ) )
( rq)−>c u r r −>s t a t i c p r i o > ( rq)−> b e s t e x p i r e d p r i o )
Wartość rq->best expired prio jest równa maksymalnej wartości priorytetu statycznego
procesu przebywającego w tablicy expired. To makro powoduje uwzględnienie preferencji
użytkownika. Proces o niższym priorytecie statycznym nigdy nie będzie włożony ponownie do active, jeśli w expired znajduje proces o wyższym priorytecie statycznym dzieje się
tak niezależnie od ich priorytetów dynamicznych. Dalsza część kodu scheduler tick wykorzystuje te makra, tzn. kwant czasu zostaje zmniejszony i sprawdzony zostaje warunek
czy procesowi skończył się jego kwant czasu:
19
i f (!−−p−>t i m e s l i c e )
Jeżeli tak, to wówczas następuje
• usunięcie procesu z kolejki;
• ustawienie flagi need resched ;
• przypisanie nowego priorytetu dynamicznego i nowego kwant czasu;
• jeśli pole rq->expired timestamp nie zostało jeszcze ustawione (obsługujemy pierwszy proces od zamiany wskaźników), to przypisujemy na to pole aktualny czas;
• jeśli proces nie jest interaktywny lub procesy w expired są głodzone, to proces trafia
do tablicy expired ; w przeciwnym wypadku trafia do tablicy active.
d e q u e u e t a s k ( p , rq−>a c t i v e ) ;
set tsk need resched (p ) ;
p−>p r i o = e f f e c t i v e p r i o ( p ) ;
p−>t i m e s l i c e = t a s k t i m e s l i c e ( p ) ;
p−> f i r s t t i m e s l i c e = 0 ;
i f ( ! rq−>e x p i r e d t i m e s t a m p )
rq−>e x p i r e d t i m e s t a m p = j i f f i e s ;
i f ( ! TASK INTERACTIVE( p ) | | EXPIRED STARVING( rq ) )
{
e n q u e u e t a s k ( p , rq−>e x p i r e d ) ;
i f ( p−>s t a t i c p r i o < rq−>b e s t e x p i r e d p r i o )
rq−>b e s t e x p i r e d p r i o = p−>s t a t i c p r i o ;
}
e l s e e n q u e u e t a s k ( p , rq−>a c t i v e ) ;
Sprawdzamy następnie, czy proces jest interaktywny i czy jego kwant czasu powinien
być dzielony. Jeżeli tak to, przekładamy go do odpowiedniej kolejki i ustawiamy flagę
NEED RESCHED. W przeciwnym razie proces będzie się dalej wykonywał:
i f (TASK INTERACTIVE( p ) && ! ( ( t a s k t i m e s l i c e ( p ) −
p−>t i m e s l i c e ) % TIMESLICE GRANULARITY( p ) ) &&
( p−>t i m e s l i c e >= TIMESLICE GRANULARITY( p ) ) &&
( p−>a r r a y == rq−>a c t i v e ) ) {
d e q u e u e t a s k ( p , rq−>a c t i v e ) ;
set tsk need resched (p ) ;
20
p−>p r i o = e f f e c t i v e p r i o ( p ) ;
e n q u e u e t a s k ( p , rq−>a c t i v e ) ;
2.4.3
Funkcja sched yield
Funkcja sched yield pozwala procesowi zrezygnować z procesora. W jądrze 2.5 wywołanie
tej funkcji powoduje dla procesów czasu rzeczywistego efekt jak w jądrze 2.4, tj. proces
zostaje przełożony na koniec kolejki active odpowiadającej jego priorytetowi. Natomiast
proces zwykły zostaje przełożony do kolejki expired, co sprawia, że może stracić dostęp
do procesora na czas znacznie dłuższy niż to miało miejsce w przypadku jądra 2.4.
3
Moduły jądra
Moduł to relokowalny kod/dane, które mogą być wstawiane i usuwane z jądra w czasie
działania systemu. Moduł może odwoływać się do eksportowanych symboli jądra tak,
jakby był skompilowany jako część jądra oraz sam może udostępniać, tzn. eksportować
symbole, z których mogą korzystać inne moduły. Moduł odpowiada za pewną określoną
usługę w jądrze [2].
3.1
Konstruktor i destruktor modułu
Niezbędną deklaracją do jakiejkolwiek pracy z modułami jądra systemu Linux jest dodanie
pliku nagłówkowego <linux/module.h>. Ten plik zawiera szerg funkcji oraz makr, które
zapewniają interakcję z jądrem. Każdy moduł musi definiować funkcję inicjującą moduł
(konstruktor ) oraz funkcję zwalniającą moduł (destruktor ). Standardowo funkcje te muszą
być zdefiniowane w następujący sposób: [3]
module init ( konstruktor ) ;
module exit ( destruktor ) ;
Funkcja inicjująca jest wywoływana przy ładowaniu modułu. Zwraca kod błędu, jeżeli
nie udało się zainicjować modułu, w przeciwnym przypadku 0. Funkcja zwalniająca jest
wywoływana przy usuwaniu modułu.
3.2
Zależności między modułami
Jądro udostępnia zestaw eksportowanych symboli widocznych dla modułów w taki sposób, jakby były skompilowane w jądrze. Listę tych symboli można uzyskać przeglądając
21
zawartość pliku /proc/kallsyms. Po załadowaniu modułu niektóre symbole mogą być dodane do symboli jądra i stać się widoczne dla nowoładowanych modułów, podobnie jak
pozostałe symbole jądra. Symbole eksportowane przez kolejno ładowane moduły przechowywane są na zasadzie stosu. Deklaracja w module C zmiennej eksportowanej o nazwie
Z przykrywa poprzednią deklarację zmiennej eksportowanej Z z modułu A, który został
załadowany wcześniej. Wiązanie zmiennych odbywa się jednak podczas ładowania modułu, więc deklaracja nowego adresu pod nazwą już wykorzystywaną nie wpływa na moduły
rezydujące w pamięci, tylko na te, które będą załadowane po module eksportującym. Czyli
moduł B, załadowany pomiędzy modułami A i C, które eksportują symbol Z, będzie widział zmienną Z eksportowaną z modułu A, zaś moduł D załadowany po module C będzie
korzystał ze zmiennej pochodzącej z modułu C. Jeśli moduł B używa symbolu definiowanego przez moduł A, to jest od niego zależny, gdyż B nie może zostać załadowany zanim
nie zostanie załadowany moduł A. Symbole zdefiniowane w jądrze nie mogą być zastąpione
symbolem znajdującym się w module. Program wiążący symbole najpierw sprawdza, czy
symbol został zdefiniowany w jądrze, a dopiero potem sprawdza moduły. Tylko symbole
oznaczone makrem EXPORT SYMBOL() są eksportowane z modułu. Takie symbole nie
powinny być definiowane jako static. Wyeksportowany symbol może być używany przez
inne moduły. Istnieje dodatkowa wersja makra: EXPORT SYMBOL GPL(), która powoduje wyeksportowanie symbolu, ale taki symbol może być używany tylko przez moduły,
których licencja określona makrem MODULE LICENSE() jest zgodna z licencją jądra,
czyli jest GPL.
Kmod to podsystem jądra zajmujący się ładowaniem modułów na żądanie, tzn. gdy
wystąpi odwołanie do usługi związanej z danym modułem. Jego działanie przedstawia
się następująco. Jeśli użytkownik żąda dostępu do urządzenia, które jest obsługiwane
przez moduł, który nie jest załadowany, to jądro zawiesza wykonanie programu i wykonuje funkcję request module() żądając załadowania odpowiedniego modułu. Funkcja ta jest
obsługiwana przez kmod i polega na wykonaniu programu, którym domyślnie jest /sbin/modprobe (można to zmienić za pomocą /proc) dla żądanego modułu. Na przykład,
jeśli użytkownik próbuje zamontować partycję systemu DOS, a obsługa systemu plików
FAT nie została wkompilowana w jądro, to jądro żąda załadowania modułu msdos [4].
3.3
Licznik odwołań
Moduły zazwyczaj implementują obsługę urządzeń, których wykorzystanie wiąże się z zajęciem pewnych zasobów. Licznik odwołań służy do kontroli, ile razy zasoby modułu
zostały zarezerwowane i jeszcze nie zwolnione przez inne części jądra.
struct module
{
22
/∗ R e f e r e n c e c o u n t s ∗/
struct m o d u l e r e f r e f [ NR CPUS ] ;
/∗ What modules depend on me? ∗/
struct l i s t h e a d m o d u l e s w h i c h u s e m e ;
/∗ Who i s w a i t i n g f o r us t o be u n l o a d e d ∗/
struct t a s k s t r u c t ∗ w a i t e r ;
...
};
Jeżeli montujemy system plików implementowany przez moduł, to wywoływana jest funkcja module get, która powoduje zwiększenie licznika odwołań do modułu i uniemożliwia
jego wyładowanie.
s t a t i c i n l i n e void
m o d u l e g e t ( struct module ∗ module )
{
i f ( module ) {
BUG ON( m o d u l e r e f c o u n t ( module ) == 0 ) ;
l o c a l i n c (&module−>r e f [ g e t c p u ( ) ] . count ) ;
put cpu ( ) ;
}
}
Użycie symbolu wyeksportowanego przez moduł w innym module również automatycznie
uniemożliwia jego wyładowanie [5].
struct m o d u l e r e f
{
l o c a l t count ;
}
cacheline aligned ;
3.4
Parametryzacja modułów
Tworząc moduł można zadeklarować w nim, że określona zmienna będzie zawierała parametr, który może zostać zmieniony przy ładowaniu modułu. Nazwa parametru musi
być taka sama jak nazwa zmiennej. W czasie ładowania modułu, w miejsce podanych
zmiennych, zostaną wstawione wartości podane przez użytkownika. Np. insmod modul.ko
irq=5 spowoduje podstawienie w miejsce zmiennej irq wartości 5. Do deklaracji, że pewna
zmienna ma być wykorzystana jako parametr modułu służy makro:
module param ( zmienna , typ , u p r a w n i e n i a )
23
Parametr typ może przybierać następujące wartości: byte, short, ushort, int, uint, long,
ulong, charp, bool, invbool. Typ charp jest używany do przekazywania napisów (char *).
Typ invbool oznacza parametr bool, który jest zaprzeczeniem wartości. Można definiować
własne typy parametrów, trzeba wówczas zdefiniować również funkcje param get XXX,
param set XXX oraz param check XXX. uprawnienia oznaczają uprawnienia, które zostaną nadane parametrowi w sysfs; należy je ustawić na 0. Każdy parametr powinien
posiadać także opis, który można potem odczytać wraz z opisem sposobu użycia modułu
za pomocą programu modinfo. Opis nadaje się za pomocą makra:
MODULE PARM DESC( zmienna , o p i s )
Jako przykład rozważmy następujący modułu
l i n u x −o l u v : / home/ u s e r # insmod example . ko path=” / b i n / cp ” i r q =1
int i r q = 7 ;
module param ( i r q , int , 0 ) ;
MODULE PARM DESC( i r q , ” I r q used f o r d e v i c e ” ) ;
char ∗ path=” / s b i n / modprobe ” ;
module param ( path , charp , 0 ) ;
MODULE PARM DESC( path , ” Path t o modprobe ” ) ;
p r i n t k (KERN INFO ” Using i r q : %d” , i r q ) ;
p r i n t k (KERN INFO ” W i l l u s e path : %s ” , path ) ;
l i n u x −o l u v : / home/ u s e r #dmesg
Using i r q : 1
W i l l u s e path : / b i n / cp
Aby zadeklarować tablicę parametrów trzeba użyć innej funkcji:
module param array ( zmienna , typ , w s k a z n i k n a l i c z n i k , u p r a w n i e n i a ) ;
Wszystkie pola, z wyjątkiem wskaźnik na licznik, mają takie samo znaczenie jak w module param(): wskaźnik na licznik zawiera wskaźnik do zmiennej, do której wpisana zostanie liczba elementów tablicy. Jeśli nie interesuje nas liczba argumentów, to można podać
NULL, ale wtedy trzeba rozpoznawać, czy argument jest czy też nie na podstawie jego
zawartości (nie jest to wskazane podejście). Maksymalna liczba elementów tablicy jest
określona przez deklarację tablicy, np. jeśli zadeklarujemy jej rozmiar na 4, to użytkownik
będzie mógł przekazać maksymalnie 4 elementy. W opisie parametru tablicowego zwyczajowo umieszcza się w nawiasach kwadratowych maksymalną liczbę parametrów. Jako
przykład rozważmy następującą parametryzację:
24
int num paths = 2 ;
char ∗ p a t h s [ 4 ] = { ” / b i n ” , ” / s b i n ” , NULL , NULL} ;
module param array ( paths , charp , &num paths , 0 ) ;
MODULE PARM DESC( paths , ” S e a r c h p a t h s [ 4 ] ” ) ;
int i ;
f o r ( i =0; i <num paths ; ++i )
p r i n t k (KERN INFO ” Path[%d ] : %s \n” , i , p a t h s [ i ] ) ;
W obecnej wersji jądra opis parametrów jest umieszczany w sekcji .modinfo pliku ELF.
3.5
Inne informacje o module
Następujące makra pozwalają opisać moduł:
• Autor modułu
MODULE AUTHOR( ” Imie Nazwisko <email >” ) ;
• Opis modułu
MODULE DESCRIPTION( ”Modul implementujacy . . . ” ) ;
• Licencja rozprowadzania
MODULE LICENSE( ” L i c e n c j a ” ) ;
Następujące rodzaje licencji są rozpoznawane jako darmowe oprogramowanie
– ”GPL” – GNU Public License v2 lub późniejsza);
– ”GPL and additional rights” – prawa GNU Public License v2 + dodatkowe;
– ”Dual BSD/GPL” – GNU Public License v2 lub licencja BSD do wyboru;
– ”Dual MPL/GPL” – GNU Public License v2 lub Mozilla do wyboru;
– ”Proprietary” oznacza produkt zamknięty.
W obecnej wersji jądra wszelkie informacje są umieszczane w sekcji .modinfo pliku typu ELF Opis modułu wraz z opisem parametrów można uzyskać za pomocą programu
modinfo, np. komenda modinfo vfat daje następujący opis:
25
filename :
license :
description :
author :
vermagic :
depends :
srcversion :
3.6
/ l i b / modules / 2 . 6 . 1 8 . 2 − 3 4 / k e r n e l / f s / v f a t / v f a t . ko
GPL
VFAT f i l e s y s t e m s u p p o r t
Gordon C h a f f e e
2 . 6 . 1 8 . 2 − 3 4 SMP mod unload 586 REGPARM gcc −4.1
fat
DDEB508593EBB46B110BA45
Wersje modułów i jądra
Ze względu na możliwość niezgodności kodu modułów z kodem jądra, gdy pochodzą z różnych wersji systemu, moduł powinien być od nowa skompilowany z każdą nową wersją jądra, do której będzie dołączany. Każdy moduł deklaruje symbol module kernel version.
Program insmod przed załadowaniem modułu porównuje ten symbol z aktualną wersją
jądra i może odmówić załadowania modułu w przypadku niezgodności wersji. Symbol jest
umieszczony w sekcji .modinfo formatu ELF. W celu zapewnienia większej przenośności modułów między wersjami jądra dodano wersjonowanie symboli. Jeśli wersjonowanie
symboli jest włączone i moduł korzysta z niego, wówczas każdy symbol posiada wersję,
będącą skrótem CRC definicji jego struktury (w C). W ten sposób można z dużą dozą
pewności uznać, że jeśli definicja struktury się nie zmieniła, to moduł będzie działać. Jeśli
wszystkie symbole w jądrze są zgodne z wersjami użytymi w module, to moduł może
zostać załadowany na innym jądrze niż na tym, na którym został skompilowany. Jeśli
moduł nie obsługuje wersji symboli, wówczas może być załadowany tylko na dokładnie
tej wersji jądra, dla której został skompilowany. Oczywiście, fakt, że nie została zmieniona
definicja symbolu nie daje gwarancji, że nie została zmieniona semantyka używania, dlatego dobrą praktyką jest rekompilacja modułu dla każdej nowej wersji jądra oraz analiza,
czy nowa wersja jądra nie wymaga dodatkowych czynności, które moduł powinien wykonać. Takie zmiany nie powinny występować w stabilnych interfejsach jądra (np. przykład
z binfmt misc pochodzący z jądra 2.4.18 został bez problemu przekompilowany na jądrze
2.6.17.13 bez zmiany funkcjonalności), ale w przypadku szybko zmieniających się części
jądra może taka konieczność zaistnieć.
3.7
Ładowanie modułów na żądanie
Jeśli w module ma być wykorzystywane ładowanie modułów na żądanie, to należy dołączyć plik nagłówkowy: [5]
#include <l i n u x /kmod . h>
26
Doładowanie modułu jest możliwe dzięki funkcji:
int r e q u e s t m o d u l e ( const char ∗ module name )
Ładowanie modułów na żądanie nie może być wykorzystane w kodzie modułu do automatycznego dołączania modułów, które eksportują symbole wykorzystywane przez ten
moduł, bo przed uruchomieniem funkcji inicjalizującej, w której możemy zażądać dodania
potrzebnego modułu wszystkie symbole muszą być już związane. Ładowaniem modułów,
od których zależy dany moduł, zajmuje się program modprobe, pod warunkiem, że moduł
zostanie umieszczony razem z innymi modułami w katalogu /lib/modules/wersja jądra
oraz że zostanie zbudowane drzewo ich powiązań, które określać bedzie ich zależności.
Takie drzewo budowane jest za pomocą polecenia depmod.
3.8
Dynamiczne łączenie i relokacja kodu
Opis kodu relokowalnego i dynamicznego łączenia jest opisem ogólnym, więc praktyczna
realizacja może się od niego trochę różnić (np. zamiast relokacji zmiennych względem segmentu danych mogą być relokowane względem nazwy symbolu odpowiadającej zmiennej).
Kod relokowalny to skompilowany kod, w którym odwołania do zmiennych o adresach bezwzględnych są specjalnie oznaczone i w czasie ładowania poprawiane tak, by wskazywały
w odpowiednie miejsce w załadowanym kodzie. Jest to realizowane w ten sposób, że adres
zapisany w kodzie jest adresem względnym w danym segmencie i razem z plikiem trzymana jest tablica relokacji, czyli spis wszystkich miejsc, które trzeba poprawić przy relokacji
danego segmentu. Przy ładowaniu we wszystkich miejscach wskazanych w tablicy relokacji
dodawana jest liczba o jaką przesunięty jest segment, np.
kod :
0 : MOV ax , [ 4 ]
4 : JMP p r o c e d u r a
procedura :
dane :
0 : DD 0
4 : DW 2
Oznacza, że mamy załadować zmienną z segmentu danych spod adresu względnego 4,
a następnie skoczyć pod adres w segmencie kodu odpowiadający offsetowi etykiety ”procedura”. Ponieważ przy kompilacji nie znamy adresu, pod którym znajdzie się segment
kodu ani segment danych, wiec kompilator wstawi kod odwołujący się do adresów względnych w segmencie i dołączy tablicę relokacji o zawartości: w kodzie w miejscu argumentu
instrukcji MOV dodaj wartość początku segmentu danych, a w miejscu argumentu instrukcji JMP – wartość początku segmentu kodu. Przy ładowaniu, jeśli segment danych
27
znajdzie się pod adresem 0x2000, a segment kodu pod adresem 0x1000, to kod zostanie
poprawiony do następującej postaci:
0 x1000 : MOV ax , [ 0 x2004 ]
0 x1004 : JMP [ 0 x1000 + OFFSET p r o c e d u r a ]
Może również zaistnieć konieczność relokacji danych, np. gdy umieścimy deklaracje:
int f u n k c j a ( ) { . . . } ; void ∗ p r o c = ( void ∗ ) f u n k c j a ;
3.9
Budowa pliku ELF
Plik typu ELF (Executable Linking and Format) podzielony jest na sekcje [6]. Każda
z sekcji ma określoną nazwę i atrybuty:
• text – sekcja kodu;
• data – sekcja danych zainicjowanych;
• bss – sekcja danych niezainicjowanych;
• comment – komentarze, np. opis modulu.
Atrybuty określają, czy dana sekcja ma zostać załadowana z pliku typu ELF (np. kod
i dane), czy tylko przydzielona w pamięci (np. bss) lub czy zawiera informacje, które mają
po prostu być w pliku (np. comment). Do każdej sekcji może być podana tablica relokacji.
Do oglądania zawartości plików .ko, czyli plików zawierajacych skompilowane moduły
jądra, służy program nm lub objdump. Pozwalają one uzyskać listę symboli używanych
w danym module, w tym symbole niezdefiniowane, czyli takie, które mają być dynamicznie
połączone przy ładowaniu, np. funkcje z bibliotek dynamicznych.
l i n u x −o l u v : / home/ u s e r / Desktop / k e r n s t a t # objdump
f i l e format e l f 3 2 −i 3 8 6
k e r n s t a t . ko :
SYMBOL TABLE:
00000000 l
00000000 l
00000000 l
00000000 l
00000000 l
00000000 l
00000000 l
−t k e r n s t a t . ko
d
d
d
d
d
d
d
. t e x t 00000000 . t e x t
. f i x u p 00000000 . f i x u p
. rodata
00000000 . r o d a t a
. r o d a t a . s t r 1 . 1 00000000 . r o d a t a . s t r 1 . 1
ex table
00000000
ex table
. modinfo
00000000 . modinfo
versions
00000000
versions
28
00000000
00000000
00000000
00000000
00000000
00000000
00000000
00000060
00000041
00000060
0000005 c
000000 ea
0000013 c
000001 a4
3.10
l
l
l
l
l
l
l
l
l
l
l
l
l
l
d
d
d
d
d
df
F
O
F
O
O
F
F
F
. eh frame
00000000 . e h f r a m e
. data 00000000 . data
. bss
00000000 . b s s
. comment
00000000 . comment
. n o t e .GNU−s t a c k
00000000 . n o t e .GNU−s t a c k
∗ABS∗ 00000000 k e r n s t a t . c
. t e x t 00000041 k e r n s t a t c l e a n u p m o d u l e
. bss
00000004 k e r n s t a t u n r e g i s t e r
. t e x t 000000 a9 k e r n s t a t i n i t i a l i z e m o d u l e
. data 00000074 k e r n s t a t f o p s
. bss
00000004 k e r n s t a t r e g i s t e r
. t e x t 00000052 k e r n s t a t o p e n
. t e x t 00000068 k e r n s t a t r e l e a s e
. t e x t 00000107 s c h e d p i o a r r a y t a s k s
Zastosowanie modułów
Moduły najczęściej są wykorzystywane jako części jądra implementujące sterowniki urządzeń, systemy plików oraz interpretery poleceń. Interpretery poleceń (executable interpreters), to funkcje jądra umożliwiające załadowanie i wykonanie plików. Przynajmniej
jeden taki interpreter musi być na stałe wkompilowany w jądro, żeby umożliwić ładowanie
i uruchamianie innych modułów. W obecnych wersjach jądra jest to zazwyczaj interpreter
plików typu ELF.
Program insmod próbuje dołączyć moduł do uruchomionego jądra systemu przez rozwiązanie wszystkich symboli z tabeli udostępnianych symboli jądra. Jeśli nazwa pliku
modułu podana jest bez katalogu i rozszerzenia, to insmod będzie szukał modułu w kilku
standardowych katalogach. Można użyć zmiennej środowiska MODPATH, aby zmienić
to standardowe zachowanie. Jeśli istnieje standardowy plik konfiguracyjny modułów /etc/modules.conf, to ścieżki w nim zdefiniowane będą miały priorytet nad zdefiniowanymi
w zmiennej MODPATH. Można również posłużyć się zmienną środowiskową MODULECONF dla wybrania innego pliku konfiguracyjnego niż standardowy. Ta zmienna środowiska będzie miała priorytet nad wszystkimi powyższymi definicjami.
Programy narzędziowe modprobe i depmod służą do zarządzania zmodularyzowanym
jądrem Linuksa przez zwykłych użytkowników, administratorów i twórców dystrybucji.
W celu automatycznego ładowania właściwych modułów (dostępnych w predefiniowanych drzewach katalogów) program modprobe korzysta z pliku podobnego do Makefile,
który jest tworzony przez depmod. Program modprobe jest używany do załadowania pojedynczego modułu, zbioru powiązanych modułów lub wszystkich modułów oznaczonych
podanym znacznikiem. Program ten automatycznie załaduje wszystkie moduły podsta29
wowe zgodnie z opisem zawartym w pliku powiązań modules.dep. Jeśli ładowanie jednego
z tych modułów się nie powiedzie, to cały zbiór modułów załadowanych w bieżącej sesji
zostanie automatycznie wyładowany. modprobe może ładować moduły na dwa różne sposoby. Przede wszystkim modprobe próbuje załadować moduł z podanej listy (zdefiniowany
przez wzorzec) i kończy pracę, jeśli uda mu się załadować któryś z modułów. Inny sposób
użycia modprobe, to załadowanie wszystkich modułów z listy.
Program modprobe uruchomiony z opcja -r automatycznie usuwa podany zbiór modułów oraz inne, nieużywane moduły, a także powoduje wykonanie poleceń z sekcji preremove i post-remove pliku konfiguracyjnego /etc/modules.conf.
depmod tworzy podobny do Makefile plik z zależnościami, oparty na symbolach, które
znajdzie w zbiorze modułów podanych w linii poleceń lub w katalogach wymienionych
w pliku konfiguracyjnym. Plik zależności może być potem wykorzystany przez modprobe
w celu automatycznego ładowania odpowiednich modułów lub zestawów modułów. Typowe użycie depmod polega na umieszczeniu linii /sbin/depmod -a, gdzieś w skryptach
startowych, aby odpowiednie powiązania modułów były dostępne zaraz po uruchomieniu
systemu. Należy zauważyć, że parametr -a jest obecnie opcjonalny. Przy uruchamianiu
systemu użycie opcji -q może być bardziej właściwe, gdyż wówczas depmod nie generuje
komunikatów dotyczących nierozwiązanych symboli. Istnieje także możliwość utworzenia
pliku zależności zaraz po skompilowaniu nowego jądra. Podczas tworzenia związków pomiędzy modułami i symbolami udostępnionymi przez inne moduły depmod nie bierze
pod uwagę stosunku modułu lub udostępnionych symboli do licencji GPL. To znaczy,
że depmod nie zgłosi błędu, jeśli moduł na licencji niezgodnej z GPL będzie się odwoływać do symboli zastrzeżonych dla GPL (EXPORT SYMBOL GPL w jądrze). Jednakże
insmod użyty dla modułu nie-GPL odmówi rozwiązania symboli zastrzeżonych dla GPL,
więc takiego modułu nie uda się załadować.
lsmod wyświetla informację o wszystkich załadowanych modułach. Informacja podawana jest w następującym formacie: nazwa, rozmiar, licznik użycia, lista odwołujących się
do niego modułów. Podawana informacja jest identyczna z tą, która jest dostępna poprzez
plik /proc/modules. Jeżeli moduł może usunąć się za pomocą funkcji can unload, to lsmod
w polu licznik użycia wyświetla zawsze -1, niezależnie od rzeczywistej wartości licznika.
4
Sterowniki urządzeń
Sterownik, czyli podprogram obsługi urządzenia, to zbiór funkcji służących do komunikacji
z urządzeniem zewnętrznym. Żeby umożliwić programom użytkownika prosty mechanizm
operacji na urządzeniu, sterownik jest reprezentowany przez specjalny plik, który znajduje się zazwyczaj w katalogu /dev. Operacje na tym pliku, takie jak otwarcie, odczyt,
30
zapis oraz zamkniecie, są obsługiwane właśnie przez odpowiedni podprogram obsługi.
Na przykład, odczyt z tego pliku jest realizowany przez funkcję odczytu dostarczaną
przez sterownik, a nie przez standardową funkcję open służącą do odczytu zwykłych plików. Wśród urządzeń wyróżnia się urządzenia znakowe (ang. character devices), które
umożliwiają dostęp do danych bajt po bajcie oraz urządzenia blokowe (ang. block devices), które umożliwiają dostęp do danych w większych porcjach, tzw. blokach. Sterowniki
urządzeń mogą być albo na stałe wbudowane w jądro, albo dołączane dynamicznie w postaci modułów jądra.
Zazwyczaj, ze względu na optymalne wykorzystanie pamięci, sterowniki urządzeń buduje się w postaci modułów. Plik specjalny urządzenia zawiera min. następujące informacje:
• flagę określającą typ urządzenia (b - blokowe, c - znakowe);
• numer główny (ang. major device number ), czyli indeks podprogramu danego urządzenia w tablicy rozdzielczej;
• numer drugorzędny (ang. minor device number ), czyli liczbę przekazywaną podprogramowi obsługi podczas operacji na urządzeniu. Zwykle numer drugorzędny jest
używany w podprogramie obsługi urządzenia do rozróżniania urządzeń obsługiwanych przez ten podprogram. W pliku Documentation/devices.txt znajduje się lista
numerów przypisanych standardowym urządzeniom.
Pobieranie numeru głównego i drugorzędnego odbywa się następująco:
MAJOR( inode −>i r d e v ) ;
MINOR( inode −>i r d e v ) ;
W systemie istnieją dwie tablice podprogramów obsługi urządzeń. Jedna dla urządzeń blokowych, druga dla znakowych. Tablice te nazywane są tablicami rozdzielczymi. Każdemu
podprogramowi obsługi odpowiada pozycja w odpowiedniej tablicy. Podprogram obsługi
jest identyfikowany parą (rodzaj, liczba). Rodzaj określa, w której z dwóch tablic znajdują sie informacje dotyczące danego programu obsługi, a liczba – pod którym indeksem
znajduje się opisująca ten program struktura device struct. W strukturze tej znajduje się
wskaźnik do nazwy podprogramu obsługi oraz wskaźnik do odpowiadającej mu struktury
operacji na urządzeniu. Oto te dwie tablice:
struct d e v i c e s t r u c t c h r d e v s [MAX CHRDEV ] ;
struct d e v i c e s t r u c t b l k d e v s [MAX BLKDEV ] ;
Każda komórka tablicy to struktura, odpowiadająca sterownikowi urządzenia o numerze
nadrzędnym równym indeksowi.
31
struct d e v i c e s t r u c t {
const char ∗ name ;
struct f i l e o p e r a t i o n s ∗ f o p s ;
};
W strukturze file operations przechowywane są adresy odpowiednich funkcji. Standardowo
oba pola struktury są inicjalizowane poprzez przypisanie im wartości NULL. Deklaracje
struktur znajdują się w pliku <fs/devices.c>.
Pliki specjalne reprezentujące urządzenia są zazwyczaj umieszczone w katalogu /dev.
Wywołując ls -l w tym katalogu można zobaczyć listę dostępnych plików specjalnych. Litera ”c” lub ”b” przed listą uprawnień określa rodzaj urządzenia. Dwa numery, tuż przed
datą ostatniej modyfikacji, oznaczają odpowiednio numer główny (major ) i drugorzędny (minor ) urządzenia. Komenda mknod służy do tworzenia plików specjalnych. Przy
wywołaniu należy podać 4 argumenty: mknod /dev/nazwa pliku typ major minor. Plik
specjalny pozostaje w systemie plików aż do usunięcia go komendą rm. Dodanie nowego
sterownika urządzenia wiąże się z przypisaniem mu numeru głównego. W kodzie jądra
typ dev t zdefiniowany w pliku <linux/types.h> jest używany do przechowywania zarówno numeru głównego jak i numeru drugorzędnego. W pliku <linux/kdev t.h> znajduje się
zbiór przydatnych makr takich jak m.in:
MAJOR( d e v t ) ;
MINOR( d e v t ) ;
MKDEV( int major , int minor ) ;
Pierwsze makro zwraca numer główny urządzenia, drugie numer poboczny, natomiast makro trzecie koduje numer główny i poboczny w wartoś typu dev t. Ponieważ wiele numerów
głównych jest już przypisanych istniejącym urządzeniom, wybranie nieprzydzielonego numeru dla nowego urządzenia może być zadaniem trudnym (pomimo zwiększenia liczby
dostępnych numerów w jądrach z serii 2.6). Indeks zarejestrowanych numerów znajduje
się w pliku Documentation/devices.txt. Jednak najczęściej korzysta się z możliwości dynamicznego przydziału numeru głównego. Można to osiągnać wpisując przy rejestracji
sterownika liczbę 0 zamiast właściwego numeru głównego. Przydzielony numer zostanie
zwrócony przez funkcję rejestrującą. Standardowo, tzn. gdy uzywamy’ ?) nie używamy
devfs przy dynamicznym przydziale numeru głównego, nie ma możliwości utworzenia pliku specjalnego przed załadowaniem modułu, bo moduł ładowany wielokrotnie może mieć
za każdym razem przydzielony inny numer główny urządzenia (przy korzystaniu z devfs
można utworzyć ten pliku podczas ładowania modułu). W takich przypadkach ładowanie modułu trzeba połączyć z wywołaniem komendy mknod, a usuwanie z komendą rm.
Najłatwiej to osiągnąć pisząc prosty skrypt, który po załadowaniu modułu odczyta przydzielony numer urządzenia z /proc/devices i utworzy odpowiedni plik specjalny. Należy
32
pamiętać o umieszczeniu instrukcji chmod dającej, zgodnie z przeznaczeniem urządzenia, odpowiednie uprawnienia do odczyty/zapisu dla zwykłych użytkowników. Prawdopodobnie najlepszym rozwiązaniem jest próba zarejestrowania urządzenia pod numerem
domyślnym, a wykorzystanie przydziału dynamicznego w przypadku błędu rejestracji.
4.1
Rejestrowanie i wyrejestrowywanie urządzeń
Do rejestracji sterowników urządzeń znakowych i blokowych służą następujące funkcje:[6]
int r e g i s t e r c h r d e v r e g i o n ( d e v t f i r s t , unsigned int count , const char ∗name ) ;
int r e g i s t e r c h r d e v ( unsigned int major , const char ∗ name ,
struct f i l e o p e r a t i o n s ∗ f o p s ) ;
int r e g i s t e r b l k d e v ( unsigned int major , const char ∗ name ,
struct b l o c k d e v i c e o p e r a t i o n s ∗ bdops ) ;
Operacje te rejestrują urządzenie znakowe/blokowe o numerze głównym major, nazwie
name i operacjach na urządzeniu fops/bdops. Nazwa to napis wyświetlany w pliku /proc/devices. Do wyrejestrowywania sterowników urządzeń służą następujące funkcje:
int u n r e g i s t e r c h r d e v ( int major , char ∗name ) ;
int u n r e g i s t e r b l k d e v ( int major , char ∗name ) ;
Przed wyrejestrowaniem urządzenia sprawdzana jest poprawność obu argumentów. Usunięcie modułu sterownika przed wyrejestrowaniem urządzenia może mieć bardzo poważne
konsekwencje. Próba obejrzenia /proc/devices będzie wywoływała błąd oops, bo system,
starając się wyświetlić nazwy urządzeń, będzie odwoływał się do regionów pamięci usuniętego modułu. Aby unikać takich sytuacji należy wypełniać pole owner w strukturze
file operations.
4.2
Operacje na pliku urządzenia
W operacjach na pliku specjalnym sterowników urządzeń znakowych wykorzystywany
jest mechanizm obiektowy mianowicie sterownik dostarcza specjalnych operacji na sobie
samym. Struktura file operations, która jest zdefiniowana w pliku <linux/fs.h> zawiera
zestaw funkcji, które będą wykonywane przy operacjach na pliku specjalnym związanym
z urządzeniem:
struct f i l e o p e r a t i o n s
{
struct module ∗ owner ;
l o f f t ( ∗ l l s e e k ) ( struct f i l e ∗ , l o f f t , int ) ;
s s i z e t ( ∗ r e a d ) ( struct f i l e ∗ , char
user ∗ , size t , l o f f t ∗);
33
s s i z e t ( ∗ w r i t e ) ( struct f i l e ∗ , const char
user ∗ , size t , l o f f t ∗);
int ( ∗ i o c t l ) ( struct i n o d e ∗ , struct f i l e ∗ , unsigned int , unsigned long ) ;
int ( ∗mmap) ( struct f i l e ∗ , struct v m a r e a s t r u c t ∗ ) ;
int ( ∗ open ) ( struct i n o d e ∗ , struct f i l e ∗ ) ;
int ( ∗ r e l e a s e ) ( struct i n o d e ∗ , struct f i l e ∗ ) ;
...
};
Tworząc nowe urządzenie należy określić funkcje niekoniecznie wszystkie, które mają być
wykonane w poniższych przypadkach:
• open - przeprowadzenie czynności przygotowawczych przed innymi operacjami;
• release - służy do wykonania czynności odwrotnych do tych w operacji open;
• read - przepisanie pewnej porcji danych z przestrzeni adresowej jądra pod wskazany
adres buff w przestrzeni adresowej użytkownika;
• write - przepisanie pewnej porcji danych z przestrzeni adresowej użytkownika buff
do przestrzeni adresowej jądra.
Standardowo pole owner powinno być zainicjalizowane na THIS MODULE, które zdefiniowane jest w pliku <linux/module.h>. Umożliwia to jądru automatyczne zarządzanie
licznikiem odwołań do modułu.
Struktura file, która jest zdefiniowana w <linux/fs.h> reprezentuje w jądrze otwarty
plik. Jest tworzona przez jądro w momencie wywołania open i przekazywana do wszystkich
operacji na pliku, aż do ostatniego wywołania close, czyli momentu, kiedy wywołana
zostanie funkcja release. Warto zauważyć, że otwarty plik reprezentowany przez strukture
struct file to co innego, niż plik na dysku reprezentowany przez strukturę struct inode.
struct f i l e
{
mode t
loff t
unsigned int
struct f i l e o p e r a t i o n s
void
...
};
f mode ;
f pos ;
f flags ;
∗ f op ;
∗ private data ;
Pole f mode pozwala określić, czy plik jest otwarty do odczytu FMODE READ, zapisu FMODE WRITE lub obu. Pola tego nie trzeba sprawdzać w funkcjach read i write,
bo jądro wykonuje taki test przed wywołaniem odpowiedniej funkcji sterownika. Pole
34
f pos określa pozycję do pisania lub odczytu. Sterownik może odczytywać wartość pola,
ale nie powinien go zmieniać. Flagi f flags wykorzystuje się głównie do sprawdzenia, czy
operacja ma być blokująca (O BLOCK ), czy też nie (O NONBLOCK ). Pole f op określa
zestaw funkcji implementujących operacje na pliku. Pole to jest jednorazowo ustawiane
przy wywołaniu open i wskazuje operacje określone w momencie rejestrowania urządzenia.
później to pole nie jest ani modyfikowane, ani sprawdzane. Taka konstrukcja umożliwia
zrealizowanie wielu zachowań sterownika w zależności od numeru drugorzędnego bez narzutu systemowego przy każdym wywołaniu operacji na pliku. Wskaźnik private data jest
ustawiany na NULL przy otwieraniu pliku. Sterownik może wykorzystać ten wskaźnik
do własnych celów, ale jest wówczas odpowiedzialny za zwolnienie pamięci przydzielonej
na rzecz tego pola.
4.2.1
Funkcja open
Prototyp funkcji open jest następujący:
int open ( struct i n o d e ∗ inode , struct f i l e ∗ f i l p )
Operacja open umożliwia sterownikowi przeprowadzenie czynności przygotowawczych przed
innymi operacjami. Zazwyczaj wykonywane są następujące czynności:
• sprawdzenie błędów związanych z urządzeniem, np. sprawdzenie, czy urządzenie jest
gotowe;
• inicjalizacja urządzenia, gdy jest otwierane po raz pierwszy;
• identyfikacja numeru drugorzędnego MINOR(inode->i rdev ) oraz, jeśli jest to konieczne, podmiana zestawu operacji wskazywanej przez f op;
• przydzielenie pamięci na dane związane z urządzeniem, inicjalizacja struktur danych
oraz przypisanie wskaźnika private data;
W starszych wersjach jądra zadaniem operacji open było również zwiększenie licznika
odwołań do modułu, jednak obligatoryjne obecnie ustawienie pola owner struktury file operations na THIS MODULE powoduje, że system sam dba o zwiększenie licznika.
4.2.2
Funkcja release
Prototyp funkcji release jest następujący:
int r e l e a s e ( struct i n o d e ∗ inode , struct f i l e ∗ f i l p )
35
Operacja release, która wywoływana jest przy ostatnim close na otwartym pliku, służy do wykonania czynności odwrotnych do tych związanych z operacją open. Zazwyczaj
te czynności obejmują:
• zwolnienie pamięci private data;
• zamknięcie urządzenia, gdy jest to ostatnie wywołanie release.
W starszych wersjach jądra zadaniem operacji release było również zmniejszenie licznika odwołań do modułu, jednak obligatoryjne obecnie ustawienie pola owner struktury
file operations na THIS MODULE powoduje, że system sam dba o zmniejszenie licznika.
4.2.3
Funkcja read
Prototyp funkcji read jest następujący:
s s i z e t r e a d ( struct f i l e ∗ f i l p , char
u s e r ∗ buff ,
s i z e t count , l o f f t ∗ o f f p ) ;
Zadaniem operacji read jest przepisanie pewnej porcji danych z przestrzeni adresowej jądra
pod wskazany adres buff w przestrzeni adresowej użytkownika. Należy również wykonać
zwiększenie znacznika pozycji pliku *offp. Zazwyczaj jest on ustawiony na ten sam adres
pamięci co filp->f pos. Wartość zwracana przez tę funkcję jest interpretowana następująco:
• wartość większa od zera oznacza liczbę przepisanych bajtów;
– jeśli jest równa wartości argumentu przekazanego do wywołania systemowego
read, to oznacza sukces;
– jeśli zaś mniejsza, to oznacza, że tylko część danych została przekazana; należy
się wtedy spodziewać, że program powtórzy wywołanie systemowe (takie jest
np. standardowe zachowanie funkcji bibliotecznej fread);
• jeśli wartość jest równa 0, to został osiągnięty koniec pliku;
• wartość ujemna oznacza błąd.
4.2.4
Funkcja write
Prototyp funkcji write jest następujący:
s s i z e t w r i t e ( struct f i l e ∗ f i l p , const char
s i z e t count , l o f f t ∗ o f f p ) ;
36
u s e r ∗ buff ,
Zadaniem operacji write jest przepisanie pewnej porcji danych z przestrzeni adresowej
użytkownika do przestrzeni adresowej jądra. Operacja write, podobnie jak read, może
zapisać mniej danych niż zażądano. Podobnie jak i read, należy odpowiednio przesunąć
pozycję w pliku *offp. Wartość zwracana przez tę funkcję jest interpretowana następująco:
• wartość większa od zera oznacza liczbę przepisanych bajtów;
• jeśli wartość jest równa 0, to żadne dane nie zostały przepisane, ale nie ma powodu,
żeby zgłaszać błąd; należy oczekiwać, że program powtórzy wywołanie systemowe;
• wartość ujemna oznacza błąd.
4.2.5
Funkcja llseek
Prototyp funkcji llseek jest następujący:
l o f f t l l s e e k ( struct f i l e ∗ f i l p , l o f f t o f f , int whence ) ;
Operacja llseek implementuje wywołania systemowe lseek i llseek. Domyślnym działaniem
jądra dla operacji llseek gdy nie jest wyszczególniona w operacjach sterownika, jest zmiana
pola f pos struktury file. W przypadku, gdy urządzenie nie dostarcza możliwości zmiany
pozycji pliku należy zdefiniować operację pustą, np.
l o f f t l l s e e k ( struct f i l e ∗ f i l p , l o f f t o f f , int whence )
{
return −ESPIPE ;
}
4.2.6
Funkcja ioctl
Do wywoływania komend specyficznych dla urządzenia służy funkcja ioctl, której prototyp
jest następujący:
int ( ∗ i o c t l ) ( struct i n o d e ∗ inode , struct f i l e ∗ f i l p ,
unsigned int cmd , unsigned long a r g ) ;
Pierwsze dwa argumenty odpowiadają deskryptorowi pliku, który został przekazany przez
wywołanie systemowe. Argument cmd jest dokładnie taki, jak w wywołaniu systemowym.
Opcjonalny argument arg jest przekazywany w postaci liczby typu unsigned long bez
względu na typ użyty przy wywołaniu systemowym. Zazwyczaj implementacja operacji
ioctl zawiera po prostu konstrukcję switch wybierającą odpowiednie zachowanie w zależności od wartości argumentu cmd. Różne komendy są reprezentowane przez różne numery,
37
którym zazwyczaj nadaje się nazwy korzystając z definicji preprocesora. Program użytkownika powinien mieć możliwość włączenia pliku nagłówkowego z deklaracjami; jest to
zazwyczaj ten sam plik, który jest używany przy kompilacji modułu sterownika. Do twórcy sterownika należy ustalenie wartości liczbowych odpowiadających komendom interpretowanym przez sterownik. Najprostszy wybór polega na przypisaniu kolejnych małych
wartości poszczególnym komendom. Nie jest to jednak wybór dobry. Komendy powinny być unikalne w skali całego systemu, żeby uniknąć błędów, gdy poprawną komendę
wysyłamy do niepoprawnego urządzenia. Taka sytuacja może nie występować zbyt często, ale jej konsekwencje mogą by poważne. W przypadku pomyłki, jeśli stosujemy różne
komendy dla wszystkich ioctl, zostanie zwrócona wartość -EINVAL zamiast wykonania
niezamierzonej akcji. W ustalaniu wartości liczbowych dla komend pomocne mogą być
następujące makra zdefiniowane w <asm/ioctl.h>:
IO ( type , nr )
IOR ( type , nr , d a t a i t e m )
IOW( type , nr , d a t a i t e m )
IOWR( type , nr , d a t a i t e m )
Znaczenie poszczególnych makr jest następujące:
IO(type, nr) komenda ogólnego przeznaczenia nie pobierająca argumentu;
IOR(type, nr, dataitem) komenda z zapisem w przestrzeni; użytkownika.
IOW(type, nr, dataitem) komenda z odczytem z przestrzeni; użytkownika.
IOWR(type,nr,dataitem) komenda z zapisem i odczytem,
gdzie type, to unikatowy numer sterownika, tzw. numer magiczny, nr – kolejny numer
komendy, dataitem – struktura związana z komendą.
5
Implementacja modułu kernstat i programu schedstat
Niniejszy rozdział dotyczy implementacji modułu jądra kernstat, programu przestrzeni
użytkownika schedstat oraz zmian jakich należy dokonać w kodzie źródłowym jądra. Punktem wyjścia do uruchomienia modułu kernstat jest przekształcenie plików źródłowych
<linux/sched.h> oraz <kernel/sched.c>. Pierwszy plik zawiera definicje struktur danych
oraz funkcji operujących na zadaniach, natomiast drugi – implementację planisty. Przekształcenie pliku <linux/sched.h> polega na dodaniu dwóch definicji struktur danych,
38
które dotychczas znajdowały się w pliku <kernel/sched.c>, aby można było z nich korzystać. Chodzi o strukturę struct rq, która reprezentuje zadania gotowe do wykonania oraz
struct prio array, która reprezentuje kolejkę priorytetową zadań. Zmiana w stosunku do
pliku <kernel/sched.c> polega na dodaniu funkcji, która zwraca wskaźnik na kolejkę zadań gotowych do wykonania. Funkcja ponadto jest eksportowana do symboli jądra, dzięki
czemu możemy z niej korzystać w modułach jądra.
Sterownik urządzenia znakowego /dev/kernstat został napisany dla wersji 2.6.18 jądra,
ale powinien pracować także z nowszymi wersjami jądra. Poszczególne wersje jądra cechuje
duża zmienność nazw zmiennych, struktur danych oraz funkcji. Zmiany dotyczą również
położenia definicji w odpowiednich plikach oraz symboli jakie jądro eksportuje. Ten fakt
sprawia, że jeżeli w nowszej wersji jądra zmianie ulegnie któryś z symboli jądra (nazwa
zmiennej, struktury danych, funkcji, makra), to należy dokonać odpowiednich zmian nazw
symboli w module. Moduł kernstat został pomyślnie skompilowany na jądrach 2.6.18 oraz
2.6.20 wpierających procesory jedno i dwurdzeniowe.
5.1
Modyfikacja sched.h i sched.c
Pierwsza z opisanych powyżej zmian przedstawia się następująco:
struct p r i o a r r a y {
unsigned int n r a c t i v e ;
DECLARE BITMAP( bitmap , MAX PRIO+1);
struct l i s t h e a d queue [ MAX PRIO ] ;
};
struct rq {
spinlock t lock ;
unsigned long n r r u n n i n g ;
unsigned long r a w w e i g h t e d l o a d ;
#i f d e f CONFIG SMP
unsigned long c p u l o a d [ 3 ] ;
#endif
unsigned long long n r s w i t c h e s ;
unsigned long n r u n i n t e r r u p t i b l e ;
unsigned long e x p i r e d t i m e s t a m p ;
unsigned long long t i m e s t a m p l a s t t i c k ;
struct t a s k s t r u c t ∗ c u r r , ∗ i d l e ;
struct mm struct ∗prev mm ;
struct p r i o a r r a y ∗ a c t i v e , ∗ e x p i r e d , a r r a y s [ 2 ] ;
int b e s t e x p i r e d p r i o ;
atomic t nr iowait ;
39
#i f d e f CONFIG SMP
struct sched domain ∗ sd ;
int a c t i v e b a l a n c e ;
int push cpu ;
int cpu ;
struct t a s k s t r u c t ∗ m i g r a t i o n t h r e a d ;
struct l i s t h e a d m i g r a t i o n q u e u e ;
#endif
};
Druga zmiana dotyczy kodu planisty, a dokładniej pliku <kernel/sched.c >. Zmiana polega na dodaniu funkcji zwracającej wskaźnik na kolejkę zadań gotowych do wykonania
oraz jej wyeksportowaniu, tzn.
struct rq ∗ k e r n c p u r q ( unsigned long cpu )
{
struct rq ∗ rq ;
rq = c p u r q ( cpu ) ;
return rq ;
}
EXPORT SYMBOL( k e r n c p u r q )
Po dokonaniu powyższych zmian należy ponownie skompilować jądro.
make
make
make
make
make
make
make
mrproper
cloneconfig
clean
modules
modules install
bzImage
install
Po ponownym skompilowaniu jądra możemy odwoływać się do powyższej funkcji w modułach jądra. Aby upewnić się ze funkcja została poprawnie wyeksportowana należy sprawdzić, czy jej symbol został dodany do symboli jądra. Można to sprawdzić na dwa sposoby.
Pierwszy polega na odszukaniu odpowiedniego wpisu w pliku /proc/kallsyms, natomiast
drugi – na sprawdzeniu czy szukana nazwa znajduje sie w obrazie jądra.
grep kern cpu rq / proc / kallsyms
c0119230 T k e r n c p u r q
g r e p k e r n c p u r q / boot / System . map−2.6.18.2 −34 − d e f a u l t
c0119230 T k e r n c p u r q
c032ac18 r
ksymtab kern cpu rq
c032ff5c r
kcrctab kern cpu rq
c0332fe8 r
kstrtab kern cpu rq
40
5.2
Konstruktor modułu kernstat
Każdy moduł musi definiować funkcję inicjującą moduł, czyli tzw. konstruktor. Funkcja inicjująca jest wywoływana przy ładowaniu modułu. Jeżeli nie udało się zainicjować
modułu, to zwracany jest kod błędu, w przeciwnym przypadku 0. Sterownik urządzenia
kernstat implementuje konstruktor kernstat initialize module, którego zadaniem jest zarejestrowanie sterownika urządzenia znakowego /dev/kernstat. Do rejestracji sterowników
urządzeń znakowych służy funkcja:
i n t r e g i s t e r c h r d e v ( u n s i g n e d i n t major , c o n s t c h a r ∗ name ,
struct f i l e o p e r a t i o n s ∗ fops ) ;
Funkcja ta rejestruje urządzenie znakowe o numerze głównym major, nazwie name i operacjach na urządzeniu fops. Nazwa to napis wyświetlany w pliku /proc/devices. Numer
główny i nazwa sterownika urządzenia znakowego /dev/kernstat są zdefiniowane w pliku
nagłowkowym kernstat.h za pomocą poniższych definicji.
#define KERNSTAT MAJOR
#define KERNSTAT NAME
42
” kernstat ”
Poniżej przedstawiono implementację konstruktora modułu kernstat, który rejestruje urządzenie znakowe o numerze głównym KERNSTAT MAJOR, nazwie KERNSTAT NAME
i operacjach na urządzeniu kernstat fops. Poprawność rejestracji urządzenia sprawdzamy
za pomocą zmiennej kernstat register zdefiniowanej w pliku kernstat.c
s t a t i c i nt k e r n s t a t r e g i s t e r ;
Zmienna ta zawiera kod błędu funkcji register chrdev. Przy wykonywaniu tego typu operacji możemy natknąć się na dwa rodzaje błedów, są to poprawność numeru głównego
oraz jego zajętość.
s t a t i c i nt k e r n s t a t i n i t i a l i z e m o d u l e ( void )
{
k e r n s t a t r e g i s t e r = r e g i s t e r c h r d e v (KERNSTAT MAJOR,
KERNSTAT NAME,
&k e r n s t a t f o p s ) ;
i f ( k e r n s t a t r e g i s t e r == −EINVAL)
{
p r i n t k ( ”The s p e c i f i e d number %d i s not v a l i d \n” ,−EINVAL ) ;
p r i n t k ( ” F a i l e d r e g i s t e r i n g k e r n s t a t c o n t r o l d e v i c e ! \ n” ) ;
p r i n t k ( ” Module i n s t a l l a t i o n a b o r t e d . \ n” ) ;
return −EINVAL ;
}
e l s e i f ( k e r n s t a t r e g i s t e r == −EBUSY)
41
{
p r i n t k ( ”The major number %d i s busy \n” ,−EBUSY ) ;
p r i n t k ( ” F a i l e d r e g i s t e r i n g k e r n s t a t c o n t r o l d e v i c e ! \ n” ) ;
p r i n t k ( ” Module i n s t a l l a t i o n a b o r t e d . \ n” ) ;
return −EBUSY;
}
...
else
{
p r i n t k ( ” k e r n s t a t d e v i c e s u c c e s s f u l l y r e g i s t e r e d . \ n” ) ;
p r i n t k ( ” Module i n s t a l l a t i o n s u c c e s s f u l . \ n” ) ;
}
return 0 ;
}
Jak już wspomniano powyżej konstruktor sterownika urządzenia znakowego kernstat rejestruje urządzenie znakowe o numerze głównym KERNSTAT MAJOR, nazwie KERNSTAT NAME i operacjach na urządzeniu kernstat fops. Operacje na urządzeniu są reprezentowane przez strukturę typu file operations zdefiniowaną w pliku <linux/fs.h>.
Stuktura ta umożliwia podanie zestawu funkcji, które będą wykonywane przy operacjach
na pliku specjalnym /dev/kernstat. Standardowo pole owner powinno być za inicjalizowane na THIS MODULE, ktore zdefiniowane jest w pliku <linux/module.h>. Umożliwia
to jądru automatyczne zarządzanie licznikiem odwołań do modułu.
struct f i l e o p e r a t i o n s {
struct module ∗ owner ;
l o f f t ( ∗ l l s e e k ) ( struct f i l e ∗ , l o f f t , int ) ;
s s i z e t ( ∗ r e a d ) ( struct f i l e ∗ , char
user ∗ , size t , l o f f t ∗);
s s i z e t ( ∗ w r i t e ) ( struct f i l e ∗ , const char
user ∗ , size t , l o f f t ∗);
int ( ∗ i o c t l ) ( struct i n o d e ∗ , struct f i l e ∗ , unsigned int cmd ,
unsigned long a r g ) ;
int ( ∗mmap) ( struct f i l e ∗ , struct v m a r e a s t r u c t ∗ ) ;
int ( ∗ open ) ( struct i n o d e ∗ , struct f i l e ∗ ) ;
int ( ∗ r e l e a s e ) ( struct i n o d e ∗ , struct f i l e ∗ ) ;
};
Bazując na prototypach funkcji zamieszczonych w strukturze file operations definiujemy
nagłówki funkcji określające operacje na urządzeniu /dev/kernstat. Ponieważ, jak podkreślono wcześniej, nie ma wymogu definiowania wszystkich operacji zdefiniowanych w strukturze file operations, więc definiujemy tylko te, które są potrzebne, mianowicie:
42
s t a t i c i nt k e r n s t a t o p e n ( struct i n o d e ∗ inode , struct f i l e ∗ f i l e ) ;
s t a t i c i nt k e r n s t a t r e l e a s e ( struct i n o d e ∗ inode , struct f i l e ∗ f i l e ) ;
s t a t i c s s i z e t k e r n s t a t r e a d ( struct f i l e ∗ f i l e , char ∗ buf , s i z e t count ,
loff t ∗filepos );
s t a t i c i nt k e r n s t a t i o c t l ( struct i n o d e ∗ inode , struct f i l e ∗ f i l e ,
unsigned int cmd , unsigned long a r g ) ;
Ostatnią rzeczą jaką należy wykonać, to przypisanie do instancji struktury file operations,
tzn. struktury kernstat fops wcześniej zdefiniowanych funkcji:
s t a t i c struct f i l e o p e r a t i o n s k e r n s t a t f o p s =
{
owner :
THIS MODULE,
open :
kernstat open ,
release :
kernstat release ,
read :
kernstat read ,
ioctl :
kernstat ioctl ,
};
5.3
Destruktor modułu kernstat
Każdy moduł musi definiować nie tylko funkcję inicjującą moduł, ale także funkcję zwalniającą moduł, czyli tzw. destruktor. Funkcja zwalniająca jest wywoływana przy usuwaniu modułu. Sterownik urządzenia kernstat implementuje destruktor o nazwie kernstat cleanup module, a jego zadaniem jest wyrejestrowanie sterownika urządzenia znakowego o nazwie kernstat. Do wyrejestrowywania sterowników urządzeń znakowych służy
następująca funkcja:
int u n r e g i s t e r c h r d e v ( int major , char ∗name ) ;
Przed wyrejestrowaniem urządzenia sprawdzana jest poprawność obu argumentów. Usunięcie modułu sterownika przed wyrejestrowaniem urządzenia może mieć bardzo poważne
konsekwencje. Próba obejrzenia /proc/devices będzie wywoływała błąd oops, bo system,
starając się wyświetlić nazwy urządzeń, będzie odwoływał się do regionów pamięci usuniętego modułu. Aby unikać takich sytuacji należy pole owner w strukturze file operations
ustawić na THIS MODULE. Umożliwia to jądru automatyczne zarządzanie licznikiem
odwołań do modułu. Implementacja destruktora kernstat cleanup module wygląda następująco:
s t a t i c void k e r n s t a t c l e a n u p m o d u l e ( void )
{
k e r n s t a t u n r e g i s t e r = u n r e g i s t e r c h r d e v ( 4 2 ,KERNSTAT NAME) ;
i f ( k e r n s t a t u n r e g i s t e r == −EINVAL) {
43
p r i n t k ( ”%d\n” ,−EINVAL ) ;
}
else
p r i n t k ( ” k e r n s t a t c o n t r o l d e v i c e s u c c e s s f u l l y u n r e g i s t e r e d . \ n” ) ;
}
Jeżeli operacja wyrejestrowania sterownika nie powiedzie się, zdarzenie to zostanie odnotowane w zmiennej kernstat unregister. Zmienna ta jest zdefiniowana w pliku kernstat.c:
s t a t i c i nt k e r n s t a t u n r e g i s t e r ;
5.4
Struktura file operations
Struktura file operations zdefiniowana w <linux/fs.h> zawiera zestaw funkcji, które są
wykonywane przy operacjach na pliku specjalnym związanym z urządzeniem. Tworząc
nowe urządzenie należy określić niekoniecznie wszystkie operacje, które ma wykonuwać
sterownik. Do wyżej wymienionych operacji zaliczamy:
• open - przeprowadzenie czynności przygotowawczych przed innymi operacjami;
• release - wykonanie czynności odwrotnych do tych w operacji open;
• read - przepisanie pewnej porcji danych z przestrzeni adresowej jądra pod wskazany
adres (buff) w przestrzeni adresowej użytkownika;
• write - przepisanie pewnej porcji danych z przestrzeni adresowej użytkownika (buff)
do przestrzeni adresowej jądra.
W operacjach na pliku specjalnym sterowników urządzeń znakowych wykorzystywany jest
mechanizm obiektowy, mianowicie sterownik dostarcza operacji na sobie samym. Struktura file operations, która zdefiniowana jest pliku nagłówkowym <linux/fs.h >, umożliwia
podanie zestawu funkcji, które będą wykonywane przy operacjach na pliku specjalnym.
struct f i l e o p e r a t i o n s {
struct module ∗ owner ;
l o f f t ( ∗ l l s e e k ) ( struct f i l e ∗ , l o f f t , int ) ;
s s i z e t ( ∗ r e a d ) ( struct f i l e ∗ , char
user ∗ , size t , l o f f t ∗);
s s i z e t ( ∗ w r i t e ) ( struct f i l e ∗ , const char
user ∗ , size t , l o f f t ∗);
int ( ∗ i o c t l ) ( struct i n o d e ∗ , struct f i l e ∗ , unsigned int cmd ,
unsigned long a r g ) ;
int ( ∗mmap) ( struct f i l e ∗ , struct v m a r e a s t r u c t ∗ ) ;
int ( ∗ open ) ( struct i n o d e ∗ , struct f i l e ∗ ) ;
int ( ∗ r e l e a s e ) ( struct i n o d e ∗ , struct f i l e ∗ ) ;
};
44
Struktura file zdefiniowana w <linux/fs.h> reprezentuje w jądrze otwarty plik. Jest tworzona przez jądro w momencie wywołania open i jest przekazywana do wszystkich operacji
wykonywanych na pliku. Dzieje się tak, aż do ostatniego wywołania close, czyli momentu,
kiedy wywołana zostaje funkcja release. Każda operacja przeprowadzana na urządzeniu
wymagania odpowiednio jego otwarcia, wykonania zadanej czynności oraz jego zamknięcia. Warto zauważyć, że otwarty plik struct file, to nie to samo co plik na dysku, który
jest reprezentowany przez strukturę inode.
struct f i l e
{
mode t
loff t
unsigned int
struct f i l e o p e r a t i o n s
void
...
};
f mode ;
f pos ;
f flags ;
∗ f op ;
∗ private data ;
Pole f mode pozwala określić, czy plik jest otwarty do odczytu FMODE READ, zapisu FMODE WRITE lub obu. Pola tego nie trzeba sprawdzać w funkcjach read i write,
bo jądro wykonuje taki test przed wywołaniem odpowiedniej funkcji sterownika. Pole
f pos określa pozycję do pisania lub odczytu. Sterownik może odczytywać wartość pola,
ale nie powinien go zmieniać. Flagi f flags wykorzystuje się głównie do sprawdzenia, czy
operacja ma być blokująca, czy też nie. Dozwolonymi wartościami są O BLOCK oraz
O NONBLOCK. Pole f op określa zestaw funkcji implementujących operacje na pliku.
Pole to określa standardowe operacje, które są dostępne dla urządzenia. Są one określane
podczas operacji rejestrowania urządzenia przez jądro przy wywołaniu open. Później to
pole nie jest ani modyfikowane, ani sprawdzane. Takie podejście, w zależności od wyboru numeru drugorzędnego, umożliwia realizowanie wielu zachowań sterownika przy każdym wywołaniu operacji na pliku, bez powodowania dodatkowego narzutu systemowego.
Wskaźnik private data jest ustawiany na NULL przy otwieraniu pliku. Sterownik może
wykorzystać ten wskaźnik dla własnych celów, wtedy jest odpowiedzialny za zwolnienie
pamięci przydzielonej na rzecz tego pola.
5.5
Otwarcie pliku urządzenia kernstat
Operację otwarcia pliku wykonuje się przy pomocy funkcji open.
int open ( struct i n o d e ∗ inode , struct f i l e ∗ f i l p ) ;
Operacja open umożliwia sterownikowi przeprowadzenie czynności przygotowawczych, zazwyczaj wykonuje się następujące kroki:
45
• Sprawdzenie błędów związanych z urządzeniem (np. sprawdzenie, czy urządzenie
jest gotowe);
• Inicjalizacja urządzenia, gdy jest otwierane po raz pierwszy;
• Identyfikacja numeru drugorzędnego (MINOR(inode->i rdev)) i, jeśli jest to konieczne, podmiana zestawu operacji wskazywanej przez f op;
• Przydzielenie pamięci na dane związane z urządzeniem, inicjalizacja struktur danych
oraz przypisanie wskaźnika private data;
• W starszych wersjach jądra tzn. <2.6 zadaniem operacji open było również zwiększenie licznika odwołań do modułu. Teraz wystarczy ustawić pole owner struktury
file operations na THIS MODULE powoduje to, że system sam dba o zwiększenie
licznika.
Funkcja kernstat open realizuje operacje wyzerowania wskaźników odpowiedzialnych za alokowanie danych, które są następnie importowane do przestrzeni użytkownika. Wszystkie
struktury danych oraz tablice wykorzystywane w sterowniku są alokowane dynamicznie
z powodu ograniczonej ilości pamięci jądra oraz braku mechanizmów jej odśmiecania.
int k e r n s t a t o p e n ( struct i n o d e ∗ i n o d e , struct f i l e ∗ f i l e )
{
e x p o r t e d s c h e d s t a t s = NULL;
e x p o r t e d p r i o t a s k s = NULL;
e x p o r t e d t a s k s t a t s = NULL;
return 0 ;
}
Import danych realizowany jest w funkcji kernstat read za pomocą makra copy to user
oraz odpowiednio ustawionych flag.
static s s i z e t
k e r n s t a t r e a d ( struct f i l e ∗ f i l e , char ∗ buf , s i z e t count , l o f f t ∗ f i l e p o s ) {
...
if ( sched orient )
not copied =
c o p y t o u s e r ( buf , ( char ∗ ) e x p o r t e d s c h e d s t a t s + my pos , my num ) ;
if ( prio array orient )
not copied =
c o p y t o u s e r ( buf , ( char ∗ ) e x p o r t e d p r i o t a s k s + my pos , my num ) ;
if ( task orient )
not copied =
c o p y t o u s e r ( buf , ( char ∗ ) e x p o r t e d t a s k s t a t s + my pos , my num ) ;
46
...
}
5.6
Zamknięcie pliku urządzenia kernstat
Operacja zamknięcia pliku realizowana jest przy pomocy funkcji release
int r e l e a s e ( struct i n o d e ∗ inode , struct f i l e ∗ f i l p ) ;
Operacja release wywoływana jest przy operacji close na otwartym pliku służy do wykonania czynności odwrotnych do tych, które mają miejsce przy wykonywaniu operacji
open. Zazwyczaj są to:
• zwolnienie pamięci private data;
• zamknięcie urządzenia, gdy jest to ostatnie wywołanie release;
• w starszych wersjach jądra zadaniem operacji release było również zmniejszenie licznika odwołań do modułu, jednak obligatoryjne obecnie ustawienie pola owner struktury file operations na THIS MODULE powoduje, że system sam dba o zmniejszenie
licznika.
int k e r n s t a t r e l e a s e ( struct i n o d e ∗ i n o d e , struct f i l e ∗ f i l e )
{
p r i n t k ( ” k e r n s t a t r e l e a s e \n” ) ;
if ( exported sched stats ) kfree ( exported sched stats );
if ( exported prio tasks ) vfree ( exported prio tasks );
if ( exported task stats ) vfree ( exported task stats );
return 0 ;
}
Funkcja kernstat release służy do zwalniania pamięci dla wcześniej zaalokowanych dynamicznie danych, które zostały wytransportowane do przestrzeni użytkownika oraz dekrementuje licznik odwołań do modułu. Jeżeli licznik odwołań wyniesie zero, to wówczas
będzie można usunąć moduł, w przeciwnym wypadku nie mamy takiej możliwości.
47
5.7
Czytanie z pliku urządzenia kernstat
Operację czytania z pliku wykonuje się przy pomocy funkcji read.
s s i z e t r e a d ( struct f i l e ∗ f i l e , char
u s e r ∗ b u f f , s i z e t count , l o f f t ∗ o f f p ) ;
static s s i z e t
k e r n s t a t r e a d ( struct f i l e ∗ f i l e , char ∗ b u f f , s i z e t count , l o f f t ∗ f i l e p o s )
{
int my pos =0;
int my max =0;
int my num = 0 ;
unsigned long n o t c o p i e d =0;
if ( sched orient )
if ( prio array orient )
if ( task orient )
my max = e x p o r t e d s c h e d s i z e t o r e a d ;
my max = e x p o r t e d p r i o a r r a y s i z e t o r e a d ;
my max = e x p o r t e d t a s k s i z e t o r e a d ;
my pos = ( int ) ( f i l e −>f p o s ) ;
i f ( my pos >= my max )
return 0 ;
i f ( my max−my pos > count )
my num = count ;
else
my num = my max−my pos ;
if ( sched orient )
n o t c o p i e d = c o p y t o u s e r ( buf ,
( char ∗ ) e x p o r t e d s c h e d s t a t s + my pos , my num ) ;
i f ( p r i o a r r a y o r i e n t ){
n o t c o p i e d = c o p y t o u s e r ( buf ,
( char ∗ ) e x p o r t e d p r i o t a s k s + my pos , my num ) ;
}
i f ( t a s k o r i e n t ){
n o t c o p i e d = c o p y t o u s e r ( buf ,
( char ∗ ) e x p o r t e d t a s k s t a t s + my pos , my num ) ;
}
count = my num−n o t c o p i e d ;
my pos += count ;
∗ f i l e p o s = ( l o f f t ) my pos ;
return count ;
}
Zadaniem operacji kernstat read jest przepisanie pewnej porcji danych z przestrzeni adresowej jądra pod wskazany adres buff w przestrzeni adresowej użytkownika. Z tą operacją
wiąże się także zwiększenie znacznika pozycji pliku *offp. Zazwyczaj jest on ustawiony
na ten sam adres pamięci co filp->f pos. Wartość zwracana przez funkcję kernstat read
będzie interpretowana w następujący sposób. Wartość większa od zera oznacza liczbę prze48
pisanych bajtów. Jeśli jest równa wartości argumentu przekazanego do wywołania systemowego read, to oznacza to poprawne zakończenie operacji. Jeśli ta wartość jest mniejsza
od żądanej liczby bajtów, to tylko część danych została przekazana i należy się wtedy
spodziewać, że program powtórzy wywołanie systemowe; takie jest np. standardowe zachowanie funkcji bibliotecznej fread. Jeśli wartość jest równa 0, to został osiągnięty koniec
pliku. Wartość ujemna oznacza błąd. Kody błędów zdefiniowane są w pliku nagłówkowym
<linux/errno.h >. Sterowaniem funkcji kernstat read rządzą odpowiednio ustawione flagi,
które określają jaki rodzaj danych ma zostać wysłany do przestrzeni użytkownika. Są one
ustawiane przez program działający w przestrzeni użytkownika za pomocą wywołania
ioctl, np.
i o c t l e r r o r = i o c t l ( fd , KERSTAT SCHED STATS ) ;
i f ( i o c t l e r r o r <0)
{
f p r i n t f ( s t d e r r , ”KERSTAT SCHED STATS e r r o r : %s \n” , s t r e r r o r ( e r r n o ) ) ;
exit (1);
}
Wysłana komenda ioctl o nazwie KERSTAT SCHED STATS zostaje wysłana do przestrzeni jądra i ustawia odpowiednią flagę
case KERSTAT SCHED STATS :
i f ( ! c a p a b l e (CAP SYS ADMIN ) )
return −EPERM;
sched orient = 1;
prio array orient = 0;
t a s k o r i e n t =0;
export sched stat ( procesor ) ;
break ;
Komenda KERSTAT SCHED STATS oprócz ustawienia flagi sched orient odpowiedzialna jest również za wyłączenie flag prio array orient, task orient oraz wywołanie funkcji export sched stat(procesor). Funkcja export sched stat(procesor) jest odpowiedzialna
za pobranie statystyk planisty. Jej działanie sprowadza się do alokacji odpowiedniej struktury danych oraz wypełnieniu jej odpowiednimi danymi.
s t a t i c void e x p o r t s c h e d s t a t ( unsigned long cpu )
{
struct rq ∗ rq ;
exported sched stats =
( struct s c h e d r q ∗ ) k m a l l o c ( s i z e o f ( struct s c h e d r q ) ,GFP KERNEL ) ;
if (! exported sched stats ) {
p r i n t k ( ” e x p o r t e d s c h e d s t a t s = NULL\n” ) ;
}
49
rq = k e r n c p u r q ( cpu ) ;
e x p o r t e d s c h e d s t a t s −>p i d = rq−>c u r r −>p i d ;
e x p o r t e d s c h e d s t a t s −>t i m e s l i c e =
j i f f i e s t o m s e c s ( rq−>c u r r −>t i m e s l i c e ) ;
s t r c p y ( e x p o r t e d s c h e d s t a t s −>comm, rq−>c u r r −>comm ) ;
e x p o r t e d s c h e d s t a t s −>n r r u n n i n g = rq−>n r r u n n i n g ;
e x p o r t e d s c h e d s t a t s −>n r s w i t c h e s = rq−>n r s w i t c h e s ;
e x p o r t e d s c h e d s t a t s −>n r u n i n t e r r u p t i b l e =
rq−>n r u n i n t e r r u p t i b l e ;
e x p o r t e d s c h e d s t a t s −>n r i o w a i t =
a t o m i c r e a d (&rq−>n r i o w a i t ) ;
e x p o r t e d s c h e d s t a t s −>a c t i v e n r a c t i v e =
rq−>a c t i v e −>n r a c t i v e ;
e x p o r t e d s c h e d s t a t s −>e x p i r e d n r a c t i v e =
rq−>e x p i r e d −>n r a c t i v e ;
e x p o r t e d s c h e d s i z e t o r e a d = s i z e o f ( struct s c h e d r q ) ;
}
Po ustawieniu odpowiednich flag działania oraz wypełnieniu pól struktury exported sched stats użytkownik może mieć pewność, że wykonując operacje kernstat read otrzyma odpowiednie dane. Kod funkcji kernstat read dla tego przypadku (ustawionej flagi
sched orient) wygląda następująco:
if ( sched orient )
not copied =
c o p y t o u s e r ( buf , ( char ∗ ) e x p o r t e d s c h e d s t a t s + my pos , my num ) ;
Po wykonaniu powyższej operacji program przestrzeni użytkowika odczytuje dane z pliku
urządzenia znakowego /dev/kernstat w następujący sposób:
f d = open ( ” / dev / ”KERNSTAT NAME,O RDONLY) ;
i f ( fd < 0)
{
f p r i n t f ( s t d e r r , ” / dev / k e r n s t a t e r r o r : %s \n” , s t r e r r o r ( e r r n o ) ) ;
exit (1);
}
i o c t l e r r o r = i o c t l ( fd , KERSTAT SET PROCESOR,& p r o c e s o r ) ;
i f ( i o c t l e r r o r <0)
{
f p r i n t f ( s t d e r r , ”KERSTAT SET PROCESOR e r r o r : %s \n” , s t r e r r o r ( e r r n o ) ) ;
exit (1);
}
i o c t l e r r o r = i o c t l ( fd , KERSTAT SCHED STATS ) ;
i f ( i o c t l e r r o r <0)
{
50
f p r i n t f ( s t d e r r , ”KERSTAT SCHED STATS e r r o r : %s \n” , s t r e r r o r ( e r r n o ) ) ;
exit (1);
}
imported sched stats =
( struct s c h e d r q ∗ ) m a l l o c ( s i z e o f ( struct s c h e d r q ) ) ;
i f ( ! imported sched stats )
{
f p r i n t f ( s t d e r r , ” a l o c a t i o n f a u l t : %s \n” , s t r e r r o r ( e r r n o ) ) ;
exit (1);
}
Odczytanie danych z pliku /dev/kernstat:
i = 0;
while ( r e a d ( f d ,& i m p o r t e d s c h e d s t a t s [ i ] ,
sizeof ( imported sched stats [ i ] ) ) ) ;
{
p r i n t f ( ”%−6l u \ t ” , i m p o r t e d s c h e d s t a t s [ i ] . n r r u n n i n g ) ;
p r i n t f ( ”%−6l u \ t ” , i m p o r t e d s c h e d s t a t s [ i ] . n r u n i n t e r r u p t i b l e ) ;
p r i n t f ( ”%−6d\ t ” , i m p o r t e d s c h e d s t a t s [ i ] . n r i o w a i t ) ;
p r i n t f ( ”%−6d\ t ” , i m p o r t e d s c h e d s t a t s [ i ] . a c t i v e n r a c t i v e ) ;
p r i n t f ( ”%−6d\ t ” , i m p o r t e d s c h e d s t a t s [ i ] . e x p i r e d n r a c t i v e ) ;
p r i n t f ( ”%−8l u \ t ” , i m p o r t e d s c h e d s t a t s [ i ] . n r s w i t c h e s ) ;
p r i n t f ( ”%−6d\ t ” , i m p o r t e d s c h e d s t a t s [ i ] . p i d ) ;
p r i n t f ( ”%−6s \ t ” , i m p o r t e d s c h e d s t a t s [ i ] . comm ) ;
p r i n t f ( ”%−6d\n” , i m p o r t e d s c h e d s t a t s [ i ] . t i m e s l i c e ) ;
i ++;
}
5.8
Funkcja kontroli urządzenia kernstat ioctl
Urządzenia znakowe są kontrolowane przez funkcję ioctl :
int i o c t l ( struct i n o d e ∗ inode , struct f i l e ∗ f i l p , unsigned int cmd ,
unsigned long a r g ) ;
Implementacja funkcji kernstat ioctl wygląda następująco:
int k e r n s t a t i o c t l ( struct i n o d e ∗ inode , struct f i l e ∗ f i l e ,
unsigned int cmd , unsigned long a r g )
{
int e r r o r =0;
struct rq ∗ rq ;
51
i f ( IOC TYPE (cmd) != KERNSTAT IO MAGIC NR)
return −ENOTTY;
i f ( IOC NR (cmd) > KERNSTAT IOC MAXNR)
return −ENOTTY;
i f ( IOC DIR (cmd) & IOC READ)
e r r o r = ! a c c e s s o k (VERIFY WRITE,
( void
u s e r ∗ ) arg , IOC SIZE (cmd ) ) ;
e l s e i f ( IOC DIR (cmd) & IOC WRITE)
e r r o r = ! a c c e s s o k (VERIFY READ,
( void
u s e r ∗ ) arg ,
IOC SIZE (cmd ) ) ;
if ( error )
return −EFAULT;
switch (cmd)
{
case KERSTAT IO GET EXPORTED DATA COUNT:
i f ( ! c a p a b l e (CAP SYS ADMIN ) )
return −EPERM;
exported data count error =
p u t u s e r ( e x p o r t e d d a t a c o u n t , ( int
break ;
case KERSTAT SET PROCESOR :
i f ( ! c a p a b l e (CAP SYS ADMIN ) )
return −EPERM;
procesor error =
g e t u s e r ( p r o c e s o r , ( int
break ;
case KERSTAT SCHED STATS :
i f ( ! c a p a b l e (CAP SYS ADMIN ) )
return −EPERM;
sched orient = 1;
prio array orient = 0;
t a s k o r i e n t =0;
export sched stat ( procesor ) ;
break ;
case KERSTAT PRIO ARRAY TASKS :
i f ( ! c a p a b l e (CAP SYS ADMIN ) )
52
u s e r ∗) arg ) ;
u s e r ∗) arg ) ;
return −EPERM;
prio array type error =
g e t u s e r ( p r i o a r r a y t y p e , ( int
u s e r ∗) arg ) ;
prio array orient = 1;
sched orient = 0;
t a s k o r i e n t =0;
rq = k e r n c p u r q ( p r o c e s o r ) ;
i f ( p r i o a r r a y t y p e ==0){
s c h e d p i o a r r a y t a s k s ( p r o c e s o r , rq−>a c t i v e , ” a c t i v e ” ) ;
}
e l s e s c h e d p i o a r r a y t a s k s ( p r o c e s o r , rq−>e x p i r e d , ” e x p i r e d ” ) ;
break ;
case KERSTAT SCHED TASK STATS :
i f ( ! c a p a b l e (CAP SYS ADMIN ) )
return −EPERM;
t a s k o r i e n t =1;
sched orient = 0;
prio array orient = 0;
task pid error =
g e t u s e r ( t a s k p i d , ( int
export task stat ( task pid );
break ;
u s e r ∗) arg ) ;
case KERSTAT SET TIME ORIENT :
i f ( ! c a p a b l e (CAP SYS ADMIN ) )
return −EPERM;
tasks output type error =
g e t u s e r ( t a s k s o u t p u t t y p e , ( int
u s e r ∗) arg ) ;
tasks output type format = tasks output type ;
break ;
default :
return −ENOTTY;
}
return e r r o r ;
}
Pierwsze dwa argumenty funkcji odpowiadają deskryptorowi pliku przekazanemu przez
wywołanie systemowe. Argument cmd jest dokładnie taki jak w wywołaniu systemowym. Opcjonalny argument arg jest przekazywany w postaci liczby typu unsigned long
bez względu na typ użyty przy wywołaniu systemowym [7]. Zazwyczaj implementacja
operacji ioctl zawiera po prostu konstrukcję switch wybierającą odpowiednie zachowanie
53
w zależności od wartości argumentu cmd. Różne komendy są reprezentowane przez różne
numery, którym zazwyczaj nadaje się nazwy korzystając z definicji preprocesora. Program użytkownika powinien mieć możliwość włączenia tego samego pliku nagłówkowego
z deklaracjami, który jest używany przy kompilacji modułu sterownika. Do twórcy sterownika należy ustalenie wartości liczbowych odpowiadających komendom interpretowanym
przez sterownik. Najprostszy wybór przypisujący kolejne małe wartości poszczególnym
komendom nie jest zwykle dobrym rozwiązaniem. Komendy powinny być unikalne w skali całego systemu, żeby uniknąć błędów powstających wówczas, gdy poprawną komendę
wysyłamy do niepoprawnego urządzenia. Taka sytuacja może nie występować zbyt często, ale jej konsekwencje mogą by poważne. W przypadku kiedy wszystkim numerom ioctl
przyporządkowujemy odrębne komendy, pomyłka spowoduje zwrócenie -EINVAL zamiast
wykonania niezamierzonej akcji. W ustalaniu wartości liczbowych dla komend pomocne
mogą być następujące makra zdefiniowane w pliku <asm/ioctl.h>:
IO ( type , nr )
IOR ( type , nr , d a t a i t e m )
IOW( type , nr , d a t a i t e m )
IOWR( type , nr , d a t a i t e m )
Znaczenie poszczególnych makr jest następujące:
IO(type, nr) komenda ogólnego przeznaczenia nie pobierająca argumentu;
IOR(type, nr, dataitem) komenda z zapisem w przestrzeni użytkownika;
IOW(type, nr, dataitem) komenda z odczytem z przestrzeni użytkownika;
IOWR(type,nr,dataitem) komenda z zapisem i odczytem,
gdzie type, to unikatowy numer sterownika, tzw. numer magiczny, nr – kolejny numer
komendy, dataitem – struktura związana z komendą. Funkcja kernstat ioctl implementuje
następujące komendy. Są one zdefiniowane w pliku kernstat.h
#define
#define
#define
#define
#define
#define
#define
#define
#define
#define
KERNSTAT MAJOR 42
KERNSTAT NAME ” k e r n s t a t ”
KERNSTAT IO MAGIC NR ’ $ ’
KERSTAT IO GET EXPORTED DATA COUNT
KERSTAT SCHED STATS
KERSTAT PRIO ARRAY TASKS
KERSTAT SET PROCESOR
KERSTAT SCHED TASK STATS
KERSTAT SET TIME ORIENT
KERNSTAT IOC MAXNR 20
54
IOR (KERNSTAT IO MAGIC NR, 2 , int )
IO (KERNSTAT IO MAGIC NR, 9 )
IOW(KERNSTAT IO MAGIC NR, 1 0 , int )
IOW(KERNSTAT IO MAGIC NR, 1 1 , int )
IOW(KERNSTAT IO MAGIC NR, 1 2 , int )
IOW(KERNSTAT IO MAGIC NR, 1 3 , int )
KERNSTAT MAJOR Numer nadrzędny urządzenia;
KERNSTAT NAME Nazwa urządzenia;
KERNSTAT IO MAGIC NR Magiczny numer;
KERSTAT IO GET EXPORTED DATA COUNT Czyta z przestrzeni jądra liczbę wyeksportowanych danych;
KERSTAT SCHED STATS Ustawia flagę trybu działania, która pobiera statystyki
planisty;
KERSTAT PRIO ARRAY TASKS Zapisuje do przestrzeni jądra rodzaj tablicy,
której dane chcemy pobrać;
KERSTAT SET PROCESOR Ustawia numer procesora, na którym chcemy działać;
KERSTAT SCHED TASK STATS Ustawia flagę trybu działania, która pobiera statystyki zadania;
KERSTAT SET TIME ORIENT Ustawia jednostkę pomiaru czasu;
KERNSTAT IOC MAXNR Liczba określająca maksymalną liczbę komend danego
urządzenia.
5.9
Funkcja export sched stat
Pobieranie statystyk planisty realizowane jest poprzez funkcję export sched stat jako odpowiedź na rozkaz ioctl urządzenia kernstat. W celu pobrania statystyk planisty należy
wysłać z przestrzeni użytkownika do przestrzeni jądra odpowiedni rozkaz. Rozkazy, jak
już wcześniej wspomniano, wysyłane są za pomocą funkcji kontroli urządzenia ioctl. Rozkaz jest następnie odbierany w przestrzeni jądra i realizowane są odpowiednie dla niego
operacje.
s t a t i c void e x p o r t s c h e d s t a t ( unsigned long cpu ) ;
case KERSTAT SCHED STATS :
i f ( ! c a p a b l e (CAP SYS ADMIN ) )
return −EPERM;
sched orient = 1;
prio array orient = 0;
t a s k o r i e n t =0;
export sched stat ( procesor ) ;
break ;
55
Powyższy kod to odpowiedź na instrukcję zamieszczoną w programie działającym w przestrzeni użytkownika, a właściwie na rozkaz wysłany za pomocą wywołania systemowego
ioctl.
i o c t l e r r o r = i o c t l ( fd , KERSTAT SCHED STATS ) ;
i f ( i o c t l e r r o r <0)
{
f p r i n t f ( s t d e r r , ”KERSTAT SCHED STATS e r r o r : %s \n” , s t r e r r o r ( e r r n o ) ) ;
exit (1);
}
i m p o r t e d s c h e d s t a t s = ( struct s c h e d r q ∗ ) m a l l o c ( s i z e o f ( struct s c h e d r q ) ) ;
i f ( ! imported sched stats )
{
f p r i n t f ( stderr , ” imported task stats alocation fault :
%s \n” , s t r e r r o r ( e r r n o ) ) ;
exit (1);
}
i = 0;
while ( r e a d ( f d ,& i m p o r t e d s c h e d s t a t s [ i ] , s i z e o f ( i m p o r t e d s c h e d s t a t s [ i ] ) ) ) ;
{
p r i n t f ( ”%−6l u \ t ” , i m p o r t e d s c h e d s t a t s [ i ] . n r r u n n i n g ) ;
p r i n t f ( ”%−6l u \ t ” , i m p o r t e d s c h e d s t a t s [ i ] . n r u n i n t e r r u p t i b l e ) ;
p r i n t f ( ”%−6d\ t ” , i m p o r t e d s c h e d s t a t s [ i ] . n r i o w a i t ) ;
p r i n t f ( ”%−6d\ t ” , i m p o r t e d s c h e d s t a t s [ i ] . a c t i v e n r a c t i v e ) ;
p r i n t f ( ”%−6d\ t ” , i m p o r t e d s c h e d s t a t s [ i ] . e x p i r e d n r a c t i v e ) ;
p r i n t f ( ”%−8l u \ t ” , i m p o r t e d s c h e d s t a t s [ i ] . n r s w i t c h e s ) ;
p r i n t f ( ”%−6d\ t ” , i m p o r t e d s c h e d s t a t s [ i ] . p i d ) ;
p r i n t f ( ”%−6s \ t ” , i m p o r t e d s c h e d s t a t s [ i ] . comm ) ;
p r i n t f ( ”%−6d\n” , i m p o r t e d s c h e d s t a t s [ i ] . t i m e s l i c e ) ;
i ++;
}
Przedstawiony powyżej wycinek kodu odpowiedzialny jest za wysyłanie za pomocą wywołania systemowego ioctl rozkazu KERSTAT SCHED STAT do pliku urządzenia znakowego, który jest reprezentowany przy pomocy zmiennej fd. Zmienna fd to deskryptor
pliku, który trzeba wcześniej otworzyć.
f d = open ( ” / dev / ”KERNSTAT NAME,O RDONLY) ;
i f ( fd < 0){
f p r i n t f ( s t d e r r , ” / dev / k e r n s t a t e r r o r : %s \n” , s t r e r r o r ( e r r n o ) ) ;
exit (1);
}
56
W celu upewnienia się, że rozkaz został wysłany poprawnie do przestrzeni jądra sprawdzamy wartość zmiennej ioctl error, która przechowuje kod błędu funkcji ioctl. W razie
niepowodzenia wysłania rozkazu przerywamy dalsze wykonywanie instrukcji. W przeciwnym wypadku sterowanie przechodzi do przestrzeni jądra, tzn.
case KERSTAT SCHED STATS :
i f ( ! c a p a b l e (CAP SYS ADMIN ) )
return −EPERM;
sched orient = 1;
prio array orient = 0;
t a s k o r i e n t =0;
export sched stat ( procesor ) ;
break ;
Powyższe instrukcje odpowiedzialne są za sprawdzenie uprawnień, ustawienie flag wykorzystywanych przy eksportowaniu danych w funkcji kernstat read oraz wywołanie funkcji
export sched stat, czyli funkcji, która pobiera statystyki planisty. Statystyki planisty reprezentowane są poprzez strukturę typu struct sched rq:
struct s c h e d r q
{
unsigned long n r r u n n i n g ;
unsigned long long n r s w i t c h e s ;
unsigned long n r u n i n t e r r u p t i b l e ;
int n r i o w a i t ;
unsigned int a c t i v e n r a c t i v e ;
unsigned int e x p i r e d n r a c t i v e ;
p i d t pid ;
char comm [ 1 6 ] ;
unsigned int t i m e s l i c e ;
};
nr running liczba zadań w stanie TASK RUNNING;
nr switches liczba przełączeń kontekstu;
nr uninterruptible liczba zadań w stanie TASK UNINTERRUPTIBLE ;
nr iowait liczba zadań czekających na I/O;
active nr active liczba zadań w tablicy active;
expired nr active liczba zadań w tablicy expired
57
pid pid zadania aktualnie wykonującego się;
comm[16] nazwa programu aktualnie wykonującego się;
time slice kwant czasu zadania aktualnie wykonującego się.
5.10
Funkcja export sched stat
s t a t i c void e x p o r t s c h e d s t a t ( unsigned long cpu )
{
struct rq ∗ rq ;
exported sched stats =
( struct s c h e d r q ∗ ) k m a l l o c ( s i z e o f ( struct s c h e d r q ) ,GFP KERNEL ) ;
if (! exported sched stats ) {
p r i n t k ( ” e x p o r t e d s c h e d s t a t s = NULL\n” ) ;
}
rq = struct rq ∗ rq ; ( cpu ) ;
e x p o r t e d s c h e d s t a t s −>p i d = rq−>c u r r −>p i d ;
e x p o r t e d s c h e d s t a t s −>t i m e s l i c e =
j i f f i e s t o m s e c s ( rq−>c u r r −>t i m e s l i c e ) ;
s t r c p y ( e x p o r t e d s c h e d s t a t s −>comm, rq−>c u r r −>comm ) ;
e x p o r t e d s c h e d s t a t s −>n r r u n n i n g = rq−>n r r u n n i n g ;
e x p o r t e d s c h e d s t a t s −>n r s w i t c h e s = rq−>n r s w i t c h e s ;
e x p o r t e d s c h e d s t a t s −>n r u n i n t e r r u p t i b l e =
rq−>n r u n i n t e r r u p t i b l e ;
e x p o r t e d s c h e d s t a t s −>n r i o w a i t =
a t o m i c r e a d (&rq−>n r i o w a i t ) ;
e x p o r t e d s c h e d s t a t s −>a c t i v e n r a c t i v e =
rq−>a c t i v e −>n r a c t i v e ;
e x p o r t e d s c h e d s t a t s −>e x p i r e d n r a c t i v e =
rq−>e x p i r e d −>n r a c t i v e ;
e x p o r t e d s c h e d s i z e t o r e a d = s i z e o f ( struct s c h e d r q ) ;
}
Działanie funkcji export sched stat polega na zaalokowaniu pamięci dla struktury typu
struct sched rq oraz wypełnieniu jej odpowiednimi statystykami. Statystyki pobieramy
ze struktury danych typu struct rq, która reprezentuje kolejkę procesów gotowych do wykonania tożsamą z danym procesorem. Operacja wyłuskania wskaźnika na tę kolejkę realizowana jest przy pomocy funkcji kern cpu rq
58
rq = k e r n c p u r q ( cpu ) ;
Za ustawienie procesora w wywołaniu funkcji kern cpu rq(cpu) odpowiedzialne są następujące linie kodu przestrzni jądra oraz przestrzeni użytkownika.
i o c t l e r r o r = i o c t l ( fd , KERSTAT SET PROCESOR,& p r o c e s o r ) ;
i f ( i o c t l e r r o r <0){
f p r i n t f ( s t d e r r , ”KERSTAT SET PROCESOR e r r o r : %s \n” , s t r e r r o r ( e r r n o ) ) ;
exit (1);
}
Powyższe linie kodu są odpowiedzialne za wysłanie do przestrzeni jądra numeru procesora,
na którym chcemy operować. W odpowiedzi na rozkaz wysłany z przestrzeni użytkownika realizowana jest odpowiednia komenda ioctl, za pomocą której odczytujemy numer
procesora przekazywany dalej do odpowiednich wywołań.
case KERSTAT SET PROCESOR :
i f ( ! c a p a b l e (CAP SYS ADMIN ) )
return −EPERM;
procesor error =
g e t u s e r ( p r o c e s o r , ( int
break ;
u s e r ∗) arg ) ;
Operacja pobrania numeru procesora jest w tym przypadku realizowana za pomocą makra
get user(procesor,(int user *)arg); w przypadku wystąpienia błędu informacja o jego
przyczynie będzie znajdować się w zmiennej procesor error. Przekazanie danych do przestrzeni jądra odbywa się za pomocą funkcji kernstat read w sposób jaki został pokazany
przy jej wcześniejszym omawianiu.
5.11
Funkcja sched pio array tasks
Funkcja sched pio array tasks służy do pobierania zadań, które znajdują się w tablicach
active/expired. Do tego celu wykorzystuje się bitmapę bitmap reprezentującą stan tablicy
queue. queue to tablica list, w której pod indeksem P znajduje się lista procesów z priorytetem dynamicznym równym P. Znalezienie następnego procesu do wykonania o ustalonym
priorytecie polega na wyborze następnego elementu na liście. W ramach danego priorytetu procesy są szeregowane metodą karuzelową (RR). Ponieważ queue ma stałą długość
(MAX PRIO=140 ), więc bitmapa zajmuje 140 bitów. Stan bitu określa czy odpowiednia
lista jest pusta. Bitmapa ułatwia szybkie odszukanie procesu o najwyższym priorytecie,
w czasie stałym gdyż liczba priorytetów jest ustalona.
s t a t i c void s c h e d p i o a r r a y t a s k s ( unsigned long cpu ,
struct p r i o a r r a y ∗ array , char∗ array name ) ;
59
s t a t i c void s c h e d p i o a r r a y t a s k s ( unsigned long cpu ,
struct p r i o a r r a y ∗ array , char∗ array name )
{
struct t a s k s t r u c t ∗ t a s k ;
int p r i ; int i ;
int t a s k s c o u n t = array −>n r a c t i v e ;
exported data count = tasks count ;
exported prio tasks =
( struct s c h e d p r i o a r r a y ∗ ) v m a l l o c ( t a s k s c o u n t ∗
s i z e o f ( struct s c h e d p r i o a r r a y ) ) ;
if (! exported prio tasks )
p r i n t k ( ” e x p o r t e d p r i o t a s k s = NULL\n” ) ;
i =0;
f o r ( p r i = 0 ; p r i < MAX PRIO ; p r i ++)
{
struct l i s t h e a d ∗ head , ∗ c u r r ;
head = array −>queue + p r i ;
c u r r = head−>next ;
while ( c u r r != head )
{
t a s k = l i s t e n t r y ( c u r r , struct t a s k s t r u c t , r u n l i s t ) ;
e x p o r t e d p r i o t a s k s [ i ] . p i d = task −>p i d ;
e x p o r t e d p r i o t a s k s [ i ] . p r i o = task −>p r i o ;
exported prio tasks [ i ] . time slice =
j i f f i e s t o m s e c s ( task −>t i m e s l i c e ) ;
s t r c p y ( e x p o r t e d p r i o t a s k s [ i ] . comm, task −>comm ) ;
s t r c p y ( e x p o r t e d p r i o t a s k s [ i ] . array name , array name ) ;
i ++;
c u r r = c u r r −>next ;
}
}
struct s c h e d p r i o a r r a y
{
char array name [ 1 6 ] ;
p i d t pid ;
char comm [ 1 6 ] ;
int p r i o ;
unsigned int t i m e s l i c e ;
};
60
char array name[16 ] nazwa tablicy active bądz expired ;
pid t pid pid zadania w tablicy;
char comm[16] nazwa programu;
int prio priorytet dynamiczny;
unsigned int time slice kwant czasu.
Funkcja sched pio array tasks jako parametry wejściowe pobiera numer procesora, wskaźnik na daną tablicę oraz nazwę tablicy, która pełni tutaj rolę jedynie informacyjną dla
prezentowanych danych w przestrzeni jądra. Działanie funkcji sprowadza się do odczytania ilości elementów danej tablicy i zaalokowaniu odpowiedniego rozmiaru tablicy struktur
danych, które będą przekazane do przestrzeni jądra. Wypełnienie tablicy danymi sprowadza się do pobrania zadań, które znajdują się w odpowiednich kolejkach reprezentujących
priorytety dynamiczne. Przebieg działania, tzn. proces pobrania danych, jest analogiczny
do tego jaki ma miejsce w przypadku funkcji sched pio array tasks.
5.12
Funkcja export task stat
s t a t i c void e x p o r t t a s k s t a t ( p i d t p i d ) ;
Funkcja export task stat pobiera statystyki zadań oraz przedstawia działanie planisty.
Funkcja pobiera tylko jeden argument, mianowicie pid zadania. Do pobierania statystyk
funkcja wykorzystuje strukturę danych o nazwie task stats.
struct t a s k s t a t s
{
p i d t pid ;
int cpu ;
unsigned long p o l i c y ;
v o l a t i l e long s t a t e ;
int p r i o ;
int s t a t i c p r i o ;
unsigned long s l e e p a v g ;
unsigned int t i m e s l i c e ;
unsigned long r t p r i o r i t y ;
int hz ;
int u s e r h z ;
char i n t e r a c t i v e ;
61
int n i c e ;
int bonus ;
char comm [TASK COMM LEN ] ;
unsigned long c p u t i m e ;
unsigned long r u n d e l a y ;
unsigned long pcnt ;
unsigned long l a s t a r r i v a l ;
unsigned long l a s t q u e u e d ;
unsigned long s t a r t t i m e ;
unsigned long utime ;
unsigned long s t i m e ;
unsigned long cutime ;
unsigned long c s t i m e ;
unsigned long nvcsw ;
unsigned long nivcsw ;
unsigned long cnvcsw ;
unsigned long c n i v c s w ;
};
pid t pid pid zadania;
int cpu procesor na którym wykonuje się zadanie;
unsigned long policy sposób szeregowania zadania;
volatile long state stan w jakim znajduje się zadanie;
int prio priorytet dynamiczny;
int static prio priorytet statyczny;
unsigned long sleep avg średni czas spania zadania;
unsigned int time slice kwant czasu;
unsigned long rt priority priorytet zadań rt;
int hz częstotliwość zegara;
int user hz częstotliwość przestrzeni użytkownika;
char interactive interaktywność;
int nice wartość uprzejmości;
62
int bonus bonus;
char comm[TASK COMM LEN] nazwa programu;
unsigned long cpu time czas spędzony na procesorze;
unsigned long run delay czas spędzony na czekaniu na procesor;
unsigned long pcnt liczba kwantów czasu;
unsigned long last arrival czas ostatniego pobytu na procesorze;
unsigned long last queued czas ostatniego za kolejkowania;
unsigned long start time czas rozpoczęcia zadania;
unsigned long utime czas spędzony przez zadanie w trybie użytkownika;
unsigned long stime ; czas spędzony przez zadanie w trybie jądra;
unsigned long cutime suma czasów utime zadania i jego zadań potomnych;
unsigned long cstime suma czasów cstime zadania i jego zadań potomnych;
unsigned long nvcsw dobrowolne przełączenie kontekstu;
unsigned long nivcsw mimowolne przełączenie kontekstu;
unsigned long cnvcsw suma nvcsw zadania i jego zadań potomnych;
unsigned long cnivcsw suma cnivcsw zadania i jego zadań potomnych.
s t a t i c void e x p o r t t a s k s t a t ( p i d t p i d )
{
unsigned long long s t a r t t i m e ;
struct t a s k s t r u c t ∗ t a s k ;
task = f i n d t a s k b y p i d ( pid ) ;
e x p o r t e d t a s k s t a t s = v m a l l o c ( s i z e o f ( struct t a s k s t a t s ) ) ;
i f ( ! e x p o r t e d t a s k s t a t s ){
p r i n t k ( ” e x p o r t e d t a s k s t a t s = NULL\n” ) ;
}
e x p o r t e d t a s k s t a t s −>p i d = task −>p i d ;
s t r c p y ( e x p o r t e d t a s k s t a t s −>comm, task −>comm ) ;
63
e x p o r t e d t a s k s t a t s −>cpu = task −>t h r e a d i n f o −>cpu ;
e x p o r t e d t a s k s t a t s −>p o l i c y = task −>p o l i c y ;
e x p o r t e d t a s k s t a t s −>s t a t e = task −>s t a t e ;
e x p o r t e d t a s k s t a t s −>n i c e = TASK NICE( t a s k ) ;
e x p o r t e d t a s k s t a t s −>s t a t i c p r i o = task −>s t a t i c p r i o ;
i f (TASK INTERACTIVE( t a s k ) )
e x p o r t e d t a s k s t a t s −>i n t e r a c t i v e = ’Y ’ ;
e l s e e x p o r t e d t a s k s t a t s −>i n t e r a c t i v e = ’N ’ ;
e x p o r t e d t a s k s t a t s −>s l e e p a v g = task −>s l e e p a v g /1000000L ;
e x p o r t e d t a s k s t a t s −>bonus =(CURRENT BONUS( t a s k )−MAX BONUS/ 2 ) ;
e x p o r t e d t a s k s t a t s −>p r i o = task −>p r i o ;
e x p o r t e d t a s k s t a t s −>t i m e s l i c e =
j i f f i e s t o m s e c s ( t a s k t i m e s l i c e ( task ) ) ;
e x p o r t e d t a s k s t a t s −>r t p r i o r i t y = task −>r t p r i o r i t y ;
e x p o r t e d t a s k s t a t s −>nvcsw = task −>nvcsw ;
e x p o r t e d t a s k s t a t s −>nivcsw = task −>nivcsw ;
e x p o r t e d t a s k s t a t s −>cnvcsw = task −>s i g n a l −>cnvcsw ;
e x p o r t e d t a s k s t a t s −>c n i v c s w = task −>s i g n a l −>c n i v c s w ;
s t a r t t i m e = ( unsigned long long ) task −>s t a r t t i m e . t v s e c ∗
NSEC PER SEC + task −>s t a r t t i m e . t v n s e c ;
i f ( t a s k s o u t p u t t y p e f o r m a t == 0 ) {
e x p o r t e d t a s k s t a t s −>s t a r t t i m e = n s e c t o c l o c k t ( s t a r t t i m e ) ;
e x p o r t e d t a s k s t a t s −>c p u t i m e =
c p u t i m e t o c l o c k t ( task −>s c h e d i n f o . c p u t i m e ) ;
e x p o r t e d t a s k s t a t s −>r u n d e l a y =
c p u t i m e t o c l o c k t ( task −>s c h e d i n f o . r u n d e l a y ) ;
e x p o r t e d t a s k s t a t s −>l a s t a r r i v a l =
c p u t i m e t o c l o c k t ( j i f f i e s − task −>s c h e d i n f o . l a s t a r r i v a l ) ;
e x p o r t e d t a s k s t a t s −>utime = c p u t i m e t o c l o c k t ( task −>utime ) ;
e x p o r t e d t a s k s t a t s −>s t i m e = c p u t i m e t o c l o c k t ( task −>s t i m e ) ;
e x p o r t e d t a s k s t a t s −>cutime =
c p u t i m e t o c l o c k t ( task −>s i g n a l −>cutime ) ;
e x p o r t e d t a s k s t a t s −>c s t i m e =
c p u t i m e t o c l o c k t ( task −>s i g n a l −>c s t i m e ) ;
e x p o r t e d t a s k s t a t s −>pcnt = task −>s c h e d i n f o . pcnt ;
}
else
{
e x p o r t e d t a s k s t a t s −>s t a r t t i m e = s t a r t t i m e ;
e x p o r t e d t a s k s t a t s −>c p u t i m e =
j i f f i e s t o m s e c s ( task −>s c h e d i n f o . c p u t i m e ) ;
e x p o r t e d t a s k s t a t s −>r u n d e l a y =
j i f f i e s t o m s e c s ( task −>s c h e d i n f o . r u n d e l a y ) ;
e x p o r t e d t a s k s t a t s −>l a s t a r r i v a l =
64
j i f f i e s t o m s e c s ( j i f f i e s − task −>s c h e d i n f o . l a s t a r r i v a l ) ;
e x p o r t e d t a s k s t a t s −>utime = j i f f i e s t o m s e c s ( task −>utime ) ;
e x p o r t e d t a s k s t a t s −>s t i m e = j i f f i e s t o m s e c s ( task −>s t i m e ) ;
e x p o r t e d t a s k s t a t s −>cutime =
j i f f i e s t o m s e c s ( task −>s i g n a l −>cutime ) ;
e x p o r t e d t a s k s t a t s −>c s t i m e =
j i f f i e s t o m s e c s ( task −>s i g n a l −>c s t i m e ) ;
e x p o r t e d t a s k s t a t s −>pcnt = task −>s c h e d i n f o . pcnt ;
}
W celu popbrania statystyki zadania określonego danym numerem pid, należy go najpierw
znaleźć na liscie zadań. Operacja ta realizowana jest za pomocą makra:
struct t a s k s t r u c t ∗ t a s k ;
task = f i n d t a s k b y p i d ( pid ) ;
Kolejną czynnością jaką należy wykonać jest alokacja struktury danych do której zapiszemy dane aby je w poźniejszym czasi eksportować do przestrzeni użytkownika.
e x p o r t e d t a s k s t a t s = v m a l l o c ( s i z e o f ( struct t a s k s t a t s ) ) ;
i f ( ! e x p o r t e d t a s k s t a t s ){
p r i n t k ( ” e x p o r t e d t a s k s t a t s = NULL\n” ) ;
}
Po zaalokowaniu struktury, która służy do przechowywania danych, wypełniamy ją odpowiednimi statystykami. Funkcja posiada również możliwość określenia jednostki czasu:
jiffies lub sekunda.milisekunda. Przebieg procesu generowania oraz pobierania danych
przebiega analogicznie jak w funkcjach przedstawionych wcześniej. Użytkownik za pomocą odpowiedniej opcji programu schedstat wysyła rozkaz ioctl, który jest odbierany
w przestrzeni jądra za pomocą funkcji kernstat ioctl. Rozkaz ten ustawia odpowiednie flagi determinujące rodzaj pobieranych danych przez kernstat read oraz wywołuje przedstawioną powyżej funkcję, której zadaniem jest pobranie odpowiednich statystyk. Następnie
program schedstat rozpoczyna proces odczytu danych z pliku urządzenia /dev/kernstat.
Proces czytania z urządzenia uruchamia funkcję sterownika kernstat read, która jest odpowiedzialna za przekazanie statystyk z przestrzeni jądra do przestrzeni użytkownika. Dane
tak odebrane są odpowiednio formatowane i przedstawiane na konsoli.
6
Działanie planisty wg programu schedstat
W celu wykorzystania programu schedstat do zbierania informacji o działaniu planisty
należy utworzyć urządzenie znakowe /dev/kernstat, przygotować sterownik dla tego urządzenia i skompilować sam program schedstat, czyli wykonać następujące czynności:
65
mknod / dev / k e r n s t a t c 42 0
g c c s c h e d s t a t . c −o s c h e d s t a t
make
insmod k e r n s t a t . ko
Wszystkie te czynności zostały umieszczone w pliku Makefile, więc wystarczy w konsoli
wpisać make w celu przygotowania narzędzia do użytkowania. Usunięcie pliku urządzenia znakowego /dev/kernstat oraz wygenerowanych plików obiektowych wymaga podania
polecenia make clean.
Dostępną listę opcji programu schedstat można uzyskać uruchamiając program z opcją
-h lub –help. Otrzymujemy wówczas następujący komunikat:
k e r n s t a t v e r s i o n 1 . 0 2007 by Jakub Prz yby la kubprzy@gmail . com
Available options :
−h −−h e l p
Display t h i s usage information
−p −−p r i o a r r a y
[ a c t i v e / e x p i r e d ] p r o c e s o r P r i n t s c h e d u l e r runqueue t a b l e s
−r −−runqueue
procesor
Print scheduler s t a t i s t i c s
−t −−t a s k
[ j i f f i e s / time ]
pid
P r i n t t a s k time s t a t i s t i c s
−s −−s c h e d
pid
Print task s t a t i s t i c s
−n −−dela y −count m i c r o s e c count
Delay and count
Aby program schedstat działał poprawnie, musi zostać uruchomiony z prawami superużytkownika. Program realizuje swoje poszczególne zadania poprzez funkcje modułu kernstat ioctl, która sprawdza, czy operacje wykonywane na pliku urządzenia znakowego /dev/kernstat wykonuje użytkownik o odpowiednich przywilejach. Takie podejście zostało
zastosowane z uwagi na konieczność zapewnienia bezpieczeństwa i integralności całego
systemu. W funkcji kernstat ioctl operacja ta jest realizowana przez poniższy fragment
kodu:
case KERSTAT IO GET EXPORTED DATA COUNT:
i f ( ! c a p a b l e (CAP SYS ADMIN ) )
return −EPERM;
....
break ;
6.1
Zadania z tablicy active
Chcąc zobaczyć zadania jakie aktualnie znajdują się w tablicy active pierwszego procesora
należy posłużyć się komendą schedstat -p active 0. Dzięki niej jesteśmy w stanie śledzić nie
tylko średnią liczbę aktualnie wykonujących się zadań, ale również mechanizm zarządzania
czasem dostępu do jednostki centralnej poszczególnych procesów. Wiadomo z wcześniejszej dyskusji, że jeśli planista uzna kwant czasu któregoś z aktualnie wykonujących się
66
zadań za zbyt duży, to dzieli go na mniejsze części i zamiast przekładać zadanie do tablicy
expired wkłada je z powrotem do tablicy active. Aby móc obserwować na bieżąco zmiany
zachodzące w tej tablicy obieramy opóźnienie 100000 mikrosekund oraz liczbę powtórzeń
1000:
s c h e d s t a t −n 100000 1000 −p a c t i v e 0
Statystyki generowane przez tę komendę przedstawiono Tabeli 1. Pokazują one, że różne
zadania choć mają jednakową wartość priorytetu dynamicznego, to charakteryzują się inną
wartością długości kwantu czasu. Ta wartość może ulegać dość drastycznym zmianom,
co widać na przykładzie procesu cpubound, którego kwant zmienia się od 8 do 92 ms.
NR
1
2
3
NR
1
2
3
NR
1
2
NR
1
2
3
NR
1
2
3
NR
1
2
3
NR
1
2
3
4
NR
1
2
3
NAME
active
active
active
NAME
active
active
active
NAME
active
active
NAME
active
active
active
NAME
active
active
active
NAME
active
active
active
NAME
active
active
active
active
NAME
active
active
active
PID
9204
2328
9203
PID
9204
2328
9203
PID
9204
2328
PID
9204
2328
9203
PID
9204
2328
9203
PID
9204
2328
9203
PID
9204
5999
2328
9203
PID
9204
6001
2328
COMM
schedstat
klogd
cpubound
COMM
schedstat
klogd
cpubound
COMM
schedstat
klogd
COMM
schedstat
klogd
cpubound
COMM
schedstat
klogd
cpubound
COMM
schedstat
klogd
cpubound
COMM
schedstat
beagled
klogd
cpubound
COMM
schedstat
beagled
klogd
67
PRIO
115
115
125
PRIO
115
115
125
PRIO
115
115
PRIO
115
115
125
PRIO
115
115
125
PRIO
115
115
125
PRIO
115
115
115
125
PRIO
115
115
115
TIME
8
88
16
TIME
8
88
8
TIME
8
88
TIME
8
88
92
TIME
8
88
84
TIME
8
88
76
TIME
8
16
88
68
TIME
8
44
88
SLICE
SLICE
SLICE
SLICE
SLICE
SLICE
SLICE
SLICE
4
NR
1
2
3
active
NAME
active
active
active
9203
PID
9204
2328
9203
cpubound
COMM
schedstat
klogd
cpubound
125
PRIO
115
115
125
60
TIME SLICE
8
88
52
Tabela 1: Zadania, które nie wyczerpały kwantu czasu.
6.2
Zadania z tablicy expired
Chcąc zobaczyć zadania znajdujące się w tablicy expired należy wydać następującą komendę:
s c h e d s t a t −n 1 1000 −p e x p i r e d 0
W tym przykładzie mała liczba wykonujących się aktualnie procesów spowodowała, że jako opóźnienie przyjęto 1 mikrosekundę, gdyż dopiero przy takiej wartości można zaobserwować procesy, które się pojawiają w tablicy expired. Jak widać z danych przedstawionych
w Tabeli 2, w tablicy expired znajdują się zadania z nowo wyliczonymi kwantami czasu.
NR
1
2
3
NR
1
2
3
NR
1
2
3
NR
1
2
3
NR
1
2
3
NR
NAME
expired
expired
expired
NAME
expired
expired
expired
NAME
expired
expired
expired
NAME
expired
expired
expired
NAME
expired
expired
expired
NAME
PID
18143
18147
18151
PID
18143
18147
18151
PID
18143
18147
18151
PID
18143
18147
18151
PID
18143
18147
18151
PID
COMM
cpubound
cpubound
cpubound
COMM
cpubound
cpubound
cpubound
COMM
cpubound
cpubound
cpubound
COMM
cpubound
cpubound
cpubound
COMM
cpubound
cpubound
cpubound
COMM
68
PRIO
125
125
125
PRIO
125
125
125
PRIO
125
125
125
PRIO
125
125
125
PRIO
125
125
125
PRIO
TIME
100
100
100
TIME
100
100
100
TIME
100
100
100
TIME
100
100
100
TIME
100
100
100
TIME
SLICE
SLICE
SLICE
SLICE
SLICE
SLICE
1
2
3
4
NR
1
2
3
4
NR
1
2
3
4
expired
expired
expired
expired
NAME
expired
expired
expired
expired
NAME
expired
expired
expired
expired
18143
18147
18151
18159
PID
18143
18147
18151
18159
PID
18143
18147
18151
18159
cpubound
cpubound
cpubound
cpubound
COMM
cpubound
cpubound
cpubound
cpubound
COMM
cpubound
cpubound
cpubound
cpubound
125
125
125
125
PRIO
125
125
125
125
PRIO
125
125
125
125
100
100
100
100
TIME SLICE
100
100
100
100
TIME SLICE
100
100
100
100
Tabela 2: Zadania, które wyczerpały kwant czasu.
6.3
Kolejka zadań gotowych do wykonania
W celu zapoznania się ze statystyką kolejki zadań gotowych do wykonania należy skorzystać z opcji -r komendy schedstat, np.
s c h e d s t a t −n 10000 25 −r 0
Statystyki generowane przez tę komendę (Tabela 3) pozwalają śledzić liczbę aktualnie
wykonywanych zadań, określić ile z nich przebywa w stanie TASK UNINTERRUPTIBLE,
a ile czeka na operacje wejścia-wyjścia. Kolejne kolumny Tabeli 3 pokazują liczbę zadań
w tablicach active i expired. Kolumna SWTCHS przedstawia łączna sumę przełączeń
kontekstu. Ostatnie trzy kolumny pokazują zadanie aktualnie się wykonujące wraz z jego
numerem PID, kwantem czasu TSLICE oraz nazwą programu COMM. Ponieważ wiele
rzeczy w jądrze dzieje się bardzo szybko, więc należy wybierać jak najmniejsze opóźnienia,
aby uchwycić wszystkie zmiany.
RN
1
2
2
2
2
UN
1
1
1
1
1
IOW
0
1
1
1
1
ACV
1
2
2
2
2
EXP
0
0
0
0
0
SWTCHS
3644230
3644249
3644267
3644281
3644299
69
PID
9326
9326
9326
9326
9326
COMM
schedstat
schedstat
schedstat
schedstat
schedstat
TSLICE
36
36
36
36
36
2
3
3
3
3
3
4
3
4
3
3
3
3
3
4
4
3
2
2
2
1
1
1
1
1
1
0
1
1
1
1
1
1
1
1
0
1
1
1
2
1
0
1
1
1
1
0
1
1
1
1
1
1
1
1
1
1
1
1
1
2
3
3
3
3
3
4
3
4
3
3
3
3
3
4
4
3
2
2
2
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
3644313
3644331
3644351
3644372
3644384
3644398
3644410
3644423
3644437
3644453
3644465
3644487
3644499
3644513
3644521
3644532
3644554
3644571
3644581
3644595
9326
9326
9326
9326
9326
9326
9326
9326
9326
9326
9326
9326
9326
9326
9326
9326
9326
9326
9326
9326
schedstat
schedstat
schedstat
schedstat
schedstat
schedstat
schedstat
schedstat
schedstat
schedstat
schedstat
schedstat
schedstat
schedstat
schedstat
schedstat
schedstat
schedstat
schedstat
schedstat
36
36
36
36
36
36
36
36
36
36
36
36
36
36
36
36
36
36
36
36
Tabela 3: Statystyki kolejki zadań gotowych do wykonywania.
6.4
Czasowe statystyki zadania
W celu uzyskania statystyk czasowych konkretnego zadania należy użyć komendy:
s c h e d s t a t −n 100000 100 −t time 9714
Statystyki generowane przez tę komendę przedstawione są w Tabel 4. Sens wartości zawartych w dwóch pierwszych kolumnach nie wymaga już wyjaśniania. Następna kolumna,
START, podaje czas (w formacie sekunda.milisekunda ) rozpoczęcia wykonywania się zadania; czas ten jest liczony od chwili załadowania systemu. Kolejne trzy kolumny, tzn.
CPU, DELAY, ARRIV informują o sumarycznym czasie spędzonym przez zadanie na
danym procesorze, czasie spędzonym na oczekiwaniu na przydział oraz czasu, ostatniego
pobytu na procesorze (czasy te wyrażone są w milisekundach). Następne cztery kolumny
UTIME, STIME, CUTIME, CSTIME pokazują czas spędzony przez zadanie w trybie
użytkownika, w trybie jądra oraz kumulatywne czasy zużyte przez procesy potomne śledzonego procesu w trybie użytkownika i w trybie systemowym. W ostatniej kolumnie
70
71
PID
9714
9714
9714
9714
9714
9714
9714
9714
9714
9714
9714
9714
9714
9714
9714
9714
9714
9714
9714
9714
9714
9714
9714
9714
COMM
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
START
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
1867.9184
DELAY
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
ARRIV
0.0080
0.0040
0.0040
0.0040
0.0040
0.0120
0.0040
0.0080
0.0080
0.0080
0.0080
0.0080
0.0080
0.0160
0.0080
0.0080
0.0080
0.0160
0.0080
0.0160
0.0080
0.0080
0.0080
0.0160
UTIME
7.9840
8.0000
8.0160
8.0320
8.0480
8.0600
8.0760
8.0880
8.1000
8.1120
8.1240
8.1360
8.1480
8.1600
8.1720
8.1840
8.1960
8.2080
8.2200
8.2320
8.2440
8.2560
8.2680
8.2800
STIME
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
0.0040
Tabela 4: Zużycie czasu procesora.
CPU
7.9880
8.0040
8.0200
8.0360
8.0520
8.0640
8.0800
8.0920
8.1040
8.1160
8.1280
8.1400
8.1520
8.1640
8.1760
8.1880
8.2000
8.2120
8.2240
8.2360
8.2480
8.2600
8.2720
8.2840
CUTIME
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
CSTIME
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
0.0000
PCNT
712
714
717
719
722
724
727
730
732
734
736
745
747
748
750
752
754
755
757
758
760
763
765
766
PCNT podana jest liczba kwantów czasu jaką dane zadanie zużyło łącznie z kwantami
czasu powstałymi przez podział pierwotnego przedziału czasu.
Program schedstat pozwala na uzyskanie powyższych statystyk nie tylko w sekundach
i milisekundach, ale także w jednostkach jiffies, czyli tyknięciach zegara. Korzystając
z tej formy prezentacji statystyk jesteśmy w stanie zobaczyć, ile taktów zegara faktycznie
wymagała realizacja danego zadania. Chcąc upewnić się o poprawności generowanych
statystyk można zajrzeć do pliku /proc/PID/stat i odszukać odpowiednie wartości.
6.5
Statystyki zadań wg planisty
Korzystając z przełącznika -s możemy otrzymać bardzo szczegółowe informacje, które mają kluczowe znaczenie dla zrozumienia działania mechanizmu szeregowania zadań. Te dane
są używane przez planistę do wyznaczenia kwantu czasu, priorytetu oraz rozstrzygania,
czy proces jest interaktywny. Rozważmy następujący przykład:
s c h e d s t a t −n 10000 25 −s 5828
Wyniki działania powyższej komendy zostały przedstawione w Tabeli 5. Zacznijmy omawianie od kolumny trzeciej, która reprezentuje procesor, na którym wykonuje się dane
zadanie. Jak wiadomo z każdym procesorem związana jest jedna kolejka procesów gotowych do wykonania, zatem w przypadku maszyn wieloprocesorowych mamy do czynienia
z odpowiednio większą ich liczbą. Pole POLICY, to polityka z jaką dane zadanie jest szeregowane (patrz rozdz. 2.1, str.7). Dostępne tryby to SCHED NORMAL,SCHED FIFO,
SCHED RR,SCHED BATCH. Pole S określa stan w jakim w danej chwili znajduje się
zadanie. Następne dwa pola, tzn. NICE i SPRIO, podają wartość priorytetu statycznego
zadania, użytkownik może go zmienić wywołując funkcję nice() z odpowiednią wartością,
która jest relatywna do wartości w polu SPRIO. Wartość pola SPRIO przyjmuje wartości
od 0 do 139, natomiast wartość NICE przyjmuje wartości od -20 do 19 domyślnie 0.
Ostatnie pięć pól zawiera najważniejsze dane. Z punktu widzenia algorytmu szeregowania najważniejszą sprawą jest określenie, czy dane zadanie jest interaktywne, czy też
obliczeniowe. O tym rozstrzyga się na podstawie średniego czasu spania, który zawiera
pole SLPAVG. Klasyfikacja przynależności do grupy procesów interaktywnych stwierdzana jest za pomocą makra omówionego wcześniej w rozdziale poświęconym szeregowaniu,
tzn.:
#define TASK INTERACTIVE( p ) \
( ( p)−> p r i o <= ( p)−> s t a t i c p r i o − DELTA( p ) )
im większy czas spania tym lepiej z punktu widzenia procesu ponieważ otrzymuje
większy kwant czasu.
72
73
PID
5828
5828
5828
5828
5828
5828
5828
5828
5828
5828
5828
5828
5828
5828
5828
5828
5828
5828
5828
5828
5828
5828
5828
5828
5828
COMM
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
Xorg
CPU
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
POLICY
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
S
I
R
R
R
R
I
I
I
I
I
I
I
I
I
I
I
I
I
I
I
I
I
I
I
I
SPRIO
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
I
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
SLPAVG
999
999
999
999
999
999
999
999
999
999
999
999
999
1000
1000
999
999
999
999
999
999
999
999
999
999
BONUS
4
4
4
4
4
4
4
4
4
4
4
4
4
5
5
4
4
4
4
4
4
4
4
4
4
PRIO
116
116
116
116
116
115
115
115
115
115
116
115
115
115
115
115
115
115
115
115
115
116
115
115
115
TS
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
RTP
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
Tabela 5: Statystyki zadania interaktywnego
NICE
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
NVCS
586463
586464
586467
586469
586472
586474
586477
586479
586482
586484
586485
586487
586489
586491
586492
586493
586496
586498
586501
586503
586506
586507
586509
586511
586514
CNVCS
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
NIVCS
126321
126322
126324
126327
126329
126331
126333
126335
126337
126339
126341
126341
126343
126345
126345
126345
126347
126349
126351
126353
126355
126357
126357
126359
126361
CNIVCS
9
9
9
9
9
9
9
9
9
9
9
9
9
9
9
9
9
9
9
9
9
9
9
9
9
Planista działa tak, że zadania interaktywne wynagradza, a zorientowane obliczeniowo karze. Kary i nagrody, to wartości z przedziału [-5,5]; wartość dodatnia do nagroda,
a ujemna – kara. Widać, że zadanie Xorg, które zostało zakwalifikowane do zadań interaktywnych, dostało nagrodę (BONUS ) w wysokości 4 punktów. Wartość z pola BONUS ma
kluczowe znaczenie dla wyznaczenia wartości w polu PRIO, gdyż to na jego podstawie
oraz w oparciu o wartość z pola SPRIO wyliczana jest wartość priorytetu dynamicznego PRIO. Pole TS podaje wielkość kwantu czasu, który jest obliczany na podstawie
priorytetu statycznego. Trzeba jednak pamiętać, że niekoniecznie ten kwant zostanie zużyty w całości, gdyż może zostać podzielony na mniejsze kawałki i wykorzystany w kilku
cyklach procesora. Pole RTPRIO, to numer kolejki koncepcyjnej, czyli numer indeksu
w tablicy zadań gotowych do wykonania (tablicy active); wartość ta ma tylko znaczenie
dla procesów rzeczywistych. Ostatnie cztery pola podają liczbę, odpowiednio, dobrowolnych i przymusowych zmian kontekstu oraz sumy przełączeń ich, i ich zadań potomnych.
Przedstawiony powyżej przykład dotyczy zadania interaktywnego oraz heurystyk, które
wpływają na odpowiednie zachowanie się wcześniej wspomnianych algorytmów.
W kolejnych przykładach przedstawione jest zachowanie procesu normalnego nieinteraktywnego (Tabela 6) oraz procesu rzeczywistego (Tabela 7). Porównując te dwa rodzaje
zadań należy zwrócić uwagę na wartości pól STATE, IACTV, SLPAVG, BONUS, PRIO,
gdyż pokazują one wyraźnie różnice w zachowywaniu się obu rodzajów zadań oraz różnice
w traktowaniu ich przez planistę.
W przypadku zadania czasu rzeczywistego (Tabela 7) należy zwrócić uwagę na wartości
pól POLICY oraz SLPAVG. Pola te pokazują wyraźnie, że takie zadanie nigdy nie wędruje
do tablicy expired, lecz cały czas się wykonuje, o czym świadczy wartość pola SLPAVG.
7
Słownik podstawowych pojęć
Funkcje systemowe Proces użytkownika zleca jądru wykonanie pewnych czynności (np.
operacji we-wy) w jego imieniu. Wywołuje w tym celu funkcje systemowe. Do wykonania funkcji jądra może dojść w następstwie żądań zgłaszanych na dwa możliwe
sposoby:
• proces wykonujący się w trybie użytkownika wywołuje funkcję systemową lub
zgłasza wyjątek (przerwanie wewnętrzne);
• urządzenie zewnętrzne zgłasza sygnał do programowalnego kontrolera przerwań
PIC, a odpowiednie przerwania nie są zamaskowane (przerwania zewnętrzne).
Kontekst procesu to środowisko, które obejmuje zawartość rejestrów ogólnych i sterujących procesora. Te rejestry to między innymi:
74
75
PID
10568
10568
10568
10568
10568
10568
10568
10568
10568
10568
10568
10568
10568
10568
10568
10568
10568
10568
10568
10568
10568
10568
10568
10568
10568
COMM
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
cpubound
CPU
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
POLICY
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NORMAL
NICE
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
SPRIO
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
I
N
N
N
N
N
N
N
N
N
N
N
N
N
N
N
N
N
N
N
N
N
N
N
N
N
SLPAVG
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
BONUS
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
PRIO
125
125
125
125
125
125
125
125
125
125
125
125
125
125
125
125
125
125
125
125
125
125
125
125
125
TS
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
Tabela 6: Statystyki zadania nieinteraktywnego
S
R
R
R
R
R
R
R
R
R
R
R
R
R
R
R
R
R
R
R
R
R
R
R
R
R
RTP
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
NVCS
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3
CNVCS
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
NIVCS
1069
1072
1075
1076
1082
1083
1085
1086
1088
1092
1094
1096
1098
1103
1106
1107
1110
1115
1117
1118
1121
1125
1128
1130
1137
CNIVCS
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
76
PID
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
COMM
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
migration/0
CPU
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
POLICY
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
FIFO
NICE
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
SPRIO
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
120
I
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
Y
SLPAVG
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
BONUS
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
-5
PRIO
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
TS
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
100
RTP
99
99
99
99
99
99
99
99
99
99
99
99
99
99
99
99
99
99
99
99
99
99
99
99
99
Tabela 7: Statystyki zadania czasu rzeczywistego
S
I
I
I
I
I
I
I
I
I
I
I
I
I
I
I
I
I
I
I
I
I
I
I
I
I
NVCS
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
2
CNVCS
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
NIVCS
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
CNIVCS
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
• Licznik rozkazów (PC, ang.program counter );
• Wskaźnik wierzchołka stosu (SP, ang.stack pointer );
• Słowo stanu procesora (PSW, ang. processor status word );
• Rejestry zarządzania pamięcią.
Kontekst wykonania Funkcje jądra mogą się wykonywać albo w kontekście procesu,
albo w kontekście systemu.
• W kontekście procesu jądro działa w imieniu bieżącego procesu (np. wykonując
funkcję systemową), może sięgać do przestrzeni adresowej i stosu procesu. Może
także zablokować bieżący proces, jeśli musi on poczekać na zasoby;
• Czasami jądro musi wykonać pewne ogólnosystemowe czynności, jak np. przeliczenie priorytetów lub obsługa przerwania zewnętrznego. Takie czynności nie
są wykonywane w imieniu żadnego konkretnego procesu i dlatego odbywają
się w kontekście systemu. W kontekście systemu jądro nie sięga do przestrzeni
adresowej czy stosu bieżącego procesu, nie może się również zablokować.
Przestrzeń adresowa procesu to zbiór adresów pamięci, do których proces może odwoływać się podczas swojego wykonywania.
Przestrzeń adresowa jądra to kod i struktury danych jądra. Są one odwzorowywane w przestrzeń adresową każdego procesu, ale dostęp do nich jest możliwy jedynie w trybie systemowym. Ponieważ jest tylko jedno jądro, więc wszystkie procesy
współdzielą pojedynczą przestrzeń adresową jądra. Jądro ma bezpośredni dostęp
do przestrzeni adresowej bieżącego procesu.
Wielozadaniowość to mechanizm, w którym wiele procesów może działać równocześnie
i niezależnie od siebie. Jądro musi dynamicznie przydzielać zasoby niezbędne procesom do działania oraz zapewniać bezpieczeństwo. Korzysta w tym celu ze wsparcia
sprzętowego, mianowicie takiego jak:
• dwutrybowość pracy procesora: tryb użytkownika i tryb systemowy;
• instrukcje uprzywilejowane i ochrona pamięci;
• mechanizm przerwań i wyjątków.
Wielowejściowość Jądro Linuksa jest wielowejściowe (ang. reentrant), co oznacza, że może współbieżnie obsługiwać różne procesy. Zatem każdy proces potrzebuje własnego
stosu jądra, do śledzenia sekwencji wywołań funkcji podczas wykonania w trybie
77
jądra. Stos jądra jest zwykle zaalokowany w przestrzeni adresowej procesu, ale nie
ma do niego dostępu w trybie pracy użytkownika.
Przełączenie kontekstu Polega na zapamiętaniu kontekstu bieżącego procesu w strukturze stanowiącej część przestrzeni adresowej procesu i załadowanie do rejestrów
procesora kontekstu innego procesu. Czas przełączenia kontekstu jest narzutem
na działanie systemu i zależy od wsparcia ze strony sprzętu (rzędu 1 mikrosekundy).
Do przeplotu obsługi żądań pochodzących od różnych procesów i urządzeń dochodzi
w następujący sposób:
• w wyniku wykonania procedury schedule następuje przełączenie kontekstu
i przejście od obsługi jednego procesu do obsługi innego,
• nadejście niezamaskowanego przerwania powoduje zachowanie bieżącego kontekstu i przejście do wykonania procedury obsługi tego przerwania.
• kod użytkownika wykonuje się w trybie użytkownika i w kontekście procesu,
może sięgać jedynie do przestrzeni adresowej procesu;
• funkcje systemowe i wyjątki (np. dzielenie przez zero lub naruszenie ochrony
pamięci) są obsługiwane w trybie systemowym, ale w kontekście procesu, mają
dostęp do przestrzeni adresowej procesu i systemu;
• przerwania są obsługiwane w trybie systemowym w kontekście systemu z dostępem jedynie do przestrzeni systemowej.
78
Literatura
[1] R. Love. Linux Kernel Development. Novell Press, 2005.
[2] Tigran Aivazian. Linux Kernel 2.4 Internals. O’Reilly, 2002.
[3] O. Poemrantz P.J. Salzman. The Linux Kernel Module Programming Guide. O’Reilly,
2001.
[4] M. Ceasti D. Bovet. Understanding the Linux Kernel 2 edition. O’Reilly, 2004.
[5] G. Kroah-Hartman A. Rubini, J. Corbet. Linux Device Drivers, 3rd edition. O’Reilly,
2005.
[6] M. Bach. Budowa systemu operacyjnego Unix. WNT, 1995.
[7] Alex Samuel Mark Mitchell, Jeffrey Oldham. Advanced Linux Programming. O’Reilly,
2001.
79