Linux 調度系統全景指南 -下篇-
本文主要是講 Linux 的調度系統, 由於全部內容太多,分三部分來講,本篇是下篇(主要線程和進程),上篇請看(CPU 和中斷):Linux 調度系統全景指南 (上篇),調度可以說是操作系統的靈魂,爲了讓 CPU 資源利用最大化,Linux 設計了一套非常精細的調度系統,對大多數場景都進行了很多優化,系統擴展性強,我們可以根據業務模型和業務場景的特點,有針對性的去進行性能優化,在保證客戶網絡帶寬前提下,隔離客戶互相之間的干擾影響,提高 CPU 利用率,降低單位運算成本,提高市場競爭力。歡迎大家相互交流學習!
目錄
進程
一般定義是操作系統對一個正在運行的程序的一種抽象, 是運行資源的管理單位(虛擬內存空間,文件句柄,全局變量,信號等運行資源),是操作系統資源分配的最小單位。在 linux 系統下,無論是進程,還是線程,到了內核裏面,我們統一都叫任務(Task),由一個統一的結構 task_struct 進行管理:
詳細結構:
大體分爲下面幾類:
進程運行空間
Linux 按照特權等級,把進程的運行空間分爲內核空間和用戶空間,分別對應着下圖中, CPU 特權等級的 Ring 0 和 Ring 3。
-
內核空間(Ring 0)具有最高權限,可以直接訪問所有資源;
-
用戶空間(Ring 3)只能訪問受限資源,不能直接訪問內存等硬件設備,必須通過系統調用陷入到內核中,才能訪問這些特權資源;
-
進程既可以在用戶空間運行,又可以在內核空間中運行。進程在用戶空間運行時,被稱爲進程的用戶態,而陷入內核空間的時候,被稱爲進程的內核態。
進程內存空間(x86_64)
各個分區的意義:
內核空間:
在 32 位系統中,Linux 會留 1G 空間給內核,用戶進程是無法訪問的,用來存放進程相關數據和內存數據,內核代碼等;在 64 位系統裏面,Linux 會採用最低 48 位來表示虛擬內存,這可通過 /proc/cpuinfo 來查看 address sizes :
address sizes :
36 bits physical, 48 bits virtual,總的虛擬地址空間爲 256TB(2^48),在這 256TB 的虛擬內存空間中, 0000000000000000 - 00007fffffffffff(128TB) 爲用戶空間,ffff800000000000 - ffffffffffffffff(128TB) 爲內核空間, 剩下的是用戶內存空間:
stack 棧區:
專門用來實現函數調用 - 棧結構的內存塊。相對空間下(可以設置大小,Linux 一般默認是 8M,可通過 ulimit –s 查看),系統自動管理,從高地址往低地址,向下生長。
內存映射區:
包括文件映射和匿名內存映射, 應用程序的所依賴的動態庫,會在程序執行時候,加載到內存這個區域,一般包括數據(data)和代碼(text); 通過 mmap 系統調用,可以把特定的文件映射到內存中,然後在相應的內存區域中操作字節來訪問文件內容,實現更高效的 IO 操作;匿名映射,在 glibc 中 malloc 分配大內存的時候會用到匿名映射。這裏所謂的 “大” 表示是超過了 MMAP_THRESHOLD 設置的字節數,它的缺省值是 128 kB,可以通過 mallopt() 去調整這個設置值。還可以用於進程間通信 IPC(共享內存)。
heap 堆區:
主要用於用戶動態內存分配,空間大,使用靈活,但需要用戶自己管理,通過 brk 系統調用控制堆的生長,向高地址生長。
BBS 段和 DATA 段:
用於存放程序全局數據和靜態數據,一般未初始化的放在 BSS 段(統一初始化爲 0,不佔程序文件的空間),初始化的放在 data 段,只讀數據放在 rodata 段(常量存儲區)。
text 段:
主要存放程序二進制代碼。
進程調度
進程狀態機
進程是一個動態的概念,是應用程序當前正在運行的一個實例, 在進程的整個生命週期中,它會處於不同的狀態,並且在不同狀態之間轉化:
R (TASK_RUNNING)-- 執行狀態
只有在該狀態的進程纔可能在 CPU 上運行。而同一時刻可能有多個進程處於可執行狀態,這些進程的 task_struct 結構(進程控制塊)被放入對 應 CPU 的可執行隊列中(一個 task 最多隻能出現在一個 CPU 的可執行隊列中)。進程調度器的任務就是從各個 CPU 的可執行隊列中分別選擇一個 task 在該 CPU 上運行。很多操作系統教科書將正在 CPU 上執行的進程定義爲 RUNNING 狀態,而將可執行但是尚未被調度執行的進程定義爲 READY 狀態,這兩種狀態在 linux 下統一爲 TASK_RUNNING 狀態。
S (TASK_INTERRUPTIBLE)-- 可中斷的睡眠狀態
處於這個狀態的進程因爲等待某個事件的發生(比如等待 socket 連接、等待信號量,等待鎖),而被掛起。這些進程的 task_struct 結構被放入對應事件的等待隊列中。當這些事件發生時(由外部中斷觸發、或由其他進程觸發),對應的等待隊列中的一個或多個進程將被喚醒。通過 ps 命令我們會看到,一般情況下,進程列表中的絕大多數進程都處於 TASK_INTERRUPTIBLE 狀態(除非機器的負載很高)。畢竟 CPU 就一兩個,進程動輒幾十上百個,如果不是絕大多數進程都在睡眠,CPU 又怎麼響應得過來。
T (TASK_STOPPED or TASK_TRACED)-- 暫停狀態或跟蹤狀態
向進程發送一個 SIGSTOP 信號,它就會因響應該信號而進入 TASK_STOPPED 狀態(除非該進程本身處於 TASK_UNINTERRUPTIBLE 狀態而不響應信號)。(SIGSTOP 與 SIGKILL 信號一樣,是非常強制的。不允許用戶進程通過 signal 系列的系統調用重新設置對應的信號處理函數。)向進程發送一個 SIGCONT 信號,可以讓其從 TASK_STOPPED 狀態恢復到 TASK_RUNNING 狀態。
Z (TASK_DEAD - EXIT_ZOMBIE)-- 退出狀態
進程成爲殭屍進程。當父進程遺漏了用 wait() 函數等待已終止的子進程時,子進程就會進入一種無父進程的狀態,此時子進程就是殭屍進程。
D (TASK_UNINTERRUPTIBLE)-- 不可中斷的睡眠狀態
TASK_INTERRUPTIBLE 狀態類似,進程處於睡眠狀態,但是此刻進程是不可中斷的。不可中斷,指的並不是 CPU 不響應外部硬件的中斷,而是指進程不響應異步信號。絕大多數情況下,進程處在睡眠狀態時,總是應該能夠響應異步信號的。否則你將驚奇的發現,kill -9 竟然殺不死一個正在睡眠的進程了!於是我們也很好理解,爲什麼 ps 命令看到的進程幾乎不會出現 TASK_UNINTERRUPTIBLE 狀態,而總是 TASK_INTERRUPTIBLE 狀態。而 TASK_UNINTERRUPTIBLE 狀態存在的意義就在於,內核的某些處理流程是不能被打斷的。如果響應異步信號,程序的執行流程中就會被插入一段用於處理異步信號的流程(這個插入的流程可能只存在於內核態,也可能延伸到用戶態),於是原有的流程就被中斷了。
進程上下文切換
上下文切換 (有時也稱做進程切換或任務切換):是指 CPU 從一個進程或線程切換到另一個進程或線程。簡潔描述一下,上下文切換可以認爲是內核(操作系統的核心)在 CPU 上對於進程(包括線程)進行以下的活動:
-
掛起一個進程,將這個進程在 CPU 中的狀態(上下文)存儲於內存中的某處;
-
在內存中檢索下一個進程的上下文並將其在 CPU 的寄存器中恢復;
-
跳轉到程序計數器所指向的位置(即跳轉到進程被中斷時的代碼行),以恢復該進程。
因此上下文是指某一時間點 CPU 寄存器和程序計數器的內容,廣義上還包括內存中進程的虛擬地址映射信息。上下文切換隻能發生在內核態中,上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統來說意味着消耗大量的 CPU 時間,事實上,可能是操作系統中時間消耗最大的操作。Linux 相比與其他操作系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。
進程優先級
-
進程的優先級有動態優先級和靜態優先級決定;
-
它是決定進程在 CPU 的執行順序的數字;
-
優先級越高被 CPU 執行的概率越大;
-
內核採用啓發式算法決定是開啓或者關閉動態優先級,可以通過修改 nice 級別直接修改進程的靜態優先級,而獲取更多 CPU 執行時間;
-
Linux 支持的 nice 級別從 19(最低優先級)到 - 20(最高優先級),默認只是 0。只有 root 身份的用戶才能把進程的 nice 級別調整爲負數(讓其具備較高優先級)。
時間片
-
時間片(timeslice)又稱爲 “量子(quantum)” 或“處理器片(processor slice)”是分時操作系統分配給每個正在運行的進程微觀上的一段 CPU 時間(在搶佔內核中是:從進程開始運行直到被搶佔的時間);
-
時間片由操作系統內核的調度程序分配給每個進程。首先,內核會給每個進程分配相等的初始時間片,然後每個進程輪番地執行相應的時間,當所有進程都處於時間片耗盡的狀態時,內核會重新爲每個進程計算並分配時間片,如此往復;
-
時間片設得太短會導致過多的進程切換,降低了 CPU 效率;而設得太長又可能引起對短的交互請求的響應變差,不同調度算法,對時間片管理不一樣;
-
通常狀況下,一個系統中所有的進程被分配到的時間片長短並不是相等的,儘管初始時間片基本相等(在 Linux 系統中,初始時間片也不相等,而是各自父進程的一半),系統通過測量進程處於 “睡眠” 和“正在運行”狀態的時間長短來計算每個進程的交互性,交互性和每個進程預設的靜態優先級(Nice 值)的疊加即是動態優先級,動態優先級按比例縮放就是要分配給那個進程時間片的長短。一般地,爲了獲得較快的響應速度,交互性強的進程(即趨向於 IO 消耗型)被分配到的時間片要長於交互性弱的(趨向於處理器消耗型)進程。
調度框架
實際上進程是資源管理的單位,線程纔是調度的單位,內核統稱爲任務調度。操作系統最重要的任務就是把系統中的 task 調度到各個 CPU 上去執行, 不同的任務有不同的需求,因此我們需要對任務進行分類:一種是普通進程,另外一種是實時進程。對於實時進程,毫無疑問快速響應的需求是最重要的,而對於普通進程,我們需要兼顧前三點的需求。相信你也發現了,這些需求是互相沖突的,對於這些 time-sharing 的普通進程如何平衡設計呢?這裏需要進一步將普通進程細分爲交互式進程(interactive processs)和批處理進程(batch process)。交互式進程需要和用戶進行交流,因此對調度延遲比較敏感,而批處理進程屬於那種在後臺默默幹活的,因此它更注重 throughput 的需求。當然,無論如何,分享時間片的普通進程還是需要兼顧公平,不能有人大魚大肉,有人連湯都喝不上。爲了達到這些設計目標,調度器必須要考慮某些調度因素,比如說 “優先級”、“時間片” 等。在 Linux 內核中,優先級就是實時進程調度的主要考慮因素。而對於普通進程,如何細分時間片則是調度器的核心思考點。過大的時間片會嚴重損傷系統的響應延遲,讓用戶明顯能夠感知到延遲,卡頓,從而影響用戶體驗。較小的時間片雖然有助於減少調度延遲,但是頻繁的切換對系統的 throughput 會造成嚴重的影響。因爲這時候大部分的 CPU 時間用於進程切換,而忘記了它本來的功能其實就是推動任務的執行。由於 Linux 是一個通用操作系統,它的目標是星辰大海,既能運行在嵌入式平臺上,又能在服務器領域中獲得很好的性能表現,此外在桌面應用場景中,也不能讓用戶有較差的用戶體驗。Linux 任務調度算法核心就是解決調度優化問題:
(1)公平:對於 time-sharing 的進程,保證每個進程得到合理的 CPU 時間。
(2)高效:使 CPU 保持忙碌狀態,即總是有進程在 CPU 上運行。
(3)響應時間:使交互用戶的響應時間儘可能短。
(4)週轉時間:使批處理用戶等待輸出的時間儘可能短。
(5)吞吐量:使單位時間內處理的進程數量儘可能多。
Linux 調度器採用了模塊化設計的思想,從而把進程調度的軟件分成了兩個層次,一個是 core scheduler layer,另外一個是 specific scheduler layer:
從功能層面上看,進程調度仍然分成兩個部分,第一個部分是通過負載均衡模塊將各個 runnable task 根據負載情況平均分配到各個 CPU runqueue 上去。第二部分的功能是在各個 CPU 的 Main scheduler 和 Tick scheduler 的驅動下進行單個 CPU 上的調度。調度器處理的 task 各不相同,有 RT task,有 normal task,有 Deal line task,但是無論哪一種 task,它們都有共同的邏輯,這部分被抽象成 Core scheduler layer,同時各種特定類型的調度器定義自己的 sched_class,並以鏈表的形式加入到系統中。這樣的模塊化設計可以方便用戶根據自己的場景定義 specific scheduler,而不需要改動 Core scheduler layer 的邏輯。
2 個調度器
可以用兩種方法來激活調度:
-
主調度器 :一種是直接的, 比如進程打算睡眠或出於其他原因放棄 CPU;
-
週期性調度器:通過週期性的機制, 以固定的頻率運行, 不時的檢測是否有必要。
調度策略
linux 內核目前實現了 6 種調度策略 (即調度算法),用於對不同類型的進程進行調度, 或者支持某些特殊的功能:
-
SCHED_NORMAL 和 SCHED_BATCH 調度普通的非實時進程;
-
SCHED_FIFO 和 SCHED_RR 和 SCHED_DEADLINE 則採用不同的調度策略調度實時進程;
-
SCHED_IDLE 則在系統空閒時調用 idle 進程;
-
stop 任務是系統中優先級最高的任務,它可以搶佔所有的進程並且不會被任何進程搶佔,其專屬調度器類即 stop-task;
-
idle-task 調度器類與 CFS 裏要處理的 SCHED_IDLE 沒有關係;
-
idle 任務會被任意進程搶佔,其專屬調度器類爲 idle-task;
-
idle-task 和 stop-task 沒有對應的調度策略;
-
採用 SCHED_IDLE 調度策略的任務其調度器類爲 CFS。
調度器類
-
CFS (Completely_Fair_Scheduler)
-
Real-Time Scheduler
-
stop-task (sched_class_highest) Scheduler
-
Deadline Scheduler: Earliest Deadline First (EDF) + Constant Bandwidth Server (CBS)
-
Idle-task Scheduler
調度類順序
-
優先級順序:stop-task --> deadline --> real-time --> fair --> idle
-
在各調度器類定義的時候通過 next 指針定義好了下一級調度器類;
-
stop-task 是通過宏 #define sched_class_highest (&stop_sched_class) 指定的;
-
編譯時期就已決定,不能動態擴展。
調度實體
這種一般性要求調度器不直接操作進程,而是處理可調度實體, 因此需要一個通用的數據結構描述這個調度實體,即 seched_entity 結構, 其實際上就代表了一個調度對象,可以是一個進程,也可以是一個進程組,linux 中針對當前可調度的實時和非實時進程,定義了類型爲 seched_entity 的 3 個調度實體:
-
sched_dl_entity 採用 EDF 算法調度的實時調度實體;
-
sched_rt_entity 採用 Roound-Robin 或者 FIFO 算法調度的實時調度實體;
-
sched_entity 採用 CFS 算法調度的普通非實時進程的調度實體。
調度類算法
CFS(Completely Fair Scheduler)算法(完全公平調度器,對於普通進程)
-
設定一個調度週期(sched_latency_ns),目標是讓每個進程在這個週期內至少有機會運行一次。就是每個進程等待 CPU 的時間最長不超過這個調度週期;
-
根據進程的數量,大家平分這個調度週期內的 CPU 使用權,由於進程的優先級即 nice 值不同,分割調度週期的時候要加權;
-
每個進程的經過加權後的累計運行時間保存在自己的 vruntime 字段裏;
-
哪個進程的 vruntime 最小(紅黑樹 pick_next)就獲得本輪運行的權利。
Realtime Scheduler(實時)
實時系統是這樣的一種計算系統:當事件發生後,它必須在確定的時間範圍內做出響應。在實時系統中,產生正確的結果不僅依賴於系統正確的邏輯動作,而且依賴於邏輯動作的時序。換句話說,當系統收到某個請求,會做出相應的動作以響應該請求,想要保證正確地響應該請求,一方面邏輯結果要正確,更重要的是需要在最後期限(deadline)內作出響應。如果系統未能在最後期限內進行響應,那麼該系統就會產生錯誤或者缺陷。在多任務操作系統中(如 Linux),實時調度器(realtime scheduler)負責協調實時任務對 CPU 的訪問,以確保系統中所有的實時任務在其 deadline 內完成,爲了滿足實時任務的調度需求,Linux 提供了兩種實時調度器:POSIX realtime scheduler(後文簡稱 RT 調度)和 deadline scheduler(後文簡稱 DL 調度器)。
-
Linux 支持 SCHED_RR 和 SCHED_FIFO 兩種實時調度策略。
-
先進先出(SCHED_FIFO): 沒有時間片,被調度器選擇後只要不被搶佔,阻塞,或者自願放棄處理器,可以運行任意長的時間。
-
輪轉調度(SCHED_RR): 有時間片,其值在進程運行時會減少。時間片用完後,該值重置,進程置於隊列末尾。
-
兩種調度策略都是靜態優先級,內核不爲這兩種實時進程計算動態優先級。
-
這兩種實現都屬於 軟實時。
-
實時優先級的範圍:0 ~ MAX_RT_PRIO-1
-
MAX_RT_PRIO 默認值爲 100
-
故默認實時優先級範圍:0 ~ 99。
實時進程的優先級範圍 [0~99] 都高於普通進程[100~139],始終優先於普通進程得到運行,爲了防止普通進程飢餓,Linux kernel 有一個 RealTime Throttling 機制,就是爲了防止 CPU 消耗型的實時進程霸佔所有的 CPU 資源而造成整個系統失去控制。它的原理很簡單,就是保證無論如何普通進程都能得到一定比例(默認 5%)的 CPU 時間,可以通過兩個內核參數來控制:
-
/proc/sys/kernel/sched_rt_period_us
缺省值是 1,000,000 μs (1 秒),表示實時進程的運行粒度爲 1 秒。(注:修改這個參數請謹慎,太大或太小都可能帶來問題)。 -
/proc/sys/kernel/sched_rt_runtime_us
缺省值是 950,000 μs (0.95 秒),表示在 1 秒的運行週期裏所有的實時進程一起最多可以佔用 0.95 秒的 CPU 時間。
如果 sched_rt_runtime_us=-1,表示取消限制,意味着實時進程可以佔用 100% 的 CPU 時間(慎用,有可能使系統失去控制)。
Deadline Task Scheduling
-
DL 調度器是根據任務的 deadline 來確定調度的優先順序的:deadline 最早到來的那個任務最先調度執行。對於有 M 個處理器的系統,優先級最高的前 M 個 deadline 任務(即 deadline 最早到來的前 M 個任務)將被選擇在對應 M 個處理器上運行。
-
Linux DL 調度器還實現了 constant bandwidth server(CBS)算法,該算法是一種 CPU 資源預留協議。CBS 可以保證每個任務在每個 period 內都能收到完整的 runtime 時間。在一個週期內,DL 進程的 “活” 來的時候,CBS 會重新補充該任務的運行時間。在處理 “活” 的時候,runtime 時間會不斷的消耗;如果 runtime 使用完畢,該任務會被 DL 調度器調度出局。在這種情況下,該任務無法再次佔有 CPU 資源,只能等到下一次週期到來的時候,runtime 重新補充之後才能運行。因此,CBS 一方面可以用來保證每個任務的 CPU 時間按照其定義的 runtime 參數來分配,另外一方面,CBS 也保證任務不會佔有超過其 runtime 的 CPU 資源,從而防止了 DL 任務之間的互相影響。
-
爲了避免 DL 任務造成系統超負荷運行,DL 調度器有一個准入機制,在任務配置好了 period、runtime 和 deadline 參數之後並準備加入到系統的時候,DL 調度器會對該任務進行評估。這個准入機制保證了 DL 任務將不會使用超過系統的 CPU 時間的最大值。這個最大值在 sched_rt_runtime_us 和 kernel.sched_rt_period_us sysctl 參數中指定。默認值是 950000 和 1000000,表示在 1s 的週期內,CPU 用於執行實時任務(DL 任務和 RT 任務)的最大時間值是 950000µs。對於單個核心繫統,這個測試既是必要的,也是充分的。這意味着:既然接受了該 DL 任務,那麼 CPU 有信心可以保證其在截止日期之前能夠分配給它需要的 runtime 長度的 CPU 時間。
-
deadline 調度器是僅僅根據實時任務的時序約束進行調度的,從而保證實時任務正確的邏輯行爲。雖然在多核系統中,全局 deadline 調度器會面臨 Dhall 效應(把若干個任務分配給若干個處理器執行其實是一個 NP-hard 問題(本質上是一個裝箱問題),由於各種異常場景,很難說一個調度算法會優於任何其他的算法),不過我們仍然可以對系統進行分區來解決這個問題。具體的做法是採用 cpusets 的方法把 CPU 利用率高的任務放置到指定的 cpuset 上。開發人員也可以受益於 deadline 調度器:他們可以通過設計其應用程序與 DL 調度器交互,從而簡化任務的時序控制行爲。
-
在 linux 中,DL 任務比實時任務(RR 和 FIFO)具有更高的優先級。這意味着即使是最高優先級的實時任務也會被 DL 任務延遲執行。因此,DL 任務不需要考慮來自實時任務的干擾,但實時任務必須考慮 DL 任務的干擾。
Stop_Sched_Class
stop_sched_class 用於停止 CPU, 一般在 SMP 系統上使用, 用以實現負載平衡和 CPU 熱插拔. 這個類有最高的調度優先級, stop 調度器類實現了 Unix 的 stop_machine 特性, stop_machine 是一個通信信號 : 在 SMP 的情況下相當於暫時停止其他的 CPU 的運行, 它讓一個 CPU 繼續運行,而讓所有其他 CPU 空閒。在單 CPU 的情況下這個東西就相當於關中斷。一般來說,內核會在如下情況下使用 stop_machine 技術:
Idle_Sched_Class
idle 任務會被任意進程搶佔,其專屬調度器類爲 idle-task,當 CPU 沒有任務空閒時,默認的 idle 實現是 hlt 指令,hlt 指令使 CPU 處於暫停狀態,等待硬件中斷髮生的時候恢復,從而達到節能的目的。即從處理器 C0 態變到 C1 態 (見 ACPI 標準),讓 CPU 置爲 WFI(Wait for interrupt)低功耗狀態,以節省功耗,多 cpu 系統中每個 cpu 一個 idle 進程。
組調度
linux 內核實現了 control group 功能(cgroup,since linux 2.6.24),可以支持將進程分組,然後按組來劃分各種資源。比如:group-1 擁有 30% 的 CPU 和 50% 的磁盤 IO、group-2 擁有 10% 的 CPU 和 20% 的磁盤 IO、等等。cgroup 支持很多種資源的劃分,CPU 資源就是其中之一,這就引出了組調度,
linux 內核中,傳統的調度程序是基於進程來調度的, 以進程爲單位來瓜分 CPU 資源,如果我們想把進程進行分組,以進程組進行瓜分 CPU 資源,Linux 實現了組調度架構來實現這個需求:
Linux 組調度實現架構
-
在 linux 內核中,使用 task_group 結構來管理組調度的組。所有存在的 task_group 組成一個樹型結構(與 cgroup 的目錄結構相對應),task_group 可以包含具有任意調度類別的進程(具體來說是實時進程和普通進程兩種類別),於是 task_group 需要爲每一種調度策略提供一組調度結構。這裏所說的一組調度結構主要包括兩個部分,調度實體和運行隊列(兩者都是每 CPU 一份的)。調度實體會被添加到運行隊列中,對於一個 task_group,它的調度實體會被添加到其父 task_group 的運行隊列,因爲被調度的對象有 task_group 和 task 兩種,所以需要一個抽象的結構來代表它們。如果調度實體代表 task_group,則它的 my_q 字段指向這個調度組對應的運行隊列;否則 my_q 字段爲 NULL,調度實體代表 task。在調度實體中與 my_q 相對的是 X_rq(具體是針對普通進程的 cfs_rq 和針對實時進程的 rt_rq),前者指向這個組自己的運行隊列,裏面會放入它的子節點;後者指向這個組的父節點的運行隊列,也就是這個調度實體應該被放入的運行隊列;
-
調度發生的時候,調度程序從根 task_group 的運行隊列中選擇一個調度實體。如果這個調度實體代表一個 task_group,則調度程序需要從這個組對應的運行隊列繼續選擇一個調度實體。如此遞歸下去,直到選中一個進程。除非根 task_group 的運行隊列爲空,否則遞歸下去一定能找到一個進程。因爲如果一個 task_group 對應的運行隊列爲空,它對應的調度實體就不會被添加到其父節點對應的運行隊列中;
組的調度策略
組調度的主要針對 rt(實時調度)和 cfs(完全公平調度)兩種類別:
實時進程的組調度
實時進程是對 CPU 有着實時性要求的進程,它的優先級是跟具體任務相關的,完全由用戶來定義的。調度器總是會選擇優先級最高的實時進程來運行,發展到組調度,組的優先級就被定義爲 “組內最高優先級的進程所擁有的優先級。
普通進程的組調度支持 (Fair Group Scheduling)
2.6.23 引入的 CFS 調度器對所有進程完全公平對待。但是依然有個問題:設想當前機器有 2 個用戶,有一個用戶跑着 9 個進程,且都是 CPU 密集型進程;另一個用戶只跑着一個 X 進程,是交互性進程。從 CFS 的角度看,它將平等對待這 10 個進程,結果導致的是跑 X 進程的用戶受到不公平對待,他只能得到約 10% 的 CPU 時間,讓他的體驗相當差。基於此,組調度的概念被引入 [6]。CFS 處理的不再是一個進程的概念,而是調度實體(sched entity),一個調度實體可以只包含一個進程,也可以包含多個進程。因此,上述例子的困境可以這麼解決:分別爲每個用戶建立一個組,組裏放該用戶所有進程,從而保證用戶間的公平性。該功能是基於控制組(control group, cgroup) 的概念,需要內核開啓 CGROUP 的支持纔可使用。
信號處理
信號機制是進程之間相互傳遞消息的一種方法,信號全稱爲軟中斷信號,也有人稱作軟中斷。從它的命名可以看出,它的實質和使用很像中斷。所以,信號可以說是進程控制的一部分:
-
軟中斷信號(signal,又簡稱爲信號)用來通知進程發生了異步事件。進程之間可以互相通過系統調用 kill 發送軟中斷信號。內核也可以因爲內部事件而給進程發送信號,通知進程發生了某個事件。注意,信號只是用來通知某進程發生了什麼事件,並不給該進程傳遞任何數據;
-
當信號發送到某個進程中時,操作系統會中斷該進程的正常流程,並進入相應的信號處理函數執行操作,完成後再回到中斷的地方繼續執行;
-
信號分類處理, 第一種是類似中斷的處理程序,對於需要處理的信號,進程可以指定處理函數,由該函數來處 理。第二種方法是,忽略某個信號,對該信號不做任何處理,就象未發生過一樣。第三種方法是,對該信號的處理保留系統的默認值,這種缺省操作,對大部分的信 號的缺省操作是使得進程終止。進程通過系統調用 signal 來指定進程對某個信號的處理行爲;
-
在進程表的表項中有一個軟中斷信號域,該域中每一位對應一個信號,當有信號發送給進程時,對應位置位。由此可以看出,進程對不同的信號可以同時保留,但對於同一個信號,進程並不知道在處理之前來過多少個;
信號分類:
(1) 與進程終止相關的信號。當進程退出,或者子進程終止時,發出這類信號。
(2) 與進程例外事件相關的信號。如進程越界,或企圖寫一個只讀的內存區域(如程序正文區),或執行一個特權指令及其他各種硬件錯誤。
(3) 與在系統調用期間遇到不可恢復條件相關的信號。如執行系統調用 exec 時,原有資源已經釋放,而目前系統資源又已經耗盡。
(4) 與執行系統調用時遇到非預測錯誤條件相關的信號。如執行一個並不存在的系統調用。
(5) 在用戶態下的進程發出的信號。如進程調用系統調用 kill 向其他進程發送信號。
(6) 與終端交互相關的信號。如用戶關閉一個終端,或按下 break 鍵等情況。
(7) 跟蹤進程執行的信號。
多線程信號處理:
-
不要在線程的信號掩碼中阻塞不能被忽略處理的兩個信號 SIGSTOP 和 SIGKILL。
-
不要在線程的信號掩碼中阻塞 SIGFPE、SIGILL、SIGSEGV、SIGBUS。
-
確保 sigwait() 等待的信號集已經被進程中所有的線程阻塞。
-
在主線程或其它工作線程產生信號時,必須調用 kill() 將信號發給整個進程,而不能使用 pthread_kill() 發送某個特定的工作線程,否則信號處理線程無法接收到此信號。
-
因爲 sigwait()使用了串行的方式處理信號的到來,爲避免信號的處理存在滯後,或是非實時信號被丟失的情況,處理每個信號的代碼應儘量簡潔、快速,避免調用會產生阻塞的庫函數。
** 線程**
線程(英語:thread)是操作系統能夠進行運算調度的最小單位。大部分情況下,它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務。在 Unix System V 及 SunOS 中也被稱爲輕量進程(lightweight processes),但輕量進程更多指內核線程(kernel thread),而把用戶線程(user thread)稱爲線程。一個進程的組成實體可以分爲兩大部分:線程集和資源集。進程中的線程是動態的對象;代表了進程指令的執行。資源,包括地址空間、打開的文件、用戶信息等等,由進程內的線程共享。
線程和進程的關係
根據操作系統內核是否對線程可感知,可以把線程分爲內核線程和用戶線程
分類的標準主要是線程的調度者在覈內還是在覈外。前者更利於併發使用多處理器的資源,而後者則更多考慮的是上下文切換開銷。Linux 內核只提供了輕量進程的支持,限制了更高效的線程模型的實現,但 Linux 着重優化了進程的調度開銷,一定程度上也彌補了這一缺陷。目前最流行的線程機制 LinuxThreads 所採用的就是 “線程 - 進程” 一對一模型(還存在多對一,多對多模型)用戶級實現一個包括信號處理在內的線程管理機制。
Linux 線程實現採用內核級線程” 一對一” 模型
在 linux 系統下,無論是進程,還是線程,到了內核裏面,我們統一都叫任務(Task),由一個統一的結構 task_struct 進行管理。一個進程由於其運行空間的不同,從而有內核線程和用戶進程的區分。內核線程運行在內核空間,之所以稱之爲線程是因爲它沒有虛擬地址空間,只能訪問內核的代碼和數據; 而用戶進程則運行在用戶空間, 不能直接訪問內核的數據但是可以通過中斷,系統調用等方式從用戶態陷入內核態,但是內核態只是進程的一種狀態, 與內核線程有本質區別,用戶進程運行在用戶空間上,而一些通過共享資源實現的一組進程我們稱之爲線程組。Linux 下內核其實本質上沒有線程的概念,Linux 下線程其實上是與其他進程共享某些資源的進程而已。但是我們習慣上還是稱它們爲線程或者輕量級進程。
內核線程
內核線程是直接由內核本身啓動的進程。內核線程實際上是將內核函數委託給獨立的進程,它與內核中的其他進程” 並行” 執行。內核線程經常被稱之爲內核守護進程,他們執行下列任務:
-
週期性地將修改的內存頁與頁來源塊設備同步;
-
如果內存頁很少使用,則寫入交換區;
-
管理延時動作, 如2號進程接手內核進程的創建;
-
實現文件系統的事務日誌;
-
…
內核線程主要有兩種類型:
-
線程啓動後一直等待,直至內核請求線程執行某一特定操作。
-
線程啓動後按週期性間隔運行,檢測特定資源的使用,在用量超出或低於預置的限制時採取行動。
內核線程由內核自身生成,其特點在於:
-
它們在 CPU 的內核態執行,而不是用戶態;
-
它們只可以訪問虛擬地址空間的內核部分(高於 TASK_SIZE 的所有地址),但不能訪問用戶空間。
Linux 在內核線程架構設計中,內核線程建立和銷燬都是由操作系統負責、通過系統調用完成的。在內核的支持下運行,無論是用戶進程的線程,或者是系統進程的線程,他們的創建、撤銷、切換都是依靠內核實現的。線程管理的所有工作由內核完成,應用程序沒有進行線程管理的代碼,只有一個到內核級線程的編程接口. 內核爲進程及其內部的每個線程維護上下文信息,調度也是在內核基於線程架構的基礎上完成。內核線程駐留在內核空間,它們是內核對象。
內核線程就是內核的分身,一個分身可以處理一件特定事情。Linux 內核使用內核線程來將內核分成幾個功能模塊,像 kworker, kswapd, ksoftirqd, migration , rcu_bh,rcu_schd, watchdog 等 (內核線程都用 [] 括起來)。這在處理異步事件如異步 IO,阻塞任務,延後任務處理時特別有用。內核線程的使用是廉價的,唯一使用的資源就是內核棧和上下文切換時保存寄存器的空間,內核線程只運行在內核態,不受用戶態上下文的拖累,在多核系統中,很多內核線程都是 per cpu 運行粒度。
用戶線程
Linux 內核只提供了輕量級進程 (LWP) 的方式支持用戶線程,限制了更高效的線程模型的實現,但 Linux 着重優化了進程的調度開銷,一定程度上也彌補了這一缺陷。目前最流行的線程機制 LinuxThreads 所採用的就是線程 - 進程”一對一”模型,調度交給核心,而在用戶級實現一個包括信號處理在內的線程管理機制。輕量級進程由 clone()系統調用創建,參數是 CLONE_VM,即與父進程是共享進程地址空間和系統資源。
LinuxThreads 是用戶空間的線程庫,所採用的是 “線程 - 進程”1 對 1 模型(即一個用戶線程對應一個輕量級進程,而一個輕量級進程對應一個特定的內核線程),將線程的調度等同於進程的調度,調度交由內核完成, 有了內核線程,每個用戶線程被映射或綁定到一個內核線程。用戶線程在其生命期內都會綁定到該內核線程。調度器管理、調度並分派這些線程。運行時庫爲每個用戶級線程請求一個內核級線程。而線程的創建、同步、銷燬由核外線程庫完成(LinuxThtreads 已綁定到 GLIBC 中發行)。 在 LinuxThreads 中,由專門的一個管理線程處理所有的線程管理工作。當進程第一次調用 pthread_create() 創建線程時就會先 創建 (clone()) 並啓動管理線程。後續進程 pthread_create()創建線程時,都是管理線程作爲 pthread_create()的調用者的子線程,通過調用 clone()來創建用戶線程,並記錄輕量級進程號和線程 id 的映射關係,因此用戶線程其實是管理線程的子線程。LinuxThreads 只支持調度範圍爲 PTHREAD_SCOPE_SYSTEM 的調度,默認的調度策略是 SCHED_OTHER。 用戶線程調度策略也可修改成 SCHED_FIFO 或 SCHED_RR 方式,這兩種方式支持優先級爲 0-99, 而 SCHED_OTHER 只支持 0。
Linux 輕量級進程實現
LinuxThtreads 的設計存在一些侷限性,導致後面 Linux 實現了新的線程庫 NPTL,或稱爲 Native POSIX Thread Library,是 Linux 線程的一個新實現,它克服了 LinuxThreads 的缺點,同時也符合 POSIX 的需求。與 LinuxThreads 相比,它在性能和穩定性方面都提供了重大的改進。與 LinuxThreads 一樣,NPTL 也實現了一對一的模型。
輕量級進程具有侷限性:
-
首先,大多數 LWP 的操作,如建立、析構以及同步,都需要進行系統調用。系統調用的代價相對較高:需要在 user mode 和 kernel mode 中切換。
-
其次,每個 LWP 都需要有一個內核線程支持,因此 LWP 要消耗內核資源(內核線程的棧空間)。因此一個系統不能支持大量的 LWP。
優點:
(1)運行代價:LWP 只有一個最小的執行上下文和調度程序所需的統計信息。
(2)處理器競爭:因與特定內核線程關聯,因此可以在全系統範圍內競爭處理器資源。
(3)使用資源:與父進程共享進程地址空間。
(4)調度:像普通進程一樣調度。
協程
以上描述的不管是中斷,進程,線程(內核線程,用戶線程(輕量級進程實現))的調度都是由內核掌控,用戶並不能直接干預,要在用戶態實現對邏輯調度控制,需要實現類似用戶級線程,用戶級線程是完全建立在用戶空間的線程庫,用戶線程的創建、調度、同步和銷燬全部庫函數在用戶空間完成,不需要內核的幫助。因此這種線程是極其低消耗和高效的。協程本質上也是一種用戶級線程實現,在一個線程(內核執行單元)內,協程通過主動放棄時間片交由其他協程執行來協作,故名協程。協程的一些關鍵點:
-
任何代碼執行都需要上下文(CPU),協程也有自己的上下文,協程切換隻涉及基本的 CPU 上下文切換, 完全在用戶空間進行,沒有模式切換,所以比線程切換要小,開源 libco 的協程切換的彙編代碼,也就是二十來條彙編指令,一般切換代價: 協程 < 線程 < 系統調用 < 進程 ;
-
在多核多線程系統中,線程切換代價比較高(cache miss 等),爲了減少線程切換,希望在同一個線程內進行不同邏輯的僞並行(實際上還是串行),這樣降低了代碼邏輯切換代價,但這樣並沒有擁有真正併發帶來的高性能,選擇合適的使用場景;
-
協程存在兼容性問題,協程分爲有棧協程和無棧協程實現,對不同系統和處理器要進行兼容,但不同系統對上下文實現,異常等不一樣,這樣就可能產生不兼容情況,一般用戶態的代碼都不關心底層差別,而使用協程後的代碼兼容性變差。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/tuCtWOh6O54LRa5g3UFJJA