linux程式排程

甘雨火光發表於2018-12-27

為什麼要有程式優先順序?這似乎不用過多的解釋,畢竟自從多工作業系統誕生以來,程式執行佔用cpu的能力就是一個必須要可以人為控制的事情。因為有的程式相對重要,而有的程式則沒那麼重要。

本文作者:鄒立巍

Linux系統技術專家。目前在騰訊SNG社交網路運營部 計算資源平臺組,負責內部私有云平臺的建設和架構規劃設計。

曾任新浪動態應用平臺系統架構師,負責微博、新浪部落格等重點業務的內部私有云平臺架構設計和運維管理工作。

程式優先順序起作用的方式從發明以來基本沒有什麼變化,無論是隻有一個cpu的時代,還是多核cpu時代,都是通過控制程式佔用cpu時間的長短來實現的。就是說在同一個排程週期中,優先順序高的程式佔用的時間長些,而優先順序低的程式佔用的短些。

從這個角度看,程式優先順序其實也跟cgroup的cpu限制一樣,都是一種針對cpu佔用的QOS機制。我曾經一直很困惑一點,為什麼已經有了優先順序,還要再設計一個針對cpu的cgroup?得到的答案大概是因為,優先順序這個值不能很直觀的反饋出資源分配的比例吧?

不過這不重要,實際上從核心目前的程式排程器cfs的角度說,同時實現cpushare方式的cgroup和優先順序這兩個機制完全是相同的概念,並不會因為增加一個機制而提高什麼實現成本。既然如此,而cgroup又顯得那麼酷,那麼何樂而不為呢?

在系統上我們最熟悉的優先順序設定方式是nice和renice命令。那麼我們首先解釋一個概念,什麼是:

 

NICE值

nice值應該是熟悉Linux/UNIX的人很瞭解的概念了,我們都知它是反應一個程式“優先順序”狀態的值,其取值範圍是-20至19,一共40個級別。這個值越小,表示程式”優先順序”越高,而值越大“優先順序”越低。我們可以通過nice命令來對一個將要執行的命令進行nice值設定,方法是:

  1. [root@zorrozou-pc0 zorro]#nice-10bash

這樣我就又開啟了一個bash,並且其nice值設定為10,而預設情況下,程式的優先順序應該是從父程式繼承來的,這個值一般是0。我們可以通過nice命令直接檢視到當前shell的nice值

  1. [root@zorrozou-pc0 zorro]#nice
  2. 10

對比一下正常情況:

  1. [root@zorrozou-pc0 zorro]#exit

推出當前nice值為10的bash,開啟一個正常的bash:

  1. [root@zorrozou-pc0 zorro]#bash
  2. [root@zorrozou-pc0 zorro]#nice
  3. 0

另外,使用renice命令可以對一個正在執行的程式進行nice值的調整,我們也可以使用比如top、ps等命令檢視程式的nice值,具體方法我就不多說了,大家可以參閱相關manpage。

需要大家注意的是,我在這裡都在使用nice值這一稱謂,而非優先順序(priority)這個說法。當然,nice和renice的man手冊中, 也說的是priority這個概念,但是要強調一下,請大家真的不要混淆了系統中的這兩個概念,一個是nice值,一個是priority值,他們有著千 絲萬縷的關係,但對於當前的Linux系統來說,它們並不是同一個概念。

我們看這個命令:

  1. [root@zorrozou-pc0 zorro]#ps-l
  2. F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
  3. 4 S 0692457760800-17952 poll_s pts/500:00:00sudo
  4. 4 S 0692569240800-4435 wait pts/500:00:00bash
  5. 0 R 01297169250800-8514- pts/500:00:00ps

大家是否真的明白其中PRI列和NI列的具體含義有什麼區別?同樣的,如果是top命令:

  1. Tasks:1587 total,7 running,1570 sleeping,0 stopped,10 zombie
  2. Cpu(s):13.0%us,6.9%sy,0.0%ni,78.6%id,0.0%wa,0.0%hi,1.5%si,0.0%st
  3. Mem:132256952k total,107483920k used,24773032k free,2264772k buffers
  4. Swap:2101192k total,508k used,2100684k free,88594404k cached
  5. PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
  6. 3001 root 200232m21m4500 S 12.90.00:15.09 python
  7. 11541 root 200174562400888 R 7.40.00:00.06top

大家是否搞清楚了這其中PR值和NI值的差別?如果沒有,那麼我們可以首先搞清楚什麼是nice值。

nice值雖然不是priority,但是它確實可以影響程式的優先順序。

在英語中,如果我們形容一個人nice,那一般說明這個人的人緣比較好。什麼樣的人人緣好?往往是謙讓、有禮貌的人。比如,你跟一個nice的人一 起去吃午飯,點了兩個一樣的飯,先上了一份後,nice的那位一般都會說:“你先吃你先吃!”,這就是人緣好,這人nice!但是如果另一份上的很晚,那 麼這位nice的人就要餓著了。這說明什麼?越nice的人搶佔資源的能力就越差,而越不nice的人搶佔能力就越強。這就是nice值大小的含義,nice值越低,說明程式越不nice,搶佔cpu的能力就越強,優先順序就越高。在原來使用O1排程的Linux上,我們還會把nice值叫做靜態優先順序,這也基本符合nice值的特點,就是��nice值設定好了之後,除非我們用renice去改它,否則它是不變的。而priority的值在之前核心的O1排程器上表現是會變化的,所以也叫做動態優先順序

 

優先順序和實時程式

簡單瞭解nice值的概念之後,我們再來看看什麼是priority值,就是ps命令中看到的PRI值或者top命令中看到的PR值。本文為了區分這些概念,以後統一用nice值表示NI值,或者叫做靜態優先順序,也就是用nice和renice命令來調整的優先順序;而使用priority值表示PRI和PR值,或者叫動態優先順序。我們也統一將“優先順序”這個詞的概念規定為表示priority值的意思。

在核心中,程式優先順序的取值範圍是通過一個巨集定義的,這個巨集的名稱是MAX_PRIO,它的值為140。而這個值又是由另外兩個值相加組成的,一個 是代表nice值取值範圍的NICE_WIDTH巨集,另一個是代表實時程式(realtime)優先順序範圍的MAX_RT_PRIO巨集。說白了就 是,Linux實際上實現了140個優先順序範圍,取值範圍是從0-139,這個值越小,優先順序越高。nice值的-20到19,對映到實際的優先順序範圍是 100-139。新產生程式的預設優先順序被定義為:

  1. #define DEFAULT_PRIO (MAX_RT_PRIO + NICE_WIDTH /2)

實際上對應的就是nice值的0。正常情況下,任何一個程式的優先順序都是這個值,即使我們通過nice和renice命令調整了程式的優先順序,它的 取值範圍也不會超出100-139的範圍,除非這個程式是一個實時程式,那麼它的優先順序取值才會變成0-99這個範圍中的一個。這裡隱含了一個資訊,就是 說當前的Linux是一種已經支援實時程式的作業系統。

什麼是實時作業系統,我們就不再這裡詳細解釋其含義以及在工業領域的應用了,有興趣的可以參考一下實時作業系統的維基百科。簡單來說,實時作業系統需要保證相關的實時程式在較短的時間內響應,不會有較長的延時,並且要求最小的中斷延時和程式切換延時。對於這樣的需求,一般的程式排程演算法,無論是O1還是CFS都是無法滿足的,所以核心在設計的時候,將實時程式單獨對映了100個優先順序,這些優先順序都要高與正常程式的優先順序(nice值),而實時程式的排程演算法也不同,它們採用更簡單的排程演算法來減少排程開銷。總的來說,Linux系統中執行的程式可以分成兩類:

  1. 實時程式
  2. 非實時程式

它們的主要區別就是通過優先順序來區分的。所有優先順序值在0-99範圍內的,都是實時程式,所以這個優先順序範圍也可以叫做實時程式優先順序,而 100-139範圍內的是非實時程式。在系統中可以使用chrt命令來檢視、設定一個程式的實時優先順序狀態。我們可以先來看一下chrt命令的使用:

  1. [root@zorrozou-pc0 zorro]# chrt
  2. Showor change the real-time scheduling attributes of a process.
  3. Set policy:
  4. chrt [options]<priority><command>[<arg>...]
  5. chrt [options]-<priority><pid>
  6. Get policy:
  7. chrt [options]-<pid>
  8. Policy options:
  9. -b,--batch set policy to SCHED_OTHER
  10. -f,--fifo set policy to SCHED_FIFO
  11. -i,--idle set policy to SCHED_IDLE
  12. -o,--other set policy to SCHED_OTHER
  13. -r,--rr set policy to SCHED_RR (default)
  14. Scheduling flag:
  15. -R,--reset-on-fork set SCHED_RESET_ON_FORK for FIFO or RR
  16. Other options:
  17. -a,--all-tasks operate on all the tasks (threads)for a given pid
  18. -m,--max show min and max valid priorities
  19. -p,--pid operate on existing given pid
  20. -v,--verbose display status information
  21. -h,--help display this help andexit
  22. -V,--version output version information andexit
  23. Formore details see chrt(1).

我們先來關注顯示出的Policy options部分,會發現系統給個種程式提供了5種排程策略。但是這裡並沒有說明的是,這五種排程策略是分別給兩種程式用的,對於實時程式可以用的排程 策略是:SCHED_FIFO、SCHED_RR,而對於非實時程式則是:SCHED_OTHER、SCHED_OTHER、SCHED_IDLE。

系統的整體優先順序策略是:如果系統中存在需要執行的實時程式,則優先執行實時程式。直到實時程式退出或者主動讓出CPU時,才會排程執行非實時程式。實時程式可以指定的優先順序範圍為1-99,將一個要執行的程式以實時方式執行的方法為:

  1. [root@zorrozou-pc0 zorro]# chrt 10bash
  2. [root@zorrozou-pc0 zorro]# chrt -p $$
  3. pid 14840`s current scheduling policy: SCHED_RR
  4. pid 14840`s current scheduling priority:10

可以看到,新開啟的bash已經是實時程式,預設排程策略為SCHED_RR,優先順序為10。如果想修改排程策略,就加個引數:

  1. [root@zorrozou-pc0 zorro]# chrt -10bash
  2. [root@zorrozou-pc0 zorro]# chrt -p $$
  3. pid 14843`s current scheduling policy: SCHED_FIFO
  4. pid 14843`s current scheduling priority:10

剛才說過,SCHED_RR和SCHED_FIFO都是實時排程策略,只能給實時程式設定。對於所有實時程式來說,優先順序高的(就是 priority數字小的)程式一定會保證先於優先順序低的程式執行。SCHED_RR和SCHED_FIFO的排程策略只有當兩個實時程式的優先順序一樣的 時候才會發生作用,其區別也是顧名思義:

SCHED_FIFO:以先進先出的佇列方式進行排程,在優先順序一樣的情況下,誰先執行的就先排程誰,除非它退出或者主動釋放CPU。

SCHED_RR:以時間片輪轉的方式對相同優先順序的多個程式進行處理。時間片長度為100ms。

這就是Linux對於實時程式的優先順序和相關排程演算法的描述。整體很簡單,也很實用。而相對更麻煩的是非實時程式,它們才是Linux上程式的主要分類。對於非實時程式優先順序的處理,我們首先還是要來介紹一下它們相關的排程演算法:O1和CFS。

 

O1排程

O1排程演算法是在Linux 2.6開始引入的,到Linux 2.6.23之後核心將排程演算法替換成了CFS。雖然O1演算法已經不是當前核心所預設使用的排程演算法了,但是由於大量線上的伺服器可能使用的Linux版 本還是老版本,所以我相信很多伺服器還是在使用著O1排程器,那麼費一點口舌簡單交代一下這個排程器也是有意義的。這個排程器的名字之所以叫做O1,主要 是因為其演算法的時間複雜度是O1。

O1排程器仍然是根據經典的時間片分配的思路來進行整體設計的。簡單來說,時間片的思路就是將CPU的執行時間 分成一小段一小段的,假如是5ms一段。於是多個程式如果要“同時”執行,實際上就是每個程式輪流佔用5ms的cpu時間,而從1s的時間尺度上看,這些 程式就是在“同時”執行的。當然,對於多核系統來說,就是把每個核心都這樣做就行了。而在這種情況下,如何支援優先順序呢?實際上就是將時間片分配成大小不 等的若干種,優先順序高的程式使用大的時間片,優先順序小的程式使用小的時間片。這樣在一個週期結速後,優先順序大的程式就會佔用更多的時間而因此得到特殊待 遇。O1演算法還有一個比較特殊的地方是,即使是相同的nice值的程式,也會再根據其CPU的佔用情況將其分成兩種型別:CPU消耗型和IO消耗性。 典型的CPU消耗型的程式的特點是,它總是要一直佔用CPU進行運算,分給它的時間片總是會被耗盡之後,程式才可能發生排程。比如常見的各種算數運算程 序。而IO消耗型的特點是,它經常時間片沒有耗盡就自己主動先釋放CPU了,比如vi,emacs這樣的編輯器就是典型的IO消耗型程式。

為什麼要這樣區分呢?因為IO消耗型的程式經常是跟人互動的程式,比如shell、編輯器等。當系統中既有這種程式,又有CPU消耗型程式存在,並 且其nice值一樣時,假設給它們分的時間片長度是一樣的,都是500ms,那麼人的操作可能會因為CPU消耗型的程式一直佔用CPU而變的卡頓。可以想 象,當bash在等待人輸入的時候,是不佔CPU的,此時CPU消耗的程式會一直運算,假設每次都分到500ms的時間片,此時人在bash上敲入一個字 符的時候,那麼bash很可能要等個幾百ms才能給出響應,因為在人敲入字元的時候,別的程式的時間片很可能並沒有耗盡,所以系統不會排程bash程度進 行處理。為了提高IO消耗型程式的響應速度,系統將區分這兩類程式,並動態調整CPU消耗的程式將其優先順序降低,而IO消耗型的將其優先順序變高,以降低 CPU消耗程式的時間片的實際長度。已知nice值的範圍是-20 – 19,其對應priority值的範圍是100-139,對於一個預設nice值為0的程式來說,其初始priority值應該是120,隨著其不斷執 行,核心會觀察程式的CPU消耗狀態,並動態調整priority值,可調整的範圍是+-5。就是說,最��其優先順序可以唄自動調整到115,最低到 125。這也是為什麼nice值叫做靜態優先順序而priority值叫做動態優先順序的原因。不過這個動態調整的功能在排程器換成CFS之後就不需要了,因 為CFS換了另外一種CPU時間分配方式,這個我們後面再說。

再簡單瞭解了O1演算法按時間片分配CPU的思路之後,我們再來結合程式的狀態簡單看看其演算法描述。我們都知道程式有5種狀態:

  • S(Interruptible sleep):可中斷休眠狀態。
  • D(Uninterruptible sleep):不可中斷休眠狀態。
  • R(Running or runnable):執行或者在可執行佇列中。
  • Z(Zombie process):殭屍。
  • T(Stopped):暫停。

在CPU排程時,主要只關心R狀態程式,因為其他狀態程式並不會被放倒排程佇列中進行排程。排程佇列中的程式一般主要有兩種情況,一種是程式已經被 排程到CPU上執行,另一種是程式正在等待被排程。出現這兩種狀態的原因應該好理解,因為需要執行的程式數可能多於硬體的CPU核心數,比如需要執行的進 程有8個而CPU核心只有4個,此時cpu滿載的時候,一定會有4個程式處在“等待”狀態,因為此時有另外四個程式正在佔用CPU執行。

根據以上情況我們可以理解,系統當下需要同時進行排程處理的程式數(R狀態程式數)和系統CPU的比值,可以一定程度的反應系統的“繁忙”程度。需 要排程的程式越多,核心越少,則意味著系統越繁忙。除了程式執行本身需要佔用CPU以外,多個程式的排程切換也會讓系統繁忙程度增加的更多。所以,我們往 往會發現,R狀態程式數量在增長的情況下,系統的效能表現會下降。系統中可以使用uptime命令檢視系統平均負載指數(load average):

  1. [zorro@zorrozou-pc0 ~]uptime
  2. 16:40:56 up 2:12,1 user, load average:0.05,0.11,0.16

其中load average中分別顯示的是1分鐘,5分鐘,15分鐘之內的平均負載指數(可以簡單認為是相映時間範圍內的R狀態程式個數)。但是這個命令顯示的數字是 絕對個數,並沒有表示出不同CPU核心數的實際情況。比如,如果我們的1分鐘load average為16,而CPU核心數為32的話,那麼這個系統的其實並不繁忙。但是如果CPU個數是8的話,那可能就意味著比較忙了。但是實際情況往往 可能比這更加複雜,比如程式消耗型別也會對這個數字的解讀有影響。總之,這個值的絕對高低並不能直觀的反饋出來當前系統的繁忙程度,還需要根據系統的其它 指標綜合考慮。

O1排程器在處理流程上大概是這樣進行排程的:

  1. 首先,程式產生(fork)的時候會給一個程式分配一個時間片長度。這個新程式的時間片一般是父程式的一半,而父程式也會因此減少它的時間片長度 為原來的一半。就是說,如果一個程式產生了子程式,那麼它們將會平分當前時間片長度。比如,如果父程式時間片還剩100ms,那麼一個fork產生一個子 程式之後,子程式的時間片是50ms,父程式剩餘的時間片是也是50ms。這樣設計的目的是,為了防止程式通過fork的方式讓自己所處理的任務一直有時 間片。不過這樣做也會帶來少許的不公平,因為先產生的子程式獲得的時間片將會比後產生的長,第一個子程式分到父程式的一半,那麼第二個子程式就只能分到 1/4。對於一個長期工作的程式組來說,這種影響可以忽略,因為第一輪時間片在耗盡後,系統會在給它們分配長度相當的時間片。
  2. 針對所有R狀態程式,O1演算法使用兩個佇列組織程式,其中一個叫做活動佇列,另一個叫做過期佇列。活動佇列中放的都是時間片未被耗盡的程式,而過期佇列中放時間片被耗盡的程式。
  3. 如1所述,新產生的程式都會先獲得一個時間片,進入活動佇列等待排程到CPU執行。而核心會在每個tick間隔期間對正在CPU上執行的程式進行 檢查。一般的tick間隔時間就是cpu時鐘中斷間隔,每秒鐘會有1000個,即頻率為1000HZ。每個tick間隔週期主要檢查兩個內容:1、當前正 在佔用CPU的程式是不是時間片已經耗盡了?2、是不是有更高優先順序的程式在活動佇列中等待排程?如果任何一種情況成立,就把則當前程式的執行狀態終止, 放到等待佇列中,換當前在等待佇列中優先順序最高的那個程式執行。

以上就是O1排程的基本排程思路,當然實際情況是,還要加上SMP(對稱多處理)的邏輯,以滿足多核CPU的需求。目前在我的archlinux上可以用以下命令檢視核心HZ的配置:

  1. [zorro@zorrozou-pc0 ~]$ zgrep CONFIG_HZ /proc/config.gz
  2. # CONFIG_HZ_PERIODIC isnotset
  3. # CONFIG_HZ_100 isnotset
  4. # CONFIG_HZ_250 isnotset
  5. CONFIG_HZ_300=y
  6. # CONFIG_HZ_1000 isnotset
  7. CONFIG_HZ=300

我們發現我當前系統的HZ配置為300,而不是一般情況下的1000。大家也可以思考一下,配置成不同的數字(100、250、300、1000),對系統的效能到底會有什麼影響?

 

CFS完全公平排程

O1已經是上一代排程器了,由於其對多核、多CPU系統的支援效能並不好,並且核心功能上要加入cgroup等因素,Linux在2.6.23之後 開始啟用CFS作為對一般優先順序(SCHED_OTHER)程式排程方法。在這個重新設計的排程器中,時間片,動態、靜態優先順序以及IO消耗,CPU消耗 的概念都不再重要。CFS採用了一種全新的方式,對上述功能進行了比較完善的支援。

其設計的基本思路是,我們想要實現一個對所有程式完全公平的排程器。又是那個老問題:如何做到完全公平?答案跟上一篇IO排程中CFQ的思路類似: 如果當前有n個程式需要排程執行,那麼排程器應該再一個比較小的時間範圍內,把這n個程式全都排程執行一遍,並且它們平分cpu時間,這樣就可以做到所有 程式的公平排程。那麼這個比較小的時間就是任意一個R狀態程式被排程的最大延時時間,即:任意一個R狀態程式,都一定會在這個時間範圍內被排程相應。這個 時間也可以叫做排程週期,其英文名字叫做:sched_latency_ns。程式越多,每個程式在週期內被執行的時間就會被平分的越小。排程器只需要對 所有程式維護一個累積佔用CPU時間數,就可以衡量出每個程式目前佔用的CPU時間總量是不是過大或者過小,這個數字記錄在每個程式的vruntime 中。所有待執行程式都以vruntime為key放到一個由紅黑樹組成的佇列中,每次被排程執行的程式,都是這個紅黑樹的最左子樹上的那個程式,即 vruntime時間最少的程式,這樣就保證了所有程式的相對公平。

在基本驅動機制上CFS跟O1一樣,每次時鐘中斷來臨的時候,都會進行佇列排程檢查,判斷是否要程式排程。當然還有別的時機需要排程檢查,發生排程的時機可以總結為這樣幾個:

  1. 當前程式的狀態轉換時。主要是指當前程式終止退出或者程式休眠的時候。
  2. 當前程式主動放棄CPU時。狀態變為sleep也可以理解為主動放棄CPU,但是當前核心給了一個方法,可以使用sched_yield()在不發生狀態切換的情況下主動讓出CPU。
  3. 當前程式的vruntime時間大於每個程式的理想佔用時間時(delta_exec > ideal_runtime)。這裡的ideal_runtime實際上就是上文說的sched_latency_ns/程式數n。當然這個值並不是一定 這樣得出,下文會有更詳細解釋。
  4. 當程式從中斷、異常或系統呼叫返回時,會發生排程檢查。比如時鐘中斷。

 

CFS的優先順序

當然,CFS中還需要支援優先順序。在新的體系中,優先順序是以時間消耗(vruntime增長)的快慢來決定的。就是說,對於CFS來說,衡量的時間 累積的絕對值都是一樣紀錄在vruntime中的,但是不同優先順序的程式時間增長的比率是不同的,高優先順序程式時間增長的慢,低優先順序時間增長的快。比 如,優先順序為19的程式,實際佔用cpu為1秒,那麼在vruntime中就記錄1s。但是如果是-20優先順序的程式,那麼它很可能實際佔CPU用 10s,在vruntime中才會紀錄1s。CFS真實實現的不同nice值的cpu消耗時間比例在核心中是按照“每差一級cpu佔用時間差10%左右” 這個原則來設定的。這裡的大概意思是說,如果有兩個nice值為0的程式同時佔用cpu,那麼它們應該每人佔50%的cpu,如果將其中一個程式的 nice值調整為1的話,那麼此時應保證優先順序高的程式比低的多佔用10%的cpu,就是nice值為0的佔55%,nice值為1的佔45%。那麼它們 佔用cpu時間的比例為55:45。這個值的比例約為1.25。就是說,相鄰的兩個nice值之間的cpu佔用時間比例的差別應該大約為1.25。根據這 個原則,核心對40個nice值做了時間計算比例的對應關係,它在核心中以一個陣列存在:

  1. staticconstint prio_to_weight[40]={
  2. /* -20 */88761,71755,56483,46273,36291,
  3. /* -15 */29154,23254,18705,14949,11916,
  4. /* -10 */9548,7620,6100,4904,3906,
  5. /* -5 */3121,2501,1991,1586,1277,
  6. /* 0 */1024,820,655,526,423,
  7. /* 5 */335,272,215,172,137,
  8. /* 10 */110,87,70,56,45,
  9. /* 15 */36,29,23,18,15,
  10. };

我們看到,實際上nice值的最高優先順序和最低優先順序的時間比例差距還是很大的,絕不僅僅是例子中的十倍。由此我們也可以推匯出每一個nice值級別計算vruntime的公式為:

  1. delta vruntime  delta Time*1024/ load

這個公式的意思是說,在nice值為0的時候(對應的比例值為1024),計算這個程式vruntime的實際增長時間值(delta vruntime)為:CPU佔用時間(delta Time)* 1024 / load。在這個公式中load代表當前sched_entity的值,其實就可以理解為需要排程的程式(R狀態程式)個數。load越大,那麼每個程式 所能分到的時間就越少。CPU排程是核心中會頻繁進行處理的一個時間,於是上面的delta vruntime的運算會被頻繁計算。除法運算會佔用更多的cpu時間,所以核心程式設計中的一個原則就是,儘可能的不用除法。核心中要用除法的地方,基本都 用乘法和位移運算來代替,所以上面這個公式就會變成:

  1. delta vruntime  delta time*1024*(2^32/(load *2^32))=(delta time*1024*Inverseload))>>32

核心中為了方便不同nice值的Inverse(load)的相關計算,對做好了一個跟prio_to_weight陣列一一對應的陣列,在計算中可以直接拿來使用,減少計算時的CPU消耗:

  1. staticconst u32 prio_to_wmult[40]={
  2. /* -20 */48388,59856,76040,92818,118348,
  3. /* -15 */147320,184698,229616,287308,360437,
  4. /* -10 */449829,563644,704093,875809,1099582,
  5. /* -5 */1376151,1717300,2157191,2708050,3363326,
  6. /* 0 */4194304,5237765,6557202,8165337,10153587,
  7. /* 5 */12820798,15790321,19976592,24970740,31350126,
  8. /* 10 */39045157,49367440,61356676,76695844,95443717,
  9. /* 15 */119304647,148102320,186737708,238609294,286331153,
  10. };

具體計算細節不在這裡細解釋了,有興趣的可以自行閱讀程式碼:kernel/shced/fair.c(Linux 4.4)中的__calc_delta()函式實現。

根據CFS的特性,我們知道排程器總是選擇vruntime最小的程式進行排程。那麼如果有兩個程式的初始化vruntime時間一樣時,一個程式 被選擇進行排程處理,那麼只要一進行處理,它的vruntime時間就會大於另一個程式,CFS難道要馬上換另一個程式處理麼?出於減少頻繁切換程式所帶 來的成本考慮,顯然並不應該這樣。CFS設計了一個sched_min_granularity_ns引數,用來設定程式被排程執行之後的最小CPU佔用 時間。

  1. [zorro@zorrozou-pc0 ~]cat/proc/sys/kernel/sched_min_granularity_ns
  2. 2250000

一個程式被排程執行後至少要被執行這麼長時間才會發生排程切換。

我們知道無論到少個程式要執行,它們都有一個預期延遲時間,即:sched_latency_ns,系統中可以通過如下命令來檢視這個時間:

  1. [zorro@zorrozou-pc0 ~]cat/proc/sys/kernel/sched_latency_ns
  2. 18000000

在這種情況下,如果需要排程的程式個數為n,那麼平均每個程式佔用的CPU時間為sched_latency_ns/n。顯然,每個程式實際佔用的 CPU時間會因為n的增大而減小。但是實現上不可能讓它無限的變小,所以sched_min_granularity_ns的值也限定了每個程式可以獲得 的執行時間週期的最小值。當程式很多,導致使用了sched_min_granularity_ns作為最小排程週期時,對應的排程延時也就不在遵循 sched_latency_ns的限制,而是以實際的需要排程的程式個數n * sched_min_granularity_ns進行計算。當然,我們也可以把這理解為CFS的”時間片”,不過我們還是要強調,CFS是沒有跟O1類 似的“時間片“的概念的,具體區別大家可以自己琢磨一下。

 

新程式的VRUNTIME值

CFS是通過vruntime最小值來選擇需要排程的程式的,那麼可以想象,在一個已經有多個程式執行了相對較長的系統中,這個佇列中的 vruntime時間紀錄的數值都會比較長。如果新產生的程式直接將自己的vruntime值設定為0的話,那麼它將在執行開始的時間內搶佔很多的CPU 時間,直到自己的vruntime追趕上其他程式後才可能排程其他程式,這種情況顯然是不公平的。所以CFS對每個CPU的執行佇列都維護一個 min_vruntime值,這個值紀錄了這個CPU執行佇列中vruntime的最小值,當佇列中出現一個新建的程式時,它的初始化vruntime將 不會被設定為0,而是根據min_vruntime的值為基礎來設定。這樣就保證了新建程式的vruntime與老程式的差距在一定範圍內,不會因為 vruntime設定為0而在程式開始的時候佔用過多的CPU。

新建程式獲得的實際vruntime值跟一些設定有關,比如:

  1. [zorro@zorrozou-pc0 ~]cat/proc/sys/kernel/sched_child_runs_first
  2. 0

這個檔案是fork之後是否讓子程式優先於父程式執行的開關。0為關閉,1為開啟。如果這個開關開啟,就意味著子程式建立後,保證子程式在父程式之 前被排程。另外,在原始碼目錄下的kernel/sched/features.h檔案中,還規定了一系列排程器屬性開關。而其中:

  1. /*
  2. * Place new tasks ahead so that they do not starve already running
  3. * tasks
  4. */
  5. SCHED_FEAT(START_DEBIT,true)

這個引數規定了新程式啟動之後第一次執行會有延時。這意味著新程式的vruntime設定要比預設值大一些,這樣做的目的是防止應用通過不停的 fork來儘可能多的獲得執行時間。子程式在建立的時候,vruntime的定義的步驟如下,首先vruntime被設定為min_vruntime。然 後判斷START_DEBIT位是否被值為true,如果是則會在min_vruntime的基礎上增大一些,增大的時間實際上就是一個程式的排程延時時 間,即上面描述過的calc_delta_fair()函式得到的結果。這個時間設定完畢之後,就檢查sched_child_runs_first開關 是否開啟,如果開啟(值被設定為1),就比較新程式的vruntime和父程式的vruntime哪個更小,並將新程式的vruntime設定為更小的那 個值,而父程式的vruntime設定為更大的那個值,以此保證子程式一定在父程式之前被排程。

 

IO消耗型程式的處理

根據前文,我們知道除了可能會一直佔用CPU時間的CPU消耗型程式以外,還有一類叫做IO消耗型別的程式,它們的特點是基本不佔用CPU,主要行 為是在S狀態等待響應。這類程式典型的是vim,bash等跟人互動的程式,以及一些壓力不大的,使用了多程式(執行緒)的或select、poll、 epoll的網路代理程式。如果CFS採用預設的策略處理這些程式的話,相比CPU消耗程式來說,這些應用由於絕大多數時間都處在sleep狀態,它們的 vruntime時間基本是不變的,一旦它們進入了排程佇列,將會很快被選擇排程執行。對比O1排程演算法,這種行為相當於自然的提高了這些IO消耗型程式 的優先順序,於是就不需要特殊對它們的優先順序進行“動態調整”了。

但這樣的預設策略也是有問題的,有時CPU消耗型和IO消耗型程式的區分不是那麼明顯,有些程式可能會等一會,然後排程之後也會長時間佔用CPU。 這種情況下,如果休眠的時候程式的vruntime保持不變,那麼等到休眠被喚醒之後,這個程式的vruntime時間就可能會比別人小很多,從而導致不 公平。所以對於這樣的程式,CFS也會對其進行時間補償。補償方式為,如果程式是從sleep狀態被喚醒的,而且 GENTLE_FAIR_SLEEPERS屬性的值為true,則vruntime被設定為sched_latency_ns的一半和當前程式的 vruntime值中比較大的那個。sched_latency_ns的值可以在這個檔案中進行設定:

  1. [zorro@zorrozou-pc0 ~]cat/proc/sys/kernel/sched_latency_ns
  2. 18000000

因為系統中這種排程補償的存在,IO消耗型的程式總是可以更快的獲得響應速度。這是CFS處理與人互動的程式時的策略,即:通過提高響應速度讓人的 操作感受更好。但是有時候也會因為這樣的策略導致整體效能受損。在很多使用了多程式(執行緒)或select、poll、epoll的網路代理程式,一般是 由多個程式組成的程式組進行工作,典型的如apche、nginx和php-fpm這樣的處理程式。它們往往都是由一個或者多個程式使用 nanosleep()進行週期性的檢查是否有新任務,如果有責喚醒一個子程式進行處理,子程式的處理可能會消耗CPU,而父程式則主要是sleep等待 喚醒。這個時候,由於系統對sleep程式的補償策略的存在,新喚醒的程式就可能會打斷正在處理的子程式的過程,搶佔CPU進行處理。當這種打斷很多很頻 繁的時候,CPU處理的過程就會因為頻繁的程式上下文切換而變的很低效,從而使系統整體吞吐量下降。此時我們可以使用開關禁止喚醒搶佔的特性。

  1. [root@zorrozou-pc0 zorro]#cat/sys/kernel/debug/sched_features
  2. GENTLE_FAIR_SLEEPERS START_DEBIT NO_NEXT_BUDDY LAST_BUDDY CACHE_HOT_BUDDY WAKEUP_PREEMPTION NO_HRTICK NO_DOUBLE_TICK LB_BIAS NONTASK_CAPACITY TTWU_QUEUE RT_PUSH_IPI NO_FORCE_SD_OVERLAP RT_RUNTIME_SHARE NO_LB_MIN ATTACH_AGE_LOAD

上面顯示的這個檔案的內容就是系統中用來控制kernel/sched/features.h這個檔案所列內容的開關檔案,其中WAKEUP_PREEMPTION表示:目前的系統狀態是開啟sleep喚醒程式的搶佔屬性的。可以使用如下命令關閉這個屬性:

  1. [root@zorrozou-pc0 zorro]#echo NO_WAKEUP_PREEMPTION >/sys/kernel/debug/sched_features
  2. [root@zorrozou-pc0 zorro]#cat/sys/kernel/debug/sched_features
  3. GENTLE_FAIR_SLEEPERS START_DEBIT NO_NEXT_BUDDY LAST_BUDDY CACHE_HOT_BUDDY NO_WAKEUP_PREEMPTION NO_HRTICK NO_DOUBLE_TICK LB_BIAS NONTASK_CAPACITY TTWU_QUEUE RT_PUSH_IPI NO_FORCE_SD_OVERLAP RT_RUNTIME_SHARE NO_LB_MIN ATTACH_AGE_LOAD

其他相關引數的調整也是類似這樣的方式。其他我沒講到的屬性的含義,大家可以看kernel/sched/features.h檔案中的註釋。

系統中還提供了一個sched_wakeup_granularity_ns配置檔案,這個檔案的值決定了喚醒程式是否可以搶佔的一個時間粒度條 件。預設CFS的排程策略是,如果喚醒的程式vruntime小於當前正在執行的程式,那麼就會發生喚醒程式搶佔的情況。而 sched_wakeup_granularity_ns這個引數是說,只有在當前程式的vruntime時間減喚醒程式的vruntime時間所得的差 大於sched_wakeup_granularity_ns時,才回發生搶佔。就是說sched_wakeup_granularity_ns的值越 大,越不容易發生搶佔。

 

CFS和其他排程策略

SCHED_BATCH

在上文中我們說過,CFS排程策略主要是針對chrt命令顯示的SCHED_OTHER範圍的程式,實際上就是一般的非實時程式。我們也已經知道, 這樣的一般程式還包括另外兩種:SCHED_BATCH和SCHED_IDLE。在CFS的實現中,整合了對SCHED_BATCH策略的支援,並且其功 能和SCHED_OTHER策略幾乎是一致的。唯一的區別在於,如果一個程式被用chrt命令標記成SCHED_OTHER策略的話,CFS將永遠認為這 個程式是CPU消耗型的程式,不會對其進行IO消耗程式的時間補償。這樣做的唯一目的是,可以在確認程式是CPU消耗型的程式的前提下,對其儘可能的進行 批處理方式排程(batch),以減少程式切換帶來的損耗,提高吞度量。實際上這個策略的作用並不大,核心中真正的處理區別只是在標記為 SCHED_BATCH時程式在sched_yield主動讓出cpu的行為發生是不去更新cfs的佇列時間,這樣就讓這些程式在主動讓出CPU的時候 (執行sched_yield)不會紀錄其vruntime的更新,從而可以繼續優先被排程到。對於其他行為,並無不同。

SCHED_IDLE

如果一個程式被標記成了SCHED_IDLE策略,排程器將認為這個優先順序是很低很低的,比nice值為19的優先順序還要低。系統將只在CPU空閒的時候才會對這樣的程式進行排程執行。若果存在多個這樣的程式,它們之間的排程方式跟正常的CFS相同。

SCHED_DEADLINE

最新的Linux核心還實現了一個最新的排程方式叫做SCHED_DEADLINE。跟IO排程類似,這個演算法也是要實現一個可以在最終期限到達前讓程式可以排程執行的方法,保證程式不會餓死。目前大多數系統上的chrt還沒給配置介面,暫且不做深入分析。

另外要注意的是,SCHED_BATCH和SCHED_IDLE一樣,只能對靜態優先順序(即nice值)為0的程式設定。操作命令如下:

  1. [zorro@zorrozou-pc0 ~]$ chrt -0bash
  2. [zorro@zorrozou-pc0 ~]$ chrt -p $$
  3. pid 5478`s current scheduling policy: SCHED_IDLE
  4. pid 5478`s current scheduling priority:0
  5. [zorro@zorrozou-pc0 ~]$ chrt -0bash
  6. [zorro@zorrozou-pc0 ~]$ chrt -p $$
  7. pid 5502`s current scheduling policy: SCHED_BATCH
  8. pid 5502`s current scheduling priority:0

 

多CPU的CFS排程

在上面的敘述中,我們可以認為系統中只有一個CPU,那麼相關的排程佇列只有一個。實際情況是系統是有多核甚至多個CPU的,CFS從一開始就考慮 了這種情況,它對每個CPU核心都維護一個排程佇列,這樣每個CPU都對自己的佇列程式排程即可。這也是CFS比O1排程演算法更高效的根本原因:每個 CPU一個佇列,就可以避免對全域性佇列使用大核心鎖,從而提高了並行效率。當然,這樣最直接的影響就是CPU之間的負載可能不均,為了維持CPU之間的負 載均衡,CFS要定期對所有CPU進行load balance操作,於是就有可能發生程式在不同CPU的排程佇列上切換的行為。這種操作的過程也需要對相關的CPU佇列進行鎖操作,從而降低了多個執行 佇列帶來的並行性。不過總的來說,CFS的並行佇列方式還是要比O1的全域性佇列方式要高效。尤其是在CPU核心越來越多的情況下,全域性鎖的效率下降顯著增 加。

CFS對多個CPU進行負載均衡的行為是idle_balance()函式實現的,這個函式會在CPU空閒的時候由schedule()進行呼叫, 讓空閒的CPU從其他繁忙的CPU佇列中取程式來執行。我們可以通過檢視/proc/sched_debug的資訊來檢視所有CPU的排程佇列狀態資訊以 及系統中所有程式的排程資訊。內容較多,我就不在這裡一一列出了,有興趣的同學可以自己根據相關參考資料(最好的資料就是核心原始碼)瞭解其中顯示的相關內 容分別是什麼意思。

在CFS對不同CPU的排程佇列做均衡的時候,可能會將某個程式切換到另一個CPU上執行。此時,CFS會在將這個程式出隊的時候將 vruntime減去當前佇列的min_vruntime,其差值作為結果會在入隊另一個佇列的時候再加上所入佇列的min_vruntime,以此來保 持佇列切換後CPU佇列的相對公平。

 

最後

本文的目的是從Linux系統程式的優先順序為出發點,通過了解相關的知識點,希望大家對系統的程式排程有個整體的瞭解。其中我們也對CFS排程演算法 進行了比較深入的分析。在我的經驗來看,這些知識對我們在觀察系統的狀態和相關優化的時候都是非常有用的。比如在使用top命令的時候,NI和PR值到底 是什麼意思?類似的地方還有ps命令中的NI和PRI值、ulimit命令-e和-r引數的區別等等。當然,希望看完本文後,能讓大家對這些命令顯示的了 解更加深入。除此之外,我們還會發現,雖然top命令中的PR值和ps -l命令中的PRI值的含義是一樣的,但是在優先順序相同的情況下,它們顯示的值確不一樣。那麼你知道為什麼它們顯示會有區別嗎?這個問題的答案留給大家自 己去尋找吧。

相關文章