非對齊記憶體訪問¶
- 作者:
Daniel Drake <dsd@gentoo.org>,
- 作者:
Johannes Berg <johannes@sipsolutions.net>
- 鳴謝:
Alan Cox, Avuton Olrich, Heikki Orsila, Jan Engelhardt, Kyle McMartin, Kyle Moffett, Randy Dunlap, Robert Hancock, Uli Kunitz, Vadim Lobanov
Linux 執行在多種架構上,這些架構在記憶體訪問方面表現出不同的行為。本文件詳細介紹了非對齊訪問,說明了為何需要編寫不會導致非對齊訪問的程式碼,以及如何編寫此類程式碼!
非對齊訪問的定義¶
當你嘗試從一個不能被 N 整除的地址(即 addr % N != 0)開始讀取 N 位元組資料時,就會發生非對齊記憶體訪問。例如,從地址 0x10004 讀取 4 位元組資料是沒問題的,但從地址 0x10005 讀取 4 位元組資料則會是非對齊記憶體訪問。
上述描述可能有些模糊,因為記憶體訪問可以透過多種方式發生。這裡指的是機器碼層面的上下文:某些指令從記憶體讀取或向記憶體寫入若干位元組(例如 x86 彙編中的 movb、movw、movl)。稍後會清楚地看到,很容易識別出會編譯成多位元組記憶體訪問指令的 C 語句,尤其是在處理 u16、u32 和 u64 等型別時。
自然對齊¶
上述規則構成了我們所稱的自然對齊:當訪問 N 位元組記憶體時,基記憶體地址必須能被 N 整除,即 addr % N == 0。
編寫程式碼時,請假設目標架構有自然對齊要求。
實際上,只有少數架構對所有大小的記憶體訪問都要求自然對齊。然而,我們必須考慮所有支援的架構;編寫滿足自然對齊要求的程式碼是實現完全可移植性的最簡單方法。
為何非對齊訪問是糟糕的¶
執行非對齊記憶體訪問的影響因架構而異。在這裡寫一篇關於這些差異的完整文件很容易;下面總結了常見情況
有些架構能夠透明地執行非對齊記憶體訪問,但通常會帶來顯著的效能開銷。
有些架構在發生非對齊訪問時會引發處理器異常。異常處理程式能夠糾正非對齊訪問,但會以顯著的效能損失為代價。
有些架構在發生非對齊訪問時會引發處理器異常,但這些異常不包含足夠的資訊來糾正非對齊訪問。
有些架構不具備非對齊記憶體訪問能力,但會悄無聲息地執行與請求不同的記憶體訪問,從而導致難以檢測到的隱蔽程式碼錯誤!
從以上內容可以清楚地看出,如果你的程式碼導致非對齊記憶體訪問,那麼你的程式碼在某些平臺上將無法正常工作,並在其他平臺上引起效能問題。
不會導致非對齊訪問的程式碼¶
首先,上述概念可能有點難以與實際編碼實踐聯絡起來。畢竟,你對某些變數的記憶體地址等沒有很大的控制權。
幸運的是,事情並不太複雜,因為在大多數情況下,編譯器會確保程式碼為你正常工作。例如,考慮以下結構體
struct foo {
u16 field1;
u32 field2;
u8 field3;
};
假設上述結構體的一個例項駐留在從地址 0x10000 開始的記憶體中。憑藉基本的理解,期望訪問 field2 會導致非對齊訪問並非不合理。你可能會預期 field2 位於結構體偏移 2 位元組處,即地址 0x10002,但該地址不能被 4 整除(請記住,我們在這裡讀取的是一個 4 位元組的值)。
幸運的是,編譯器理解對齊約束,因此在上述情況下,它會在 field1 和 field2 之間插入 2 位元組的填充。因此,對於標準結構體型別,你總是可以依賴編譯器對結構體進行填充,以便對欄位的訪問適當地對齊(假設你沒有將該欄位轉換為不同長度的型別)。
同樣,你也可以依賴編譯器根據變數型別的大小,將變數和函式引數對齊到自然對齊方案。
至此,應該清楚的是,訪問單個位元組(u8 或 char)永遠不會導致非對齊訪問,因為所有記憶體地址都可以被一整除。
關於一個相關主題,考慮到上述因素,你可能會發現可以重新排列結構體中的欄位,以便將欄位放置在原本會插入填充的位置,從而減少結構體例項的總體駐留記憶體大小。上述示例的最佳佈局是
struct foo {
u32 field2;
u16 field1;
u8 field3;
};
對於自然對齊方案,編譯器只需在結構體末尾新增一個位元組的填充。新增此填充是為了滿足這些結構體陣列的對齊約束。
另一個值得一提的是在結構體型別上使用 __attribute__((packed))。這個 GCC 特有的屬性告訴編譯器永遠不要在結構體內部插入任何填充,這在你希望使用 C 結構體來表示以固定排列“線外”傳入的資料時非常有用。
你可能會傾向於認為,當訪問不滿足架構對齊要求的欄位時,使用此屬性很容易導致非對齊訪問。然而,同樣地,編譯器清楚對齊約束,並將生成額外的指令以避免非對齊訪問的方式執行記憶體訪問。當然,與非 packed 情況相比,額外的指令顯然會導致效能損失,因此 packed 屬性只應在避免結構體填充至關重要時使用。
導致非對齊訪問的程式碼¶
考慮到上述內容,我們來看一個可能導致非對齊記憶體訪問的實際函式示例。以下函式取自 include/linux/etherdevice.h,是一個用於比較兩個乙太網 MAC 地址是否相等的最佳化例程
bool ether_addr_equal(const u8 *addr1, const u8 *addr2)
{
#ifdef CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS
u32 fold = ((*(const u32 *)addr1) ^ (*(const u32 *)addr2)) |
((*(const u16 *)(addr1 + 4)) ^ (*(const u16 *)(addr2 + 4)));
return fold == 0;
#else
const u16 *a = (const u16 *)addr1;
const u16 *b = (const u16 *)addr2;
return ((a[0] ^ b[0]) | (a[1] ^ b[1]) | (a[2] ^ b[2])) == 0;
#endif
}
在上述函式中,當硬體具備高效的非對齊訪問能力時,這段程式碼沒有問題。但是,當硬體無法在任意邊界上訪問記憶體時,對 a[0] 的引用會導致從地址 addr1 開始讀取 2 位元組(16 位)記憶體。
想想如果 addr1 是一個奇數地址(例如 0x10003)會發生什麼。(提示:這將是一個非對齊訪問。)
儘管上述函式存在潛在的非對齊訪問問題,但它仍然被包含在核心中,只是它被理解為僅在 16 位對齊的地址上正常工作。呼叫者有責任確保這種對齊,或者根本不使用此函式。這個對齊不安全的函式仍然有用,因為它對於能夠確保對齊的情況來說是一個不錯的最佳化,這在乙太網網路環境中幾乎總是如此。
這是另一段可能導致非對齊訪問的程式碼示例
void myfunc(u8 *data, u32 value)
{
[...]
*((u32 *) data) = cpu_to_le32(value);
[...]
}
每當 data 引數指向的地址不能被 4 整除時,這段程式碼都會導致非對齊訪問。
總而言之,可能遇到非對齊訪問問題的兩種主要情況包括
將變數強制轉換為不同長度的型別
指標運算後訪問至少 2 位元組的資料
避免非對齊訪問¶
避免非對齊訪問最簡單的方法是使用 <linux/unaligned.h> 標頭檔案提供的 get_unaligned() 和 put_unaligned() 宏。
回到前面可能導致非對齊訪問的程式碼示例
void myfunc(u8 *data, u32 value)
{
[...]
*((u32 *) data) = cpu_to_le32(value);
[...]
}
為避免非對齊記憶體訪問,你可以將其重寫如下
void myfunc(u8 *data, u32 value)
{
[...]
value = cpu_to_le32(value);
put_unaligned(value, (u32 *) data);
[...]
}
get_unaligned() 宏的工作方式類似。假設 ‘data’ 是一個指向記憶體的指標,並且你希望避免非對齊訪問,其用法如下
u32 value = get_unaligned((u32 *) data);
這些宏適用於任何長度的記憶體訪問(不僅僅是上述示例中的 32 位)。請注意,與標準對齊記憶體訪問相比,使用這些宏訪問非對齊記憶體可能會在效能方面付出高昂代價。
如果使用此類宏不方便,另一個選擇是使用 memcpy(),其中源或目標(或兩者)的型別為 u8* 或 unsigned char*。由於此操作的位元組級特性,可以避免非對齊訪問。
對齊與網路¶
在需要對齊載入的架構上,網路要求 IP 頭在四位元組邊界上對齊以最佳化 IP 棧。對於常規乙太網硬體,使用常量 NET_IP_ALIGN。在大多數架構上,此常量的值為 2,因為正常的乙太網頭長度為 14 位元組,因此為了獲得正確的對齊,需要 DMA 到一個可以表示為 4*n + 2 的地址。一個值得注意的例外是 powerpc,它將 NET_IP_ALIGN 定義為 0,因為 DMA 到非對齊地址可能非常昂貴,並使非對齊載入的成本相形見絀。
對於某些不能 DMA 到 4*n+2 這樣的非對齊地址的乙太網硬體或非乙太網硬體,這可能是一個問題,此時需要將傳入的幀複製到對齊的緩衝區中。由於這在能夠進行非對齊訪問的架構上是不必要的,因此程式碼可以依賴於 CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS,如下所示
#ifdef CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS
skb = original skb
#else
skb = copy skb
#endif