1. 程式人生 > >如何將所有的程序執行在一個指定的CPU上

如何將所有的程序執行在一個指定的CPU上

簡單地說,CPU 親和性(affinity) 就是程序要在某個給定的 CPU 上儘量長時間地執行而不被遷移到其他處理器的傾向性。Linux 核心程序排程器天生就具有被稱為 軟 CPU 親和性(affinity) 的特性,這意味著程序通常不會在處理器之間頻繁遷移。這種狀態正是我們希望的,因為程序遷移的頻率小就意味著產生的負載小。

2.6 版本的 Linux 核心還包含了一種機制,它讓開發人員可以程式設計實現 硬 CPU 親和性(affinity)。這意味著應用程式可以顯式地指定程序在哪個(或哪些)處理器上執行。

什麼是 Linux 核心硬親和性(affinity)?

在 Linux 核心中,所有的程序都有一個相關的資料結構,稱為 task_struct

。這個結構非常重要,原因有很多;其中與 親和性(affinity)相關度最高的是 cpus_allowed 位掩碼。這個位掩碼由 n 位組成,與系統中的 n 個邏輯處理器一一對應。 具有 4 個物理 CPU 的系統可以有 4 位。如果這些 CPU 都啟用了超執行緒,那麼這個系統就有一個 8 位的位掩碼。

如果為給定的程序設定了給定的位,那麼這個程序就可以在相關的 CPU 上執行。因此,如果一個程序可以在任何 CPU 上執行,並且能夠根據需要在處理器之間進行遷移,那麼位掩碼就全是 1。實際上,這就是 Linux 中程序的預設狀態。

Linux 核心 API 提供了一些方法,讓使用者可以修改位掩碼或檢視當前的位掩碼:

  • sched_set_affinity() (用來修改位掩碼)
  • sched_get_affinity() (用來檢視當前的位掩碼)

注意,cpu_affinity 會被傳遞給子執行緒,因此應該適當地呼叫 sched_set_affinity

通常 Linux 核心都可以很好地對程序進行排程,在應該執行的地方執行程序(這就是說,在可用的處理器上執行並獲得很好的整體效能)。核心包含了一些用來檢測 CPU 之間任務負載遷移的演算法,可以啟用程序遷移來降低繁忙的處理器的壓力。

一般情況下,在應用程式中只需使用預設的排程器行為。然而,您可能會希望修改這些預設行為以實現效能的優化。讓我們來看一下使用硬親和性(affinity) 的 3 個原因。

原因 1. 有大量計算要做

基於大量計算的情形通常出現在科學和理論計算中,但是通用領域的計算也可能出現這種情況。一個常見的標誌是您發現自己的應用程式要在多處理器的機器上花費大量的計算時間。

原因 2. 您在測試複雜的應用程式

測試複雜軟體是我們對核心的親和性(affinity)技術感興趣的另外一個原因。考慮一個需要進行線性可伸縮性測試的應用程式。有些產品宣告可以在 使用更多硬體 時執行得更好。

我們不用購買多臺機器(為每種處理器配置都購買一臺機器),而是可以:

  • 購買一臺多處理器的機器
  • 不斷增加分配的處理器
  • 測量每秒的事務數
  • 評估結果的可伸縮性

如果應用程式隨著 CPU 的增加可以線性地伸縮,那麼每秒事務數和 CPU 個數之間應該會是線性的關係(例如斜線圖 —— 請參閱下一節的內容)。這樣建模可以確定應用程式是否可以有效地使用底層硬體。

Amdahl 法則

Amdahl 法則是有關使用並行處理器來解決問題相對於只使用一個序列處理器來解決問題的加速比的法則。加速比(Speedup) 等於序列執行(只使用一個處理器)的時間除以程式並行執行(使用多個處理器)的時間:

      T(1)
S = ------
      T(j)

其中 T(j) 是在使用 j 個處理器執行程式時所花費的時間。

Amdahl 法則說明這種加速比在現實中可能並不會發生,但是可以非常接近於該值。對於通常情況來說,我們可以推論出每個程式都有一些序列的元件。隨著問題集不斷變大,序列元件最終會在優化解決方案時間方面達到一個上限。

Amdahl 法則在希望保持高 CPU 快取命中率時尤其重要。如果一個給定的程序遷移到其他地方去了,那麼它就失去了利用 CPU 快取的優勢。實際上,如果正在使用的 CPU 需要為自己快取一些特殊的資料,那麼所有其他 CPU 都會使這些資料在自己的快取中失效。

因此,如果有多個執行緒都需要相同的資料,那麼將這些執行緒繫結到一個特定的 CPU 上是非常有意義的,這樣就確保它們可以訪問相同的快取資料(或者至少可以提高快取的命中率)。否則,這些執行緒可能會在不同的 CPU 上執行,這樣會頻繁地使其他快取項失效。

原因 3. 您正在執行時間敏感的、決定性的程序

我們對 CPU 親和性(affinity)感興趣的最後一個原因是實時(對時間敏感的)程序。例如,您可能會希望使用硬親和性(affinity)來指定一個 8 路主機上的某個處理器,而同時允許其他 7 個處理器處理所有普通的系統排程。這種做法確保長時間執行、對時間敏感的應用程式可以得到執行,同時可以允許其他應用程式獨佔其餘的計算資源。

下面的樣例應用程式顯示了這是如何工作的。

現在讓我們來設計一個程式,它可以讓 Linux 系統非常繁忙。可以使用前面介紹的系統呼叫和另外一些用來說明系統中有多少處理器的 API 來構建這個應用程式。實際上,我們的目標是編寫這樣一個程式:它可以讓系統中的每個處理器都繁忙幾秒鐘。


清單 1. 讓處理器繁忙

                
/* This method will create threads, then bind each to its own cpu. */
bool do_cpu_stress(int numthreads)
{
   int ret = TRUE;
   int created_thread = 0;
   /* We need a thread for each cpu we have... */
   while ( created_thread < numthreads - 1 )
   {
      int mypid = fork();
      if (mypid == 0) /* Child process */
       {
          printf("\tCreating Child Thread: #%i\n", created_thread);
          break;
      }
      else /* Only parent executes this */
      {
          /* Continue looping until we spawned enough threads! */ ;
          created_thread++;
      }
   }
   /* NOTE: All threads execute code from here down! */

正如您可以看到的一樣,這段程式碼只是通過 fork 呼叫簡單地建立一組執行緒。每個執行緒都執行這個方法中後面的程式碼。現在我們讓每個執行緒都將親和性(affinity)設定為自己的 CPU。


清單 2. 為每個執行緒設定 CPU 親和性(affinity)

                
   cpu_set_t mask;
   /* CPU_ZERO initializes all the bits in the mask to zero. */
        CPU_ZERO( &mask );
   /* CPU_SET sets only the bit corresponding to cpu. */
        CPU_SET( created_thread, &mask );
   /* sched_setaffinity returns 0 in success */
        if( sched_setaffinity( 0, sizeof(mask), &mask ) == -1 )
   {
      printf("WARNING: Could not set CPU Affinity, continuing...\n");
   }

如果程式可以執行到這兒,那麼我們的執行緒就已經設定了自己的親和性(affinity)。呼叫 sched_setaffinity 會設定由 pid 所引用的程序的 CPU 親和性(affinity)掩碼。如果 pid 為 0,那麼就使用當前程序。

親和性(affinity)掩碼是使用在 mask 中儲存的位掩碼來表示的。最低位對應於系統中的第一個邏輯處理器,而最高位則對應於系統中最後一個邏輯處理器。

每個設定的位都對應一個可以合法排程的 CPU,而未設定的位則對應一個不可排程的 CPU。換而言之,程序都被綁定了,只能在那些對應位被設定了的處理器上執行。通常,掩碼中的所有位都被置位了。這些執行緒的親和性(affinity)都會傳遞給從它們派生的子程序中。

注意不應該直接修改位掩碼。應該使用下面的巨集。雖然在我們的例子中並沒有全部使用這些巨集,但是在本文中還是詳細列出了這些巨集,您在自己的程式中可能需要這些巨集。


清單 3. 間接修改位掩碼的巨集

                
void CPU_ZERO (cpu_set_t *set)
這個巨集對 CPU 集 set 進行初始化,將其設定為空集。
void CPU_SET (int cpu, cpu_set_t *set)
這個巨集將 cpu 加入 CPU 集 set 中。
void CPU_CLR (int cpu, cpu_set_t *set)
這個巨集將 cpu 從 CPU 集 set 中刪除。
int CPU_ISSET (int cpu, const cpu_set_t *set)
如果 cpu 是 CPU 集 set 的一員,這個巨集就返回一個非零值(true),否則就返回零(false)。

對於本文來說,樣例程式碼會繼續讓每個執行緒都執行某些計算量較大的操作。


清單 4. 每個執行緒都執行一個計算敏感的操作

                
    /* Now we have a single thread bound to each cpu on the system */
    int computation_res = do_cpu_expensive_op(41);
    cpu_set_t mycpuid;
    sched_getaffinity(0, sizeof(mycpuid), &mycpuid);
    if ( check_cpu_expensive_op(computation_res) )
    {
      printf("SUCCESS: Thread completed, and PASSED integrity check!\n",
         mycpuid);
      ret = TRUE;
    }
    else
    {
      printf("FAILURE: Thread failed integrity check!\n",
         mycpuid);
      ret = FALSE;
    }
   return ret;
}

現在您已經瞭解了在 Linux 2.6 版本的核心中設定 CPU 親和性(affinity)的基本知識。接下來,我們使用一個 main 程式來封裝這些方法,它使用一個使用者指定的引數來說明要讓多少個 CPU 繁忙。我們可以使用另外一個方法來確定系統中有多少個處理器:

int NUM_PROCS = sysconf(_SC_NPROCESSORS_CONF);

這個方法讓程式能夠自己確定要讓多少個處理器保持繁忙,例如預設讓所有的處理器都處於繁忙狀態,並允許使用者指定系統中實際處理器範圍的一個子集。

當執行前面介紹的樣例程式時,可以使用很多工具來檢視 CPU 是否是繁忙的。如果只是簡單地進行測試,可以使用 Linux 命令top。在執行 top 命令時按下 “1” 鍵,可以看到每個 CPU 執行程序所佔用的百分比。

這個樣例程式雖然非常簡單,但是它卻展示了使用 Linux 核心中實現的硬親和性(affinity)的基本知識。(任何使用這段程式碼的應用程式都無疑會做一些更有意義的事情。)瞭解了 CPU 親和性(affinity)核心 API 的基本知識,您就可以從複雜的應用程式中榨取出最後一點兒效能了。