W każdym systemie operacyjnym istnieje struktura, która przechowuje dane niezbędne do działania procesu takie jak: stan procesu (aktywny, gotowy, wstrzymany), stan procesora (wartości rejestrów, w tym licznika rozkazów i wskaźnika stosu), przydzielona pamięć, informacje potrzebne do szeregowania, otwarte pliki, nieobsłużone sygnały i wiele innych. Ze względu na modularną budowę w systemie MINIX dane te są podzielone – dana składowa systemu przechowuje tę część informacji, której potrzebuje:
mproc
serwera pm
zawiera dane potrzebne do zarządzania procesami, np. pid, uid procesu (/usr/src/minix/servser/pm/mproc.h
)
fproc
serwera vfs
przechowuje informacje związane z systemem plików, np. tablicę deskryptorów procesu (/usr/src/minix/servser/vfs/fproc.h
)
proc
w strukturach jądra zawiera informacje niezbędne przy zmianie kontekstu i szeregowaniu (/usr/src/minix/kernel/proc.h
)
W ostatniej z wymienionych struktur spróbuj odnaleźć pola zawierające:
Za szeregowanie w MINIX-ie odpowiadają dwie składowe systemu.
Pierwsza z nich to segment działający w jądrze systemu (kernel
),
odpowiedzialny za przełączanie zadań.
Drugą jest serwer sched
działający w przestrzeni użytkownika.
Taki podział umożliwia zmianę algorytmu szeregowania bez modyfikowania jądra.
W systemie może być uruchomiony więcej niż jeden serwer sched
.
Każdy z procesów jest zarządzany przez co najwyżej jeden z nich.
Serwer otrzymuje od jądra informację, kiedy zarządzany przez niego proces
wykorzystał swój kwant czasu.
Każdy z procesów ma zdefiniowany priorytet, który przyjmuje wartość od 0 do 15
(im mniejsza wartość tym wyższy priorytet).
Procesy o tym samym priorytecie umieszczane są we wspólnej kolejce.
W kolejce o indeksie 0 działają procesy jądra (zegar i zadanie obsługujące
wywołania systemowe), a w kolejce o indeksie 15 znajduje się tylko proces idle
.
Rys 1. Operating Systems Design and Implementation, Third Edition A. S. Tanenbaum, A. S. Woodhull (Fig. 2-43)
W każdej z kolejek umieszczone są tylko procesy będące w stanie gotowym. Procesy wstrzymane (np. za pomocą sygnału) są usuwane z kolejki.
W obrębie jednej kolejki zastosowany jest algorytm karuzelowy (ang. Round Robin).
W momencie, w którym działający proces zużyje swój kwant czasu, następuje wywłaszczenie.
Następnie, jeśli proces nie ma zarządzającego serwera sched
, to jest on
przenoszony na koniec swojej kolejki i otrzymuje nowy kwant czasu. W
przeciwnym wypadku odpowiedni serwer otrzymuje wiadomość o
wykorzystaniu kwantu czasu przez proces i decyduje o przydziale nowego
kwantu i o nowym priorytecie procesu.
Proces, który został uśpiony w trakcie działania, po pobudce wkładany jest na początek kolejki, ale nie jest przydzielany mu nowy kwant – pozostaje mu dokładnie tyle czasu procesora, ile miał w momencie uśpienia.
Algorytm wyboru procesu do wykonania jest bardzo prosty. Polega na znalezieniu
niepustej kolejki o najniższym indeksie (najwyższym priorytecie) i wybraniu
pierwszego zadania z tej kolejki. Ze względu na to, że proces idle
zawsze jest w
stanie gotowym, w przypadku braku procesów do wykonania procesor zostanie
przekazany procesowi idle
.
Jakie zagrożenia niesie ze sobą taka implementacja,
jeśli nie działają żadne serwery sched
w systemie?
sched
Domyślny serwer działa w następujący sposób: jeśli proces wykorzysta kwant czasu, to jest mu przydzielany nowy kwant, a wartość jego priorytetu jest zwiększana o jeden. Co ustalony okres czasu (domyślnie 5 sekund) następuje obniżenie wartości priorytetu o jeden wszystkich procesów, których priorytet jest większy od początkowego.
sched
i pm
Jądro przesyła do serwera sched
tylko jeden typ wiadomości:
SCHEDULING_NO_QUANTUM
– gdy proces zarządzany przez serwer
wykorzystał swój kwant czasu.
Oprócz tego serwer sched
otrzymuje polecenia od serwera pm
:
SCHEDULING_START
– gdy serwer ma zacząć zarządzać nowym procesem (wykorzystywane przy uruchamianiu serwerów)
SCHEDULING_INHERIT
– gdy serwer ma zacząć zarządzać nowym procesem, który ma dziedziczyć po procesie rodzicu (wykorzystywane przy tworzeniu nowego procesu poprzez fork()
)
SCHEDULING_STOP
– gdy serwer ma przestać zarządzać procesem (np. w momencie zakończenia procesu)
SCHEDULING_SET_NICE
– gdy serwer ma zmienić politykę zarządzania procesem
Serwer sched
komunikuje się z jądrem poprzez wywołania systemowe
sys_schedctl()
i sys_schedule()
zadeklarowane w /usr/include/minix/syslib.h
.
int sys_schedctl(unsigned flags, endpoint_t proc_ep, unsigned priority, unsigned quantum, unsigned cpu);
Za pomocą tej funkcji serwer może przejąć zarządzanie lub zrzec się praw do zarządzania procesem.
Jeśli zmienna flags
jest ustawiona na SCHEDCTL_FLAG_KERNEL
, to serwer
zrzeka się zarządzania procesem proc_ep
, ustawiając jego
kwant na quantum
, a priorytet na priority
(zarządzanie przejmuje jądro).
Jeśli zmienna flags
jest równa 0, to serwer przejmuje
zarządzanie procesem proc_ep
, a wartości argumentów
priority
i quantum
są ignorowane.
int sys_schedule(endpoint_t proc_ep, unsigned priority, unsigned quantum, unsigned_cpu);
Jeśli serwer zarządza procesem proc_ep
, to wywołanie ustawia
priorytet procesu na priority
i przydziela mu kwant czasu równy quantum
.
Następujące pola w strukturze proc
są istotne dla szeregowania:
short p_rts_flags
– flagi określające powód wstrzymania procesu; równe 0, gdy proces jest gotowy do wykonania
char p_priority
– priorytet procesu
u64_t p_cpu_time_left
– pozostały czas procesora
struct proc *p_scheduler
– serwer zarządzający procesem (równe NULL
, jeśli proces nie ma serwera)
struct proc *p_nextready
– następny proces w kolejce
Oprócz tego w pliku \usr\src\minix\kernel\cpulocals.h
zdefiniowane są tablice
struct proc *run_q_head[NR_SCHED_QUEUES]
struct proc *run_q_tail[NR_SCHED_QUEUES]
wskaźników do początków i do końców kolejek z gotowymi procesami. Pojedyncza kolejka procesów gotowych jest jednokierunkowa. Dzięki istnieniu tablicy wskaźników run_q_tail
dodanie procesu na końcu kolejki jest szybkie.
Podstawowa część algorytmu szeregowania, czyli wybór
następnego procesu do wykonania, zawarta jest w funkcji
pick_proc()
zdefiniowanej w /usr/src/minix/kernel/proc.c
/*===========================================================================* * pick_proc * *===========================================================================*/ static struct proc * pick_proc(void) { /* Decide who to run now. A new process is selected an returned. * When a billable process is selected, record it in 'bill_ptr', so that the * clock task can tell who to bill for system time. * * This function always uses the run queues of the local cpu! */ register struct proc *rp; /* process to run */ struct proc **rdy_head; int q; /* iterate over queues */ /* Check each of the scheduling queues for ready processes. The number of * queues is defined in proc.h, and priorities are set in the task table. * If there are no processes ready to run, return NULL. */ rdy_head = get_cpulocal_var(run_q_head); for (q = 0; q < NR_SCHED_QUEUES; q++) { if (!(rp = rdy_head[q])) { TRACE(VF_PICKPROC, printf("cpu %d queue %d empty\n", cpuid, q);); continue; } assert(proc_is_runnable(rp)); if (priv(rp)->s_flags & BILLABLE) get_cpulocal_var(bill_ptr) = rp; /* bill for system time */ return rp; } return NULL; }
Funkcja pick_proc()
jest wywoływana przez, zdefiniowaną w tym samym
pliku, funkcję switch_to_user()
, która jest odpowiedzialna za
przełączenie kontekstu.
/*===========================================================================* * switch_to_user * *===========================================================================*/ void switch_to_user(void) { /* This function is called an instant before proc_ptr is * to be scheduled again. */ struct proc * p; #ifdef CONFIG_SMP int tlb_must_refresh = 0; #endif p = get_cpulocal_var(proc_ptr); /* * if the current process is still runnable check the misc flags and let * it run unless it becomes not runnable in the meantime */ if (proc_is_runnable(p)) goto check_misc_flags; /* * if a process becomes not runnable while handling the misc flags, we * need to pick a new one here and start from scratch. Also if the * current process wasn't runnable, we pick a new one here */ not_runnable_pick_new: if (proc_is_preempted(p)) { p->p_rts_flags &= ~RTS_PREEMPTED; if (proc_is_runnable(p)) { if (p->p_cpu_time_left) enqueue_head(p); else enqueue(p); } } /* * if we have no process to run, set IDLE as the current process for * time accounting and put the cpu in an idle state. After the next * timer interrupt the execution resumes here and we can pick another * process. If there is still nothing runnable we "schedule" IDLE again */ while (!(p = pick_proc())) { idle(); } /* update the global variable */ get_cpulocal_var(proc_ptr) = p; #ifdef CONFIG_SMP if (p->p_misc_flags & MF_FLUSH_TLB && get_cpulocal_var(ptproc) == p) tlb_must_refresh = 1; #endif switch_address_space(p); check_misc_flags: /* ... */ /* * check the quantum left before it runs again. We must do it only here * as we are sure that a possible out-of-quantum message to the * scheduler will not collide with the regular ipc */ if (!p->p_cpu_time_left) proc_no_time(p); /* ... */ }
Jeśli proces nie wykorzystał przydzielonego mu kwantu czasu (p_cpu_time_left
jest większe od 0), to jest umieszczany na początku kolejki,
w przeciwnym przypadku – na końcu.
Jeśli funkcja pick_proc()
zwróci NULL
, to znaczy że aktualnie nie ma
gotowego procesu do wykonania i wykonywany jest proces idle()
.
Kiedy proces wykorzysta przydzielony kwant, jądro powiadamia odpowiedni serwer:
proc_no_time()
wywołuje notify_scheduler()
, co
skutkuje przekazaniem do serwera komunikatu SCHEDULING_NO_QUANTUM
.
Oprócz wyboru nowego procesu do działania i przełączania kontekstu, jądro
musi implementować wspomniane wcześniej wywołania systemowe.
Są one zdefiniowane w plikach
/usr/src/kernel/system/do_schedctl.c
i /usr/src/kernel/system/do_schedule.c
.
Domyślny serwer działa w nieskończonej pętli, odpowiednio przetwarzając
otrzymywane wiadomości omówione wyżej.
Wszelkie informacje na temat procesów (maksymalny priorytet,
aktualny priorytet itp.) serwera sched
przechowywane są w pomocniczej tablicy schedproc
zdefiniowanej w pliku
/usr/src/minix/servers/sched/schedproc.h
.
Wszystkie funkcje przetwarzające otrzymywane wiadomości są zdefiniowane w
/usr/src/minix/servers/sched/schedule.c
.
Ostatnim elementem wymagającym omówienia jest mechanizm zwiększania priorytetów zadań co określony czas (domyślnie 5 sekund). Jest on zaimplementowany z użyciem budzika, dzięki któremu odpowiednia metoda zostanie wywołana po upłynięciu żądanego czasu.
Początkowo budzik jest ustawiany w funkcji init_scheduling()
wywoływanej
zaraz po uruchomieniu serwera. Po upłynięciu odpowiedniego
czasu wołana jest metoda balance_queues()
, która jest odpowiedzialna za
podniesienie o 1 priorytetów wszystkich procesów (czyli zmniejszenie ich wartości o 1)
oraz za ponowne ustawienie budzika.
/*===========================================================================* * start_scheduling * *===========================================================================*/ void init_scheduling(void) { balance_timeout = BALANCE_TIMEOUT * sys_hz(); init_timer(&sched_timer); set_timer(&sched_timer, balance_timeout, balance_queues, 0); } /*===========================================================================* * balance_queues * *===========================================================================*/ /* This function in called every 100 ticks to rebalance the queues. The current * scheduler bumps processes down one priority when ever they run out of * quantum. This function will find all proccesses that have been bumped down, * and pulls them back up. This default policy will soon be changed. */ static void balance_queues(minix_timer_t *tp) { struct schedproc *rmp; int proc_nr; for (proc_nr = 0, rmp = schedproc; proc_nr < NR_PROCS; proc_nr++, rmp++) { if (rmp->flags & IN_USE) { if (rmp->priority > rmp->max_priority) { rmp->priority -= 1; /* increase priority */ schedule_process_local(rmp); } } } set_timer(&sched_timer, balance_timeout, balance_queues, 0); }
Zaimplementuj algorytm szeregowania, który:
Zastanów się, którą część tego algorytmu ma wykonywać jądro, a którą serwer.
W katalogu testy
znajdziesz źródła trzech programów:
scanfiles.c
, który wykonuje dużą ilość operacji wejścia-wyjścia
primes.c
, który wykonuje dużą ilość obliczeń
scanprimes.c
, który na zmianę wykonuje dużą ilość operacji wejścia-wyjścia i dużą ilość obliczeń.
Zbadaj, jak wraz ze zmianą algorytmu zmienia się rzeczywisty czas wykonania programów scanfiles
i scanprimes
, jeśli w tle działa wiele instancji programu primes
.
Przykładowy zestaw testów znajduje się w skrypcie testuj.sh
.