1. Procesy w systemie MINIX

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:

  • znana nam już tablica mproc serwera pm zawiera dane potrzebne do zarządzania procesami, np. pid, uid procesu (/usr/src/minix/servser/pm/mproc.h)
  • tablica fproc serwera vfs przechowuje informacje związane z systemem plików, np. tablicę deskryptorów procesu (/usr/src/minix/servser/vfs/fproc.h)
  • tablica proc w strukturach jądra zawiera informacje niezbędne przy zmianie kontekstu i szeregowaniu (/usr/src/minix/kernel/proc.h)

Zadanie H1

W ostatniej z wymienionych struktur spróbuj odnaleźć pola zawierające:

  • aktualny stan procesu
  • pozostały czas procesora
  • wielkość kwantu
  • następny proces w kolejce procesów gotowych
  • nieobsłużone sygnały

2. Szeregowanie procesów w systemie MINIX

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.

2.1. Szeregowania w jądrze

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.

sched_queues.png

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.

Zadanie H2

Jakie zagrożenia niesie ze sobą taka implementacja, jeśli nie działają żadne serwery sched w systemie?

2.2. Domyślny serwer 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.

2.3. Komunikacja między jądrem a serwerem 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.

2.4. Implementacja szeregowania w jądrze

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.

2.5. Implementacja domyślnego serwera

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);
}

Zadanie H3

Zaimplementuj algorytm szeregowania, który:

  • bazuje na dotychczasowym algorytmie z tablicą kolejek procesów
  • początkowo procesy dostają kwant czasu na swoim maksymalnym priorytecie
  • kiedy skończy im się kwant, dostają nowy, ale na niższym priorytecie
  • śpiące procesy zachowują swoje kwanty i jeżeli się obudzą w tej samej epoce, mogą wykorzystać je do końca
  • co ustalony czas epoka jest kończona, a każdy z procesów otrzymuje nowy kwant czasu na swoim maksymalnym priorytecie
  • epoka trwa 5 sekund

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.

3. Do poczytania