學習交流加
- 個人qq:
1126137994- 個人微信:
liu1126137994- 學習交流資源分享qq羣:
962535112
上一篇文章(點擊鏈接:點擊鏈接閱讀上一篇文章)講了:
- CPU/IO消耗型進程
- 吞吐率 vs. 響應
- SCHED_FIFO算法 與 SCHED_RR算法
- SCHED_NORMAL算法 和 CFS算法
- nice與renice
- chrt
本篇文章接着上一篇文章講解以下內容:
- 多核下負載均衡
- 中斷負載均衡,RPS軟中斷負載均衡
- cgroups與CPU資源分羣分配
- Linux爲什麼不是硬實時
- preempt-rt對Linux實時性的改造
文章目錄
1、多核下負載均衡
我們知道現在的CPU都是多核的。我們可以認爲在同一時刻,同一個核中只能有一個進程(task_struct,調度單位但是task_struct)在運行。但是多核的時候,在同一個時刻,不同的核中的進程是可以同時運行的。
假設你電腦有四個核,現在每個核都在跑一個線程。各個核上都是獨立的使用SCHED_FIFO算法、SCHED_RR算法與CFS完全公平調度算法去調度自己核上的task_struct,但是爲了能夠使整個系統的負載能夠達到均衡(各個核的調度情況儘量保持一致,不要使某一個核太忙也不能使某一個核太輕鬆),某一個核也有可能會把自己的task_struct給另一個核,讓另一個核來調度它。各個核都是以勞動爲樂,會接收更多的任務,核與核之間進行pull與push操作將各自的task_struct給其他核或者拿其他核的task_struct來調度。這樣的話,整個系統就會達到一種負載均衡的效果。我們稱之爲多核下的負載均衡。
那麼不同的進程如何做到負載均衡呢?
- RT進程
N個優先級最高的進程分不到N個不同的核,使用pull_rt_task與push_rt_task來達到負載均衡的效果。RT進程的話,實際上強調的是實時性而不是負載均衡。
- 普通進程
分爲:
- 週期性負載均衡(普通進程不會搶佔,就所有的進程週期性的被各個核調度達到多個CPU的負載均衡)
- IDLE時負載均衡(某一個核假設爲CPU1是空閒的,0號進程想要過來讓CPU1跑,CPU1纔不會去跑0號進程,CPU1會去看其他核是否在忙,如果其他核在忙,CPU1就會拿其他核的任務過來跑,CPU是儘量不會去跑0號進程的,因爲一旦跑了0號進程,說明整個系統處於一種低功耗的狀態,這種狀態下整個系統只有0號進程會跑,其他進程都在休眠)
- fork和exec時負載均衡(fork會創建一個新的task_struct,而exec只是替換進程虛擬地址空間的.data與.text,當創建一個新的進程或者替換了一個新進程,就會把這個新的task_struct推給一個最空閒的核去調度。)
- 實驗
two-loops.c
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>
void *thread_fun(void *param)
{
printf("thread pid:%d, tid:%lu\n", getpid(), pthread_self());
while (1) ;
return NULL;
}
int main(void)
{
pthread_t tid1, tid2;
int ret;
printf("main pid:%d, tid:%lu\n", getpid(), pthread_self());
ret = pthread_create(&tid1, NULL, thread_fun, NULL);
if (ret == -1) {
perror("cannot create new thread");
return 1;
}
ret = pthread_create(&tid2, NULL, thread_fun, NULL);
if (ret == -1) {
perror("cannot create new thread");
return 1;
}
if (pthread_join(tid1, NULL) != 0) {
perror("call pthread_join function fail");
return 1;
}
if (pthread_join(tid2, NULL) != 0) {
perror("call pthread_join function fail");
return 1;
}
return 0;
}
編譯運行:
$ gcc two-loops.c -pthread
$ time ./a.out
- 結果分析
由於我的虛擬機中Linux是兩個核的,在兩個核中分別跑的時間加起來,大概等於我們用戶態的時間。這說明兩個線程被分配到兩個核中分別跑的。
2、CPU task affinity
affinity的意思是親和,實際上我們這裏是指,讓task_struct對某一個或若干個CPU親和。也就是讓task_struct只在某幾個核上跑,不去其他核上跑。這樣實際上破壞了多核的負載均衡。
如何實現CPU task affinity?
- 可以在程序中直接寫代碼設置掩碼
int pthread_attr_setaffinity_np(pthread_attr_t *, size_t, const cpu_set_t *);
int pthread_attr_getaffinity_np(pthread_attr_t *, size_t, cpu_set_t *);
int sched_setaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask);
int sched_getaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask);
設置掩碼來保證某一個線程對某幾個核親和,比如下方的0x6(110),就是設置線程只能在核2與核1上運行
- taskset工具
比如:
$ taskset -a -p 01 19999
-a:進程中的所有線程,01掩碼,19999進程pid
- 實驗
編譯上述two-loops.c, gcc two-loops.c -pthread,運行一份
$ ./a.out &
top查看CPU佔用率:
把它的所有線程affinity設置爲01, 02, 03後分辨來看看CPU利用率
$ taskset -a -p 02 進程PID
$ taskset -a -p 01 進程PID
$ taskset -a -p 03 進程PID
- 前兩次設置後,a.out CPU利用率應該接近100%,最後一次接近200%
3、IRQ affinity
中斷也可以達到負載均衡。
假設有四個網卡,當網卡收到數據,會觸發中斷,將四個網卡隊列的中斷均分給四個CPU
- my ethernet
/proc/irq/74/smp_affinity 000001
/proc/irq/75/smp_affinity 000002
/proc/irq/76/smp_affinity 000004
/proc/irq/77/smp_affinity 000008
以上四個網卡的中斷全部均分給了四個CPU。
當然中斷也可以像進程一樣讓其affinity某個進程,比如向下面這樣,可以讓01號中斷分配給某個CPU,讓其affinity該CPU。
分配IRQ到某個CPU
[root@boss ~]# echo 01 > /proc/irq/145/smp_affinity
[root@boss ~]# cat /proc/irq/145/smp_affinity
00000001
有一種情況比較特殊:假設一個CPU0上有一箇中斷IRQ,該中斷處理函數中可能會調用軟中斷(soft_irq)處理函數。那麼這個軟中斷處理函數又會佔用該CPU0。那麼該CPU0就會處於非常忙的狀態,達不到負載均衡。如何使軟中斷去其他核執行?
使用RPS解決多核間的softIRQ scaling 。
RPS可以將包處理(中斷裏面的處理,其實就是軟中斷)負載均衡到多個CPU
例如:
[root@machine1 ~]# echo fffe > /sys/class/net/eth1/queues/rx-0/rps_cpus
將中斷分配給0~15的核,這樣可以使所有核共同處理中斷以及中斷內部的軟中斷,處理TCP/IP包的解析過程
4、進程間的分羣(cgroup)
進程間的分羣:假設有一個編譯Android系統的服務器,兩個人A與B同時要使用該服務器編譯程序,A編譯程序創建了1000個線程,B編譯程序創建了32個線程,那麼如果按正常的CFS調度的話,A的程序會獲得1000/1032的CPU時間,B的程序會獲得32/1032的CPU時間,這樣的話就會導致編譯B可能會花與A相同的時間才能將程序編譯完,這樣就顯得很對B不公平(想想我B本身可能是一個小程序,卻要編譯半天,多難受啊)。Linux爲了解決類似的這種問題,採用了進程間的分羣思想:讓A的線程放在一個羣中,B的線程放在一個羣中,給A羣與B羣採用CFS調度各個羣,然後再在A羣與B羣內部採用CFS調度羣內的進程。這樣的話,就顯得公平一些。不會說編譯一個小程序花費太多時間。
實際上分羣使用的是樹結構,上圖可以清晰的理解。
- 實驗
編譯two-loops.c, gcc two-loops.c -pthread,運行三份
$ ./a.out &
$ ./a.out &
$ ./a.out &
用top觀察CPU利用率,大概各自66%。
-
創建A,B兩個cgroup
/sys/fs/cgroup/cpu$ sudo mkdir A
/sys/fs/cgroup/cpu$ sudo mkdir B -
把3個a.out中的2個加到A,1個加到B。
/sys/fs/cgroup/cpu/A$ sudo sh -c ‘echo 3407 > cgroup.procs’
/sys/fs/cgroup/cpu/A$ sudo sh -c ‘echo 3413 > cgroup.procs’
/sys/fs/cgroup/cpu/A$ cd …
/sys/fs/cgroup/cpu$ cd B/
/sys/fs/cgroup/cpu/B$ sudo sh -c ‘echo 3410 > cgroup.procs’ -
這次發現3個a.out的CPU利用率大概是50%, 50%, 100%。
5、 Hard realtime - 可預期性
硬實時:可預期性。當一個線程被喚醒,直到這個線程被調度的這個時間段,不超過某一個預定的截止期限。稱爲硬實時。如果該線程被喚醒後,被調度的時間允許超過那個截止期限,那麼就不是硬實時。Linux系統不是硬實時。
以上圖我們可以看到。Linux系統並不是硬實時系統,也就是說對於一個進程,什麼時間之前(注意我們不能說在什麼時候能夠被調度到,因爲我們無法確定一個進程什麼時候能夠被調度到)能夠被調度到,我們並不知道。那麼Linux爲什麼不是硬實時?
首先我們需要理解Linux系統中,有四類區間:
當Linux跑起來後,CPU的時間都花在上述四類區間上。
- 中斷狀態:
當系統中有中斷,CPU不能再調度任何其他進程,就算RT進程來了也一樣得等着中斷結束後的一瞬間才能搶佔CPU。而且,在中斷中,不能再進行中斷,也就是說,中斷必須結束才能幹其他事。中斷是必須要被處理的。
- 軟中斷
軟中斷中可以被中斷。但是軟中斷中如果喚醒一個RT進程,此RT進程也不會被調度。
- 進程處於spin_lock(自旋鎖)
自旋鎖是發生在兩個核之間的。當某一個核如CPU0上的進程獲取spin_lock後,該核的調度器將被關閉。如果另一個核如CPU1的進程task_struct1此時想要獲取spin_lock,那麼task_struct1將自旋。自旋的意思就是不停的來查看是否spin_lock被解鎖,不停的佔用CPU直到可以獲取spin_lock爲止。
所以進程如果處於spin_lock,那麼其他任何進程不會被調度。
- 進程處於THREAD_RUNNING態
當進程處於THREAD_RUNNING態,它就是可調度的,只有在這種狀態下,CPU才支持搶佔。也就是說在這種狀態,加入CPU正在運行一個普通進程,此時如果某一個RT進程被喚醒,那麼該RT進程就會去搶佔CPU。
上述四類區間:如果可搶佔的RT進程被喚醒在前三類區間,那麼該RT進程,必須等待這三類區間的事件完成結束的一瞬間搶佔,否則RT進程也不會被CPU調度。如果在第四類區間上喚醒一個RT進程,則該RT進程立即搶佔CPU。
理解了以上四類區間,就很容易理解Linux爲什麼不是硬實時了,看下圖:
分析:
上圖橫軸爲時間軸。T0,T1,T2…爲某一時刻。
- 系統運行分析:
在T0時刻,假設有一個系統調用陷入到內核中。此時在跑的是一個普通進程(Normal task),在T1時刻,該Normal task獲取了一個spin_lock。
到了T2時刻,突然來了一箇中斷IRQ1,則系統執行中斷處理函數IRQ1 handle人(),再中斷處理函數中又調用軟中斷(Soft IRQ),在軟中斷中的T3時刻,喚醒了一個RT進程。此時由於系統處於軟中斷狀態,所以RT進程無法搶佔CPU(紅色虛線部分爲無法搶佔CPU)。在T4(軟中斷執行期間)時刻,又來了一箇中斷IRQ2(說明軟中斷中可以中斷),然後系統執行中斷處理函數IRQ2 handler(),然後執行軟中斷處理函數。到T5時刻中斷與軟中斷執行完畢。但是由於此時Normal task還處於spin_lock狀態,所以之前被喚醒的RT進程還是依然無法佔用CPU。直到T6時刻,Normal task釋放了spin_lock的一瞬間,RT進程搶佔了CPU。當RT進程執行完,纔會把CPU還給最開始還沒有執行完的Normal task。Normal task執行完後,退出內核的系統調用。
- 結果分析:
從以上分析可以看出,從T3時刻RT進程被喚醒,到T6時刻,RT進程開始執行,這段時間,我們是無法預測的,我們無法給出一個有限的上限值來度量T6-T3的值。因爲在這期間,有可能會來各種中斷,有可能進程會一直處於spin_lock狀態不放。所以,我們無法確定T6-T3的時間段。所以根據硬實時的概念知,Linux系統,不是硬實時。
6、PREEMPT_RT補丁
可以對Linux系統打實時補丁來增加Linux 的實時性。
比如PREEMPT_RT補丁,主要的原理如下:
- spinlock遷移爲可調度的mutex,同時報了raw_spinlock_t
- 實現優先級繼承協議
- 中斷線程化
- 軟中斷線程化
將spin_lock與中斷,軟中斷,都改造成第四類區間的可調度區間,就可以實現Linux系統的硬實時性。比如當中斷線程化,當產生中斷時,不執行中斷處理程序,直接返回。只有很小的區間是不可搶佔的。
- 以上四種方法原理,後期會詳細研究,這裏不再贅述。
7、總結
本文主要掌握:
- 多核下負載均衡
- 中斷負載均衡,RPS軟中斷負載均衡
- cgroups與CPU資源分羣分配
- Linux爲什麼不是硬實時
- preempt-rt對Linux實時性的改造
探討學習加:
qq:1126137994
微信:liu1126137994