1、綜述
Linux作為多任務、多用戶的操作系統,其進程/線程調度管理是實現這些特性的關鍵部分。調度管理決定系統中的眾多線程中哪個線程獲得執行、什么時候開始執行、執行多久。一個好的調度算法能優化系統資源的使用,提高系統使用效率。
Linux內核中實現了Scheduler Classes,來實現多個調度類(Scheduler class)的協同工作,每個不同的調度類對應不同的類型的線程,而且每個調度類都有自身的優先級,Linux調度管理基礎代碼會遍歷在內核中注冊了的調度類,選擇高優先級的調度類,然后讓此調度類按照自己的調度算法選擇下一個執行的線程。Linux系統中常用的幾種調度類為SCHED_NORMAL、SCHED_FIFO、SCHED_RR。其中SCHED_NORMAL是用于普通線程的調度類,而SCHED_FIFO和SCHED_RR是用于實時線程的調度類,優先級高于SCHED_NORMAL。內核中區分普通線程與實時線程是根據線程的優先級,實時線程擁有實時優先級(real-time priority),默認取值為0~99,數值越高優先級越高,而普通線程只具有nice值,nice值映射到用戶層的取值范圍為-20~+19,數值越高優先級越低,默認初始值為0 ,子線程會繼承父線程的優先級。對于實時線程,Linux系統會盡量使其調度延時在一個時間期限內,但是不能保證總是如此,不過正常情況下已經可以滿足比較嚴格的時間要求了。下面將分別介紹這些調度類。
2、SCHED_NORMAL
對于SCHED_NORMAL,2.6之前的版本內核中的調度管理程序是根據線程的優先級(nice值)來分配一個固定的時間片(timeslice)給線程,比如nice值0對應于100ms的時間片,而nice值-20對應于5ms時間片,線程在執行過程中進入阻塞狀態(I/O操作等引起)或者是執行的時間達到分配的時間片后將會被搶占,而新進入運行態的線程會根據其優先級和可用時間片來決定是否搶占當前正在執行的程序。
2.6之后版本的Linux中SCHED_NORMAL使用的是Linux 內核在2.6.23版本中引入的CFS(Complete Fair Scheduler)調度管理程序。CFS與之前的調度不同的是,線程的優先級與時間片之間并沒有一個固定的關系,而是影響該線程在整個系統CPU運行時間中占有比例的一個因素。比如有兩個線程,對應的nice值分別為0(普通線程)和+19(低優先級線程),那么普通線程將會占有19/20×100%的CPU時間,而低優先級線程將會占有1/20×100%的CPU時間(具體數值只做舉例說明用,Linux內核中的計算出來的數值會不一樣)。而如果同時運行的只有兩個相同優先級的線程,那么他們分到的CPU時間各是50%。這樣每個線程能夠分配到的CPU時間占有比例跟系統當前的負載(所有處于運行態的線程數以及各線程的優先級)有關,同一個線程在他本身優先級不變的情況下分到的CPU時間占比會根據系統負載變化而發生變化,也即與時間片沒有一個固定的對應關系。
理想情況下CFS對CPU時間占比的衡量是在一個無限小的時間片內計算單個線程執行時間的占比,CFS的目標是所有線程在這個小的時間片周期內執行的時間都是相同的,無限小在現實中顯然是不存在的,Linux系統中這個時間片周期是與系統CPU數有關的,默認情況下單核CPU對應6ms,雙核CPU對應12ms,8核CPU對應24ms,當線程數增加到很大數量時,CFS會保證每個線程獲得最小執行時間, 單核CPU對應0.75ms,雙核CPU對應1.5ms,8核CPU對應3ms。在CFS管理下,某個線程運行時如果進入阻塞態(或其他非運行態)或者當前時間片周期內的CPU時間占比達到后將會暫停運行,CFS然后將會選擇當前時間片周期內已執行時間最少的運行態線程繼續運行。當然CPU時間占比在內核中也是以運行時間衡量的,比如某個單核CPU系統中只有兩個相同優先級的線程同時處于運行態,那么CFS將會以6ms為周期來調度所有線程,而每個6ms周期內每個線程分得3ms執行時間,如果某個線程中有I/O操作等其他操作使該線程進入非運行態,CFS將會立即使另外一個線程繼續運行,如果兩個線程都是基本沒有I/O操作等會引起阻塞的操作(比如忙循環),那么線程將會在自己的執行時間結束(本質上是超出CPU時間占比)后被CFS程序調度出當前正在執行的狀態,另外一個線程獲得CPU資源開始執行。
需要注意的是,進入CFS(或其他調度算法)需要調度事件的產生,調度事件可以是線程自己調用函數顯示執行調度,或者線程執行I/O操作等會進入阻塞的操作以及等待的事件發生線程進入運行態等(內核中有固定的調度點),如果一個程序一直處于忙計算(比如忙循環程序),那么就會需要系統軟時間中斷來產生調度事件從而進入CFS調度判斷下一個可執行程序。目前我們的Linux內核普遍配置的系統軟時間中斷產生的頻率為100Hz,也即每10ms產生一次中斷,那么系統中只有忙計算類(如忙循環)線程的情況下,只有當系統產生軟時間中斷時,CFS才會被調用來判斷下一個執行的線程并使其占有CPU開始執行,這個現象看起來就好象是Linux調度策略簡單的給每個線程分配了10ms的時間片,其實并不是這樣。如果系統中同時有忙計算類的線程和經常進行I/O操作類的線程,由于I/O類線程基本處于等待事件的阻塞態中,執行的時間很少,而計算類線程在執行的時間會比較長,如果計算類線程正在執行時,I/O類線程等待的事件發生了,CFS馬上就會判斷出I/O類線程在之前時間段內執行的時間很少,即已使用的CPU占比與分配給他的相比很小,而計算類線程很有可能已經超過了分配的CPU占比,那么CFS將會馬上使I/O類的線程占有CPU開始執行,如此系統總是能及時響應I/O類線程。
3、SCHED_FIFO和SCHED_RR
SCHED_FIFO和SCHED_RR是實時線程使用的調度管理算法。SCHED_FIFO即先進先出,處于相同優先級的實時線程會根據進入運行態的次序依次執行。正在執行的線程會一直執行直到線程阻塞或者其主動調用調度線程放棄執行,處于此調度策略下的線程沒有預先分配的時間片,可以永遠執行下去。只有擁有更高實時優先級且處于SCHED_RR或者SCHED_FIFO管理下的線程能搶占正在運行的實時線程。
SCHED_RR在SCHED_FIFO的基礎上會預先給定線程一個時間片,時間片達到后會使其他相同優先級的線程開始執行。SCHED_RR的時間片輪詢機制只對同等實時優先級的線程有效,更高實時優先級的線程總是會搶占正在執行的線程,而低優先級的線程不能搶占高優先級的線程,即使其時間片已到。
實時線程優先級高于所有普通線程,如果有實時線程處于運行態,則系統調度時一定會選擇調用實時線程;正在運行的實時線程只會被擁有更高實時優先級的線程搶占。所以在應用中如果需要將某個線程設置為實時線程,則需要用戶自己確保該線程不會處于忙執行而完全占用CPU資源,導致其他普通線程沒法獲得CPU資源而一直被阻塞得不到執行,并且需要合理給予優先級的值,太高有可能會影響重要系統線程的運行。所有用戶態線程默認沒有實時優先級,都屬于普通線程。
4、相關接口函數
Linux系統提供了一系列函數,這些函數可以讓用戶方便的修改線程/進程的優先級(包括nice值和real-time priority)、以及修改調度策略、設置運行線程的CPU核心等。下面簡單介紹一下常用的函數。
(1)修改nice值
int nice ( int incr )
將調用進程的nice值增加incr,incr為負數是提高優先級,為正數時降低優先級。成功返回0。
int setpriority ( int which, id_t who, int prio )
將指定的線程/線程組/用戶的nice值設置為prio,whice對應可以取值PRIO_PROCESS、PRIO_PGRP、PRIO_USER,who對應為線程/進程id,組id或者用戶id,prio取值范圍為-20~19。成功返回0,錯誤返回-1。
(2)修改real-time priority以及調度策略
int pthread_attr_setschedparam (pthread_attr_t *attr, const struct sched_param *param)
設置調度屬性。對于SCHED_FIFO和SCHED_RR,sched_param值包含 int sched_priority,也即real-time priority。下面所有param定義相同。
int pthread_attr_getschedparam (const pthread_attr_t *attr, struct sched_param *param)
獲取調度屬性。
int pthread_attr_setschedpolicy (pthread_attr_t *attr, int policy)
設置調度策略,policy可設置為SCHED_FIFO,SCHED_RR和SCHED_NORMAL。
int pthread_attr_getschedpolicy (const pthread_attr_t *attr, int *policy)
獲取調度策略。
int sched_setparam (pid_t pid, const struct sched_param *param)
設置pid進程的real-time priority,需要pid進程出具SCHED_FIFO或者SCHED_RR調度策略管理下。
int sched_getparam(pid_t pid, struct sched_param *param)
獲取real-time priority。
int sched_setscheduler (pid_t pid, int policy, const struct sched_param *param)
設置pid進程的調度策略以及調度屬性。
int sched_getscheduler (pid_t pid)
返回pid進程調度策略。
(3)設置線程在哪個CPU核心上運行
int pthread_setaffinity_np (pthread_t thread, size_t cpusetsize, const cpu_set_t *cpuset)
int pthread_getaffinity_np (pthread_t thread, size_t cpusetsize, cpu_set_t *cpuset)
int sched_setaffinity (pid_t pid, size_t cpusetsize, const cpu_set_t *cpuset)
int sched_getaffinity(pid_t pid, size_t cpusetsize, cpu_set_t *cpuset)
設置/獲取線程可以運行的CPU核心, cpusetsize 可以設置為sizeof(cpu_set_t), cpuset可以用宏CPU_ZERO和CPU_SET設置,函數設置成功后,線程將只會在設置的CPU核心(比如8核CPU可以設置核心1、3、4)上運行,如果cpuset只指定了一個核心,那么線程將只會在此核心上運行。函數出錯返回-1,成功返回0。
關于以上函數以及更多與調度相關的函數的詳細信息請參考
http://pubs.opengroup.org/onlinepubs/7908799/xsh/pthread.h.html
http://pubs.opengroup.org/onlinepubs/7908799/xsh/sched.h.html
以及相關函數的Linux man手冊和其他相關書籍資料。
5、編程示例
下面提供一個示例程序,程序用于展示real-time priority對于進程運行時調度管理的影響,程序運行于ESM6802(雙核)工控主板上。程序會在主進程中使用fork新建一個進程,然后調用sched_setscheduler設置子進程的實時優先級為30, 同時設置其使用SCHED_FIFO調度策略,而主進程只有nice值為0 的普通優先級。兩個進程主體部分相同,均為在忙循環中置高然后置低一位GPIO的輸出電平,通過示波器觀察GPIO的狀態,如果進程一直執行,則會看到連續的周期較固定的方波,而如果進程被其他進程搶占,則會看到GPIO的狀態很長時間沒有發生變化,以此來展示實時優先級對系統調度的影響。程序部分代碼如下:
int gpio = GPIO6;
structsched_param rt_param = {
.sched_priority = 30 }; //實時優先級30
child_pid = fork(); //創建子進程
if(child_pid!=0) //child_pid不等于0為主進程,等于0為子進程
{
if( -1 == sched_setscheduler ( child_pid, SCHED_FIFO, &rt_param ) )
printf("sched_setscheduler failed\n");
//設置子進程實時優先級以及調度算法
printf ( "child_pid = %d\n", child_pid );
gpio = GPIO5; //主進程和子進程操作不同的GPIO
}
printf("Driver esm6800_gpio test\n");
fd = open("/dev/esm6800_gpio", O_RDWR);
printf("open file = %d\n", fd);
rc = GPIO_OutEnable(fd, 0xff); //set GPIO as output
if(rc < 0)
{
printf("GPIO_OutEnable::failed %d\n", rc);
returnrc;
}
for(;;) //無限循環
{
//忙循環中置高然后置低gpio輸出電平
rc = GPIO_OutSet(fd, gpio); //使GPIO輸出高電平
if(rc < 0)
{
printf("GPIO_OutSet::failed %d\n", rc);
returnrc;
}
rc = GPIO_OutClear(fd, gpio); //使GPIO輸出低電平
if(rc < 0)
{
printf("GPIO_OutClear::failed %d\n", rc);
returnrc;
}
}
使用示波器查看到的GPIO狀態如下圖,其中黃色信號為主進程操作的GPIO5,藍色信號為有實時優先級的子進程操作的GPIO6:
可以看出藍色信號代表的擁有實時優先級的進程一直處于執行當中,沒有被其他低優先級的進程搶占,而黃色信號代表的普通優先級的程序gpio狀態切換有很明顯的中斷,也即其他進程被搶占而中斷執行。此結果與第一節介紹的Linux調度策略一致:實時線程只會被擁有更高實時優先級的線程搶占,處于SCHED_FIFO下的實時線程可以無限執行。
用戶在實際編程中應該仔細規劃自己的程序,合理利用系統調度接口函數,來優化自己程序的執行效果,同時避免錯誤的使用導致系統正常運行。
-
Linux
+關注
關注
87文章
11232瀏覽量
208960 -
嵌入式主板
+關注
關注
7文章
6085瀏覽量
35227
發布評論請先 登錄
相關推薦
評論