一、前言?
Linux調度器神秘而充滿誘惑,每個Linux工程師都想深入其內部一探究竟。不過中國有一句古話叫做“相由心生”,一個模塊精巧的內部邏輯(也就是所謂的“心”)其外延就是簡潔而優雅的接口(我稱之為“相”)。通過外部接口的定義,其實我們也可以收獲百分之六七十的該模塊的內部信息。因此,本文主要描述Linux調度器開放給用戶空間的接口,希望可以通過用戶空間的調度器接口來理解Linux調度器的行為。
二、nice函數
nice函數用來修改調用進程的nice value,其接口定義如下:
#include?
?????? int nice(int inc);
為了方便說明該接口的作用,我們還是舉實際的例子說明。程序調用nice(3),則將當前進程的nice value增加3,這也就是意味著該進程的優先級降低3個level(提升nice value也就是對別人更加nice,自己的優先級就會低)。如果程序調用nice(-5),則將當前進程的nice value減去5,這也就是意味著該進程的優先級提升5個level。當調用錯誤的時候返回-1,調用成功會稍微有一些歧義。POSIX標準規定了nice函數返回新的nice value,但是linux的系統調用和c庫都是采用了操作成功返回0的方式。這樣的處理方式使得在調用nice函數的時候無法得到當前的優先級,如果想要得到當前優先級,需要調用getpriority函數,我們在下一小節描述。
雖然說nice函數是用來調整優先級,實際上調整nice value就是調整調度器分配給該進程的CPU時間,具體是如何影響cpu time的呢?我們在后面描述內核代碼的時候再詳聊。此外,需要注意的是:根據POSIX標準,nice value是一個per process的設定,但是在linux中,nice value沒有遵從這個標準,它是per-thread的一個屬性。
三、getpriority/setpriority函數
從上節的描述中,我們了解到了nice的函數的限制,例如只能修改自己的nice value,無法獲取當前的nice value值等,為此我們給出加強版本的nice接口,也就是getpriority/setpriority函數了。getpriority/setpriority函數定義如下:
#include?
#include?
int getpriority(int which, int who);?
int setpriority(int which, int who, int prio);
你說接口增加功能是好事,怎么就把名字也改了呢?為何不是getnice/setnice呢?其實從上節的描述也看出稍許端倪,我們并沒有區分調度優先級和nice value這兩個值,歷史上,首先被使用的是nice value,很快大家覺得這個詞不是那么好理解,特別是對于初學者,因此改成優先級(priority)這樣的名詞可以讓用戶更好的理解這個API的作用,當然,事實證明這個改動并不是非常理想,我們后面會描述。
getpriority/setpriority功能比較強大,能處理多種請求,不同的請求通過which和who這兩個參數來制定。當which等于PRIO_PROCESS的時候,who需要傳入一個process id的參數,getpriority將返回指定進程的nice value。當which等于PRIO_PGRP的時候,who需要傳入一個process group id的參數,此時getpriority將返回指定進程組中優先級最高的那個(BTW,nice value是最小的)。當which等于PRIO_USER的時候,who需要user id的信息,這時候,getpriority將返回屬于該user的所有進程中nice value最小的那個。who等于0說明要get或者set的對象是當前進程(或者當前進程組,或者當前的user)。
setpriority類似與nice,當然功能要強那么一點點,因為它可以接收PRIO_PROCESS,PRIO_PGRP或者PRIO_USER參數用來設定一組進程的nice value。setpriority的返回值和其他函數類似,0表示成功,-1表示操作失敗,不過getpriority就稍微有一點繞了。作為linux程序員,我們都知道的nice value是[-20, 19],如果getpriority返回這個范圍,那么這里的-1優先級就有點尷尬了,因為一般的linux c庫接口函數返回-1表示調用錯誤,我們是如何區分-1調用錯誤的返回還是優先級-1的返回值呢?getpriority是少數返回-1也是有可能正確的接口函數:在調用getpriority之前,我們需要首先將errno清零,調用getpriority之后,如果返回-1,我們需要看看errno是否還是保持0值,如果是,那么說明返回的是優先級-1,否則說明發生了錯誤。
四、操作rt priority的接口
傳統的類unix內核,調度器是采用round-robin time-sharing的算法:如果有若干個進程是runnable的,那么不著急,大家排排隊、吃果果,每個進程分配一個cpu時間片,大家輪流按照分配的時間片來獲取cpu資源,所有的時間片用完,那么就重新一輪的分配。在這樣的模型下面,間接影響cpu時間片的nice接口函數就夠用了。當然,分配了更多的時間片也就是意味著有更高的優先級,因此nice vlaue也被稱為進程的優先級。
但是,新的需求層出不窮(人類的欲望是無窮D),特別是實時性方面的需求,因此,POSIX標準(2008版本)增加了實時調度的內容,并且提供了POSIX realtime scheduling API來讓用戶空間來修改調度策略和調度優先級。這下子有點尷尬了,原來的nice value大家已經習慣稱之為進程優先級了,現在真正的進程優先級登場了,怎么區分?為了解決這個問題,我們引入一個新的名詞叫做調度策略(scheduling policy)。調度器在運作的時候往往設定一組規則來決定何時,選擇哪一個進程進入執行狀態,執行多長的時間。那些“規則”就是調度策略。
好的調度策略依賴于對進程的分類,有一類進程是大家都灰常的熟悉了就是普通進程,使用時間片輪轉算法的那些進程。當然這類進程還可以細分,例如運算密集型進程(SCHED_BATCH,調度器最好不要太經常的喚醒這種進程),例如idle類進程(SCHED_IDLE),idle類進程優先級非常低,也就是說如果系統有其他事情要處理就去干別的事情(調度其他進程執行),實在沒有活干了,再考慮IDLE類型的進程。不論哪一種普通進程,其優先級使用nice value這樣一個調度參數來描述就OK了。
除了普通進程,還有一類是嚴格按照優先級來調度的進程,如果熟悉RTOS的話,對priority-base的調度器應該不會陌生,官大一級壓死人,只要優先級高的進程是runnable的,那么優先級低的進程是根本沒有機會執行的。這里的優先級才是真正意義的優先級,但是nice value已經被稱為進程優先級了,因此這里的優先級被叫做rt priority。rt進程的調度又被細分成兩類:SCHED_FIFO和SCHED_RR。這兩種調度策略在相同rt priority的時候稍有差別,SCHED_FIFO是誰先到誰先獲取cpu資源,并且一直占用,直到主動讓出cpu或者退出,相同rt priority的進程才有機會執行。SCHED_RR稍微人性化了一點,相同rt priority的進程有時間片,大家輪流執行。對于實時進程而言,rt priority這個調度參數就描述了全部。
介紹到這里,是時候總結一下了:進程優先級有兩個范圍,一個是nice value,用前兩個小節的API來set或者get。另外一個優先級是rt priority,完全碾壓nice value這種優先級,操作rt priority的接口就在這一小節描述。
OK,經過漫長的鋪墊過程,我們終于可以介紹realtime process scheduling API了,具體API定義如下:
#include
int sched_setscheduler(pid_t pid, int policy, const struct sched_param *param);
int sched_getscheduler(pid_t pid);
int sched_get_priority_max(int policy);--返回指定policy的最大的rt priority?
int sched_get_priority_min(int policy);--返回指定policy的最小的rt priority
int sched_setparam(pid_t pid, const struct sched_param *param);?
int sched_getparam(pid_t pid, struct sched_param *param);
sched_get_priority_max和sched_get_priority_min分別返回了指定調度策略的最大和最小的rt priority,不同的操作系統實現不同的優先級數量。在linux中,實時進程(SCHED_FIFO和SCHED_RR)的rt priority共計99個level,最小是1,最大是99。對于其他的調度策略,這些函數返回0。
sched_getscheduler函數可以獲取指定進程的scheduling policy(如果pid等于0,那么是獲取調用進程的調度策略)。sched_setscheduler函數是用來設定指定進程的scheduling policy,對于實時進程,該接口函數還可以設定rt priority。如果設定進程的調度策略是非實時的調度策略的時候(例如SCHED_NORMAL),那么param參數是沒有意義的,其sched_priority成員必須設定為0。sched_setparam/sched_getparam非常簡單,大家自己看man page好了。
五、一統江湖的接口
看起來前面小節描述的API已經夠用了,然而,故事并未結束。經過前面關于調度接口的討論,基本上我們對調度器的行為也已經有了了解:調度器就是按照優先級(指rt priority)來工作,優先級高的永遠是優先調度。范圍落在[1,99]的rt priority是實時進程,而rt priority等于0的是普通進程。對于普通進程,調度器還要根據nice value(這個也曾經被稱為優先級,不要和rt priority弄混了)來進行調整。用戶空間的進程可以通過各種前面描述的接口API來修改調度策略、nice value以及rt priority。一切看上去已經完美,CFS類型的調度器處理普通的運算密集形(例如編譯內核)和用戶交互形的應用(例如vi編輯文件)。如果有應用有實時需求,可以考慮讓rt類型的調度器來運籌帷幄。但是,如何混合了一些realtime的應用以及有一些timing要求的應用的時候,SCHED_FIFO和SCHED_RR并不能解決問題,因為在這種調度策略下,高優先級的任務會永遠的delay低優先級的任務,如果低優先級的任務有一些timing的需求,這時候,你根本控制不了調度延遲時間。
為了解決上一節中描述的問題,一類新的進程被定義出來,這類進程的優先級比實時進程和普通進程的優先級都要高,這類進行有自己的特點,參考下圖:
這類進程的特點就是每隔固定的周期都會起來干活,需要一定的時間來處理事務。這類進程很牛,一上來就告訴調度器,我可是有點脾氣的進程,和其他的那些妖艷的進程不一樣的,我每隔一段時間(period)你就得固定分配給我一定的cpu資源(computer time),當然,分配的cpu time必須在該周期內執行完畢,因此就有deadline的概念。為了應對這種需求,3.14內核引入了一類新的進程叫做deadline進程,這類進程的調度策略是SCHED_DEADLINE。調度器對這類進程也會高看一眼,每當一個周期的開始時間到來的時候(也就是該deadline進程被喚醒的時間),調度器要優先處理這個deadline進程對cpu timer的需求,并且在某個指定的deadline時間內調度該進程執行。執行了指定的cpu time后,可以考慮調度走該進行,不過,當下一個周期到來的時候,調度器仍然要奮不顧身的在deadline時間內,再次調度該deadline進程執行。
雖然deadline進程優先級高于其他兩類進程,但是用“優先級”來描述這類進程當然是不合理的,應該使用下面的三個參數來描述:
(1)周期時間(上圖中的period)
(2)deadline時間(上圖中的relative deadline)
(3)一次調度周期內分配多少的cpu時間(上圖中的comp. time)
至此,估計您也已經發現,前面描述的接口其實都是不適合設定這些參數的,因此,GNU/linux操作系統中增加了下面的接口API:
#include
int sched_setattr(pid_t pid, const struct sched_attr *attr, unsigned int flags);?
int sched_getattr(pid_t pid, const struct sched_attr *attr, unsigned int size, unsigned int flags);
attr這個參數的數據類型是struct sched_attr,這個數據結構囊括了一切你想要的關于調度的控制參數:policy,nice value,rt priority,period,deadline等等。用這個接口可以完成所有前面幾個小節描述API能完成的任務,唯一的不好的地方就是這個接口是linux特有的,不是posix標準,是否應用這個接口就是見仁見智了。更細節的知識這里就不描述了,大家還是參考man page好了。
六、其他
上面描述的接口API都是和調度器參數相關,其實Linux調度器還有兩類接口。一個是sched_getaffinity和sched_setaffinity,用于操作一個線程的CPU affinity。另外一個接口是sched_yield,該接口可以讓出CPU資源,讓Linux調度器選擇一個合適的線程執行。這些接口很簡單,大家仔細學習就OK了。
參考文檔:
1、POSIX標準2008
2、linux下的各種man page
3、linux 4.4.6內核源代碼
評論
查看更多