轉自:https://www.kernel.org/doc/html/latest/translations/zh_CN/core-api/unaligned-memory-access.html
- Original
- 翻譯
-
司延騰 Yanteng Si <siyanteng@loongson.cn>
- 校譯
-
時奎亮 <alexs@kernel.org>
非對齊內存訪問
- 作者
-
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)永遠不會導致無對齊訪問,因為所有的內 存地址都可以被1均勻地整除。
在一個相關的話題上,考慮到上述因素,你可以觀察到,你可以對結構體中的字段進行重新排序, 以便將字段放在不重排就會插入填充物的地方,從而減少結構體實例的整體常駐內存大小。上述 例子的最佳布局是:
struct foo {
u32 field2;
u16 field1;
u8 field3;
};
對於一個自然對齊方案,編譯器只需要在結構的末尾添加一個字節的填充。添加這種填充是為了滿 足這些結構的數組的對齊約束。
另一點值得一提的是在結構體類型上使用__attribute__((packed))。這個GCC特有的屬性告訴編 譯器永遠不要在結構體中插入任何填充,當你想用C結構體來表示一些“off the wire”的固定排列 的數據時,這個屬性很有用。
你可能會傾向於認為,在訪問不滿足架構對齊要求的字段時,使用這個屬性很容易導致不對齊的訪 問。然而,編譯器也意識到了對齊的限制,並且會產生額外的指令來執行內存訪問,以避免造成不 對齊的訪問。當然,與non-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);
[...]
}
每當數據參數指向的地址不被4均勻整除時,這段代碼就會導致非對齊訪問。
綜上所述,你可能遇到非對齊訪問問題的兩種主要情況包括:
將變量定義不同長度的類型
指針運算后訪問至少2個字節的數據
避免非對齊訪問
避免非對齊訪問的最簡單方法是使用<asm/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*或 非對齊char*。由於這種操作的字節性質,避免了非對齊訪問。
對齊 vs. 網絡
在需要對齊負載的架構上,網絡要求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