【Linux進程、線程、任務調度】四多核下負載均衡 中斷負載均衡,RPS軟中斷負載均衡 cgroups與CPU資源分羣分配 Linux爲什麼不是硬實時 preempt-rt對Linux實時性的改造

學習交流加

  • 個人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進程的話,實際上強調的是實時性而不是負載均衡。

  • 普通進程

分爲:

  1. 週期性負載均衡(普通進程不會搶佔,就所有的進程週期性的被各個核調度達到多個CPU的負載均衡)
  2. IDLE時負載均衡(某一個核假設爲CPU1是空閒的,0號進程想要過來讓CPU1跑,CPU1纔不會去跑0號進程,CPU1會去看其他核是否在忙,如果其他核在忙,CPU1就會拿其他核的任務過來跑,CPU是儘量不會去跑0號進程的,因爲一旦跑了0號進程,說明整個系統處於一種低功耗的狀態,這種狀態下整個系統只有0號進程會跑,其他進程都在休眠)
  3. 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?

  1. 可以在程序中直接寫代碼設置掩碼
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上運行

  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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章