成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Linux操作系統實戰:調度器源碼分析

系統 Linux
在生成的調度報告中,我們可以看到每個進程的調度時間、調度次數、等待時間等信息。例如,報告中可能會顯示某個進程在某段時間內被調度了 100 次,總運行時間為 5 秒,等待時間為 3 秒等。

在當今的技術領域中,Linux 系統猶如一座巍峨的高山,屹立于服務器、開發環境等眾多關鍵場景的核心位置。據統計,全球超 90% 的超級計算機運行著 Linux 操作系統,在服務器市場中,Linux 更是憑借其高穩定性、安全性以及開源特性,占據了相當可觀的份額 ,眾多大型網站、企業級應用的服務器都基于 Linux 搭建。對于開發者而言,Linux 也是不可或缺的開發環境,大量的開源軟件和豐富的開發工具都能在 Linux 上完美運行。

在 Linux 系統中,進程是其核心概念,是系統進行資源分配和調度的基本單位??梢哉f,Linux 系統中的一切活動幾乎都離不開進程的參與,從啟動一個簡單的應用程序,到執行復雜的系統命令,再到管理系統資源,進程就像幕后的 “隱形引擎”,驅動著整個 Linux 系統的穩定運行。

一、Linux調度器簡介

隨著時代的發展,linux也從其初始版本穩步發展到今天,從2.4的非搶占內核發展到今天的可搶占內核,調度器無論從代碼結構還是設計思想上也都發生了翻天覆地的變化,其普通進程的調度算法也從O(1)到現在的CFS,一個好的調度算法應當考慮以下幾個方面:

  • 公平:保證每個進程得到合理的CPU時間。
  • 高效:使CPU保持忙碌狀態,即總是有進程在CPU上運行。
  • 響應時間:使交互用戶的響應時間盡可能短。
  • 周轉時間:使批處理用戶等待輸出的時間盡可能短。
  • 吞吐量:使單位時間內處理的進程數量盡可能多。
  • 負載均衡:在多核多處理器系統中提供更高的性能

而整個調度系統至少包含兩種調度算法,是分別針對實時進程和普通進程,所以在整個linux內核中,實時進程和普通進程是并存的,但它們使用的調度算法并不相同,普通進程使用的是CFS調度算法(紅黑樹調度)。之后會介紹調度器是怎么調度這兩種進程。

1.1進程分類

在linux中,進程主要分為兩種,一種為實時進程,一種為普通進程。

  • 實時進程:對系統的響應時間要求很高,它們需要短的響應時間,并且這個時間的變化非常小,典型的實時進程有音樂播放器,視頻播放器等。
  • 普通進程:包括交互進程和非交互進程,交互進程如文本編輯器,它會不斷的休眠,又不斷地通過鼠標鍵盤進行喚醒,而非交互進程就如后臺維護進程,他們對IO,響應時間沒有很高的要求,比如編譯器。

它們在linux內核運行時是共存的,實時進程的優先級為0~99,實時進程優先級不會在運行期間改變(靜態優先級),而普通進程的優先級為100~139,普通進程的優先級會在內核運行期間進行相應的改變(動態優先級)。

1.2調度策略

在linux系統中,調度策略分為:

  • SCHED_NORMAL:普通進程使用的調度策略,現在此調度策略使用的是CFS調度器。
  • SCHED_FIFO:實時進程使用的調度策略,此調度策略的進程一旦使用CPU則一直運行,直到有比其更高優先級的實時進程進入隊列,或者其自動放棄CPU,適用于時間性要求比較高,但每次運行時間比較短的進程。
  • SCHED_RR:實時進程使用的時間片輪轉法策略,實時進程的時間片用完后,調度器將其放到隊列末尾,這樣每個實時進程都可以執行一段時間。適用于每次運行時間比較長的實時進程。

1.3調度選擇

首先,我們需要清楚,什么樣的進程會進入調度器進行選擇,就是處于TASK_RUNNING狀態的進程,而其他狀態下的進程都不會進入調度器進行調度。系統發生調度的時機如下:

  • 調用cond_resched()時
  • 顯式調用schedule()時
  • 從系統調用或者異常中斷返回用戶空間時
  • 從中斷上下文返回用戶空間時

當開啟內核搶占(默認開啟)時,會多出幾個調度時機,如下:

  • 在系統調用或者異常中斷上下文中調用preempt_enable()時(多次調用preempt_enable()時,系統只會在最后一次調用時會調度)
  • 在中斷上下文中,從中斷處理函數返回到可搶占的上下文時(這里是中斷下半部,中斷上半部實際上會關中斷,而新的中斷只會被登記,由于上半部處理很快,上半部處理完成后才會執行新的中斷信號,這樣就形成了中斷可重入, 但是即使是中斷下半部, 也是不能夠被調度的)

而在系統啟動調度器初始化時會初始化一個調度定時器,調度定時器每隔一定時間執行一個中斷,在中斷會對當前運行進程運行時間進行更新,如果進程需要被調度,在調度定時器中斷中會設置一個調度標志位,之后從定時器中斷返回,因為上面已經提到從中斷上下文返回時是可能有調度時機的,如果定時器中斷返回時正好是返回到用戶態空間, 而且調度標志位又置位了, 這時候就會做進程切換. 在內核源碼的匯編代碼中所有中斷返回處理都必須去判斷調度標志位是否設置,如設置則執行schedule()進行調度。

而我們知道實時進程和普通進程是共存的,調度器是怎么協調它們之間的調度的呢,其實很簡單,每次調度時,會先在實時進程運行隊列中查看是否有可運行的實時進程,如果沒有,再去普通進程運行隊列找下一個可運行的普通進程,如果也沒有,則調度器會使用idle進程進行運行。之后的章節會放上代碼進行詳細說明。

系統并不是每時每刻都允許調度的發生,當處于中斷期間的時候(無論是上半部還是下半部),調度是被系統禁止的,之后中斷過后才重新允許調度。而對于異常,系統并不會禁止調度,也就是在異常上下文中,系統是有可能發生調度的。

二、調度器理論基礎

2.1調度器的關鍵作用與設計目標

Linux調度器的核心職責是進行CPU資源分配,在多任務環境下,系統中存在大量等待執行的進程,調度器如同一位精準的分配者,決定每個進程何時能夠獲得 CPU 時間片,確保各個進程都能在有限的CPU資源下有序運行。

為了實現高效穩定的系統運行,調度器有著明確的設計目標。公平性是其中的重要一環,它要求調度器避免某些進程長時間占用 CPU,而其他進程卻長時間處于等待狀態,確保每個進程都能按照其權重合理地分享 CPU 時間。以多個用戶同時使用系統資源為例,公平的調度能讓每個用戶的任務都能得到及時處理,不會因為某個用戶的大型任務而導致其他用戶的任務被無限期擱置。

快速響應也是調度器追求的目標之一。在用戶進行交互操作時,如點擊應用程序圖標、輸入命令等,調度器需要迅速將 CPU 資源分配給相應的進程,以確保系統能夠快速響應用戶的請求,提供流暢的交互體驗。這對于桌面環境和實時應用來說尤為重要,比如在進行視頻會議時,調度器需要快速調度相關音頻、視頻處理進程,保證音視頻的實時性和流暢性。

高吞吐量意味著調度器要盡可能地提高系統在單位時間內完成的任務數量。它通過合理安排進程的執行順序和時間片,充分利用 CPU 資源,減少 CPU 的空閑時間,從而提高系統的整體處理能力。在服務器環境中,高吞吐量可以讓服務器同時處理大量的請求,提高服務的效率和質量。

在如今注重能源效率的時代,低功耗也是調度器需要考慮的因素。對于移動設備和一些對功耗有嚴格要求的服務器,調度器通過優化調度策略,減少 CPU 不必要的工作時間,在保證系統性能的前提下,降低 CPU 的功耗,延長設備的續航時間或降低能源消耗。

2.2核心調度因素剖析

(1)優先級

優先級是調度器決定進程執行順序的重要依據。在 Linux 系統中,進程分為實時進程和普通進程,它們有著不同的優先級范圍。實時進程的優先級通常較高,范圍一般是 1 - 99(數值越大優先級越高) ,普通進程則通過 Nice 值來表示優先級,范圍是 - 20 - 19(數值越小優先級越高)。

例如,在一個同時運行視頻播放(實時進程)和文件備份(普通進程)的系統中,視頻播放進程由于其對實時性要求高,被賦予較高的優先級。當 CPU 資源有限時,調度器會優先調度視頻播放進程,確保視頻的流暢播放,而文件備份進程則需要等待視頻播放進程暫時釋放 CPU 資源后才有機會執行。這體現了優先級在調度中的關鍵作用,它能根據進程的重要性和實時性需求來合理分配 CPU 資源。

(2)時間片

時間片是指進程一次能夠占用 CPU 的時間長度。在早期的調度算法中,時間片通常是固定的,而現代的 Linux 調度器如 CFS(完全公平調度器)雖然不再使用傳統意義上固定的時間片概念,但也有類似的時間分配機制。在 CFS 中,每個進程都有一個虛擬運行時間(vruntime),它根據進程的權重來計算進程實際占用 CPU 的時間,從而實現公平的調度。

以一個簡單的多進程場景為例,假設有兩個進程 A 和 B,進程 A 的權重較高,進程 B 的權重較低。在 CFS 調度下,雖然沒有固定的時間片分配,但進程 A 會因為其權重高,在相同的物理時間內,其虛擬運行時間增長相對較慢,從而有更多機會獲得 CPU 資源,而進程 B 則會根據其權重相對較少地獲得 CPU 執行時間,這就保證了在時間分配上的公平性,同時也實現了類似于時間片分配的效果,使得各個進程都能得到執行的機會 。

(3)I/O 需求

進程的 I/O 需求也是調度器需要考慮的重要因素。I/O 密集型進程通常大部分時間用于等待 I/O 操作完成,如文件讀寫、網絡數據傳輸等,它們對 CPU 的占用時間相對較少。而 CPU 密集型進程則主要進行大量的計算工作,長時間占用 CPU 資源。

調度器會根據進程的 I/O 特性來調整調度策略。對于 I/O 密集型進程,調度器會盡量在 I/O 操作完成后及時調度它們,以減少 I/O 設備的空閑時間,提高系統的整體效率。比如在一個同時運行數據庫查詢(I/O 密集型)和科學計算(CPU 密集型)的系統中,當數據庫查詢操作完成 I/O 讀取數據后,調度器會迅速調度該進程,讓其盡快處理讀取到的數據,而不是讓其等待較長時間,這樣可以充分利用 I/O 設備和 CPU 資源,提高系統的響應速度和吞吐量。

2.3經典調度策略解讀

①FIFO(First - In - First - Out,先進先出)

原理:FIFO 調度策略按照進程進入就緒隊列的先后順序進行調度。當一個進程進入就緒隊列后,它會排在隊列末尾,調度器總是選擇隊列頭部的進程運行,并且該進程會一直運行直到它完成任務、主動放棄 CPU(例如進行 I/O 操作進入阻塞狀態)或者被更高優先級的實時進程搶占。

特點:這種調度策略實現簡單,不需要復雜的算法來計算調度順序。但它存在明顯的缺點,對于長進程而言,如果它先進入就緒隊列,就會一直占用 CPU,導致后面進入的短進程等待時間過長,這在實際應用中可能會造成系統響應性變差,用戶體驗不佳。

適用場景:FIFO 適用于一些對順序性要求較高且任務執行時間相對固定的場景,比如某些批處理任務,它們之間沒有嚴格的時間限制和優先級差異,按照先后順序依次執行即可。

②RR(Round - Robin,輪轉調度)

原理:RR 調度策略為每個進程分配一個固定的時間片,當進程的時間片用完后,它會被放回就緒隊列末尾,調度器接著調度下一個進程。這樣所有處于就緒隊列的進程會按照輪轉的方式依次獲得 CPU 時間片進行執行。

特點:RR 調度策略的公平性較好,每個進程都有機會在一定時間間隔內獲得 CPU 資源,不會出現某個進程長時間被餓死的情況。然而,它的時間片大小設置較為關鍵,如果時間片設置過大,會退化為類似 FIFO 的調度方式,影響短進程的響應;如果時間片設置過小,會導致進程上下文切換過于頻繁,增加系統開銷。

適用場景:RR 適用于那些對響應時間要求較高且進程執行時間差異不大的場景,如早期的分時操作系統,多個用戶通過終端同時連接到系統,RR 調度可以保證每個用戶的任務都能得到及時響應,感覺系統是在為自己單獨服務。

③CFS(Completely Fair Scheduler,完全公平調度器)

原理:CFS 是 Linux 內核中廣泛使用的調度器,它基于虛擬運行時間(vruntime)來實現調度。每個進程都有一個 vruntime,它會隨著進程占用 CPU 時間的增加而增長,增長的速度與進程的權重成反比。調度器總是選擇 vruntime 最小的進程運行,這樣權重高的進程會獲得更多的 CPU 時間,從而實現了公平的調度。CFS 使用紅黑樹來維護就緒隊列中的進程,通過紅黑樹的高效查找功能,可以快速找到 vruntime 最小的進程。

特點:CFS 的優點在于它能夠在不同類型的進程之間實現較好的公平性,無論是 CPU 密集型進程還是 I/O 密集型進程,都能根據其權重合理地獲得 CPU 資源。它還支持多核 CPU 和 CPU 熱插拔,能夠動態調整調度策略以適應系統硬件的變化。同時,CFS 通過引入虛擬運行時間的概念,避免了傳統時間片調度方式中時間片大小難以確定的問題。

適用場景:CFS 適用于大多數通用場景,包括桌面環境和服務器環境。在桌面環境中,它可以保證用戶的交互操作(如鼠標點擊、鍵盤輸入等)能夠得到快速響應,同時也能合理調度后臺的各種任務(如文件同步、系統更新等)。在服務器環境中,CFS 可以高效地管理多個用戶的任務請求,確保每個用戶的服務質量,提高服務器的整體性能和吞吐量。

三、調度器源碼深度解析

3.1源碼環境搭建與關鍵文件定位

在開始深入剖析 Linux 調度器源碼之前,搭建一個合適的源碼分析環境是至關重要的。首先,你需要獲取 Linux 內核源碼??梢詮墓俜降?Linux 內核官網(https://www.kernel.org/)下載你想要分析的內核版本源碼。例如,若要分析較新的穩定版本,可選擇對應的.tar.xz 或.tar.gz 壓縮包下載。

下載完成后,解壓源碼包。假設你將源碼解壓到了/home/user/linux - kernel目錄下。接下來,安裝一些必要的工具,如make、gcc等編譯工具。在基于 Debian 或 Ubuntu 的系統中,可以通過以下命令安裝:

sudo apt - get install build - essential

在基于 Red Hat 或 CentOS 的系統中,使用以下命令:

sudo yum install make gcc

對于源碼閱讀,一款好用的工具能大大提高效率。Source Insight 是一個不錯的選擇,它能夠提供語法高亮、代碼導航等功能,方便我們快速定位和理解代碼。安裝并打開 Source Insight 后,通過 “Project” -> “New Project” 創建一個新項目,然后將解壓后的 Linux 內核源碼目錄添加到項目中,Source Insight 會自動解析源碼,生成符號數據庫,便于后續的代碼瀏覽和分析。

在 Linux 內核源碼中,調度器相關的關鍵文件主要位于kernel/sched目錄下。其中,core.c文件包含了調度器的核心邏輯,如調度入口函數schedule等;fair.c實現了完全公平調度器(CFS)的相關代碼,包括 CFS 調度類的各種操作函數;rt.c則主要負責實時調度相關的實現,處理實時進程的調度邏輯。這些文件相互協作,共同實現了 Linux 調度器的各種功能 。

3.2核心數據結構剖析

①運行隊列(rq)

運行隊列是調度器管理進程的關鍵數據結構之一,每個 CPU 核心都有一個對應的運行隊列。在kernel/sched/sched.h文件中可以找到其定義。它包含了當前正在運行的任務指針curr,用于指向當前在該 CPU 上執行的進程;nr_running記錄了就緒隊列中處于可運行狀態的任務數量,調度器可以通過這個值快速了解當前 CPU 的負載情況。

例如,在多核系統中,每個 CPU 核心的運行隊列可以獨立管理自己的就緒進程,當某個 CPU 核心的nr_running值較高時,說明該核心的負載較重,可能需要進行負載均衡操作,將部分進程遷移到其他負載較輕的 CPU 核心上。運行隊列還包含了不同調度類的隊列,如完全公平調度隊列cfs和實時調度隊列rt,這種設計使得不同類型的進程可以按照各自的調度策略進行管理和調度。

②調度類(sched_class)

調度類是一個抽象的概念,它為不同類型的進程提供了不同的調度策略接口。在include/linux/sched.h中定義,每個調度類都有一系列的函數指針,用于實現將任務添加到就緒隊列(enqueue_task)、從就緒隊列移除任務(dequeue_task)、檢查當前任務是否需要被搶占(check_preempt_curr)、選擇下一個要運行的任務(pick_next_task)等操作。

以完全公平調度類fair_sched_class為例,它主要用于普通進程的調度,通過實現這些函數指針,實現了基于虛擬運行時間的公平調度算法。當一個新的普通進程進入就緒狀態時,會調用fair_sched_class的enqueue_task函數將其添加到 CFS 就緒隊列中;當需要選擇下一個運行的普通進程時,會調用pick_next_task函數從 CFS 就緒隊列中選擇虛擬運行時間最小的進程。

③調度實體(sched_entity)

調度實體是連接調度器與進程的橋梁,每個進程都有一個對應的調度實體。在include/linux/sched.h中定義,它包含了進程的調度相關參數,如負載權重load,用于計算進程在調度時的相對優先級;虛擬運行時間vruntime,這是 CFS 調度算法的核心參數,進程的vruntime會隨著其占用 CPU 時間的增加而增長,增長速度與進程的權重成反比。

比如,有兩個進程 A 和 B,進程 A 的權重較高,進程 B 的權重較低。在運行過程中,進程A的vruntime增長相對較慢,這意味著它在相同的物理時間內,能獲得更多的 CPU 執行時間,從而實現了公平調度。調度實體還包含紅黑樹節點run_node,用于將調度實體組織成紅黑樹結構,方便調度器快速查找和管理就緒隊列中的進程 。

這些核心數據結構相互關聯,運行隊列通過調度類來管理不同類型的調度實體,調度類通過操作調度實體來實現具體的調度策略,它們共同構成了 Linux 調度器高效運行的基礎。

3.3調度入口函數詳解

調度入口函數schedule是整個調度流程的起點,定義在kernel/sched/core.c文件中 。當系統需要進行進程調度時,無論是因為時間片耗盡、進程主動放棄 CPU 還是有更高優先級的進程就緒等原因,最終都會直接或間接調用schedule函數。

asmlinkage __visible void __sched schedule(void)
{
    struct task_struct *tsk = current;
    sched_submit_work(tsk);
    do {
        preempt_disable();
        __schedule(SM_NONE);
        sched_preempt_enable_no_resched();
    } while (need_resched());
    sched_update_worker(tsk);
}
EXPORT_SYMBOL(schedule);

首先,schedule函數獲取當前任務結構體的指針tsk,這里的current是一個宏,指向當前正在運行的進程。然后,通過sched_submit_work函數將當前任務提交到調度工作隊列中,這一步主要是為了處理一些與任務相關的工作,比如更新任務的一些狀態信息等。

接下來,進入一個循環,在循環中,首先調用preempt_disable禁用搶占,這是為了保證在調度過程中,不會被其他高優先級的搶占事件打斷,確保調度操作的原子性。然后調用實際的調度函數__schedule,并傳入調度策略參數SM_NONE,__schedule函數負責執行具體的調度操作,包括選擇下一個要運行的進程以及進行進程上下文切換等關鍵步驟。

完成調度操作后,調用sched_preempt_enable_no_resched啟用搶占,但不進行重新調度,這是因為在調度過程中,可能已經處理了所有需要調度的情況,此時不需要立即再次調度。

循環會一直執行,直到need_resched函數返回假,即表示沒有需要重新調度的任務為止。最后,通過sched_update_worker函數更新工作隊列中任務的狀態,確保任務的狀態信息與實際的調度情況一致 。

schedule函數作為調度入口,它有條不紊地協調了各個調度步驟,為系統的進程調度提供了一個統一的入口點,使得整個調度流程能夠有序地進行,保證了系統中進程對 CPU 資源的合理分配和高效利用。

3.4選擇下一個進程的實現邏輯

在調度過程中,選擇下一個要執行的進程是關鍵步驟之一,這主要由pick_next_task等函數來完成。pick_next_task函數定義在kernel/sched/core.c中,其實現邏輯較為復雜,涉及到多個調度類和調度策略的協同工作。

static inline struct task_struct *__pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
    const struct sched_class *class;
    struct task_struct *p;
    if (likely(!sched_class_above(prev->sched_class, &fair_sched_class) && rq->nr_running == rq->cfs.h_nr_running)) {
        p = pick_next_task_fair(rq, prev, rf);
        if (unlikely(p == RETRY_TASK))
            goto restart;
        if (!p) {
            put_prev_task(rq, prev);
            p = pick_next_task_idle(rq);
        }
        return p;
    }
restart:
    put_prev_task_balance(rq, prev, rf);
    for_each_class(class) {
        p = class->pick_next_task(rq);
        if (p)
            return p;
    }
    BUG();
}

首先,函數會進行一個優化判斷。如果前一個任務是公平調度類中的任務,且運行隊列中的任務數與 CFS 隊列中的任務數相等,這意味著當前運行隊列中的任務全部屬于公平調度類,此時可以直接調用pick_next_task_fair函數從 CFS 隊列中選擇下一個公平調度類任務。pick_next_task_fair函數在kernel/sched/fair.c中定義,它主要根據調度實體的虛擬運行時間vruntime來選擇下一個運行的任務,總是選擇vruntime最小的任務,以實現公平調度。

如果在調用pick_next_task_fair函數時選擇任務失敗(返回RETRY_TASK),則跳轉到restart標簽處,重新進行任務選擇。如果pick_next_task_fair函數返回空指針,表示 CFS 隊列中沒有可運行的任務,此時會調用put_prev_task函數將前一個任務放回隊列,并調用pick_next_task_idle函數選擇下一個空轉調度類任務,空轉調度類任務通常在系統空閑時運行,以避免 CPU 空閑浪費。

如果不滿足上述優化條件,即運行隊列中存在其他調度類的任務,函數會通過put_prev_task_balance函數將前一個任務放回隊列進行重新平衡。然后,通過for_each_class宏遍歷所有調度類,依次調用每個調度類的pick_next_task函數來選擇下一個任務。如果在遍歷過程中找到了可運行的任務,則返回該任務;如果遍歷完所有調度類都沒有找到可運行的任務,會觸發BUG,因為系統中應該始終存在可運行的任務(至少有空轉任務) 。

pick_next_task函數通過這種復雜而有序的邏輯,能夠根據系統中不同調度類任務的狀態和優先級,準確地選擇出下一個要執行的進程,保證了調度的公平性和高效性,滿足了不同類型進程對 CPU 資源的需求。

3.5進程切換的底層實現

進程切換是調度器實現多任務并發執行的關鍵操作,它使得 CPU 能夠在不同進程之間快速切換,實現多個進程看似同時運行的效果。在 Linux 調度器中,進程切換主要由context_switch函數完成,該函數定義在kernel/sched/core.c中。

static inline void context_switch(struct rq *rq, struct task_struct *prev, struct task_struct *next, unsigned long *switch_count)
{
    prepare_task_switch(rq, prev, next);
    /*
     * For paravirt, this is coupled with an exit in switch_to to
     * combine the page table reload and the switch backend into
     * one hypercall.
     */
    arch_start_context_switch(prev);
    /*
     * kernel -> kernel   lazy + transfer active
     *   user -> kernel   lazy + mmgrab() active
     *
     * kernel -> user     switch + mmdrop() active
     *   user -> user     switch
     */
    if (!next->mm) {                                /* 1 */
        enter_lazy_tlb(prev->active_mm, next);
        next->active_mm = prev->active_mm;
        if (prev->mm)                                /* 2 */
            mmdrop(prev->mm);
    } else {
        membarrier_switch_mm(rq, prev->active_mm, next->mm);
        enter_lazy_tlb(next->mm, next);
        next->active_mm = next->mm;
        if (prev->mm)
            mmdrop(prev->mm);
    }
    rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
    rq->skip_clock_update = 0;
    /* Here we just switch the register state and the stack. */
    switch_to(prev, next, prev);
    barrier();
    sched_preempt_enable_no_resched();
    finish_task_switch(prev, next);
}

context_switch函數首先調用prepare_task_switch函數進行一些切換前的準備工作,比如更新一些與任務相關的狀態信息等。然后,調用arch_start_context_switch函數,這個函數主要是與體系結構相關的操作,用于啟動上下文切換的一些底層準備工作,不同的硬件體系結構(如 x86、ARM 等)會有不同的實現。

接下來,根據切換的目標進程next是否有自己的內存管理結構體mm來進行不同的處理。如果next沒有自己的mm(例如內核線程,它共享內核空間,沒有獨立的用戶空間內存映射),則執行enter_lazy_tlb函數進入延遲 TLB(Translation Lookaside Buffer,地址轉換后備緩沖器)模式,將next的活動內存管理結構體active_mm設置為prev的active_mm,并在prev有自己的mm時調用mmdrop函數減少prev內存管理結構體的引用計數。

如果next有自己的mm,則先調用membarrier_switch_mm函數處理內存屏障相關的操作,然后進入延遲 TLB 模式,將next的active_mm設置為它自己的mm,同樣在prev有mm時調用mmdrop函數。

完成內存相關的切換操作后,調用switch_to函數進行真正的寄存器狀態和棧的切換。switch_to函數是一個非常底層的函數,它負責保存當前進程(prev)的寄存器狀態和棧指針,然后恢復下一個進程(next)的寄存器狀態和棧指針,使得 CPU 能夠從next進程的上次執行點繼續執行。

在switch_to函數執行完成后,通過barrier函數提供內存屏障,確保內存操作的順序性。最后,調用sched_preempt_enable_no_resched函數啟用搶占但不進行重新調度,并調用finish_task_switch函數完成任務切換的后續工作,比如更新一些統計信息等 。

context_switch函數通過這一系列復雜而精細的操作,實現了進程在 CPU 上的高效切換,保證了多任務系統中各個進程能夠快速、有序地交替執行,是 Linux 調度器實現多任務并發的重要基礎。

四、調度器初始化

通過代碼說明調度器在系統啟動初始化階段是如何初始化和工作的

4.1 init_task和init進程

當linux啟動時,最先會通過匯編代碼進行硬件和CPU的初始化,最后會跳轉到C代碼,而最初跳轉到的C代碼入口為:

/* 代碼地址:linux/init/Main.c */
asmlinkage __visible void __init start_kernel(void)

在start_kerenl函數中,進行了系統啟動過程中幾乎所有重要的初始化(有一部分在boot中初始化,有一部分在start_kernel之前的匯編代碼進行初始化),包括內存、頁表、必要數據結構、信號、調度器、硬件設備等。而這些初始化是由誰來負責的?就是由init_task這個進程。

init_task是靜態定義的一個進程,也就是說當內核被放入內存時,它就已經存在,它沒有自己的用戶空間,一直處于內核空間中運行,并且也只處于內核空間運行。當它執行到最后,將start_kernel中所有的初始化執行完成后,會在內核中啟動一個kernel_init內核線程和一個kthreadd內核線程,kernel_init內核線程執行到最后會通過execve系統調用執行轉變為我們所熟悉的init進程,而kthreadd內核線程是內核用于管理調度其他的內核線程的守護線程。在最后init_task將變成一個idle進程,用于在CPU沒有進程運行時運行它,它在此時僅僅用于空轉。

4.2 sched_init

在start_kernel中對調度器進行初始化的函數就是sched_init,其主要工作為:

  • 對相關數據結構分配內存
  • 初始化root_task_group
  • 初始化每個CPU的rq隊列(包括其中的cfs隊列和實時進程隊列)
  • 將init_task進程轉變為idle進程

需要說明的是init_task在這里會被轉變為idle進程,但是它還會繼續執行初始化工作,相當于這里只是給init_task掛個idle進程的名號,它其實還是init_task進程,只有到最后init_task進程開啟了kernel_init和kthreadd進程之后,才轉變為真正意義上的idle進程。

/* 代碼路徑:內核源代碼目錄/kernel/sched/Core.c */

/* 執行到此時內核只有一個進程init_task,current就為init_task。之后的init進程在初始化到最后的rest_init中啟動 */
void __init sched_init(void)
{
    int i, j;
    unsigned long alloc_size = 0, ptr;

    /* 計算所需要分配的數據結構空間 */
#ifdef CONFIG_FAIR_GROUP_SCHED
    alloc_size += 2 * nr_cpu_ids * sizeof(void **);
#endif

#ifdef CONFIG_RT_GROUP_SCHED
    alloc_size += 2 * nr_cpu_ids * sizeof(void **);
#endif
#ifdef CONFIG_CPUMASK_OFFSTACK
    alloc_size += num_possible_cpus() * cpumask_size();
#endif
    if (alloc_size) {
        /* 分配內存 */
        ptr = (unsigned long)kzalloc(alloc_size, GFP_NOWAIT);

#ifdef CONFIG_FAIR_GROUP_SCHED
        /* 設置 root_task_group 每個CPU上的調度實體指針se */
        root_task_group.se = (struct sched_entity **)ptr;
        ptr += nr_cpu_ids * sizeof(void **);

        /* 設置 root_task_group 每個CPU上的CFS運行隊列指針cfs_rq */
        root_task_group.cfs_rq = (struct cfs_rq **)ptr;
        ptr += nr_cpu_ids * sizeof(void **);

#endif /* CONFIG_FAIR_GROUP_SCHED */
#ifdef CONFIG_RT_GROUP_SCHED
        /* 設置 root_task_group 每個CPU上的實時調度實體指針se */
        root_task_group.rt_se = (struct sched_rt_entity **)ptr;
        ptr += nr_cpu_ids * sizeof(void **);

        /* 設置 root_task_group 每個CPU上的實時運行隊列指針rt_rq */
        root_task_group.rt_rq = (struct rt_rq **)ptr;
        ptr += nr_cpu_ids * sizeof(void **);

#endif /* CONFIG_RT_GROUP_SCHED */
#ifdef CONFIG_CPUMASK_OFFSTACK
        for_each_possible_cpu(i) {
            per_cpu(load_balance_mask, i) = (void *)ptr;
            ptr += cpumask_size();
        }
#endif /* CONFIG_CPUMASK_OFFSTACK */
    }
    /* 初始化實時進程的帶寬限制,用于設置實時進程在CPU中所占用比的 */
    init_rt_bandwidth(&def_rt_bandwidth,
            global_rt_period(), global_rt_runtime());
    init_dl_bandwidth(&def_dl_bandwidth,
            global_rt_period(), global_rt_runtime());

#ifdef CONFIG_SMP
    /* 初始化默認的調度域,調度域包含一個或多個CPU,負載均衡是在調度域內執行的,相互之間隔離 */
    init_defrootdomain();
#endif

#ifdef CONFIG_RT_GROUP_SCHED
    /* 初始化實時進程的帶寬限制,用于設置實時進程在CPU中所占用比的 */
    init_rt_bandwidth(&root_task_group.rt_bandwidth,
            global_rt_period(), global_rt_runtime());
#endif /* CONFIG_RT_GROUP_SCHED */

#ifdef CONFIG_CGROUP_SCHED
    /* 將分配好空間的 root_task_group 加入 task_groups 鏈表 */
    list_add(&root_task_group.list, &task_groups);
    INIT_LIST_HEAD(&root_task_group.children);
    INIT_LIST_HEAD(&root_task_group.siblings);
    /* 自動分組初始化,每個tty(控制臺)動態的創建進程組,這樣就可以降低高負載情況下的桌面延遲 */
    autogroup_init(&init_task);

#endif /* CONFIG_CGROUP_SCHED */
    /* 遍歷設置每一個CPU */
    for_each_possible_cpu(i) {
        struct rq *rq;
        /* 獲取CPUi的rq隊列 */
        rq = cpu_rq(i);
        /* 初始化rq隊列的自旋鎖 */
        raw_spin_lock_init(&rq->lock);
        /* CPU運行隊列中調度實體(sched_entity)數量為0 */
        rq->nr_running = 0;
        /* CPU負載 */
        rq->calc_load_active = 0;
        /* 負載下次更新時間 */
        rq->calc_load_update = jiffies + LOAD_FREQ;
        /* 初始化CFS運行隊列 */
        init_cfs_rq(&rq->cfs);
        /* 初始化實時進程運行隊列 */
        init_rt_rq(&rq->rt, rq);
        init_dl_rq(&rq->dl, rq);
#ifdef CONFIG_FAIR_GROUP_SCHED
        root_task_group.shares = ROOT_TASK_GROUP_LOAD;
        INIT_LIST_HEAD(&rq->leaf_cfs_rq_list);
        /*
         * How much cpu bandwidth does root_task_group get?
         *
         * In case of task-groups formed thr' the cgroup filesystem, it
         * gets 100% of the cpu resources in the system. This overall
         * system cpu resource is divided among the tasks of
         * root_task_group and its child task-groups in a fair manner,
         * based on each entity's (task or task-group's) weight
         * (se->load.weight).
         *
         * In other words, if root_task_group has 10 tasks of weight
         * 1024) and two child groups A0 and A1 (of weight 1024 each),
         * then A0's share of the cpu resource is:
         *
         *    A0's bandwidth = 1024 / (10*1024 + 1024 + 1024) = 8.33%
         *
         * We achieve this by letting root_task_group's tasks sit
         * directly in rq->cfs (i.e root_task_group->se[] = NULL).
         */
        /* 初始化CFS的帶寬限制,用于設置普通進程在CPU中所占用比的 */
        init_cfs_bandwidth(&root_task_group.cfs_bandwidth);
        init_tg_cfs_entry(&root_task_group, &rq->cfs, NULL, i, NULL);
#endif /* CONFIG_FAIR_GROUP_SCHED */

        rq->rt.rt_runtime = def_rt_bandwidth.rt_runtime;
#ifdef CONFIG_RT_GROUP_SCHED
        init_tg_rt_entry(&root_task_group, &rq->rt, NULL, i, NULL);
#endif
        /* 初始化該隊列所保存的每個CPU的負載情況 */
        for (j = 0; j < CPU_LOAD_IDX_MAX; j++)
            rq->cpu_load[j] = 0;
        /* 該隊列最后一次更新cpu_load的時間值為當前 */
        rq->last_load_update_tick = jiffies;

#ifdef CONFIG_SMP
        /* 這些參數都是負載均衡使用的 */
        rq->sd = NULL;
        rq->rd = NULL;
        rq->cpu_capacity = SCHED_CAPACITY_SCALE;
        rq->post_schedule = 0;
        rq->active_balance = 0;
        rq->next_balance = jiffies;
        rq->push_cpu = 0;
        rq->cpu = i;
        rq->online = 0;
        rq->idle_stamp = 0;
        rq->avg_idle = 2*sysctl_sched_migration_cost;
        rq->max_idle_balance_cost = sysctl_sched_migration_cost;

        INIT_LIST_HEAD(&rq->cfs_tasks);
        /* 將CPU運行隊列加入到默認調度域中 */
        rq_attach_root(rq, &def_root_domain);
#ifdef CONFIG_NO_HZ_COMMON
        /* 動態時鐘使用的標志位,初始時動態時鐘是不使用的 */
        rq->nohz_flags = 0;
#endif
#ifdef CONFIG_NO_HZ_FULL
        /* 也是動態時鐘才使用的標志位,用于保存上次調度tick發生時間 */
        rq->last_sched_tick = 0;
#endif
#endif
        /* 初始化運行隊列定時器,這個是高精度定時器,但是只是初始化,這時并沒有使用 */
        init_rq_hrtick(rq);
        atomic_set(&rq->nr_iowait, 0);
    }
    /* 設置 init_task 進程的權重 */
    set_load_weight(&init_task);

#ifdef CONFIG_PREEMPT_NOTIFIERS
    /* 初始化通知鏈 */
    INIT_HLIST_HEAD(&init_task.preempt_notifiers);
#endif

    /*
     * The boot idle thread does lazy MMU switching as well:
     */
    atomic_inc(&init_mm.mm_count);
    enter_lazy_tlb(&init_mm, current);

    /*
     * Make us the idle thread. Technically, schedule() should not be
     * called from this thread, however somewhere below it might be,
     * but because we are the idle thread, we just pick up running again
     * when this runqueue becomes "idle".
     */
    /* 將當前進程初始化為idle進程,idle進程用于當CPU沒有進程可運行時運行,空轉 */
    init_idle(current, smp_processor_id());
    /* 下次負載更新時間(是一個相對時間) */
    calc_load_update = jiffies + LOAD_FREQ;

    /*
     * During early bootup we pretend to be a normal task:
     */
    /* 設置idle進程使用CFS調度策略 */
    current->sched_class = &fair_sched_class;

#ifdef CONFIG_SMP
    zalloc_cpumask_var(&sched_domains_tmpmask, GFP_NOWAIT);
    /* May be allocated at isolcpus cmdline parse time */
    if (cpu_isolated_map == NULL)
        zalloc_cpumask_var(&cpu_isolated_map, GFP_NOWAIT);
    idle_thread_set_boot_cpu();
    set_cpu_rq_start_time();
#endif
    init_sched_fair_class();
    /* 這里只是標記調度器開始運行了,但是此時系統只有一個init_task(idle)進程,并且定時器都還沒啟動。并不會調度到其他進程,也沒有其他進程可供調度 */
    scheduler_running = 1;
}

五、調度器加入時機

只有處于TASK_RUNNING狀態下的進程才能夠加入到調度器,其他狀態都不行,也就說明了,當一個進程處于睡眠、掛起狀態的時候是不存在于調度器中的,而進程加入調度器的時機如下:

  • 當進程創建完成時,進程剛創建完成時,即使它運行起來立即調用sleep()進程睡眠,它也必定先會加入到調度器,因為實際上它加入調度器后自己還需要進行一定的初始化和操作,才會調用到我們的“立即”sleep()。
  • 當進程被喚醒時,也使用sleep的例子說明,我們平常寫程序使用的sleep()函數實現原理就是通過系統調用將進程狀態改為TASK_INTERRUPTIBLE,然后移出運行隊列,并且啟動一個定時器,在定時器到期后喚醒進程,再重新放入運行隊列。

5.1sched_fork

copy_process()這個創建函數,而里面有一個函數專門用于進程調度的初始化,就是sched_fork(),其代碼如下:

int sched_fork(unsigned long clone_flags, struct task_struct *p)
{
    unsigned long flags;
    /* 獲取當前CPU,并且禁止搶占 */
    int cpu = get_cpu();

    /* 初始化跟調度相關的值,比如調度實體,運行時間等 */
    __sched_fork(clone_flags, p);
    /*
     * 標記為運行狀態,表明此進程正在運行或準備好運行,實際上沒有真正在CPU上運行,這里只是導致了外部信號和事件不能夠喚醒此進程,之后將它插入到運行隊列中
     */
    p->state = TASK_RUNNING;

    /*
     * 根據父進程的運行優先級設置設置進程的優先級
     */
    p->prio = current->normal_prio;

    /*
     * 更新該進程優先級
     */
    /* 如果需要重新設置優先級 */
    if (unlikely(p->sched_reset_on_fork)) {
        /* 如果是dl調度或者實時調度 */
        if (task_has_dl_policy(p) || task_has_rt_policy(p)) {
            /* 調度策略為SCHED_NORMAL,這個選項將使用CFS調度 */
            p->policy = SCHED_NORMAL;
            /* 根據默認nice值設置靜態優先級 */
            p->static_prio = NICE_TO_PRIO(0);
            /* 實時優先級為0 */
            p->rt_priority = 0;
        } else if (PRIO_TO_NICE(p->static_prio) < 0)
            /* 根據默認nice值設置靜態優先級 */
            p->static_prio = NICE_TO_PRIO(0);

        /* p->prio = p->normal_prio = p->static_prio */
        p->prio = p->normal_prio = __normal_prio(p);
        /* 設置進程權重 */
        set_load_weight(p);

         /* sched_reset_on_fork成員在之后已經不需要使用了,直接設為0 */
        p->sched_reset_on_fork = 0;
    }

    if (dl_prio(p->prio)) {
        /* 使能搶占 */
        put_cpu();
        /* 返回錯誤 */
        return -EAGAIN;
    } else if (rt_prio(p->prio)) {
        /* 根據優先級判斷,如果是實時進程,設置其調度類為rt_sched_class */
        p->sched_class = &rt_sched_class;
    } else {
        /* 如果是普通進程,設置其調度類為fair_sched_class */
        p->sched_class = &fair_sched_class;
    }
    /* 調用調用類的task_fork函數 */
    if (p->sched_class->task_fork)
        p->sched_class->task_fork(p);

    /*
     * The child is not yet in the pid-hash so no cgroup attach races,
     * and the cgroup is pinned to this child due to cgroup_fork()
     * is ran before sched_fork().
     *
     * Silence PROVE_RCU.
     */
    raw_spin_lock_irqsave(&p->pi_lock, flags);
    /* 設置新進程的CPU為當前CPU */
    set_task_cpu(p, cpu);
    raw_spin_unlock_irqrestore(&p->pi_lock, flags);

#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)
    if (likely(sched_info_on()))
        memset(&p->sched_info, 0, sizeof(p->sched_info));
#endif
#if defined(CONFIG_SMP)
    p->on_cpu = 0;
#endif
    /* task_thread_info(p)->preempt_count = PREEMPT_DISABLED; */
    /* 初始化該進程為內核禁止搶占 */
    init_task_preempt_count(p);
#ifdef CONFIG_SMP
    plist_node_init(&p->pushable_tasks, MAX_PRIO);
    RB_CLEAR_NODE(&p->pushable_dl_tasks);
#endif
    /* 使能搶占 */
    put_cpu();
    return 0;
}

在sched_fork()函數中,主要工作如下:

  • 獲取當前CPU號
  • 禁止內核搶占(這里基本就是關閉了搶占,因為執行到這里已經是內核態,又禁止了被搶占)
  • 初始化進程p的一些變量(實時進程和普通進程通用的那些變量)
  • 設置進程p的狀態為TASK_RUNNING(這一步很關鍵,因為只有處于TASK_RUNNING狀態下的進程才會被調度器放入隊列中)
  • 根據父進程和clone_flags參數設置進程p的優先級和權重。
  • 根據進程p的優先級設置其調度類(實時進程優先級:0~99  普通進程優先級:100~139)
  • 根據調度類進行進程p類型相關的初始化(這里就實現了實時進程和普通進程獨有的變量進行初始化)
  • 設置進程p的當前CPU為此CPU。
  • 初始化進程p禁止內核搶占(因為當CPU執行到進程p時,進程p還需要進行一些初始化)
  • 使能內核搶占

可以看出sched_fork()進行的初始化也比較簡單,需要注意的是不同類型的進程會使用不同的調度類,并且也會調用調度類中的初始化函數。在實時進程的調度類中是沒有特定的task_fork()函數的,而普通進程使用cfs策略時會調用到task_fork_fair()函數,我們具體看看實現:

static void task_fork_fair(struct task_struct *p)
{
    struct cfs_rq *cfs_rq;

    /* 進程p的調度實體se */
    struct sched_entity *se = &p->se, *curr;

    /* 獲取當前CPU */
    int this_cpu = smp_processor_id();

    /* 獲取此CPU的運行隊列 */
    struct rq *rq = this_rq();
    unsigned long flags;

    /* 上鎖并保存中斷記錄 */
    raw_spin_lock_irqsave(&rq->lock, flags);

    /* 更新rq運行時間 */
    update_rq_clock(rq);

    /* cfs_rq = current->se.cfs_rq; */
    cfs_rq = task_cfs_rq(current);

    /* 設置當前進程所在隊列為父進程所在隊列 */
    curr = cfs_rq->curr;

    /*
     * Not only the cpu but also the task_group of the parent might have
     * been changed after parent->se.parent,cfs_rq were copied to
     * child->se.parent,cfs_rq. So call __set_task_cpu() to make those
     * of child point to valid ones.
     */
    rcu_read_lock();
    /* 設置此進程所屬CPU */
    __set_task_cpu(p, this_cpu);
    rcu_read_unlock();

    /* 更新當前進程運行時間 */
    update_curr(cfs_rq);

    if (curr)
        /* 將父進程的虛擬運行時間賦給了新進程的虛擬運行時間 */
        se->vruntime = curr->vruntime;
    /* 調整了se的虛擬運行時間 */
    place_entity(cfs_rq, se, 1);

    if (sysctl_sched_child_runs_first && curr && entity_before(curr, se)) {
        /*
         * Upon rescheduling, sched_class::put_prev_task() will place
         * 'current' within the tree based on its new key value.
         */
        swap(curr->vruntime, se->vruntime);
        resched_curr(rq);
    }

    /* 保證了進程p的vruntime是運行隊列中最小的(這里占時不確定是不是這個用法,不過確實是最小的了) */
    se->vruntime -= cfs_rq->min_vruntime;

    /* 解鎖,還原中斷記錄 */
    raw_spin_unlock_irqrestore(&rq->lock, flags);
}

在task_fork_fair()函數中主要就是設置進程p的虛擬運行時間和所處的cfs隊列,值得我們注意的是 cfs_rq = task_cfs_rq(current); 這一行,在注釋中已經表明task_cfs_rq(current)返回的是current的se.cfs_rq,注意se.cfs_rq保存的并不是根cfs隊列,而是所處的cfs_rq,也就是如果父進程處于一個進程組的cfs_rq中,新創建的進程也會處于這個進程組的cfs_rq中。

5.2 wake_up_new_task()

到這里新進程關于調度的初始化已經完成,但是還沒有被調度器加入到隊列中,其是在do_fork()中的wake_up_new_task(p);中加入到隊列中的,我們具體看看wake_up_new_task()的實現:

void wake_up_new_task(struct task_struct *p)
{
    unsigned long flags;
    struct rq *rq;

    raw_spin_lock_irqsave(&p->pi_lock, flags);
#ifdef CONFIG_SMP
    /*
     * Fork balancing, do it here and not earlier because:
     *  - cpus_allowed can change in the fork path
     *  - any previously selected cpu might disappear through hotplug
     */
     /* 為進程選擇一個合適的CPU */
    set_task_cpu(p, select_task_rq(p, task_cpu(p), SD_BALANCE_FORK, 0));
#endif

    /* Initialize new task's runnable average */
    /* 這里是跟多核負載均衡有關 */
    init_task_runnable_average(p);
    /* 上鎖 */
    rq = __task_rq_lock(p);
    /* 將進程加入到CPU的運行隊列 */
    activate_task(rq, p, 0);
    /* 標記進程p處于隊列中 */
    p->on_rq = TASK_ON_RQ_QUEUED;
    /* 跟調試有關 */
    trace_sched_wakeup_new(p, true);
    /* 檢查是否需要切換當前進程 */
    check_preempt_curr(rq, p, WF_FORK);
#ifdef CONFIG_SMP
    if (p->sched_class->task_woken)
        p->sched_class->task_woken(rq, p);
#endif
    task_rq_unlock(rq, p, &flags);
}

在wake_up_new_task()函數中,將進程加入到運行隊列的函數為activate_task(),而activate_task()函數最后會調用到新進程調度類中的enqueue_task指針所指函數,這里我們具體看一下cfs調度類的enqueue_task指針所指函數enqueue_task_fair():

static void
enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags)
{
    struct cfs_rq *cfs_rq;
    struct sched_entity *se = &p->se;

    /* 這里是一個迭代,我們知道,進程有可能是處于一個進程組中的,所以當這個處于進程組中的進程加入到該進程組的隊列中時,要對此隊列向上迭代 */
    for_each_sched_entity(se) {
        if (se->on_rq)
            break;
        /* 如果不是CONFIG_FAIR_GROUP_SCHED,獲取其所在CPU的rq運行隊列的cfs_rq運行隊列
         * 如果是CONFIG_FAIR_GROUP_SCHED,獲取其所在的cfs_rq運行隊列
         */
        cfs_rq = cfs_rq_of(se);
        /* 加入到隊列中 */
        enqueue_entity(cfs_rq, se, flags);

        /*
         * end evaluation on encountering a throttled cfs_rq
         *
         * note: in the case of encountering a throttled cfs_rq we will
         * post the final h_nr_running increment below.
        */
        if (cfs_rq_throttled(cfs_rq))
            break;
        cfs_rq->h_nr_running++;

        flags = ENQUEUE_WAKEUP;
    }

    /* 只有se不處于隊列中或者cfs_rq_throttled(cfs_rq)返回真才會運行這個循環 */
    for_each_sched_entity(se) {
        cfs_rq = cfs_rq_of(se);
        cfs_rq->h_nr_running++;

        if (cfs_rq_throttled(cfs_rq))
            break;

        update_cfs_shares(cfs_rq);
        update_entity_load_avg(se, 1);
    }

    if (!se) {
        update_rq_runnable_avg(rq, rq->nr_running);
        /* 當前CPU運行隊列活動進程數 + 1 */
        add_nr_running(rq, 1);
    }
    /* 設置下次調度中斷發生時間 */
    hrtick_update(rq);
}

在enqueue_task_fair()函數中又使用了enqueue_entity()函數進行操作,如下:

static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
    /*
     * Update the normalized vruntime before updating min_vruntime
     * through calling update_curr().
     */
    if (!(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_WAKING))
        se->vruntime += cfs_rq->min_vruntime;

    /*
     * Update run-time statistics of the 'current'.
     */
    /* 更新當前進程運行時間和虛擬運行時間 */
    update_curr(cfs_rq);
    enqueue_entity_load_avg(cfs_rq, se, flags & ENQUEUE_WAKEUP);
    /* 更新cfs_rq隊列總權重(就是在原有基礎上加上se的權重) */
    account_entity_enqueue(cfs_rq, se);
    update_cfs_shares(cfs_rq);

    /* 新建的進程flags為0,不會執行這里 */
    if (flags & ENQUEUE_WAKEUP) {
        place_entity(cfs_rq, se, 0);
        enqueue_sleeper(cfs_rq, se);
    }

    update_stats_enqueue(cfs_rq, se);
    check_spread(cfs_rq, se);

    /* 將se插入到運行隊列cfs_rq的紅黑樹中 */
    if (se != cfs_rq->curr)
        __enqueue_entity(cfs_rq, se);
    /* 將se的on_rq標記為1 */
    se->on_rq = 1;

    /* 如果cfs_rq的隊列中只有一個進程,這里做處理 */
    if (cfs_rq->nr_running == 1) {
        list_add_leaf_cfs_rq(cfs_rq);
        check_enqueue_throttle(cfs_rq);
    }
}

六、調度器如何運行?

6.1系統定時器

因為我們主要講解的是調度器,而會涉及到一些系統定時器的知識,這里我們簡單講解一下內核中定時器是如何組織,又是如何通過通過定時器實現了調度器的間隔調度。首先我們先看一下內核定時器的框架:

圖片圖片

在內核中,會使用strut clock_event_device結構描述硬件上的定時器,每個硬件定時器都有其自己的精度,會根據精度每隔一段時間產生一個時鐘中斷。

而系統會讓每個CPU使用一個tick_device描述系統當前使用的硬件定時器(因為每個CPU都有其自己的運行隊列),通過tick_device所使用的硬件時鐘中斷進行時鐘滴答(jiffies)的累加(只會有一個CPU負責這件事),并且在中斷中也會調用調度器,而我們在驅動中常用的低精度定時器就是通過判斷jiffies實現的。而當使用高精度定時器(hrtimer)時,情況則不一樣,hrtimer會生成一個普通的高精度定時器,在這個定時器中回調函數是調度器,其設置的間隔時間同時鐘滴答一樣,所以在系統中,每一次時鐘滴答都會使調度器判斷一次是否需要進行調度。

6.2時鐘中斷

當時鐘發生中斷時,首先會調用的是tick_handle_periodic()函數,在此函數中又主要執行tick_periodic()函數進行操作。我們先看一下tick_handle_periodic()函數:

void tick_handle_periodic(struct clock_event_device *dev)
{
    /* 獲取當前CPU */
    int cpu = smp_processor_id();
    /* 獲取下次時鐘中斷執行時間 */
    ktime_t next = dev->next_event;

    tick_periodic(cpu);

    /* 如果是周期觸發模式,直接返回 */
    if (dev->mode != CLOCK_EVT_MODE_ONESHOT)
        return;

    /* 為了防止當該函數被調用時,clock_event_device中的計時實際上已經經過了不止一個tick周期,這時候,tick_periodic可能被多次調用,使得jiffies和時間可以被正確地更新。*/
    for (;;) {
        /*
         * Setup the next period for devices, which do not have
         * periodic mode:
         */
        /* 計算下一次觸發時間 */
        next = ktime_add(next, tick_period);

        /* 設置下一次觸發時間,返回0表示成功 */
        if (!clockevents_program_event(dev, next, false))
            return;
        /*
         * Have to be careful here. If we're in oneshot mode,
         * before we call tick_periodic() in a loop, we need
         * to be sure we're using a real hardware clocksource.
         * Otherwise we could get trapped in an infinite(無限的)
         * loop, as the tick_periodic() increments jiffies,
         * which then will increment time, possibly causing
         * the loop to trigger again and again.
         */
        if (timekeeping_valid_for_hres())
            tick_periodic(cpu);
    }
}

此函數主要工作是執行tick_periodic()函數,然后判斷時鐘中斷是單觸發模式還是循環觸發模式,如果是循環觸發模式,則直接返回,如果是單觸發模式,則執行如下操作:

  • 計算下一次觸發時間
  • 設置下次觸發時間
  • 如果設置下次觸發時間失敗,則根據timekeeper等待下次tick_periodic()函數執行時間。
  • 返回第一步

而在tick_periodic()函數中,程序主要執行路線為tick_periodic()->update_process_times()->scheduler_tick()。最后的scheduler_tick()函數則是跟調度相關的主要函數。我們在這具體先看看tick_periodic()函數和update_process_times()函數:

/* tick_device 周期性調用此函數
 * 更新jffies和當前進程
 * 只有一個CPU是負責更新jffies的,其他的CPU只會更新當前自己的進程
 */
static void tick_periodic(int cpu)
{

    if (tick_do_timer_cpu == cpu) {
        /* 當前CPU負責更新時間 */
        write_seqlock(&jiffies_lock);

        /* Keep track of the next tick event */
        tick_next_period = ktime_add(tick_next_period, tick_period);

        /* 更新 jiffies計數,jiffies += 1 */
        do_timer(1);
        write_sequnlock(&jiffies_lock);
        /* 更新墻上時間,就是我們生活中的時間 */
        update_wall_time();
    }
    /* 更新當前進程信息,調度器主要函數 */
    update_process_times(user_mode(get_irq_regs()));
    profile_tick(CPU_PROFILING);
}




void update_process_times(int user_tick)
{
    struct task_struct *p = current;
    int cpu = smp_processor_id();

    /* Note: this timer irq context must be accounted for as well. */
    /* 更新當前進程的內核態和用戶態占用率 */
    account_process_tick(p, user_tick);
    /* 檢查有沒有定時器到期,有就運行到期定時器的處理 */
    run_local_timers();
    rcu_check_callbacks(cpu, user_tick);
#ifdef CONFIG_IRQ_WORK
    if (in_irq())
        irq_work_tick();
#endif
    /* 調度器的tick */
    scheduler_tick();
    run_posix_cpu_timers(p);
}

這兩個函數主要工作為將jiffies加1、更新系統的墻上時間、更新當前進程的內核態和用戶態的CPU占用率、檢查是否有定時器到期,運行到期的定時器。當執行完這些操作后,就到了最重要的scheduler_tick()函數,而scheduler_tick()函數主要做什么呢,就是更新CPU和當前進行的一些數據,然后根據當前進程的調度類,調用task_tick()函數。這里普通進程調度類的task_tick()是task_tick_fair()函數。

void scheduler_tick(void)
{
    /* 獲取當前CPU的ID */
    int cpu = smp_processor_id();
    /* 獲取當前CPU的rq隊列 */
    struct rq *rq = cpu_rq(cpu);
    /* 獲取當前CPU的當前運行程序,實際上就是current */
    struct task_struct *curr = rq->curr;
    /* 更新CPU調度統計中的本次調度時間 */
    sched_clock_tick();

    raw_spin_lock(&rq->lock);
    /* 更新該CPU的rq運行時間 */
    update_rq_clock(rq);
    curr->sched_class->task_tick(rq, curr, 0);
    /* 更新CPU的負載 */
    update_cpu_load_active(rq);
    raw_spin_unlock(&rq->lock);

    perf_event_task_tick();

#ifdef CONFIG_SMP
    rq->idle_balance = idle_cpu(cpu);
    trigger_load_balance(rq);
#endif
    /* rq->last_sched_tick = jiffies; */
    rq_last_tick_reset(rq);
}




/*
 * CFS調度類的task_tick()
 */
static void task_tick_fair(struct rq *rq, struct task_struct *curr, int queued)
{
    struct cfs_rq *cfs_rq;
    struct sched_entity *se = &curr->se;
    /* 向上更新進程組時間片 */
    for_each_sched_entity(se) {
        cfs_rq = cfs_rq_of(se);
        /* 更新當前進程運行時間,并判斷是否需要調度此進程 */
        entity_tick(cfs_rq, se, queued);
    }

    if (numabalancing_enabled)
        task_tick_numa(rq, curr);

    update_rq_runnable_avg(rq, 1);
}

顯然,到這里最重要的函數應該是entity_tick(),因為是這個函數決定了當前進程是否需要調度出去。我們必須先明確一點就是,CFS調度策略是使用紅黑樹以進程的vruntime為鍵值進行組織的,進程的vruntime越小越在紅黑樹的左邊,而每次調度的下一個目標就是紅黑樹最左邊的結點上的進程。

而當進行運行時,其vruntime是隨著實際運行時間而增加的,但是不同權重的進程其vruntime增加的速率不同,正在運行的進程的權重約大(優先級越高),其vruntime增加的速率越慢,所以其所占用的CPU時間越多。而每次時鐘中斷的時候,在entity_tick()函數中都會更新當前進程的vruntime值。當進程沒有處于CPU上運行時,其vruntime是保持不變的。

static void
entity_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr, int queued)
{
    /*
     * Update run-time statistics of the 'current'.
     */
    /* 更新當前進程運行時間,包括虛擬運行時間 */
    update_curr(cfs_rq);

    /*
     * Ensure that runnable average is periodically updated.
     */
    update_entity_load_avg(curr, 1);
    update_cfs_rq_blocked_load(cfs_rq, 1);
    update_cfs_shares(cfs_rq);

#ifdef CONFIG_SCHED_HRTICK
    /*
     * queued ticks are scheduled to match the slice, so don't bother
     * validating it and just reschedule.
     */
    /* 若queued為1,則當前運行隊列的運行進程需要調度 */
    if (queued) {
        /* 標記當前進程需要被調度出去 */
        resched_curr(rq_of(cfs_rq));
        return;
    }
    /*
     * don't let the period tick interfere with the hrtick preemption
     */
    if (!sched_feat(DOUBLE_TICK) && hrtimer_active(&rq_of(cfs_rq)->hrtick_timer))
        return;
#endif
    /* 檢查是否需要調度 */
    if (cfs_rq->nr_running > 1)
        check_preempt_tick(cfs_rq, curr);
}

在entity_tick()中,首先會更新當前進程的實際運行時間和虛擬運行時間,這里很重要,因為要使用更新后的這些數據去判斷是否需要被調度。在entity_tick()函數中最后面的check_preempt_tick()函數就是用來判斷進程是否需要被調度的,其判斷的標準有兩個:

  • 先判斷當前進程的實際運行時間是否超過CPU分配給這個進程的CPU時間,如果超過,則需要調度。
  • 再判斷當前進程的vruntime是否大于下個進程的vruntime,如果大于,則需要調度。

清楚了這兩個標準,check_preempt_tick()的代碼則很好理解了。

/*
 * 檢查當前進程是否需要被搶占
 * 判斷方法有兩種,一種就是判斷當前進程是否超過了CPU分配給它的實際運行時間
 * 另一種就是判斷當前進程的虛擬運行時間是否大于下個進程的虛擬運行時間
 */
static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
    /* ideal_runtime為進程應該運行的時間
     * delta_exec為進程增加的實際運行時間
     * 如果delta_exec超過了ideal_runtime,表示該進程應該讓出CPU給其他進程
     */
    unsigned long ideal_runtime, delta_exec;
    struct sched_entity *se;
    s64 delta;


    /* slice為CFS隊列中所有進程運行一遍需要的實際時間 */
    /* ideal_runtime保存的是CPU分配給當前進程一個周期內實際的運行時間,計算公式為:  一個周期內進程應當運行的時間 = 一個周期內隊列中所有進程運行一遍需要的時間 * 當前進程權重 / 隊列總權重
     * delta_exec保存的是當前進程增加使用的實際運行時間
     */
    ideal_runtime = sched_slice(cfs_rq, curr);
    delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
    if (delta_exec > ideal_runtime) {
        /* 增加的實際運行實際 > 應該運行實際,說明需要調度出去 */
        resched_curr(rq_of(cfs_rq));
        /*
         * The current task ran long enough, ensure it doesn't get
         * re-elected due to buddy favours.
         */
        /* 如果cfs_rq隊列的last,next,skip指針中的某個等于當前進程,則清空cfs_rq隊列中的相應指針 */
        clear_buddies(cfs_rq, curr);
        return;
    }

    /*
     * Ensure that a task that missed wakeup preemption by a
     * narrow margin doesn't have to wait for a full slice.
     * This also mitigates buddy induced latencies under load.
     */
    if (delta_exec < sysctl_sched_min_granularity)
        return;
    /* 獲取下一個調度進程的se */
    se = __pick_first_entity(cfs_rq);
    /* 當前進程的虛擬運行時間 - 下個進程的虛擬運行時間 */
    delta = curr->vruntime - se->vruntime;

    /* 當前進程的虛擬運行時間 大于 下個進程的虛擬運行時間,說明這個進程還可以繼續運行 */
    if (delta < 0)
        return;

    if (delta > ideal_runtime)
        /* 當前進程的虛擬運行時間 小于 下個進程的虛擬運行時間,說明下個進程比當前進程更應該被CPU使用,resched_curr()函數用于標記當前進程需要被調度出去 */
        resched_curr(rq_of(cfs_rq));
}




/*
 * resched_curr - mark rq's current task 'to be rescheduled now'.
 *
 * On UP this means the setting of the need_resched flag, on SMP it
 * might also involve a cross-CPU call to trigger the scheduler on
 * the target CPU.
 */
/* 標記當前進程需要調度,將當前進程的thread_info->flags設置TIF_NEED_RESCHED標記 */
void resched_curr(struct rq *rq)
{
    struct task_struct *curr = rq->curr;
    int cpu;

    lockdep_assert_held(&rq->lock);

    /* 檢查當前進程是否已經設置了調度標志,如果是,則不用再設置一遍,直接返回 */
    if (test_tsk_need_resched(curr))
        return;

    /* 根據rq獲取CPU */
    cpu = cpu_of(rq);
    /* 如果CPU = 當前CPU,則設置當前進程需要調度標志 */
    if (cpu == smp_processor_id()) {
        /* 設置當前進程需要被調度出去的標志,這個標志保存在進程的thread_info結構上 */
        set_tsk_need_resched(curr);
        /* 設置CPU的內核搶占 */
        set_preempt_need_resched();
        return;
    }

    /* 如果不是處于當前CPU上,則設置當前進程需要調度,并通知其他CPU */
    if (set_nr_and_not_polling(curr))
        smp_send_reschedule(cpu);
    else
        trace_sched_wake_idle_without_ipi(cpu);
}

好了,到這里實際上如果進程需要被調度,則已經被標記,如果進程不需要被調度,則繼續執行。這里大家或許有疑問,只標記了進程需要被調度,但是為什么并沒有真正處理它?進程調度的發生時機之一就是發生在中斷返回時,這里是在匯編代碼中實現的,而我們知道這里我們是時鐘中斷執行上述的這些操作的,當執行完這些后,從時鐘中斷返回去的時候,會調用到匯編函數ret_from_sys_call,在這個函數中會先檢查調度標志被置位,如果被置位,則跳轉至schedule(),而schedule()最后調用到__schedule()這個函數進行處理。

static void __sched __schedule(void)
{
    /* prev保存換出進程(也就是當前進程),next保存換進進程 */
    struct task_struct *prev, *next;
    unsigned long *switch_count;
    struct rq *rq;
    int cpu;

need_resched:
    /* 禁止搶占 */
    preempt_disable();
    /* 獲取當前CPU ID */
    cpu = smp_processor_id();
    /* 獲取當前CPU運行隊列 */
    rq = cpu_rq(cpu);
    rcu_note_context_switch(cpu);
    prev = rq->curr;

    schedule_debug(prev);

    if (sched_feat(HRTICK))
        hrtick_clear(rq);

    /*
     * Make sure that signal_pending_state()->signal_pending() below
     * can't be reordered with __set_current_state(TASK_INTERRUPTIBLE)
     * done by the caller to avoid the race with signal_wake_up().
     */
    smp_mb__before_spinlock();
    /* 隊列上鎖 */
    raw_spin_lock_irq(&rq->lock);
    /* 當前進程非自愿切換次數 */
    switch_count = &prev->nivcsw;

    /*
     * 當內核搶占時會置位thread_info的preempt_count的PREEMPT_ACTIVE位,調用schedule()之后會清除,PREEMPT_ACTIVE置位表明是從內核搶占進入到此的
     * preempt_count()是判斷thread_info的preempt_count整體是否為0
     * prev->state大于0表明不是TASK_RUNNING狀態
     *
     */
    if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) {
        /* 當前進程不為TASK_RUNNING狀態并且不是通過內核態搶占進入調度 */
        if (unlikely(signal_pending_state(prev->state, prev))) {
            /* 有信號需要處理,置為TASK_RUNNING */
            prev->state = TASK_RUNNING;
        } else {
            /* 沒有信號掛起需要處理,會將此進程移除運行隊列 */
            /* 如果代碼執行到此,說明當前進程要么準備退出,要么是處于即將睡眠狀態 */
            deactivate_task(rq, prev, DEQUEUE_SLEEP);
            prev->on_rq = 0;

            /*
             * If a worker went to sleep, notify and ask workqueue
             * whether it wants to wake up a task to maintain
             * concurrency.
             */
            if (prev->flags & PF_WQ_WORKER) {
                /* 如果當前進程處于一個工作隊列中 */
                struct task_struct *to_wakeup;

                to_wakeup = wq_worker_sleeping(prev, cpu);
                if (to_wakeup)
                    try_to_wake_up_local(to_wakeup);
            }
        }
        switch_count = &prev->nvcsw;
    }

    /* 更新rq運行隊列時間 */
    if (task_on_rq_queued(prev) || rq->skip_clock_update < 0)
        update_rq_clock(rq);

    /* 獲取下一個調度實體,這里的next的值會是一個進程,而不是一個調度組,在pick_next_task會遞歸選出一個進程 */
    next = pick_next_task(rq, prev);
    /* 清除當前進程的thread_info結構中的flags的TIF_NEED_RESCHED和PREEMPT_NEED_RESCHED標志位,這兩個位表明其可以被調度調出(因為這里已經調出了,所以這兩個位就沒必要了) */
    clear_tsk_need_resched(prev);
    clear_preempt_need_resched();
    rq->skip_clock_update = 0;

    if (likely(prev != next)) {
        /* 該CPU進程切換次數加1 */
        rq->nr_switches++;
        /* 該CPU當前執行進程為新進程 */
        rq->curr = next;

        ++*switch_count;

        /* 這里進行了進程上下文的切換 */
        context_switch(rq, prev, next); /* unlocks the rq */
        /*
         * The context switch have flipped the stack from under us
         * and restored the local variables which were saved when
         * this task called schedule() in the past. prev == current
         * is still correct, but it can be moved to another cpu/rq.
         */
        /* 新的進程有可能在其他CPU上運行,重新獲取一次CPU和rq */
        cpu = smp_processor_id();
        rq = cpu_rq(cpu);
    }
    else
        raw_spin_unlock_irq(&rq->lock);        /* 這里意味著下個調度的進程就是當前進程,釋放鎖不做任何處理 */
    /* 上下文切換后的處理 */
    post_schedule(rq);

    /* 重新打開搶占使能但不立即執行重新調度 */
    sched_preempt_enable_no_resched();
    if (need_resched())
        goto need_resched;
}

在__schedule()中,每一步的作用注釋已經寫得很詳細了,選取下一個進程的任務在__schedule()中交給了pick_next_task()函數,而進程切換則交給了context_switch()函數。我們先看看pick_next_task()函數是如何選取下一個進程的:

static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev)
{
    const struct sched_class *class = &fair_sched_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(prev->sched_class == class && rq->nr_running == rq->cfs.h_nr_running)) {
        /* 所有進程都處于CFS運行隊列中,所以就直接使用cfs的調度類 */
        p = fair_sched_class.pick_next_task(rq, prev);
        if (unlikely(p == RETRY_TASK))
            goto again;

        /* assumes fair_sched_class->next == idle_sched_class */
        if (unlikely(!p))
            p = idle_sched_class.pick_next_task(rq, prev);

        return p;
    }

again:
    /* 在其他調度類中包含有其他進程,從最高優先級的調度類迭代到最低優先級的調度類,并選擇最優的進程運行 */
    for_each_class(class) {
        p = class->pick_next_task(rq, prev);
        if (p) {
            if (unlikely(p == RETRY_TASK))
                goto again;
            return p;
        }
    }

    BUG(); /* the idle class will always have a runnable task */
}

在pick_next_task()中完全體現了進程優先級的概念,首先會先判斷是否所有進程都處于cfs隊列中,如果不是,則表明有比普通進程更高優先級的進程(包括實時進程)。內核中是將調度類重優先級高到低進行排列,然后選擇時從最高優先級的調度類開始找是否有進程需要調度,如果沒有會轉到下一優先級調度類,在代碼27行所體現,27行展開是:

#define for_each_class(class) \
for (class = sched_class_highest; class; class = class->next)

而調度類的優先級順序為:

stop_sched_class -> dl_sched_class -> rt_sched_class -> fair_sched_class -> idle_sched_class

在pick_next_task()函數中返回了選定的進程的進程描述符,接下來就會調用context_switch()進行進程切換了。

static inline void
context_switch(struct rq *rq, struct task_struct *prev,
           struct task_struct *next)
{
    struct mm_struct *mm, *oldmm;

    prepare_task_switch(rq, prev, next);

    mm = next->mm;
    oldmm = prev->active_mm;
    /*
     * For paravirt, this is coupled with an exit in switch_to to
     * combine the page table reload and the switch backend into
     * one hypercall.
     */
    arch_start_context_switch(prev);

    if (!mm) {
        /* 如果新進程的內存描述符為空,說明新進程為內核線程 */
        next->active_mm = oldmm;
        atomic_inc(&oldmm->mm_count);
        /* 通知底層不需要切換虛擬地址空間
         *     if (this_cpu_read(cpu_tlbstate.state) == TLBSTATE_OK)
         *        this_cpu_write(cpu_tlbstate.state, TLBSTATE_LAZY);
         */
        enter_lazy_tlb(oldmm, next);
    } else
        /* 切換虛擬地址空間 */
        switch_mm(oldmm, mm, next);

    if (!prev->mm) {
        /* 如果被切換出去的進程是內核線程 */
        prev->active_mm = NULL;
        /* 歸還借用的oldmm  */
        rq->prev_mm = oldmm;
    }
    /*
     * Since the runqueue lock will be released by the next
     * task (which is an invalid locking op but in the case
     * of the scheduler it's an obvious special-case), so we
     * do an early lockdep release here:
     */
    spin_release(&rq->lock.dep_map, 1, _THIS_IP_);

    context_tracking_task_switch(prev, next);

    /* 切換寄存器和內核棧,還會重新設置current為切換進去的進程 */
    switch_to(prev, next, prev);

    /* 同步 */
    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);
}

到這里整個進程的選擇和切換就已經完成了。

七、調度器實踐與應用

7.1基于實際場景的調度策略調整

(1)服務器場景:在 Web 服務器環境中,通常面臨大量的并發請求,對響應速度要求極高。例如,一個繁忙的電商網站后臺服務器,每秒可能會接收數千個用戶的商品查詢、訂單提交等請求。此時,可以將處理用戶請求的進程設置為較高優先級,利用 Linux 的實時調度策略(如 SCHED_FIFO 或 SCHED_RR)。通過chrt命令可以實現這一調整,假設 Web 服務器進程的 PID 為 1234,要將其設置為 SCHED_FIFO 調度策略且優先級為 99(實時優先級范圍 1 - 99,數值越大優先級越高),可以執行以下命令:

chrt -f -p 99 1234

這樣,當有新的用戶請求到達時,Web 服務器進程能夠優先獲得 CPU 資源進行處理,減少用戶等待時間,提高用戶體驗。同時,對于一些后臺任務,如日志寫入、數據備份等 I/O 密集型進程,可以適當降低其優先級,設置為普通調度策略(SCHED_NORMAL),以避免它們占用過多 CPU 資源,影響前端用戶請求的處理速度。

(2)桌面場景:在日常的桌面使用中,用戶同時運行著多種應用程序,如文字處理軟件、瀏覽器、音樂播放器等。為了保證用戶交互的流暢性,需要優先調度與用戶交互密切相關的進程。比如,當用戶在進行文字編輯時,文字處理軟件的進程應具有較高優先級,確保用戶輸入的字符能夠及時顯示和處理。而對于后臺自動更新的進程,如軟件更新程序、系統備份進程等,可以設置為較低優先級,在系統空閑時再進行執行。在 Linux 系統中,可以通過調整進程的 Nice 值來實現優先級的改變。假設文字處理軟件進程的 PID 為 5678,將其 Nice 值設置為 - 5(Nice 值范圍 - 20 - 19,數值越小優先級越高),命令如下:

renice -5 5678

這樣,文字處理軟件進程在競爭 CPU 資源時會更具優勢,為用戶提供更流暢的交互體驗。

7.2利用工具分析調度器行為

  1. perf 工具簡介:perf是 Linux 內核自帶的一款強大的性能分析工具,它可以用于分析調度器行為、進程的 CPU 使用情況、函數調用關系等多個方面。通過perf,我們可以獲取詳細的調度信息,幫助我們深入了解系統的運行狀態。
  2. 獲取調度信息示例:使用perf sched record命令可以記錄系統中的調度事件,然后通過perf sched report命令查看調度報告。例如,要記錄一段時間內的調度事件并生成報告,可以執行以下操作:
# 記錄調度事件,持續10秒
perf sched record -a -g -- sleep 10
# 查看調度報告
perf sched report

在生成的調度報告中,我們可以看到每個進程的調度時間、調度次數、等待時間等信息。例如,報告中可能會顯示某個進程在某段時間內被調度了 100 次,總運行時間為 5 秒,等待時間為 3 秒等。這些信息可以幫助我們分析哪些進程的調度存在問題,比如某個進程等待時間過長,可能是因為它的優先級較低,或者系統中存在其他高優先級進程占用了過多 CPU 資源。通過分析這些信息,我們可以針對性地調整調度策略,優化系統性能。此外,perf還支持生成火焰圖,通過火焰圖可以更直觀地展示進程的調用關系和 CPU 使用情況,幫助我們快速定位性能瓶頸。例如,使用perf record記錄性能數據,然后通過FlameGraph工具將數據轉換為火焰圖,命令如下:

# 記錄性能數據
perf record -a -g -o perf.data
# 生成火焰圖
./FlameGraph/stackcollapse-perf.pl perf.data |./FlameGraph/flamegraph.pl > perf.svg

打開生成的perf.svg文件,就可以看到一個直觀的火焰圖,不同顏色的條帶表示不同的函數調用,條帶的長度表示函數的執行時間,通過火焰圖可以一目了然地看到哪些函數占用了大量 CPU 時間,以及它們之間的調用關系,為優化調度策略提供有力依據。

責任編輯:武曉燕 來源: 深度Linux
相關推薦

2025-05-14 09:12:13

2025-01-21 10:54:28

2023-06-09 08:06:14

操作系統調度器LLM

2010-04-15 10:41:13

2009-12-09 17:25:19

Linux操作系統

2009-12-22 10:56:33

2009-12-10 17:48:35

Linux操作系統

2025-05-12 09:12:59

2020-12-29 16:39:01

Linux代碼命令

2011-05-18 09:30:40

Linux瀏覽器

2009-06-19 20:32:00

Linux

2019-03-05 11:22:17

操作系統調度算法

2009-04-27 16:23:15

LinuxUnix操作系統

2009-12-14 17:46:40

Linux桌面操作系統

2010-04-29 14:08:38

Unix操作系統

2009-12-16 09:43:12

Linux操作系統

2009-12-22 13:44:33

Linux操作系統

2014-09-10 09:54:43

2011-01-10 16:34:13

linux安裝

2014-07-28 17:25:25

國產Linux
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 欧美爱爱视频 | 日韩欧美国产精品一区 | av在线一区二区三区 | 国产在线精品一区二区 | 国产精品a久久久久 | 久久久国产一区二区三区 | 99精品国产成人一区二区 | 91在线看 | 亚洲精品综合 | 草久久免费视频 | 在线亚洲免费 | 中文成人在线 | 亚洲国产一区在线 | 成人网av | 国产欧美二区 | 欧美日韩电影一区二区 | 一区二区在线 | 欧美一级黄视频 | 欧美国产精品一区二区三区 | 日韩欧美天堂 | 99热在线播放 | 美女高潮网站 | 国产高清在线精品一区二区三区 | 欧美中文一区 | 久久久久久国产精品免费免费男同 | 日韩成人在线视频 | 动漫www.被爆羞羞av44 | 国产精品一区二区日韩 | 国产91在线播放精品91 | 日本在线小视频 | av手机免费在线观看 | 欧美日韩中文字幕在线 | 一区二区三区高清 | 国产乱肥老妇国产一区二 | a视频在线观看 | 色接久久 | 久久伊人一区 | 在线观看国产三级 | 免费的色网站 | 日韩免费一级 | 亚洲精品久久久久中文字幕二区 |