Szeregowanie procesów w linuxie - trendy rozwojowe

Autorzy: Łukasz Chmielewski, Daniel Hans, Mateusz Łupiński

Wstęp

Algorytm szeregowania (ang. scheduler - planista) to algorytm rozwiązujący jedno z najważniejszych zagadnień informatyki - jak rozdzielić czas procesora i dostęp do innych zasobów pomiędzy zadania, które w praktyce zwykle o te zasoby konkurują. - Wikipedia

Szeregowanie procesów jest jedną z najważniejszych cech systemu operacyjnego.

To ono właśnie zapewnia wieloprogramowość: poprzez odpowiednio częstą zmianę aktualnie wykonywanego procesu użytkownik systemu ma wrażenie, że wszystkie wykonują się w tym samym czasie.
Scheduler, czyli część systemu odpowiedzialna za to szeregowanie musi więc być zaprojektowana z wielką starannością. Wkracza on bowiem do akcji za każdym razem kiedy jakiś inny proces traci procesor, kończy się lub pojawia się proces o wyższym priorytecie. Jest jasne, że dzieje się to bardzo często, a więc pojedyńcze przeszeregowanie musi odbywać się bardzo szybko.

Nie istnieje optymalny algorytm szeregujący

Co więcej trudno byłoby nawet ustalić do dokładnie miałaby znaczyć optymalność dla szeregowania procesów. Względem czego? Łącznego czasu wykonania wszystkich procesów? Średniego czasu wykonania pojedyńczego procesu? Minimalizacja takich współczynników jest niemożliwa do wykonania chociażby dlatego, że procesy napływają dynamicznie.
Wymyślane są więc (z różnym skutkiem) rozmaite herystyki. Projektanci systemów operacyjnych kładą nacisk na to, aby działały one jak najbardziej wydajnie:

Scheduler w początkowych wersjach systemu Linux

Początkowa niezmienniczość

Chociaż sposób szeregowania jest tak bardzo ważnym aspektem systemu, to w systemie Linux przez bardzo długi okres ten element nie był poważniej modyfikowany. Generalnie od samego powstania systemu (mam tu na myśli wydanie jądra 1.0, a nawet wcześniej),do wersji 2.4 idea, na której opierało się działanie schedulera pozostawała niezmieniona (co jakiś czas zmieniane były jedynie szczegóły imlplementacyje niektórych procedur).

Poniżej zostanie omówiony scheduler z wersji jądra 2.4.

Idea działania

Jest bardzo prosta. Zawsze kiedy proces traci procesor uruchamiany jest scheduler, który dla każdego z aktualnych procesów oblicza jego dynamiczny priorytet w danej chwili i wybiera ten, który uzna za najlepszy.

Podział na epoki

Działanie algorytmu szeregującego podzielone jest na epoki. Na początku każdej z nich moduł szeregujący przydziela każdemu procesowi liczbę taktów procesora wynikającą z jego priorytetu. Epoka kończy się, gdy wszystkie procesy wykorzystają już swoje takty lub dobrowolnie zrezygnują z procesora.

Klasy procesów

Procesy podzielone są na trzy klasy wobec których moduł szeregujący stosuje różne strategie szeregowania. Wyróżnia się:
Dla procesów czasu rzeczywistego: Dla procesów zwykłych: Procesy czasu rzeczywistego to takie, które będą wykonywały się przed wszystkimi procesami zwykłymi. Również mają swoją pulę priorytetów (od 1 do 99). Proces SCHED_FIFO jak już dostanie procesor, to wykonuje się aż do zakończenia, chyba że: Jak widać mechanizm ten jest dobry, ale jeżeli jest kilka procesów o takim samym priorytecie i wśród nich proces, który zaczął się wykonywać w nieskończoność, to reszta procesów zostaje zagłodzona (nigdy nie otrzyma procesowa).
Aby temu zaradzić można używać procesów SCHED_RR (round-robin), które różnią się tym, że proces czasu rzeczywistego, który dostaje procesor, otrzymuje jedynie kwant czasu i po jego wykorzystaniu zostaje przeniesiony na koniec kolejki procesów oczekujących o takim samym priorytecie. W tej strategii zostanie on ponownie wybrany tylko wtedy, gdy jest jedynym procesem o takim priorytecie.

Struktury procesu wykorzystywane przez algorytm szeregujący

W strukturze task_struct znajduje się kilka pól wykorzystywanych przez algorytm szeregujący. Najważniejsze z nich to:

Funkcja schedule()

Najistotniejszą ze względu na sposób działania algorytmu szeregującego jest funkcja schedule().
Może być ona wywołana na dwa sposoby:
Funkcja schedule() ma za zadanie na podstawie ostatnio wykonywanego zadania, ktore zapamietuje na zmiennej prev, wyznaczenie kolejnego, które zostanie zapisane na zmienną next. Sam algorytm szeregowania działa w skrócie następująco:
	if (prev->policy == SCHED_RR)
	    if (!prev->counter) {
	        prev->counter = NICE_TO_TICKS(prev->nice);
	        move_last_runqueue(prev);
	        }
	 
	switch (prev->state) {
	    case TASK_INTERRUPTIBLE:
	        if (signal_pending(prev)) {
	            prev->state = TASK_RUNNING;
	            break;
	        }
	    default:
	        del_from_runqueue(prev);
	    case TASK_RUNNING:
	} 
	 
	 prev->need_resched = 0;
	 
	next = idle_task(this_cpu);
	c = -1000;
	if (prev->state == TASK_RUNNING) {	 
	    c = goodness(prev, this_cpu, prev->active_mm);
	    next = prev;
	}	 
	 
	list_for_each(tmp, &runqueue_head) {
	   p = list_entry(tmp, struct task_struct, run_list);
	   if (can_schedule(p, this_cpu)) {
	        int weight = goodness(p, this_cpu, prev->active_mm);
	        if (weight > c)
	            c = weight, next = p;
		}
	}
	 
	if (!c)
	{
	    struct task_struct *p;
	    for_each_task(p)
	        p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice);
	}
	 
	switch_to(prev, next, prev);	
	
	if (current->need_resched)
	    goto poczatek;
	return;	 
	 

Funkcja goodness()

Jest to bardzo ważna funkcja, która dla danego procesu p ublicza jego priorytet w danej chwili. Również przez cały czas istnienia omawianego schedulera doczekała się jedynie drobnych modyfikacji.
Jako wynik rezultat ona wartość całokowtoliczbową. Im jest ona większa, tym proces jest bardziej "odpowiedni" do tego, aby dostać procesor.
Wartość wynikowa obliczana jest według następującego schematu:
Kod tej funkcji jest tak samo krótki i prosty jak zasada jej działania.
	static inline int goodness(struct task_struct * p, int this_cpu, struct mm_struct *this_mm)
	{
	    int weight;
	    weight = -1;

	/* Przypadek, że proces sam oddał procesor: ustawiona flaga SCHED_YIELD */
	    if (p->policy & SCHED_YIELD)
		    goto out;

	    if (p->policy == SCHED_OTHER) {
		    weight = p->counter;
		    if (!weight)
			    goto out;
			
	/* Przypadek dla wielu procesorów */ 			
	#ifdef CONFIG_SMP
		    if (p->processor == this_cpu)
			    weight += PROC_CHANGE_PENALTY;
	#endif

		    if (p->mm == this_mm || !p->mm)
			    weight += 1;
		    weight += 20 - p->nice;
		    goto out;
	    }

	/* Przypadek dla procesów czasu rzeczywistego */
	    weight = 1000 + p->rt_priority;
	out:
	return weight;
	}
	

Podsumowanie schedulera z wersji jądra 2.4

Scheduler stosowany w systemie operacyjnym Linux aż do wersji jądra 2.4 posiadał niewątpliwie jedną fundamentalną zaletę: był prosty.
Nie wystarczło to jednak do zaspokojenia potrzeb użytkowników, ponieważ posiadał on również wiele istotnych wad i ograniczeń.

Opisane problemy wskazywane były w różnego rodzaju pracach oraz artykułach naukowych.
Do roku 2001 ukazało się bardzo dużo takowych, z których warto zwrócić uwagę np. na "Enhancing Linux Scheduler Scalability", którego autorem jest Mike Kravetz z IBM Linux Technology Center. Podane wady schedulera z wersji jądra 2.4 zostały poparte testami:

Measurements using Java benchmarks show that the scheduler can consume up to 25% of the total system time for workloads with a large number of tasks. Another study has observed run queue lock contention to be as high as 75% on a 32-way SMP.

Co więcej, praca ta nie tylko wskazywała błędy, ale też podsuwała gotowe rozwiązania w celu ich wyeliminowania lub przynajmniej minimalizacji. Opisany jest: Wobec wszystkich wymienionych powyżej wad (a także na pewno innych, które nie zostały wymienione) jasne było, że musi pojawić się nowa wersja modułu szeregującego procesy. Szczególnie duże znaczenie miał przy tym fakt, iż Linux stawał się coraz bardziej popularnym systemem dla komuterów domowych, które jak wiadomo mają inne, bardziej interaktywne typy procesów, które muszą być wykonywane płynnie. Dla przeciętnego użtykownika komputera osobistego najważniejsze znaczenie mają m.in.: płynnie dziłające środowisko graficzne oraz płynnie odtwarzające się filmy i muzyka.
Jakikolwiek nowy scheduler musiał być ukłonem także w strone takich właśnie użytkowników.

Zupełnie nowy scheduler

W styczniu 2002 roku ogłoszony został zupełnie nowy scheduler nazwany O(1). Jego autorem jest węgier Ingo Molnar. Z powodu swoich niewątpliwych zalet, bardzo szybko został wprowadzony do oficjalnej linii jądra 2.5. Co więcej, okazał się on tak dobry, że został wprowadzony do kolejnej stabilnej wersji jądra. Pojawił się od wydania 2.6.8.
Scheduler ten został szczegółowo omówiony na wykładzie.
Jego najważniejsze cechy to:

Oczywiście rozwiązywał on większość problemów, z którymi nie radził sobie poprzedni scheduler. Jako dowód wystarczy spojrzeć na wyniki testów (z wykładu):

Oraz uzyskany efekt dla architektur wieloprocesorowych:

Powstanie scheduler'ów sprawiedliwych

Źródło: lwn.net, ck.wikia.com

Po co?

Szeregowanie procesów jest jednym z tych elementów systemu, nad którym praca nigdy się nie kończy. Programiści mogą pracować na schedulerem stale go usprawniając, jednak zawsze istnieją przypadki, w których planista nie działa tak dobrze jak by mógl, lub tak dobrze, jak życzyłby sobie tego użytkownik. Jeśli chodzi o systemy interaktywne, dla użytkownika najważniejszy jest jak najkrótszy czas, w jakim system reaguje na jego działanie. Aby zapewnić tą cechę ostatni planista, zwany O(1) wyposażany był w coraz to nowe heurystyki, które miały za zadanie wykryć które procesy są rzeczywiście interaktywne i zapewnić im wyższy priorytet w użyciu procesora. Powodowało to, że jego kod się rozrastał i stawał się coraz bardziej skomplikowany, jednak planista nadal nie działał idealnie.

Główne wady schedulera O(1)

Pierwsze próby - Staircase scheduler

W odpowiedzi na skargi wielu użytkowników dotyczace nadmiernej złożoności obecnego planisty, w 2004 roku Con Kolivas stworzył patch, zwany "staircase scheduler". Patch ten w sporej mierze uprościł planistę, usuwając 400 liniu kodu, dodając mniej niż 200. Dodać należy, że większość usuniętego kodu stanowiły "magiczne" obliczenia ustalające stopień interaktywności procesu.

Sposób działania

"Staircase scheduler" implementował pojedynczą tablicę, indeksowaną priorytetami, dla każdego procesora. Początkowo każdy proces trafiał w miejsce określone przez jego bazowy priorytet(wyliczany na podstawie nice). Wtedy proces o najwyższym priorytecie był lokalizowany i przydzielany mu był procesor (póki co podobnie jak w O(1)). Główna różnica polegała na tym, gdzie wędruje proces po zużyciu swojego timeslice'u (równego w pierwszej wersji 10ms * liczba_cpu). W O(1) proces następnie trafiał do drugiej tablicy expired, tu natomiast trafia z powrotem do początkowej tablicy, jednakże z priorytetem o 1 mniejszym. Dzieje się tak do momentu, gdy skończy się tablica. Wtedy proces wskakuje do miejsca o 1 niższego niż jego początkowe położenie, jednak na czas działania na tym poziomie dostaje o 1 wiecej timeslice niż miał na początku. Na kolejnych schodach znów dostaje po jednym timeslice i, podobnie jak wcześniej, schodzi tak do końca.
Wygląda to tak:
 						Priority rank
	Iteration Base	-1	-2	-3	-4	-5 	-6	-7	-8	-9	...
	1 	 1 	 1 	 1 	 1 	 1 	 1 	 1 	 1 	 1 	 1
	2 		 2	 1	 1	 1 	 1	 1	 1	 1	 1
	3 			 3	 1	 1 	 1	 1	 1	 1	 1
	
Jeśli proces śpi przez pewien określony czas, zostaje wepchnięty na górę o jeden stopień. Dzięki temu mechanizmowi procesy interaktywne, które standardowo śpią przez jakiś czas, praktycznie stale będą pozostawały na szczycie "schodów", co sprawia, że będą lepiej i szybciej obsługiwane.
Chociaz przy kolejnych iteracjach proces zaczynał z niższym priorytetem, to dostawał więcej czasu, zatem w pewien spoób różnice priorytetów pomiędzy procesami pozostawały zachowane.

Główne zalety:

Pierwszy "całkiem" sprawiedliwy - The Rotating Staircase Deadline Scheduler

Mimo, iż patch Con'a Kolivas'a nie wszedł do głównej gałęzi rozwojowej, Con dalej pracował nad zwiększeniem interaktywności schedulerów. Efektem jego pracy był RSDL (The Rotating Staircase Deadline Scheduler) przemianowany później na SD (Staircase Deadline Scheduler). Scheduler ten powstał na podstawie, zarówno pomysłów zaczerpniętych z jego wczesniejszego dziecka - Staircase Schedulera, oraz starego, dobrego O(1), jak i kilku nowych.

Podstawowe cechy:

Sposób działania:

Podobnie jak większości scheduler'ów RSDL posiada tablicę priorytetów. Na każdym poziomie widać listę procesów aktualnie czekających. żeby rozpocząć działanie z danym priorytetem. Każdy z procesów ma przydział czasu, jaki może działać z danym priorytetem.
Wygląda mniej więcej tak:
                                       

Staircase

Procesy z najwyższym priorytetem otrzymują timeslice'y (zależne jedynie od RR_INTERVAL - jedyna modyfikowalna zmienna, od której zależne są wszystkie przedziały czasowe), po czym scheduler zaczyna działać używając klasycznego algorytmu round-robin. Gdy dany proces zużyje swój przydział czasu, przesuwa się na tablicy priorytetów o jeden niżej i otrzymuje nowy przydział czasu (wielkość przydziału zależna jest od RR_INTRVAL i obecnego priorytetu) Tutaj podobnie jak procesy o niższym priorytecie, czeka, aż scheduler dojdzie do odpowiedniego "piętra". W rezultacie także procesy o najniższym priorytecie otrzymają w koncu przynajmniej trochę czasu procesora.

Minor rotation

Ciekawą cechą tego planisty jest, iż każdy poziom o danym priorytecie ma również pewien przydział czasu. Gdy przydział czasu danego poziomu sie skończy, wszystkie procesy należące do niego zostają przesunięte o 1 w dół, bez względu na to, czy zużyły swoje indywidualne przedziały czy nie. Rezultatem tego mechanizmu, zwanego "minor rotation" jest fakt, iż dla każdego procesu wystarczy, że upłynie określony czas, żeby procesy o wyższych priorytetach "zeszły" do jego priorytetu. Maksymalne okres oczekiwania dla każdego procesu da się łatwo wyliczyć, zatem nie jest możliwe zagłodzenie.

Major rotation

Po tym jak dany proces dojdzie na sam dół listy priorytetów, wędruje do drugiej tablicy zwanej expired (podobnie jak w O(1)) na to samo miejsce, które zajmował oryginalnie w pierwszej tablicy. Gdy w tablicy pierwszej nie będzie już zadnego aktywnego procesu, lub procesy "wypadną" z tablicy w wyniku działania "minor rotation". Wtedy zachodzi, podobnie jak w standardowym O(1) tzw. "major rotation", tzn. tablica aktywnych procesów i tablica procesów expired są zamieniane i wszystkie wyżej opisane wydarzenia zachodzą ponownie.
Standardowy scheduler przed zajściem "major rotation" stara sie na podstawie m.in. informacji o tym jak często proces spał, wyliczyć które procesy są interaktywne. Procesy te dostają bonus do swojego priorytetu. W RSDL całe te wyliczenia są pominięte. Sam jego mechanizm działania sprawia, że procesy śpiące dłużej będą dłużej pozostawały na wyższych piętrach, przez co będą lepiej obsługiwane.

Modelling Deadline Behaviour

Jako że działanie tego algorytmu nie zależy od różnych wyliczanych dynamicznie współczynników interaktywności, czy bonusów na priorytetach, czas trwania jednego obrotu dużej pętli ("major epoch") zależy wyłącznie od liczby procesów, jakie działają oraz ich współczynnika nice, dzięki czemu możemy go łątwo wyliczyć. Podobnie jest z czasem oczekiwania danego procesu na CPU. Więdząc jakie procesy, o wyższych priorytetach niż dany, działają możemy, posługując się prostą matematyką, wyliczyć opóźnienie procesu w dostępie do procesora (latency).

Kolejny sprawiedliwy - CFS

RSDL vs. CFS

Pomimo iż SD działał dużo lepiej od oryginalnego O(1) oraz tego, że był od niego znacznie prostszy, nie wszedl do głównej linii rozwojowej jądra. Do jądra wszedl natomiast, stworzony przez jednego z głównych developerów linuxa Ingo Molnara CFS. Wzbudziło to wiele kontrowersji. Nie pomagał fakt, iż Ingo skorzystał z ideii całkowitej sprawiedliwości, której użyteczność wykazał właśnie pominięty SD. Jeszcze więcej sporów wzbudził fakt, iż w niektórych testach CFS wypadal gorzej niż RSDL, a także to, iż jego kod był znacznie bardziej złożony.

Uzasadnienie Torvalds'a

Linus Torvalds, uzasadniając fakt, iż do jądra nie wszedl RSDL, nie powoływał sie tak naprawde na argumenty merytoryczne. Uważał, że Con zamiast usprawniać schedulera, kłócił się z ludźmi, którzy zgłaszali błędy. Jego porównanie wydajności obu schedulerów sprowadziło się do stwierdzenia, iż na podstawie testów można ustalić z pewnością, iż oba schedulery były dużo lepsze od starego, jednak pomiędzy nimi nie widać znaczącej różnicy.

Kwestia nierozstrzygalna

Oczywiscie, jak można się domyślić, użytkownicy linuxa podzielili się na dwa obozy. Pro RSDL i pro CFS i tak naprawde, śledząc ich dyskusje na różnych forach i portalach nie możliwe jest obiektywne stwierdzenie kto miał rację. Zarówno ideę jak i implementacje schedulera CFS przedstawi jednak kolega.

CFS - Completely Fair Scheduler

Jak się to wszystko zaczęło?

"Modular Scheduler Core and Completely Fair Scheduler"

W kwietniu roku 2007 po 62 godzinach kodowania Ingo Molnar wydał zestaw patchy zatytułowany: "Modular Scheduler Core and Completely Fair Scheduler", a dokładniej jego pierwszą wersje v1. Nazwa patch nie do końca oddaje ideę, gdyż w praktyce kod schedulera został napisany zupełnie na nowo od zera, zbierając różne pomysły pojawiające sie w czasie królowania O(1). Wprowadzona została rozszerzalna hierarchia modułów schedulera. Każdy moduł ma prezentować pewien sposób szeregowania czyli policy, a obsługiwane są przez scheduler-core, który nie zakłada praktycznie żadnych szczegółów o ich działaniu. Należy się tu jednak pewne wyjaśnienie. Nie jest to żaden pełen framework do wygodnego i sprawnego podmieniania schedulerów. Całość działa na zasadzie tego, że kompilując jądro wybieramy plik sched_*.c implemetujący daną politykę szeregowania, a później możemy ew. wybrać by przy uruchomieniu załadował się inny scheduler, niż ten który wybraliśmy przy instalacji. Nie jest możliwe podmienianie tego schedulera w runtime, a wspomniane ustawienia wymagają modyfikacji pewnych flag. [Even if I make that sched_sd.c patch, people cannot use SD as their default scheduler unless they hack SCHED_FAIR 0 to read SCHED_SD 0 or similar. The same goes for original staircase cpusched, nicksched, zaphod, spa_ws, ebs and so on.]

Jednym z modułów jest też sched_fair.c, implementujący CFS. (zastępuje dotychczasowy SCHED_OTHER). Autor Ingo Molnar prezentując go szczególnie dziękował i chwalił Cona Kolivasa, z którego RSDL/SD, zaczerpnięto zasadniczą ideę: Fairness (uczciwość, sprawiedliwość, bezstronność :) ), jednocześnie zaznaczając, że CFS jest implementacyjnie zupełnie różny od RSDL/SD. Na nim skupi się właśnie ta część prezentacji.

Ale zanim to nastąpi wymienię inne zmiany wprowadzenie jednocześnie: nowy sched_rt.c obsługujący procesy typu real-time, zmiany w obsłudze SMP load-balncing (rozkładanie wykonywania procesów na procesory w procesorach wielordzeniowych), ogólnie kod scheduler-core dzięki modularyzacji zmalał do ok 700linii. Wydawałoby się, że robi to wrażenie, ale tak naprawdę poprostu kod który do tej pory znajdował się w sched.c dotyczący polityk, został przerzucony do plików implementujących konkretne polityki, więc defacto nie ubyło go znowu zbyt wiele.
80% of CFS's design can be summed up in a single sentence: CFS basically models an "ideal, precise multi-tasking CPU" on real hardware.
te słowa znajdujemy na początku patcha Co nowego wprowadza CFS? CFS rozwijał się bardzo dynamicznie, już po 4rech dniach pojawiła się kolejna wersja v2, poprawiająca wiele szczegółów, w tym możliwość pewnych zagłodzeń związanych z poziomamii nice w bardzo specyficznych przypadkach, uruchamianie dzieci z zerowymi statystykami, bez dziedziczenia ich po rodzicach (podobno zwiększa to wydajność przy duzych obciążeniach), uniezależnienie poziomów nice od wartości granularity_level, dodanie nowych plików i nowych pozycji w plikach z rodziny /proc/ dla lepszego podglądu pracy schedulera i jeszcze kilka innych. W tej chwili najnowsze implementacje korzystają z pomysłów przedstawionego pod koniec wakacji 2007 Really Fair Schedulera autorstwa Romana Zippela, i ponownie jak z Conem Kolivasem, pojawia się spór, że z całej pracy niektórych osób Ingo wybiera tylko najciekawsze rzeczy i przepisuje je do swojego CFS.

problemy: obecnie w wydaniach jadra mamy do czynienia z podstawowa wersja schedulera (z lipca 2007), choc opracowano juz -v24 (24tą) wersję tegoż schedulera, jej dodanie jest planowane wraz z wydaniem stabilnej wersji jadra 2.6.24, wiadomo, że:

wydanie z wrzesnia odchudzające kod i wprowadzające pomysły z RFS:
http://kerneltrap.org/mailarchive/linux-kernel/2007/9/11/230691

a to już treść maila dotyczącego najnowszego wydania -v24 z 19.listopada 2007
There are countless improvements in -v24 (see the shortlog further below 
for details), but here are a few highlights:

 - improved interactivity via Peter Ziljstra's "virtual slices" feature.
   As load increases, the scheduler shortens the virtual timeslices that 
   tasks get, so that applications observe the same constant latency for 
   getting on the CPU. (This goes on until the slices reach a minimum 
   granularity value)

 - CONFIG_FAIR_USER_SCHED is now available across all backported 
   kernels and the per user weights are configurable via 
   /sys/kernel/uids/. Group scheduling got refined all around.

 - performance improvements

 - bugfixes

99% of these changes are already upstream in Linus's git tree and they 
will be released as part of v2.6.24. (there are 4 pending commits that 
are in the small 2.6.24-rc3-v24 patch.)

Mówimy o cechach, pomówmy o implementacji...

Ideowo proces działania CFS jest bardzo prosty: dla każdego procesu sumujemy czas, przez jaki pozostawał w uśpieniu (oczekując na wznowienie) dzielony przez liczbę aktywnych i oczekujących w tym czasie procesów. Wg tych sum trzymamy procesy w drzewie rbtree. W momencie przełączenia konktekstu, gdy mamy zdecydować który proces mamy wybrac do działania decydujemy się na ten o największej wartości (ścieżka maksymalnie "w prawo" w naszym drzewie). Gdy dany proces zostaje odłączony (przechodzi w stan oczekiwania), ale nie kończy swego działania, od jego sumy odejmowana jest wartość równa długości czasu przez jaki ten proces był obsługiwany przez procesor.

opisuje to Ingo Molnar (autor) w swoim liscie:
http://people.redhat.com/mingo/cfs-scheduler/sched-design-CFS.txt
"Ideal multi-tasking CPU" is a (non-existent  :-))  CPU that has 100%
physical power and which can run each task at precise equal speed, in
parallel, each at 1/nr_running speed. For example: if there are 2 tasks
running then it runs each at 50% physical power - totally in parallel.

On real hardware, we can run only a single task at once, so while that
one task runs, the other tasks that are waiting for the CPU are at a
disadvantage - the current task gets an unfair amount of CPU time. In
CFS this fairness imbalance is expressed and tracked via the per-task
p->wait_runtime (nanosec-unit) value. "wait_runtime" is the amount of
time the task should now run on the CPU for it to become completely fair
and balanced.

( small detail: on 'ideal' hardware, the p->wait_runtime value would
  always be zero - no task would ever get 'out of balance' from the
  'ideal' share of CPU time. )

CFS's task picking logic is based on this p->wait_runtime value and it
is thus very simple: it always tries to run the task with the largest
p->wait_runtime value. In other words, CFS tries to run the task with
the 'gravest need' for more CPU time. So CFS always tries to split up
CPU time between runnable tasks as close to 'ideal multitasking
hardware' as possible.

Most of the rest of CFS's design just falls out of this really simple
concept, with a few add-on embellishments like nice levels,
multiprocessing and various algorithm variants to recognize sleepers.

In practice it works like this: the system runs a task a bit, and when
the task schedules (or a scheduler tick happens) the task's CPU usage is
'accounted for': the (small) time it just spent using the physical CPU
is deducted from p->wait_runtime. [minus the 'fair share' it would have
gotten anyway]. Once p->wait_runtime gets low enough so that another
task becomes the 'leftmost task' of the time-ordered rbtree it maintains
(plus a small amount of 'granularity' distance relative to the leftmost
task so that we do not over-schedule tasks and trash the cache) then the
new leftmost task is picked and the current task is preempted.

The rq->fair_clock value tracks the 'CPU time a runnable task would have
fairly gotten, had it been runnable during that time'. So by using
rq->fair_clock values we can accurately timestamp and measure the
'expected CPU time' a task should have gotten. All runnable tasks are
sorted in the rbtree by the "rq->fair_clock - p->wait_runtime" key, and
CFS picks the 'leftmost' task and sticks to it. As the system progresses
forwards, newly woken tasks are put into the tree more and more to the
right - slowly but surely giving a chance for every task to become the
'leftmost task' and thus get on the CPU within a deterministic amount of
time.
Some implementation details:

 - the introduction of Scheduling Classes: an extensible hierarchy of
   scheduler modules. These modules encapsulate scheduling policy
   details and are handled by the scheduler core without the core
   code assuming about them too much.

 - sched_fair.c implements the 'CFS desktop scheduler': it is a
   replacement for the vanilla scheduler's SCHED_OTHER interactivity
   code.

   I'd like to give credit to Con Kolivas for the general approach here:
   he has proven via RSDL/SD that 'fair scheduling' is possible and that
   it results in better desktop scheduling. Kudos Con!

   The CFS patch uses a completely different approach and implementation
   from RSDL/SD. My goal was to make CFS's interactivity quality exceed
   that of RSDL/SD, which is a high standard to meet :-) Testing
   feedback is welcome to decide this one way or another. [ and, in any
   case, all of SD's logic could be added via a kernel/sched_sd.c module
   as well, if Con is interested in such an approach. ]

   CFS's design is quite radical: it does not use runqueues, it uses a
   time-ordered rbtree to build a 'timeline' of future task execution,
   and thus has no 'array switch' artifacts (by which both the vanilla
   scheduler and RSDL/SD are affected).

   CFS uses nanosecond granularity accounting and does not rely on any
   jiffies or other HZ detail. Thus the CFS scheduler has no notion of
   'timeslices' and has no heuristics whatsoever. There is only one
   central tunable:

         /proc/sys/kernel/sched_granularity_ns

   which can be used to tune the scheduler from 'desktop' (low
   latencies) to 'server' (good batching) workloads. It defaults to a
   setting suitable for desktop workloads. SCHED_BATCH is handled by the
   CFS scheduler module too.

   Due to its design, the CFS scheduler is not prone to any of the
   'attacks' that exist today against the heuristics of the stock
   scheduler: fiftyp.c, thud.c, chew.c, ring-test.c, massive_intr.c all
   work fine and do not impact interactivity and produce the expected
   behavior.

   the CFS scheduler has a much stronger handling of nice levels and
   SCHED_BATCH: both types of workloads should be isolated much more
   agressively than under the vanilla scheduler.

   ( another detail: due to nanosec accounting and timeline sorting,
     sched_yield() support is very simple under CFS, and in fact under
     CFS sched_yield() behaves much better than under any other
     scheduler i have tested so far. )

 - sched_rt.c implements SCHED_FIFO and SCHED_RR semantics, in a simpler
   way than the vanilla scheduler does. It uses 100 runqueues (for all
   100 RT priority levels, instead of 140 in the vanilla scheduler)
   and it needs no expired array.

 - reworked/sanitized SMP load-balancing: the runqueue-walking
   assumptions are gone from the load-balancing code now, and
   iterators of the scheduling modules are used. The balancing code got
   quite a bit simpler as a result.