linux內核調度
Ⅰ 一文讀懂linux任務間調度原理和整個執行過程
在前文中,我們分析了內核中進程和線程的統一結構體task_struct,並分析進程、線程的創建和派生的過程。在本文中,我們會對任務間調度進行詳細剖析,了解其原理和整個執行過程。由此,進程、線程部分的大體框架就算是介紹完了。本節主要分為三個部分:Linux內核中常見的調度策略,調度的基本結構體以及調度發生的整個流程。下面將詳細展開說明。
Linux 作為一個多任務操作系統,將每個 CPU 的時間劃分為很短的時間片,再通過調度器輪流分配給各個任務使用,因此造成多任務同時運行的錯覺。為了維護 CPU 時間,Linux 通過事先定義的節拍率(內核中表示為 HZ),觸發時間中斷,並使用全局變數 Jiffies 記錄了開機以來的節拍數。每發生一次時間中斷,Jiffies 的值就加 1。節拍率 HZ 是內核的可配選項,可以設置為 100、250、1000 等。不同的系統可能設置不同的數值,可以通過查詢 /boot/config 內核選項來查看它的配置值。
Linux的調度策略主要分為實時任務和普通任務。實時任務需求盡快返回結果,而普通任務則沒有較高的要求。在前文中我們提到了task_struct中調度策略相應的變數為policy,調度優先順序有prio, static_prio, normal_prio, rt_priority幾個。優先順序其實就是一個數值,對於實時進程來說,優先順序的范圍是 0 99;對於普通進程,優先順序的范圍是 100 139。數值越小,優先順序越高。
實時調度策略主要包括以下幾種
普通調度策略主要包括以下幾種:
首先,我們需要一個結構體去執行調度策略,即sched_class。該類有幾種實現方式
普通任務調度實體源碼如下,這裡麵包含了 vruntime 和權重 load_weight,以及對於運行時間的統計。
在調度時,多個任務調度實體會首先區分是實時任務還是普通任務,然後通過以時間為順序的紅黑樹結構組合起來,vruntime 最小的在樹的左側,vruntime最多的在樹的右側。以CFS策略為例,則會選擇紅黑樹最左邊的葉子節點作為下一個將獲得 CPU 的任務。而這顆紅黑樹,我們稱之為運行時隊列(run queue),即struct rq。
其中包含結構體cfs_rq,其定義如下,主要是CFS調度相關的結構體,主要有權值相關變數、vruntime相關變數以及紅黑樹指針,其中結構體rb_root_cached即為紅黑樹的節點
對結構體dl_rq有類似的定義,運行隊列由紅黑樹結構體構成,並按照deadline策略進行管理
對於實施隊列相應的rt_rq則有所不同,並沒有用紅黑樹實現。
下面再看看調度類sched_class,該類以函數指針的形式定義了諸多隊列操作,如
調度類分為下面幾種:
隊列操作中函數指針指向不同策略隊列的實際執行函數函數,在linux/kernel/sched/目錄下,fair.c、idle.c、rt.c等文件對不同類型的策略實現了不同的函數,如fair.c中定義了
以選擇下一個任務為例,CFS對應的是pick_next_task_fair,而rt_rq對應的則是pick_next_task_rt,等等。
由此,我們來總結一下:
有了上述的基本策略和基本調度結構體,我們可以形成大致的骨架,下面就是需要核心的調度流程將其拼湊成一個整體,實現調度系統。調度分為兩種,主動調度和搶占式調度。
說到調用,逃不過核心函數schele()。其中sched_submit_work()函數完成當前任務的收尾工作,以避免出現如死鎖或者IO中斷等情況。之後首先禁止搶占式調度的發生,然後調用__schele()函數完成調度,之後重新打開搶占式調度,如果需要重新調度則會一直重復該過程,否則結束函數。
而__schele()函數則是實際的核心調度函數,該函數主要操作包括選取下一進程和進行上下文切換,而上下文切換又包括用戶態空間切換和內核態的切換。具體的解釋可以參照英文源碼注釋以及中文對各個步驟的注釋。
其中核心函數是獲取下一個任務的pick_next_task()以及上下文切換的context_switch(),下面詳細展開剖析。首先看看pick_next_task(),該函數會根據調度策略分類,調用該類對應的調度函數選擇下一個任務實體。根據前文分析我們知道,最終是在不同的紅黑樹上選擇最左節點作為下一個任務實體並返回。
下面來看看上下文切換。上下文切換主要干兩件事情,一是切換任務空間,也即虛擬內存;二是切換寄存器和 CPU 上下文。關於任務空間的切換放在內存部分的文章中詳細介紹,這里先按下不表,通過任務空間切換實際完成了用戶態的上下文切換工作。下面我們重點看一下內核態切換,即寄存器和CPU上下文的切換。
switch_to()就是寄存器和棧的切換,它調用到了 __switch_to_asm。這是一段匯編代碼,主要用於棧的切換, 其中32位使用esp作為棧頂指針,64位使用rsp,其他部分代碼一致。通過該段匯編代碼我們完成了棧頂指針的切換,並調用__switch_to完成最終TSS的切換。注意switch_to中其實是有三個變數,分別是prev, next, last,而實際在使用時,我們會對last也賦值為prev。這里的設計意圖需要結合一個例子來說明。假設有ABC三個任務,從A調度到B,B到C,最後C回到A,我們假設僅保存prev和next,則流程如下
最終調用__switch_to()函數。該函數中涉及到一個結構體TSS(Task State Segment),該結構體存放了所有的寄存器。另外還有一個特殊的寄存器TR(Task Register)會指向TSS,我們通過更改TR的值,會觸發硬體保存CPU所有寄存器在當前TSS,並從新的TSS讀取寄存器的值載入入CPU,從而完成一次硬中斷帶來的上下文切換工作。系統初始化的時候,會調用 cpu_init()給每一個 CPU 關聯一個 TSS,然後將 TR 指向這個 TSS,然後在操作系統的運行過程中,TR 就不切換了,永遠指向這個 TSS。當修改TR的值得時候,則為任務調度。
更多Linux內核視頻教程文本資料免費領取後台私信【 內核大禮包 】自行獲取。
在完成了switch_to()的內核態切換後,還有一個重要的函數finish_task_switch()負責善後清理工作。在前面介紹switch_to三個參數的時候我們已經說明了使用last的重要性。而這里為何讓prev和last均賦值為prev,是因為prev在後面沒有需要用到,所以節省了一個指針空間來存儲last。
至此,我們完成了內核態的切換工作,也完成了整個主動調度的過程。
搶占式調度通常發生在兩種情況下。一種是某任務執行時間過長,另一種是當某任務被喚醒的時候。首先看看任務執行時間過長的情況。
該情況需要衡量一個任務的執行時間長短,執行時間過長則發起搶占。在計算機裡面有一個時鍾,會過一段時間觸發一次時鍾中斷,通知操作系統時間又過去一個時鍾周期,通過這種方式可以查看是否是需要搶占的時間點。
時鍾中斷處理函數會調用scheler_tick()。該函數首先取出當前CPU,並由此獲取對應的運行隊列rq和當前任務curr。接著調用該任務的調度類sched_class對應的task_tick()函數進行時間事件處理。
以普通任務隊列為例,對應的調度類為fair_sched_class,對應的時鍾處理函數為task_tick_fair(),該函數會獲取當前的調度實體和運行隊列,並調用entity_tick()函數更新時間。
在entity_tick()中,首先會調用update_curr()更新當前任務的vruntime,然後調用check_preempt_tick()檢測現在是否可以發起搶占。
check_preempt_tick() 先是調用 sched_slice() 函數計算出一個調度周期中該任務運行的實際時間 ideal_runtime。sum_exec_runtime 指任務總共執行的實際時間,prev_sum_exec_runtime 指上次該進程被調度時已經佔用的實際時間,所以 sum_exec_runtime - prev_sum_exec_runtime 就是這次調度佔用實際時間。如果這個時間大於 ideal_runtime,則應該被搶佔了。除了這個條件之外,還會通過 __pick_first_entity 取出紅黑樹中最小的進程。如果當前進程的 vruntime 大於紅黑樹中最小的進程的 vruntime,且差值大於 ideal_runtime,也應該被搶佔了。
如果確認需要被搶占,則會調用resched_curr()函數,該函數會調用set_tsk_need_resched()標記該任務為_TIF_NEED_RESCHED,即該任務應該被搶占。
某些任務會因為中斷而喚醒,如當 I/O 到來的時候,I/O進程往往會被喚醒。在這種時候,如果被喚醒的任務優先順序高於 CPU 上的當前任務,就會觸發搶占。try_to_wake_up() 調用 ttwu_queue() 將這個喚醒的任務添加到隊列當中。ttwu_queue() 再調用 ttwu_do_activate() 激活這個任務。ttwu_do_activate() 調用 ttwu_do_wakeup()。這裡面調用了 check_preempt_curr() 檢查是否應該發生搶占。如果應該發生搶占,也不是直接踢走當前進程,而是將當前進程標記為應該被搶占。
由前面的分析,我們知道了不論是是當前任務執行時間過長還是新任務喚醒,我們均會對現在的任務標記位_TIF_NEED_RESCUED,下面分析實際搶占的發生。真正的搶占還需要一個特定的時機讓正在運行中的進程有機會調用一下 __schele()函數,發起真正的調度。
實際上會調用__schele()函數共有以下幾個時機
從系統調用返回用戶態:以64位為例,系統調用的鏈路為do_syscall_64->syscall_return_slowpath->prepare_exit_to_usermode->exit_to_usermode_loop。在exit_to_usermode_loop中,會檢測是否為_TIF_NEED_RESCHED,如果是則調用__schele()
內核態啟動:內核態的執行中,被搶占的時機一般發生在 preempt_enable() 中。在內核態的執行中,有的操作是不能被中斷的,所以在進行這些操作之前,總是先調用 preempt_disable() 關閉搶占,當再次打開的時候,就是一次內核態代碼被搶占的機會。preempt_enable() 會調用 preempt_count_dec_and_test(),判斷 preempt_count 和 TIF_NEED_RESCHED 是否可以被搶占。如果可以,就調用 preempt_schele->preempt_schele_common->__schele 進行調度。
本文分析了任務調度的策略、結構體以及整個調度流程,其中關於內存上下文切換的部分尚未詳細敘述,留待內存部分展開剖析。
1、調度相關結構體及函數實現
2、schele核心函數
Ⅱ 如何將linux內核的CFS調度演算法修改為隨機調
1、實時調度優先於CFS。 2、CFS是按照各進程靜態優先順序的比例進行時間的分配,在一個周期內各進程都有運行機會;實時調度總是調度實時優先順序最高的進程運行,運行時間和機會上都不公平。
Ⅲ linux操作系統的組成有哪幾部分
Linux操作系統是當前非常火的服務端系統,所有的it方向的大學生,都應該好好掌握它。
Ⅳ 為什麼說LINUX 內核調度的是線程,而不是進程呢 難道內核中進程是不切換,只切換線程
貌似不對哦,在LINUX系統之中,被調度的應該是進程。因為只有進程才擁有一個獨立的上下文環境,是分配系統資源的最小單位……而線程在SMP體系中加速了執行的效率……
在LINUX之中,線程也可稱作輕量級進程,它能享有自己的堆棧,線程ID等獨立資源,但大多還是要依賴其創建進程,比如地址空間,信號,文件句柄……
Ⅳ linux內核同一個優先順序的任務怎麼調度的
Linux內核中用task指代一切進程和線程。
調度的作用是安排所有可以運行的進程在CPU上的運行時間和次序
內核中主要有兩類調度演算法。其中的實時調度演算法中,對task有優先順序的概念,同一優先順序內的進程可以按照FIFO或RoundRobin的演算法進行調度。這兩種演算法都需要維護一個可運行進程的隊列。
Ⅵ linux內核同步問題
Linux內核設計與實現 十、內核同步方法
手把手教Linux驅動5-自旋鎖、信號量、互斥體概述
== 基礎概念: ==
並發 :多個執行單元同時進行或多個執行單元微觀串列執行,宏觀並行執行
競態 :並發的執行單元對共享資源(硬體資源和軟體上的全局變數)的訪問而導致的竟態狀態。
臨界資源 :多個進程訪問的資源
臨界區 :多個進程訪問的代碼段
== 並發場合: ==
1、單CPU之間進程間的並發 :時間片輪轉,調度進程。 A進程訪問列印機,時間片用完,OS調度B進程訪問列印機。
2、單cpu上進程和中斷之間並發 :CPU必須停止當前進程的執行中斷;
3、多cpu之間
4、單CPU上中斷之間的並發
== 使用偏向: ==
==信號量用於進程之間的同步,進程在信號量保護的臨界區代碼裡面是可以睡眠的(需要進行進程調度),這是與自旋鎖最大的區別。==
信號量又稱為信號燈,它是用來協調不同進程間的數據對象的,而最主要的應用是共享內存方式的進程間通信。本質上,信號量是一個計數器,它用來記錄對某個資源(如共享內存)的存取狀況。它負責協調各個進程,以保證他們能夠正確、合理的使用公共資源。它和spin lock最大的不同之處就是:無法獲取信號量的進程可以睡眠,因此會導致系統調度。
1、==用於進程與進程之間的同步==
2、==允許多個進程進入臨界區代碼執行,臨界區代碼允許睡眠;==
3、信號量本質是==基於調度器的==,在UP和SMP下沒有區別;進程獲取不到信號量將陷入休眠,並讓出CPU;
4、不支持進程和中斷之間的同步
5、==進程調度也是會消耗系統資源的,如果一個int型共享變數就需要使用信號量,將極大的浪費系統資源==
6、信號量可以用於多個線程,用於資源的計數(有多種狀態)
==信號量加鎖以及解鎖過程:==
sema_init(&sp->dead_sem, 0); / 初始化 /
down(&sema);
臨界區代碼
up(&sema);
==信號量定義:==
==信號量初始化:==
==dowm函數實現:==
==up函數實現:==
信號量一般可以用來標記可用資源的個數。
舉2個生活中的例子:
==dowm函數實現原理解析:==
(1)down
判斷sem->count是否 > 0,大於0則說明系統資源夠用,分配一個給該進程,否則進入__down(sem);
(2)__down
調用__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);其中TASK_UNINTERRUPTIBLE=2代表進入睡眠,且不可以打斷;MAX_SCHEDULE_TIMEOUT休眠最長LONG_MAX時間;
(3)list_add_tail(&waiter.list, &sem->wait_list);
把當前進程加入到sem->wait_list中;
(3)先解鎖後加鎖;
進入__down_common前已經加鎖了,先把解鎖,調用schele_timeout(timeout),當waiter.up=1後跳出for循環;退出函數之前再加鎖;
Linux內核ARM構架中原子變數的底層實現研究
rk3288 原子操作和原子位操作
原子變數適用於只共享一個int型變數;
1、原子操作是指不被打斷的操作,即它是最小的執行單位。
2、最簡單的原子操作就是一條條的匯編指令(不包括一些偽指令,偽指令會被匯編器解釋成多條匯編指令)
==常見函數:==
==以atomic_inc為例介紹實現過程==
在Linux內核文件archarmincludeasmatomic.h中。 執行atomic_read、atomic_set這些操作都只需要一條匯編指令,所以它們本身就是不可打斷的。 需要特別研究的是atomic_inc、atomic_dec這類讀出、修改、寫回的函數。
所以atomic_add的原型是下面這個宏:
atomic_add等效於:
result(%0) tmp(%1) (v->counter)(%2) (&v->counter)(%3) i(%4)
注意:根據內聯匯編的語法,result、tmp、&v->counter對應的數據都放在了寄存器中操作。如果出現上下文切換,切換機制會做寄存器上下文保護。
(1)ldrex %0, [%3]
意思是將&v->counter指向的數據放入result中,並且(分別在Local monitor和Global monitor中)設置獨占標志。
(2)add %0, %0, %4
result = result + i
(3)strex %1, %0, [%3]
意思是將result保存到&v->counter指向的內存中, 此時 Exclusive monitors會發揮作用,將保存是否成功的標志放入tmp中。
(4) teq %1, #0
測試strex是否成功(tmp == 0 ??)
(5)bne 1b
如果發現strex失敗,從(1)再次執行。
Spinlock 是內核中提供的一種比較常見的鎖機制,==自旋鎖是「原地等待」的方式解決資源沖突的==,即,一個線程獲取了一個自旋鎖後,另外一個線程期望獲取該自旋鎖,獲取不到,只能夠原地「打轉」(忙等待)。由於自旋鎖的這個忙等待的特性,註定了它使用場景上的限制 —— 自旋鎖不應該被長時間的持有(消耗 CPU 資源),一般應用在==中斷上下文==。
1、spinlock是一種死等機制
2、信號量可以允許多個執行單元進入,spinlock不行,一次只能允許一個執行單元獲取鎖,並且進入臨界區,其他執行單元都是在門口不斷的死等
3、由於不休眠,因此spinlock可以應用在中斷上下文中;
4、由於spinlock死等的特性,因此臨界區執行代碼盡可能的短;
==spinlock加鎖以及解鎖過程:==
spin_lock(&devices_lock);
臨界區代碼
spin_unlock(&devices_lock);
==spinlock初始化==
==進程和進程之間同步==
==本地軟中斷之間同步==
==本地硬中斷之間同步==
==本地硬中斷之間同步並且保存本地中斷狀態==
==嘗試獲取鎖==
== arch_spinlock_t結構體定義如下: ==
== arch_spin_lock的實現如下: ==
lockval(%0) newval(%1) tmp(%2) &lock->slock(%3) 1 << TICKET_SHIFT(%4)
(1)ldrex %0, [%3]
把lock->slock的值賦值給lockval;並且(分別在Local monitor和Global monitor中)設置獨占標志。
(2)add %1, %0, %4
newval =lockval +(1<<16); 相當於next+1;
(3)strex %2, %1, [%3]
newval =lockval +(1<<16); 相當於next+1;
意思是將newval保存到 &lock->slock指向的內存中, 此時 Exclusive monitors會發揮作用,將保存是否成功的標志放入tmp中。
(4) teq %2, #0
測試strex是否成功
(5)bne 1b
如果發現strex失敗,從(1)再次執行。
通過上面的分析,可知關鍵在於strex的操作是否成功的判斷上。而這個就歸功於ARM的Exclusive monitors和ldrex/strex指令的機制。
(6)while (lockval.tickets.next != lockval.tickets.owner)
如何lockval.tickets的next和owner是否相等。相同則跳出while循環,否則在循環內等待判斷;
* (7)wfe()和smp_mb() 最終調用#define barrier() asm volatile ("": : :"memory") *
阻止編譯器重排,保證編譯程序時在優化屏障之前的指令不會在優化屏障之後執行。
== arch_spin_unlock的實現如下: ==
退出鎖時:tickets.owner++
== 出現死鎖的情況: ==
1、擁有自旋鎖的進程A在內核態阻塞了,內核調度B進程,碰巧B進程也要獲得自旋鎖,此時B只能自旋轉。 而此時搶占已經關閉,(單核)不會調度A進程了,B永遠自旋,產生死鎖。
2、進程A擁有自旋鎖,中斷到來,CPU執行中斷函數,中斷處理函數,中斷處理函數需要獲得自旋鎖,訪問共享資源,此時無法獲得鎖,只能自旋,產生死鎖。
== 如何避免死鎖: ==
1、如果中斷處理函數中也要獲得自旋鎖,那麼驅動程序需要在擁有自旋鎖時禁止中斷;
2、自旋鎖必須在可能的最短時間內擁有
3、避免某個獲得鎖的函數調用其他同樣試圖獲取這個鎖的函數,否則代碼就會死鎖;不論是信號量還是自旋鎖,都不允許鎖擁有者第二次獲得這個鎖,如果試圖這么做,系統將掛起;
4、鎖的順序規則(a) 按同樣的順序獲得鎖;b) 如果必須獲得一個局部鎖和一個屬於內核更中心位置的鎖,則應該首先獲取自己的局部鎖 ;c) 如果我們擁有信號量和自旋鎖的組合,則必須首先獲得信號量;在擁有自旋鎖時調用down(可導致休眠)是個嚴重的錯誤的;)
== rw(read/write)spinlock: ==
加鎖邏輯:
1、假設臨界區內沒有任何的thread,這個時候任何的讀線程和寫線程都可以鍵入
2、假設臨界區內有一個讀線程,這時候信賴的read線程可以任意進入,但是寫線程不能進入;
3、假設臨界區有一個寫線程,這時候任何的讀、寫線程都不可以進入;
4、假設臨界區內有一個或者多個讀線程,寫線程不可以進入臨界區,但是寫線程也無法阻止後續的讀線程繼續進去,要等到臨界區所有的讀線程都結束了,才可以進入,可見:==rw(read/write)spinlock更加有利於讀線程;==
== seqlock(順序鎖): ==
加鎖邏輯:
1、假設臨界區內沒有任何的thread,這個時候任何的讀線程和寫線程都可以鍵入
2、假設臨界區內沒有寫線程的情況下,read線程可以任意進入;
3、假設臨界區有一個寫線程,這時候任何的讀、寫線程都不可以進入;
4、假設臨界區內只有read線程的情況下,寫線程可以理解執行,不會等待,可見:==seqlock(順序鎖)更加有利於寫線程;==
讀寫速度 : CPU > 一級緩存 > 二級緩存 > 內存 ,因此某一個CPU0的lock修改了,其他的CPU的lock就會失效;那麼其他CPU就會依次去L1 L2和主存中讀取lock值,一旦其他CPU去讀取了主存,就存在系統性能降低的風險;
mutex用於互斥操作。
互斥體只能用於一個線程,資源只有兩種狀態(佔用或者空閑)
1、mutex的語義相對於信號量要簡單輕便一些,在鎖爭用激烈的測試場景下,mutex比信號量執行速度更快,可擴展
性更好,
2、另外mutex數據結構的定義比信號量小;、
3、同一時刻只有一個線程可以持有mutex
4、不允許遞歸地加鎖和解鎖
5、當進程持有mutex時,進程不可以退出。
• mutex必須使用官方API來初始化。
• mutex可以睡眠,所以不允許在中斷處理程序或者中斷下半部中使用,例如tasklet、定時器等
==常見操作:==
struct mutex mutex_1;
mutex_init(&mutex_1);
mutex_lock(&mutex_1)
臨界區代碼;
mutex_unlock(&mutex_1)
==常見函數:==
=
Ⅶ linux內核怎麼調度系統
1.調度器的概述
多任務操作系統分為非搶占式多任務和搶占式多任務。與大多數現代操作系統一樣,Linux採用的是搶占式多任務模式。這表示對CPU的佔用時間由操作系統決定的,具體為操作系統中的調度器。調度器決定了什麼時候停止一個進程以便讓其他進程有機會運行,同時挑選出一個其他的進程開始運行。
2.調度策略
在Linux上調度策略決定了調度器是如何選擇一個新進程的時間。調度策略與進程的類型有關,內核現有的調度策略如下:
#define SCHED_NORMAL 0#define SCHED_FIFO 1#define SCHED_RR 2#define SCHED_BATCH 3/* SCHED_ISO: reserved but not implemented yet */#define SCHED_IDLE 5
0: 默認的調度策略,針對的是普通進程。
1:針對實時進程的先進先出調度。適合對時間性要求比較高但每次運行時間比較短的進程。
2:針對的是實時進程的時間片輪轉調度。適合每次運行時間比較長得進程。
3:針對批處理進程的調度,適合那些非交互性且對cpu使用密集的進程。
SCHED_ISO:是內核的一個預留欄位,目前還沒有使用
5:適用於優先順序較低的後台進程。
註:每個進程的調度策略保存在進程描述符task_struct中的policy欄位
3.調度器中的機制
內核引入調度類(struct sched_class)說明了調度器應該具有哪些功能。內核中每種調度策略都有該調度類的一個實例。(比如:基於公平調度類為:fair_sched_class,基於實時進程的調度類實例為:rt_sched_class),該實例也是針對每種調度策略的具體實現。調度類封裝了不同調度策略的具體實現,屏蔽了各種調度策略的細節實現。
調度器核心函數schele()只需要調用調度類中的介面,完成進程的調度,完全不需要考慮調度策略的具體實現。調度類連接了調度函數和具體的調度策略。
武特師兄關於sche_class和sche_entity的解釋,一語中的。
調度類就是代表的各種調度策略,調度實體就是調度單位,這個實體通常是一個進程,但是自從引入了cgroup後,這個調度實體可能就不是一個進程了,而是一個組
- static inline struct task_struct *pick_next_task(struct rq *rq){ const struct sched_class *class; struct task_struct *p; /*
- * Optimization: we know that if all tasks are in
- * the fair class we can call that function directly:
- *///基於公平調度的普通進程
- if (likely(rq->nr_running == rq->cfs.nr_running)) {
- p = fair_sched_class.pick_next_task(rq); if (likely(p)) return p;
- }//基於實時調度的實時進程
- class = sched_class_highest; for ( ; ; ) {
- p = class->pick_next_task(rq); //實時進程的類
- if (p) return p; /*
- * Will never be NULL as the idle class always
- * returns a non-NULL p:
- */
- class = class->next; //rt->next = fair; fair->next = idle
- }
- }
- struct rt_prio_array {
- DECLARE_BITMAP(bitmap, MAX_RT_PRIO+1); /* include 1 bit for delimiter */
- struct list_head queue[MAX_RT_PRIO];
- };
- define DECLARE_BITMAP(name,bits)
- unsigned long name[BITS_TO_LONGS(bits)]
- #include <sched.h>#include <stdlib.h>#include <stdio.h>#include <errno.h>#define DEATH(mess) { perror(mess); exit(errno); }void printpolicy (int policy){ /* SCHED_NORMAL = SCHED_OTHER in user-space */
- if (policy == SCHED_OTHER) printf ("policy = SCHED_OTHER = %d ", policy); if (policy == SCHED_FIFO) printf ("policy = SCHED_FIFO = %d ", policy); if (policy == SCHED_RR) printf ("policy = SCHED_RR = %d ", policy);
- }int main (int argc, char **argv){ int policy; struct sched_param p; /* obtain current scheling policy for this process */
- //獲取進程調度的策略
- policy = sched_getscheler (0);
- printpolicy (policy); /* reset scheling policy */
- printf (" Trying sched_setscheler... ");
- policy = SCHED_FIFO;
- printpolicy (policy);
- p.sched_priority = 50; //設置優先順序為50
- if (sched_setscheler (0, policy, &p))
- DEATH ("sched_setscheler:"); printf ("p.sched_priority = %d ", p.sched_priority); exit (0);
- }
- [root@wang schele]# ./get_schele_policy policy = SCHED_OTHER = 0
- Trying sched_setscheler...
- policy = SCHED_FIFO = 1
- p.sched_priority = 50
4.schele()函數
linux 支持兩種類型的進程調度,實時進程和普通進程。實時進程採用SCHED_FIFO 和SCHED_RR調度策略,普通進程採用SCHED_NORMAL策略。
preempt_disable():禁止內核搶占
cpu_rq():獲取當前cpu對應的就緒隊列。
prev = rq->curr;獲取當前進程的描述符prev
switch_count = &prev->nivcsw;獲取當前進程的切換次數。
update_rq_clock() :更新就緒隊列上的時鍾
clear_tsk_need_resched()清楚當前進程prev的重新調度標志。
deactive_task():將當前進程從就緒隊列中刪除。
put_prev_task() :將當前進程重新放入就緒隊列
pick_next_task():在就緒隊列中挑選下一個將被執行的進程。
context_switch():進行prev和next兩個進程的切換。具體的切換代碼與體系架構有關,在switch_to()中通過一段匯編代碼實現。
post_schele():進行進程切換後的後期處理工作。
5.pick_next_task函數
選擇下一個將要被執行的進程無疑是一個很重要的過程,我們來看一下內核中代碼的實現
對以下這段代碼說明:
1.當rq中的運行隊列的個數(nr_running)和cfs中的nr_runing相等的時候,表示現在所有的都是普通進程,這時候就會調用cfs演算法中的pick_next_task(其實是pick_next_task_fair函數),當不相等的時候,則調用sched_class_highest(這是一個宏,指向的是實時進程),這下面的這個for(;;)循環中,首先是會在實時進程中選取要調度的程序(p = class->pick_next_task(rq);)。如果沒有選取到,會執行class=class->next;在class這個鏈表中有三種類型(fair,idle,rt).也就是說會調用到下一個調度類。
在這段代碼中體現了Linux所支持的兩種類型的進程,實時進程和普通進程。回顧下:實時進程可以採用SCHED_FIFO 和SCHED_RR調度策略,普通進程採用SCHED_NORMAL調度策略。
在這里首先說明一個結構體struct rq,這個結構體是調度器管理可運行狀態進程的最主要的數據結構。每個cpu上都有一個可運行的就緒隊列。剛才在pick_next_task函數中看到了在選擇下一個將要被執行的進程時實際上用的是struct rq上的普通進程的調度或者實時進程的調度,那麼具體是如何調度的呢?在實時調度中,為了實現O(1)的調度演算法,內核為每個優先順序維護一個運行隊列和一個DECLARE_BITMAP,內核根據DECLARE_BITMAP的bit數值找出非空的最高級優先隊列的編號,從而可以從非空的最高級優先隊列中取出進程進行運行。
我們來看下內核的實現
數組queue[i]裡面存放的是優先順序為i的進程隊列的鏈表頭。在結構體rt_prio_array 中有一個重要的數據構DECLARE_BITMAP,它在內核中的第一如下:
5.1對於實時進程的O(1)演算法
這個數據是用來作為進程隊列queue[MAX_PRIO]的索引點陣圖。bitmap中的每一位與queue[i]對應,當queue[i]的進程隊列不為空時,Bitmap的相應位就為1,否則為0,這樣就只需要通過匯編指令從進程優先順序由高到低的方向找到第一個為1的位置,則這個位置就是就緒隊列中最高的優先順序(函數sched_find_first_bit()就是用來實現該目的的)。那麼queue[index]->next就是要找的候選進程。
如果還是不懂,那就來看兩個圖
由結果可以看出當nice的值越小的時候,其睡眠時間越短,則表示其優先順序升高了。
7.關於獲取和設置優先順序的系統調用:sched_getscheler()和sched_setscheler
輸出結果:
可以看出進程的優先順序已經被改變。
Ⅷ Linux中常見IO調度器
對於磁碟I/O,Linux提供了cfq, deadline和noop三種調度策略
考慮到硬體配置、實際應用場景(讀寫比例、順序還是隨機讀寫)的差異,上面的簡單解釋對於實際選擇沒有太大幫助,實際該選擇哪個基本還是要實測來驗證。不過下面幾條說明供參考:
NOOP全稱No Operation,中文名稱電梯式調度器,該演算法實現了最簡單的FIFO隊列,所有I/O請求大致按照先來後到的順序進行操作。NOOP實現了一個簡單的FIFO隊列,它像電梯的工作方式一樣對I/O請求進行組織。它是基於先入先出(FIFO)隊列概念的 Linux 內核里最簡單的I/O 調度器。此調度程序最適合於固態硬碟。
Deadline翻譯成中文是截止時間調度器,是對Linus Elevator的一種改進,它避免有些請求太長時間不能被處理。另外可以區分對待讀操作和寫操作。DEADLINE額外分別為讀I/O和寫I/O提供了FIFO隊列。
Deadline對讀寫request進行了分類管理,並且在調度處理的過程中讀請求具有較高優先順序。這主要是因為讀請求往往是同步操作,對延遲時間比較敏感,而寫操作往往是非同步操作,可以盡可能的將相鄰訪問地址的請求進行合並,但是,合並的效率越高,延遲時間會越長。因此,為了區別對待讀寫請求類型,deadline採用兩條鏈表對讀寫請求進行分類管理。但是,引入分類管理之後,在讀優先的情況下,寫請求如果長時間得到不到調度,會出現餓死的情況,因此,deadline演算法考慮了寫餓死的情況,從而保證在讀優先調度的情況下,寫請求不會被餓死。
總體來講,deadline演算法對request進行了優先權控制調度,主要表現在如下幾個方面:
CFQ全稱Completely Fair Scheler ,中文名稱完全公平調度器,它是現在許多 Linux 發行版的默認調度器,CFQ是內核默認選擇的I/O調度器。它將由進程提交的同步請求放到多個進程隊列中,然後為每個隊列分配時間片以訪問磁碟。 對於通用的伺服器是最好的選擇,CFQ均勻地分布對I/O帶寬的訪問 。CFQ為每個進程和線程,單獨創建一個隊列來管理該進程所產生的請求,以此來保證每個進程都能被很好的分配到I/O帶寬,I/O調度器每次執行一個進程的4次請求。該演算法的特點是按照I/O請求的地址進行排序,而不是按照先來後到的順序來進行響應。簡單來說就是給所有同步進程分配時間片,然後才排隊訪問磁碟。
多隊列無操作I / O調度程序。不對請求進行重新排序,最小的開銷。NVME等快速隨機I / O設備的理想選擇。
這是對最後期限I / O調度程序的改編,但設計用於 多隊列設備。一個出色的多面手,CPU開銷相當低。