關於進程的調度策略,不同的操作系統有不同的整體目標,因此調度算法也就各不相同。
這需要根據進程的類型(計算密集型?IO密集型?)、優先級等因素來進行選擇。
對於 Linux x86 平台來說,一般采用的是 CFS
:完全公平調度算法。
之所以叫做完全公平,是因為操作系統以每個線程占用 CPU
的比率來進行動態的計算,操作系統希望每一個進程都能夠平均的使用 CPU
這個資源,雨露均沾。
我們在創建一個線程的時候,默認就是這個調度算法 SCHED_OTHER
,默認的優先級為 0
。
PS: 在 Linux 操作系統中,線程的內核對象與進程的內核對象(其實就是一些結構體變量)是很類似的,所以線程可以說是輕量級的進程。
在本文中,可以把線程約等於進程,有的地方也可能稱為任務,在不同的語境下有一些不同的慣用說法。
可以這么理解:如果系統中一共有 N
個進程,那么每個進程會得到 1/N
的執行機會。每個進程執行一段時間之后,就被調出,換下一個進程執行。
如果這個 N
的數量太大了,導致每個進程剛開始執行時,分給它的時間就到了。如果這個時候就進行任務調度,那么系統的資源就耗費在進程上下文切換上去了。
因此,操作系統引入了最小粒度,也就是每個進程都有一個最小的執行時間保證,稱作時間片。
除了 SCHED_OTHER
調度算法,Linux
系統還支持兩種實時調度策略:
SCHED_FIFO:根據進程的優先級進行調度,一旦搶占到 CPU 則一直運行,直達自己主動放棄或被被更高優先級的進程搶占;
SCHED_RR:在 SCHED_FIFO 的基礎上,加上了時間片的概念。當一個進程搶占到 CPU 之后,運行到一定的時間后,調度器會把這個進程放在 CPU 中,當前優先級進程隊列的末尾,然后選擇另一個相同優先級的進程來執行;
本文想測試的就是 SCHED_FIFO
與普通的 SCHED_OTHER
這兩種調度策略混合的情況。
在 Linux
系統中,優先級的管理顯得比較混亂,先看下面這張圖:
這張圖表示的是內核中的優先級,分為兩段。
前面的數值 0-99
是實時任務,后面的數值 100-139
是普通任務。
數值越低,代表這個任務的優先級越高。
數值越低,代表這個任務的優先級越高。
數值越低,代表這個任務的優先級越高。
再強調一下,以上是從內核角度來看的優先級。
好了,重點來了:
我們在應用層創建線程的時候,設置了一個優先級數值,這是從應用層角度來看的優先級數值。
但是內核並不會直接使用應用層設置的這個數值,而是經過了一定的運算,才得到內核中所使用的優先級數值(0-139
)。
1. 對於實時任務
我們在創建線程的時候,可以通過下面這樣的方式設置優先級數值(0-99
):
struct sched_param param;
param.__sched_priority = xxx;
當創建線程函數進入內核層面的時候,內核通過下面這個公式來計算真正的優先級數值:
kernel priority = 100 - 1 - param.__sched_priority
如果應用層傳入數值 0
,那么在內核中優先級數值就是 99
,在所有實時任務中,它的優先級是最低的。
如果應用層傳輸數值 99
,那么在內核中優先級數值就是 0
,在所有實時任務中,它的優先級是最高的。
因此,從應用層的角度看,傳輸人優先級數值越大,線程的優先級就越高;數值越小,優先級就越低。
與內核角度是完全相反的!
2. 對於普通任務
調整普通任務的優先級,是通過 nice
值來實現的,內核中也有一個公式來把應用層傳入的 nice
值,轉成內核角度的優先級數值:
kernel prifoity = 100 + 20 + nice
nice
的合法數值是:-20 - 19。
如果應用層設置線程 nice
數值為 -20
,那么在內核中優先級數值就是 100
,在所有的普通任務中,它的優先級是最高的。
如果應用層設置線程 nice
數值為 19
,那么在內核中優先級數值就是 139
,在所有的普通任務中,它的優先級是最低的。
因此,從應用層的角度看,傳輸人優先級數值越小,線程的優先級就越高;數值越大,優先級就越低。
與內核角度是完全相同的!
背景知識交代清楚了,終於可以進行代碼測試了!
注意點:
#define _GNU_SOURCE
必須在#include <sched.h>
之前定義;
#include <sched.h>
必須在#include <pthread.h>
之前包含進來;
// filename: test.c
#define _GNU_SOURCE
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sched.h>
#include <pthread.h>
// 用來打印當前的線程信息:調度策略是什么?優先級是多少?
void get_thread_info(const int thread_index)
{
int policy;
struct sched_param param;
printf("\n====> thread_index = %d \n", thread_index);
pthread_getschedparam(pthread_self(), &policy, ¶m);
if (SCHED_OTHER == policy)
printf("thread_index %d: SCHED_OTHER \n", thread_index);
else if (SCHED_FIFO == policy)
printf("thread_index %d: SCHED_FIFO \n", thread_index);
else if (SCHED_RR == policy)
printf("thread_index %d: SCHED_RR \n", thread_index);
printf("thread_index %d: priority = %d \n", thread_index, param.sched_priority);
}
// 線程函數,
void *thread_routine(void *args)
{
// 參數是:線程索引號。四個線程,索引號從 1 到 4,打印信息中使用。
int thread_index = *(int *)args;
// 為了確保所有的線程都創建完畢,讓線程睡眠1秒。
sleep(1);
// 打印一下線程相關信息:調度策略、優先級。
get_thread_info(thread_index);
long num = 0;
for (int i = 0; i < 10; i++)
{
for (int j = 0; j < 5000000; j++)
{
// 沒什么意義,純粹是模擬 CPU 密集計算。
float f1 = ((i+1) * 345.45) * 12.3 * 45.6 / 78.9 / ((j+1) * 4567.89);
float f2 = (i+1) * 12.3 * 45.6 / 78.9 * (j+1);
float f3 = f1 / f2;
}
// 打印計數信息,為了能看到某個線程正在執行
printf("thread_index %d: num = %ld \n", thread_index, num++);
}
// 線程執行結束
printf("thread_index %d: exit \n", thread_index);
return 0;
}
void main(void)
{
// 一共創建四個線程:0和1-實時線程,2和3-普通線程(非實時)
int thread_num = 4;
// 分配的線程索引號,會傳遞給線程參數
int index[4] = {1, 2, 3, 4};
// 用來保存 4 個線程的 id 號
pthread_t ppid[4];
// 用來設置 2 個實時線程的屬性:調度策略和優先級
pthread_attr_t attr[2];
struct sched_param param[2];
// 實時線程,必須由 root 用戶才能創建
if (0 != getuid())
{
printf("Please run as root \n");
exit(0);
}
// 創建 4 個線程
for (int i = 0; i < thread_num; i++)
{
if (i <= 1) // 前2個創建實時線程
{
// 初始化線程屬性
pthread_attr_init(&attr[i]);
// 設置調度策略為:SCHED_FIFO
pthread_attr_setschedpolicy(&attr[i], SCHED_FIFO);
// 設置優先級為 51,52。
param[i].__sched_priority = 51 + i;
pthread_attr_setschedparam(&attr[i], ¶m[i]);
// 設置線程屬性:不要繼承 main 線程的調度策略和優先級。
pthread_attr_setinheritsched(&attr[i], PTHREAD_EXPLICIT_SCHED);
// 創建線程
pthread_create(&ppid[i], &attr[i],(void *)thread_routine, (void *)&index[i]);
}
else // 后兩個創建普通線程
{
pthread_create(&ppid[i], 0, (void *)thread_routine, (void *)&index[i]);
}
}
// 等待 4 個線程執行結束
for (int i = 0; i < 4; i++)
pthread_join(ppid[i], 0);
for (int i = 0; i < 2; i++)
pthread_attr_destroy(&attr[i]);
}
編譯成可執行程序的指令:
gcc -o test test.c -lpthread
首先說一下預期結果,如果沒有預期結果,那其他任何問題都壓根不用談了。
一共有 4
個線程:
線程索引號 1和2:是實時線程(調度策略是 SCHED_FIFO,優先級是 51,52);
線程索引號 3和4:是普通線程(調度策略是 SCHED_OTHER, 優先級是 0);
我的測試環境是:Ubuntu16.04
,是一台安裝在 Windows10
上面的虛擬機。
我期望的結果是:
首先打印 1 號和 2 號這兩個線程的信息,因為它倆是實時任務,需要優先被調度;
1 號線程的優先級是 51,小於 2 號線程的優先級 52,因此應該是 2 號線程結束之后,才輪到 1 號線程執行;
3 號和 4 號線程是普通進程,它倆需要等到 1 號和 2 號線程全部執行結束之后才開始執行,並且 3 號和 4 號線程應該是交替執行,因為它倆的調度策略和優先級都是一樣的。
我滿懷希望的在工作電腦中測試,打印結果如下:
====> thread_index = 4
thread_index 4: SCHED_OTHER
thread_index 4: priority = 0
====> thread_index = 1
thread_index 1: SCHED_FIFO
thread_index 1: priority = 51
====> thread_index = 2
thread_index 2: SCHED_FIFO
thread_index 2: priority = 52
thread_index 2: num = 0
thread_index 4: num = 0
====> thread_index = 3
thread_index 3: SCHED_OTHER
thread_index 3: priority = 0
thread_index 1: num = 0
thread_index 2: num = 1
thread_index 4: num = 1
thread_index 3: num = 0
thread_index 1: num = 1
thread_index 2: num = 2
thread_index 4: num = 2
thread_index 3: num = 1
后面打印內容不用輸出了,因為前面已經出現了問題。
問題很明顯:為什么 4 個線程為什么被同時執行了?
1
號和 2
號這兩個線程應該被優先執行啊,因為它倆是實時任務!
怎么結果是這個樣子?徹底凌亂了,一點都不符合預期!
想不出個所以然,只能求助網絡!但是沒有找到有價值的線索。
其中有一個信息涉及到 Linux
系統的調度策略,這里記錄一下。
Linux
系統中,為了不讓實時任務徹底占據 CPU
資源,會讓普通任務有很小的一段時間縫隙來執行。
在目錄 /proc/sys/kernel
下面,有 2
個文件,用來限制實時任務占用 CPU
的時間:
sched_rt_runtime_us: 默認值 950000
sched_rt_period_us: 默認值 1000000
意思是:在 1000000
微秒(1秒)的周期內,實時任務占用 950000
微秒(0.95秒),剩下的 0.05
秒留給普通任務。
如果沒有這個限制的話,假如某個 SCHED_FIFO
任務的優先級特別高,恰巧出了 bug
:一直占據 CPU
資源不放棄,那么我們壓根就沒有機會來 kill
掉這個實時任務,因為此時系統無法調度其他的任何進程來執行。
而有了這個限制呢,我們就可以利用這 0.05
秒的執行時間,來 kill
掉有 bug
的那個實時任務。
回到正題:資料上說,如果實時任務沒有被優先調度,可以把這個時間限制刪掉就可以了。方法是:
sysctl -w kernel.sched_rt_runtime_us=-1
我照做之后,依舊無效!
難道是電腦環境的問題嗎?於是,把測試代碼放到另一台筆記本里的虛擬機 Ubuntu14.04
里測試。
編譯的時候,有一個小問題,提示錯誤:
error: ‘for’ loop initial declarations are only allowed in C99 mode
只要把編譯指令中添加 C99 標准就可以了:
gcc -o test test.c -lpthread -std=c99
執行程序,打印信息如下:
====> thread_index = 2
====> thread_index = 1
thread_index 1: SCHED_FIFO
thread_index 1: priority = 51
thread_index 2: SCHED_FIFO
thread_index 2: priority = 52
thread_index 1: num = 0
thread_index 2: num = 0
thread_index 2: num = 1
thread_index 1: num = 1
thread_index 2: num = 2
thread_index 1: num = 2
thread_index 2: num = 3
thread_index 1: num = 3
thread_index 2: num = 4
thread_index 1: num = 4
thread_index 2: num = 5
thread_index 1: num = 5
thread_index 2: num = 6
thread_index 1: num = 6
thread_index 2: num = 7
thread_index 1: num = 7
thread_index 2: num = 8
thread_index 1: num = 8
thread_index 2: num = 9
thread_index 2: exit
====> thread_index = 4
thread_index 4: SCHED_OTHER
thread_index 4: priority = 0
thread_index 1: num = 9
thread_index 1: exit
====> thread_index = 3
thread_index 3: SCHED_OTHER
thread_index 3: priority = 0
thread_index 3: num = 0
thread_index 4: num = 0
thread_index 3: num = 1
thread_index 4: num = 1
thread_index 3: num = 2
thread_index 4: num = 2
thread_index 3: num = 3
thread_index 4: num = 3
thread_index 3: num = 4
thread_index 4: num = 4
thread_index 3: num = 5
thread_index 4: num = 5
thread_index 3: num = 6
thread_index 4: num = 6
thread_index 3: num = 7
thread_index 4: num = 7
thread_index 3: num = 8
thread_index 4: num = 8
thread_index 3: num = 9
thread_index 3: exit
thread_index 4: num = 9
thread_index 4: exit
1
號和 2
號線程同時執行,完畢之后,再 3
號和 4
號線程同時執行。
但是這同樣也不符合預期:2
號線程的優先級比 1
號線程高,應該優先執行才對!
不知道應該怎么查這個問題了,想不出思路,只好請教 Linux
內核的大神,建議檢查一下內核版本。
這時,我才想起來在 Ubuntu16.04
這台虛擬機上因為某種原因,降過內核版本。
往這個方向去排查了一下,最后確認也不是內核版本的差異導致的問題。
只好再回過頭來看一下這兩次次打印信息的差異:
工作電腦里的 Ubuntu16.04 中:4 個線程同時調度執行,調度策略和優先級都沒有起作用;
筆記本里的 Ubuntu14.04 中:1 號和 2 號實時任務被優先執行了,說明調度策略起作用了,但是優先級沒有起作用;
突然, CPU
的親和性從腦袋里蹦了出來!
緊接着立馬感覺到問題出在哪里了:這TMD大概率就是多核引起的問題!
於是我把這 4
個線程都綁定到 CPU0 上去,也就是設置 CPU 親和性。
在線程入口函數 thread_routine
的開頭,增加下面的代碼:
cpu_set_t mask;
int cpus = sysconf(_SC_NPROCESSORS_CONF);
CPU_ZERO(&mask);
CPU_SET(0, &mask);
if (pthread_setaffinity_np(pthread_self(), sizeof(mask), &mask) < 0)
{
printf("set thread affinity failed! \n");
}
然后繼續在 Ubuntu16.04
虛擬機中驗證,打印信息很完美,完全符合預期:
====> thread_index = 1
====> thread_index = 2
thread_index 2: SCHED_FIFO
thread_index 2: priority = 52
thread_index 2: num = 0
。。。
thread_index 2: num = 9
thread_index 2: exit
thread_index 1: SCHED_FIFO
thread_index 1: priority = 51
thread_index 1: num = 0
。。。
thread_index 1: num = 9
thread_index 1: exit
====> thread_index = 3
thread_index 3: SCHED_OTHER
thread_index 3: priority = 0
====> thread_index = 4
thread_index 4: SCHED_OTHER
thread_index 4: priority = 0
thread_index 3: num = 0
thread_index 4: num = 0
。。。
thread_index 4: num = 8
thread_index 3: num = 8
thread_index 4: num = 9
thread_index 4: exit
thread_index 3: num = 9
thread_index 3: exit
至此,問題真相大白:就是多核處理器導致的問題!
而且這兩台測試的虛擬機,安裝的時候分配的 CPU
核心是不同的,所以才導致打印結果的不同。
最后,再確認一下這 2
個虛擬機中的 CPU
信息:
Ubuntu 16.04
中 cpuinfo
信息:
$ cat /proc/cpuinfo
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 158
model name : Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz
stepping : 10
cpu MHz : 2807.996
cache size : 9216 KB
physical id : 0
siblings : 4
core id : 0
cpu cores : 4
。。。其他信息
processor : 1
vendor_id : GenuineIntel
cpu family : 6
model : 158
model name : Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz
stepping : 10
cpu MHz : 2807.996
cache size : 9216 KB
physical id : 0
siblings : 4
core id : 1
cpu cores : 4
。。。其他信息
processor : 2
vendor_id : GenuineIntel
cpu family : 6
model : 158
model name : Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz
stepping : 10
cpu MHz : 2807.996
cache size : 9216 KB
physical id : 0
siblings : 4
core id : 2
cpu cores : 4
。。。其他信息
processor : 3
vendor_id : GenuineIntel
cpu family : 6
model : 158
model name : Intel(R) Core(TM) i5-8400 CPU @ 2.80GHz
stepping : 10
cpu MHz : 2807.996
cache size : 9216 KB
physical id : 0
siblings : 4
core id : 3
cpu cores : 4
。。。其他信息
在這台虛擬機中,正好有 4
個核心,而我的測試代碼正好也創建了 4
個線程,於是每個核心被分配一個線程,一個都不閑着,同時執行。
因此打印信息中顯示 4
個線程是並行執行的。
這個時候,什么調度策略、什么優先級,都不起作用了!(准確的說:調度策略和優先級,在線程所在的那個 CPU
中是起作用的)
如果我在測試代碼中,一開始就創建 10
個線程,很可能會更快發現問題!
再來看看筆記本電腦里虛擬機 Ubuntu14.04
的 CPU
信息:
$ cat /proc/cpuinfo
processor : 0
vendor_id : GenuineIntel
cpu family : 6
model : 142
model name : Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
stepping : 9
microcode : 0x9a
cpu MHz : 2304.000
cache size : 4096 KB
physical id : 0
siblings : 2
core id : 0
cpu cores : 2
。。。其他信息
processor : 1
vendor_id : GenuineIntel
cpu family : 6
model : 142
model name : Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz
stepping : 9
microcode : 0x9a
cpu MHz : 2304.000
cache size : 4096 KB
physical id : 0
siblings : 2
core id : 1
cpu cores : 2
。。。其他信息
在這台虛擬機中,有 2
個核心,於是 2
個實時任務 1
號和 2
號被優先執行(因為是 2
個核心同時執行,所以這 2
個任務的優先級也就沒什么意義了),結束之后,再執行 3
號和 4
號線程。
這一圈測試下來,真的想用鍵盤敲自己的腦袋,怎么就沒有早點考慮到多核的因素呢?!
深層的原因:
之前的很多項目,都是 ARM、mips、STM32等單核情況,思維定式讓我沒有早點意識到多核這個屏體因素;
做過的一些 x86 平台項目,並沒有涉及到實時任務這樣的要求。一般都是使用系統默認的調度策略,這也是 Linux x86 作為通用電腦,在調度策略上所關注的重要指標:讓每一個任務都公平的使用 CPU 資源。
隨着 x86
平台在工控領域的逐漸應用,實時性問題就顯得更突出、更重要了。
所以才有了 Windows
系統中的 intime
,Linux
系統中的 preempt
、xenomai
等實時補丁。
推薦閱讀
【1】C語言指針-從底層原理到花式技巧,用圖文和代碼幫你講解透徹
【2】一步步分析-如何用C實現面向對象編程
【3】原來gdb的底層調試原理這么簡單
【4】內聯匯編很可怕嗎?看完這篇文章,終結它!
【5】都說軟件架構要分層、分模塊,具體應該怎么做
</pthread.h></sched.h></stdlib.h></stdio.h></unistd.h></pthread.h></sched.h></sched.h>