Linux支援具有多個處理器的單一系統。引導過程除外,支援多個處理器的大量工作由進程發送器負責。在對稱多重處理(SMP)及其上,進程發送器必須決定每個CPU上要運行哪些進程。有兩項挑戰源自此責任:發送器必須想辦法充分利用系統上的所有處理器,因為當有一個進程已就緒等待運行,卻有一個CPU閑置一旁,這顯然沒有效率。
然而一個進程一旦被安排在某個CPU上運行,往後進程發送器也會將它安排在相同的CPU上運行。這是有益的,因為將一個進程從一個處理器遷移到另一個處理器是要付出代價的。
這些代價中最大者與“遷移的緩衝區效應”(cache effect)有關。由於現在SMP系統的設計,與每個處理器相關的緩衝區是不同且獨立的。也就是說,如果資料位元於一個處理器的緩衝區中,就不會位於另一個處理器的緩衝區中。因此,如果一個進程被遷移往一個新的CPU,而且將新的資料寫入記憶體,那麼位於舊CPU緩衝區中的資料就過時了。現在若使用該緩衝區,則會導致資料被破壞。為了避免此現象,當一個緩衝區中緩衝了一個新的記憶體塊時,這會讓其他每個緩衝區中的資料失效。因此任何時刻特定的一段資料只能出現在一個處理器的緩衝區中(假設資料全部被緩衝起來)。當有一個進程從一個處理器移往另一個處理器時會付出兩種代價:被移動的進程無法訪問被緩衝起來的資料,而且位於原處理器緩衝區的資料必須作廢。因為必須付出這些代價,所以進程發送器會儘可能為一個進程安排特定的CPU來運行它。
當然,進程發送器的兩項目標可能無法實現。如果一個處理器的進程負荷比另一個處理器大很多(或者,更糟糕的是,如果一個處理器處於忙碌狀態,而另一個處理器卻閑置一旁),明智的做法是對一些進程重新調度,讓它們在比較不忙的CPU上運行。決定何時移動進程以響應此類不平衡的狀態稱為負載平衡(load balancing),這對SMP及其的效能而言非常重要。
處理器親和性(processor affinity)是指一個進程被一致安排在同一個處理器上啟動並執行可能性。軟親和性(soft affinity)是指發送器繼續安排一個進程在同一個處理器上啟動並執行自然勤想。Linux發送器會儘可能安排相同的進程在相同的處理器上運行,而且只會在負載極不平衡的情況下將一個進程從一個CPU移往另一個CPU。這讓處理器可以最小化遷移的緩衝區效應,但是仍能確保一個系統中所有處理器的負載時平衡的。
然而,有時使用者或應用程式會想把進程與處理器結合在一起。這時因為進程具有強烈的緩衝區敏感性,而且想停留在相同的處理器上。結合一個進程一個與一個特定的處理器,讓核心按照此關係行事稱為一個硬親和性(har affinity)。
sched_getaffinity()與sched_setaffinity()
進程會繼承其父進程的CPU親和性,預設情況下,進程可以運行在任何CPU之上。Linux提供了兩個系統調用可用於取得和設定一個進程的“硬親和性”:
#define _GNU_SOURCE
#include <sched.h>
typedef struct cpu_set_t
size_t CPU_SET_SIZE
void CPU_SET(unsigned long cpu, cpu_set_t *set);
void CPU_CLR(unsigned long cpu, cpu_set_t *set);
int CPU_IISET(unsigned long cpu, cpu_set_t *set);
void CPU_ZERO(cpu_set_t *set);
int sched_setaffinity(pid_t pid, size_t setsize, const cpu_set_t *set);
int sched_getaffinity(pid_t pid, size_t setsize, cpu_set_t *set);
sched_getaffinity()可用於取得進程pid的CPU親和性,而且會以特殊的cpu_set_t類型來存放它,這可通過特殊的宏來訪問。如果pid為0,則此調用會取得當前進程的親和性。setsize參數是cpu_set_t類型的大小,為了配合此類型大小未來的變化,glibc可能會用到此參數。執行成功時,sched_getaffinity()會返回0;執行失敗是,它會返回-1並且設定errno。請看下面的例子:
cpu_set_t set;<br /> int ret, i;</p><p> CPU_ZERO(&set);</p><p> ret = sched_setaffinity(0, sizeof(cpu_set_t), &set);<br /> if( ret == -1)<br /> {<br />perror("sched_se");<br /> }</p><p> for( i=0; i < CPU_SETSIZE; i++)<br /> {<br />int cpu;<br />cpu = CPU_ISSET(i, &set);<br />printf("cpu = %i is %s/n", i,<br />cpu? "set" : "unset");<br /> }
進行調用之前,我們會使用CPU_ZERO將set中所有位都清零。然後我們會從0到CPU_SETSIZE迭代處理set。請處以,CPU_SETSIZE並不是set的大小,你絕對不應該把它傳遞給setsize,而是set所可能表示的處理器數目。因為當前實現是以單一位來表示每個處理器,所以CPU_SETSIZE會比sizeof(cpu_set_t)大很多。我們還會使用CPU_ISSET來檢查系統中特定的處理器i,是否綁定或為綁定此進程。如果返回0,表示未綁定;如果返回非0值,表示綁定了。
系統中是有實體的處理器會被設定。因此,若在一個具有兩個處理器的系統上運行此程式碼,將產生如下效果:
cpu=0 is set
cpu=1 is set
cpu=2 is unset
cpu=3 is unset
...
cpu=1023 is unset
我們所關心的只是CPU#0和CPU#1,因為它們是此系統上僅有的實體處理器。也許我們想確保我們的進程只會運作在CPU#0之上,而不會運作在CPU#1之上。下面程式碼可以完成此事:
cpu_set_t set;<br /> int ret, i;</p><p> CPU_ZERO(&set);<br /> CPU_SET(0, &set);<br /> CPU_CLR(1, &set);</p><p> ret = sched_setaffinity(0, sizeof(cpu_set_t), &set);<br /> if( ret == -1)<br /> {<br /> perror("sched_se");<br /> }</p><p> for( i=0; i < 3; i++)<br /> {<br /> int cpu;<br /> cpu = CPU_ISSET(i, &set);<br /> printf("cpu = %i is %s/n", i,<br /> cpu? "set" : "unset");<br /> }<br />
一如往常,首先我們會使用CPU_ZERO將set清零。然後我們會使用CPU_SET設定CPU#0以及使用CPU_CLR清除CPU#1。CPU_CLR操作時多餘的,因為我們剛剛將整個set清零,這麼做只是為了完整性。
同樣在這個具有兩個處理器的系統上運行此程式會產生與之前稍微不同的輸出:
cpu=0 is set
cpu=1 is unset
cpu=2 is unset
...
cpu=1023 is unset
現在CPU#1會被清除。此進程只會運行在CPU#0之上。