Linux调度系统全景指南(下篇)
點擊上方藍字關注公眾號,更多經典內容等著你
| 導語本文主要是講Linux的調度系統, 由于全部內容太多,分三部分來講,本篇是下篇(主要線程和進程),上篇請看(CPU和中斷):Linux調度系統全景指南(上篇),調度可以說是操作系統的靈魂,為了讓CPU資源利用最大化,Linux設計了一套非常精細的調度系統,對大多數場景都進行了很多優化,系統擴展性強,我們可以根據業務模型和業務場景的特點,有針對性的去進行性能優化,在保證客戶網絡帶寬前提下,隔離客戶互相之間的干擾影響,提高CPU利用率,降低單位運算成本,提高市場競爭力。歡迎大家相互交流學習!
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 目錄
上篇請看(CPU和中斷):Linux調度系統全景指南(上篇)
中篇請看(搶占和時鐘):Linux調度系統全景指南(中篇)? ?
? ? ?? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?進程
?一般定義是操作系統對一個正在運行的程序的一種抽象,?是運行資源的管理單位(虛擬內存空間,文件句柄,全局變量,信號等運行資源),是操作系統資源分配的最小單位。在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等),為了減少線程切換,希望在同一個線程內進行不同邏輯的偽并行(實際上還是串行),這樣降低了代碼邏輯切換代價,但這樣并沒有擁有真正并發帶來的高性能,選擇合適的使用場景;
協程存在兼容性問題,協程分為有棧協程和無棧協程實現,對不同系統和處理器要進行兼容,但不同系統對上下文實現,異常等不一樣,這樣就可能產生不兼容情況,一般用戶態的代碼都不關心底層差別,而使用協程后的代碼兼容性變差。
?
本期結束,我們下期再見!
想要獲取linux調度全景指南精簡版,關注公眾號回復“調度”即可獲取。回復其他消息,獲取更多內容;
? ? ? ? ? ?? ? ? ? ? ? ? ? ? ? ? ? ? ??
往期推薦
Linux調度系統全景指南(上篇)
Linux調度系統全景指南(中篇)
如何成為一名大廠的優秀員工?
C++模版的本質
C++內存管理全景指南
云網絡丟包故障定位全景指南
? ? ? ? ? ?
掃碼二維碼
獲取更多精彩內容
總結
以上是生活随笔為你收集整理的Linux调度系统全景指南(下篇)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux调度系统全景指南(中篇)
- 下一篇: Linux调度系统全景指南(终结篇)