本文主要基於Linux kernel v2.6.22的源代碼,分析該版本的進程模型以及CFS調度器算法。linux
Linux kernel v2.6.22源代碼的連接地址:https://elixir.bootlin.com/linux/v2.6.22/source/kernel算法
進程是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操做系統結構的基礎。編程
在早期面向進程設計的計算機結構中,進程是程序的基本執行實體;在當代面向線程設計的計算機結構中,進程是線程的容器。數組
程序是指令、數據及其組織形式的描述,進程是程序的實體。緩存
只要咱們打開電腦的任務管理器,咱們就能夠看到各類各樣的進程,每一個進程都關聯着一項任務。數據結構
在Linux操做系統下,咱們經過ps指令實現進程的查看併發
2.2進程的組成與特徵異步
(1)在Linux系統中進程由如下三部分組成:進程控制塊PCB ;數據段 ;程序段。ide
(2)(i)動態性(ii)併發性(iii)獨立性(iv)異步性(iv)結構性性能
2.3進程的組織
全部的進程都被放在一個叫作進程控制塊( PCB )的數據結構中,能夠理解爲進程屬性的集合,該控制塊由操做系統建立和管理。
每一個進程在內核中都有一個進程控制塊來維護進程相關的信息,Linux內核的進程控制塊是 task_struct 結構體,每一個進程都
把他的信息放在 task_struct 這個數據結構裏,而且能夠在include/linux/sched.h 這個目錄結構中找到它。全部運行在
系統裏的進程都以 task_struct 鏈表的形式存在內核當中。該結構體包含如下內容:
2.3.1PCB的做用
(1)當操做系統要調度某進程執行時,要從該進程的PCB中查看其當前狀態和優先級。
(2)當調度某進程後,要根據PCB中保存的CPU狀態信息,設置進程恢復運行的現場,並根據PCB中程序和數據的內存地址找到程序和數據。
(3)當進程阻塞或掛起時,要將其斷點的CPU環境保存在PCB中。
1.進程標示符(PID)
這是描述本進程的惟一標示符,用來區分其餘進程。其中父進程id(PPID)
pid_t pid; //進程號 pid_t tgid; //進程組號
當CONFIG_BASE_SMALL等於0時,PID的取值範圍是0到0x8000-1,其中0x8000表明16進制下的32768,
即PID的取值範圍是0~32767。也就是說,在Linux操做系統中能夠有32768個進程。
/* linux-2.6.20/include/linux/threads.h */
#define PID_MAX_DEFAULT (CONFIG_BASE_SMALL ? 0x1000 :0x8000)
在Linux系統中,一個線程組中的全部線程使用和該線程組的領頭線程相同的PID,並被存放在tgid成員中。
只有線程組的領頭線程的PID成員纔會被設置爲與tgid相同的值。
2.進程狀態
Linux定義了一個長整形變量,來保存進程的狀態。至於在 state 變量前面添加 volatile 這個關鍵詞是爲了告訴
編譯器不要對其優化。編譯器有一個緩存優化的習慣,好比說:第一次在內存取數,編譯器發現後面還要用這個變量,
因而把這個變量的值就放在寄存器中。這個關鍵詞就是要求編譯器不要進行優化,每次都讓CPU到內存中取數,
以確保進程的狀態的變化可以及時地反映出來。
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
接下來簡單介紹一下進程的狀態:
1 #define TASK_RUNNING 0 2 #define TASK_INTERRUPTIBLE 1 3 #define TASK_UNINTERRUPTIBLE 2 4 #define TASK_STOPPED 4 5 #define TASK_TRACED 8 6 /* in tsk->exit_state */ 7 #define EXIT_ZOMBIE 16 8 #define EXIT_DEAD 32 9 /* in tsk->state again */ 10 #define TASK_NONINTERACTIVE 64 11 #define TASK_DEAD 128
(1) TASK_RUNNING 表示進程要麼正在執行,要麼正在準備執行。
(2) TASK_INTERRUPTIBLE 表示進程被阻塞(睡眠),只有當某個條件是TRUE時,其狀態相應的設置爲 TASK_RUNNING 。它能夠被信號和 wake_up() 喚醒。
(3) TASK_UNINTERRUPTIBLE 表示進程被阻塞(睡眠),只有當某個條件是TRUE時,其狀態相應的設置爲 TASK_RUNNING 。可是它只能被 wake_up() 喚醒。
(4) TASK_STOPPED 表示進程被中止執行。
(5) TASK_TRACED 表示進程被 debugger 等進程監視着。
(6) EXIT_ZOMBIE 表示進程的執行被終止,可是其父進程尚未使用 wait() 等系統調用來獲知它的終止信息。
(7) EXIT_DEAD 表示進程的最終狀態。
3.進程的優先級
在Linux內核下爲每一個進程分配了時間片並根據其優先級進行調度。當進程被建立時,在 task_struct 裏包含了如下幾個進程。
int prio, static_prio, normal_prio;
除此以外,在內核頭文件 include/linux/sched.h 中定義了以下宏
#define MAX_USER_RT_PRIO 100 #define MAX_RT_PRIO MAX_USER_RT_PRIO #define MAX_PRIO (MAX_RT_PRIO + 40)
內核中規定進程的優先級範圍爲[0,MAX_PRIO-1]。其中實時任務的優先級範圍是[0,MAX_RT_PRIO-1],
非實時任務的優先級範圍是[MAX_RT_PRIO,MAX_PRIO-1]。優先級值越小,意味着級別越高,任務先被內核調度。
(1) prio 指的是任務當前的動態優先級,其值影響任務的調度順序。
(2) normal_prio 指的是任務的常規優先級,其值基於 static_prio 和調度策略計算。
(3) static_prio 指的是任務的靜態優先級,在進程建立時分配,該值會影響分配給任務的時間片的長短和非實時任務的動態優先級的計算。
3.進程的狀態轉換圖
不一樣的操做系統對進程的狀態解釋不一樣,可是最基本的狀態都是同樣的。
(1)運行態:進程佔用CPU,並在CPU上運行;
(2)就緒態:進程已經具有運行條件,可是CPU尚未分配過來;
(3)阻塞態:進程因等待某件事發生而暫時不能運行;
進程在一輩子中,都處於這3種狀態之一。
運行-->就緒:這是有調度引發的,主要是進程佔用CPU的時間過長;
就緒-->運行:運行的進程的時間片用完,調度就轉到就緒隊列中選擇合適的進程分配CPU;
運行-->阻塞:發生了I/O請求或等待某事件的發生;
阻塞-->就緒:進程所等待的事件發生,就進入就緒隊列。
上圖就是Linux操做系統的進程轉換圖。
4.O(1)調度器的實現
4.1概述
要談到O(1)調度器,就從它的前身提及,也就是O(n)調度器。
調度器採用基於優先級的設計,它對 Runqueue 中全部進程的優先級依次進行比較,選擇最高優先級的進程做爲
下一個被調度的進程。( Runqueue 是Linux內核中保存全部就緒進程的對列)就是說,在每次進程切換時,內核
掃描可運行進程的鏈表,計算優先級,而後選擇「最佳」進程來運行。
可是,該調度器的可擴展性很差,調度器選擇進程時須要遍歷整個 Runqueue 隊列,在從中選出最佳進程,所以
該算法的執行時間與進程數成正比。致使系統總體的性能降低。
從名字就能夠看出來O(1)調度器主要解決了O(n)版本中的擴展性問題。O(1)調度算法所花費的時間爲常數,
與當前系統中的進程個數無關。O(1)調度器跟蹤運行隊列中可運行的任務(實際上,每一個優先級水平有兩個運行隊列,
一個用於活動任務,一個用於過時任務),這意味着要肯定接下來執行的任務,調度器只需按優先級將下一個任務從特定活動的運行隊列中取出便可。
4.2調度器運用到的數據結構
4.2.1Runqueue隊列
rq結構是每一個CPU上的主要的運行隊列數據結構。
主要的結構是優先級數組。
struct rq { spinlock_t lock; /* * nr_running and cpu_load should be in the same cacheline because * remote CPUs use both these fields when doing load calculation. */ unsigned long nr_running; unsigned long raw_weighted_load; #ifdef CONFIG_SMP unsigned long cpu_load[3]; unsigned char idle_at_tick; #ifdef CONFIG_NO_HZ unsigned char in_nohz_recently; #endif #endif unsigned long long nr_switches; /* * This is part of a global counter where only the total sum * over all CPUs matters. A task can increase this counter on * one CPU and if it got migrated afterwards it may decrease * it on another CPU. Always updated under the runqueue lock: */ unsigned long nr_uninterruptible; unsigned long expired_timestamp; /* Cached timestamp set by update_cpu_clock() */ unsigned long long most_recent_timestamp; struct task_struct *curr, *idle; unsigned long next_balance; struct mm_struct *prev_mm; struct prio_array *active, *expired, arrays[2]; ......
};
4.2.2優先級數組
該結構體中有一個用來表示進程動態優先級的數組queue,它包括了每一種優先級進程所造成的鏈表。
#define MAX_USER_RT_PRIO 100
#define MAX_RT_PRIO MAX_USER_RT_PRIO #define MAX_PRIO (MAX_RT_PRIO + 40)
struct prio_array { unsigned int nr_active; DECLARE_BITMAP(bitmap, MAX_PRIO+1); /* include 1 bit for delimiter */ struct list_head queue[MAX_PRIO]; };
由於進程優先級的最大值爲139,所以 MAX_PRIO 的最大值取140(普通進程使用100到139的優先級,實時進程使用0到99的優先級)。
所以, queue 數組中包括140個可執行狀態的進程鏈表,每一條優先級鏈表上的進程都具備一樣的優先級,而不一樣進程鏈表上的進程都擁有不一樣的優先級。
struct bitmap { struct bitmap_page *bp; unsigned long pages; /* total number of pages in the bitmap */ unsigned long missing_pages; /* number of pages not yet allocated */ mddev_t *mddev; /* the md device that the bitmap is for */ int counter_bits; /* how many bits per block counter */ ...... };
除此以外, prio_array 結構中還包含一個優先級位圖 bitmap 。該位圖使用一個位(bit)來表明一個優先級,起初該位圖中的
所有位置被置零,當某個優先級的進程處於可執行狀態時,該優先級所相應的位就被置1。所以O(1)算法中查找系統優先級最高的
就轉化成查找優先級位圖中第一個被置1的位。
4.2.3活動進程和過時進程
當處於執行態的進程用完時間片後就會處於就緒態。此時調度程序再從就緒態的進程中選擇一個做爲即將要執行的進程,在Linux中,就緒態和執行態統稱爲可執行態。
對於可執行狀態的進程,咱們能夠分爲三類:首先是正處於執行狀態的進程;其次,有一部分處於可執行狀態的進程但還沒使用完他們的時間片,它們等待被執行;最後,剩下的進程已經用完了本身的時間片,在其餘進程還沒使用完他們的時間片以前,它們不能在被執行。
因此,活動進程就是指還沒使用完時間片的進程;過時進程就是指已經用完時間片的進程。所以,調度程序的工做就是在活動進程集合中選取一個最佳優先級的進程,假設該進程時間片剛好用完,就將該進程放入過時進程集合當中。
在可執行隊列結構中,arrays數組的兩個元素分別用來表示剛纔所述的活動進程集合和過時進程集合, active 和 expired 兩個指針分別直接指向這兩個集合。
4.3時間片的計算
O(n)調度算法在每次進程切換時,內核依次掃描就緒隊列上的進程,並計算每一個進程的優先級,在選擇出優先級最高的進程來執行,可知其時間複雜度爲O(n)。
可是O(1)調度算法可以在恆定的時間內爲每個進程又一次分配號時間片,而且在恆定的時間內可以選取一個最高優先級的進程,重要的是這兩個過程都與系統中可執行的進程數無關。
O(1)算法採用過時進程數組和活躍進程數組解決以往調度算法所帶來的O(n)複雜度問題。過時數組中的進程都已經用完了時間片。而活躍數組的進程還擁有時間片。當一個進程用完本身的時間片後,它就被移動到過時進程數組中。同一時候這個過時進程在被移動以前就已經計算好了新的時間片。能夠看出來O(1)調度算法採用分散計算時間片的方法。
這時,只要活躍進程數組中沒有可執行進程了,說明全部可執行進程都用完了他們的時間片,那麼此時僅僅須要交換如下兩個數組就能夠講過時進程切換爲活躍進程。如下代碼說明了兩個數組間的交換:
if (unlikely(!array->nr_active)) { /* * Switch the active and expired arrays. */ schedstat_inc(rq, sched_switch); rq->active = rq->expired; rq->expired = array; array = rq->active; rq->expired_timestamp = 0; rq->best_expired_prio = MAX_PRIO; }
經過分散計算時間片、交換活躍和過時兩個進程集合的方法可以使得O(1)算法在恆定的時間內爲每個進程有一次計算好時間片。
進程調度的本質就是在當前可執行的進程集合 中選擇一個最佳的進程,這個最佳則是以進程的動態優先級爲選取標準的。
調度程序在選取最高優先級的進程時,首先利用優先級位圖從高到低找到第一個被設置的位,該位對應這一條進程鏈表。這個鏈表中
的進是當前系統所有可執行進程中最高優先級的。在該優先級鏈表中選取第一個進程,它擁有最高的優先級。即爲調度程序當即要執行的進程。
上述進程的選取過程可用下述代碼描述:
asmlinkage void __sched schedule(void) { struct task_struct *prev, *next; struct prio_array *array; struct list_head *queue; unsigned long long now; unsigned long run_time; int cpu, idx, new_prio; long *switch_count; struct rq *rq; ...... prev = current; array = rq->active; idx = sched_find_first_bit(array->bitmap); queue = array->queue + idx; next = list_entry(queue->next, struct task_struct, run_list); ...... if (likely(prev != next)) { next->timestamp = next->last_ran = now; rq->nr_switches++; rq->curr = next; ++*switch_count; prepare_task_switch(rq, next); prev = context_switch(rq, prev, next); barrier(); /* * this_rq must be evaluated again because prev may have moved * CPUs since it called schedule(), thus the 'rq' on its stack * frame will be invalid. */ finish_task_switch(this_rq(), prev); }
sched_find_first_bit()用於在位圖中高速查找第一個被設置的位。假設prev和next不是一個進程。那麼此時進程切換就開始運行。
經過上述的內容可以發現。在恆定的時間又一次分配時間片和選擇一個最佳進程是Q(1)算法的核心。
5.見解
操做系統是管理計算機系統的所有硬件資源包括軟件資源及數據資源;控制程序運行;改善人機界面;爲其餘應用軟件提供支持等,使計算機系統全部資源最大限度地發揮做用,爲用戶提供方便的、有效的、友善的服務界面。
進程管理便是操做系統對CPU的管理,爲了提高CPU利用率,使用多道編程,便有了多進程維護管理。操做系統已實現了各管理功能,硬件CPU及一系列進程資源抽象成爲了進程的概念,能夠說進程算是操做系統的「無中生有」,應用程序的編程人員直接利用進程的機制,達到讓應用程序高效利用硬件資源的目的。操做系統的學習過程當中,學習和體會前人在解決問題的思路, 再具體到細節時,會發現數據結構以及算法的實現,對這二者的學習和認知也會頗有幫助。能夠說,操做系統的學習,不一樣的層次,由淺及深,都會有不一樣層次的收穫。
6.參考資料
https://blog.csdn.net/a2796749/article/details/47101533