非对齐内存访问
=========================
非對齊內(nèi)存訪問
:作者: 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 運行在各種架構(gòu)上,這些架構(gòu)在內(nèi)存訪問方面具有不同的行為。這篇文檔描述了一些關(guān)于非對齊訪問的細節(jié),本文檔提供了有關(guān)未對齊訪問的一些詳細信息,為什么需要編寫不會導(dǎo)致它們的代碼,以及如何編寫此類代碼!
非對齊訪問的定義
當你嘗試從不能被 N 整除的地址(即 addr % N != 0)開始讀取 N 個字節(jié)的數(shù)據(jù)時,會發(fā)生未對齊的內(nèi)存訪問。例如,從地址0x10004處讀取4個字節(jié)的數(shù)據(jù)是不錯的,但是從地址0x10005處讀取4個字節(jié)就會導(dǎo)致非對齊內(nèi)存訪問。
以上看起來可能有些不清楚,因為內(nèi)存訪問可以以不同的方式發(fā)生。這里的上下文是在機器代碼別:某些指令從內(nèi)存讀取或?qū)懭攵鄠€字節(jié)(例如 x86 匯編中的 movb、movw、movl)。很明顯,識別那些會被編譯為多字節(jié)內(nèi)存訪問指令的 C 語句相對容易,即在處理 u16、u32 和 u64 等類型時。
自然對齊
上面提到的規(guī)則形成了我們所說的自然對齊:
訪問 N 字節(jié)內(nèi)存時,內(nèi)存基地址必須能被 N 整除,即 addr % N == 0。
在編寫代碼時,需要假設(shè)目標架構(gòu)有自然的對齊要求。
實際上,只有少數(shù)架構(gòu)需要對所有大小的內(nèi)存訪問進行自然對齊。 但是,我們必須考慮所有支持的架構(gòu); 編寫滿足自然對齊要求的代碼是實現(xiàn)完全可移植性的最簡單方法。
為什么非對齊訪問不好
執(zhí)行未對齊內(nèi)存訪問的效果因架構(gòu)而異。 在這里寫一篇關(guān)于差異的完整文檔會很容易;常見情況概述如下:
- Some architectures are able to perform unaligned memory accesses
transparently, but there is usually a significant performance cost. - 某些體系結(jié)構(gòu)能夠透明地執(zhí)行未對齊的內(nèi)存訪問,但通常會產(chǎn)生顯著的性能成本。
- 當發(fā)生未對齊的訪問時,某些架構(gòu)會引發(fā)處理器異常。異常處理程序能夠糾正未對齊的訪問,但對性能的影響很大。
- 某些架構(gòu)在發(fā)生未對齊訪問時會引發(fā)處理器異常,但這些異常沒有包含足夠的信息來糾正未對齊訪問。
- 某些架構(gòu)無法進行未對齊的內(nèi)存訪問,但會默默地對請求的內(nèi)存執(zhí)行不同的內(nèi)存訪問,從而導(dǎo)致難以檢測的微妙的代碼錯誤!
從上面應(yīng)該可以明顯看出,如果你的代碼導(dǎo)致發(fā)生未對齊的內(nèi)存訪問,您的代碼將無法在某些平臺上正常工作,并且會在其他平臺上導(dǎo)致性能問題。
不會引起非對齊訪問的代碼
乍一看,上面的概念似乎與實際的編碼實踐聯(lián)系起來有點困難。 畢竟,您對某些變量的內(nèi)存地址等沒有太多控制權(quán)。
幸運的是那并不是很復(fù)雜,就像在大多數(shù)場景下,編譯器會確保這些對你有用。例如,采用以下結(jié)構(gòu):
struct foo {u16 field1;u32 field2;u8 field3; };假設(shè)有一個上面結(jié)構(gòu)的實例駐留在以0x10000開始的地址。一個基本的理解是,訪問filed2會導(dǎo)致非對齊訪問是正常的。你期望 field2 位于結(jié)構(gòu)中偏移 2 個字節(jié)處,即地址 0x10002,但該地址不能被 4 整除(請記住,我們在這里讀取的是 4 個字節(jié)的值)。
幸運的是,編譯器理解對齊限制,所以在上面的場景中它會在field1和field2之間插入兩個字節(jié)。因此,對于標準結(jié)構(gòu)類型,你始終可以依靠編譯器來填充結(jié)構(gòu),以便對字段的訪問適當對齊(假設(shè)您沒有將字段強制轉(zhuǎn)換為不同長度的類型)。
同樣,你也可以依靠編譯器根據(jù)變量類型的大小將變量和函數(shù)參數(shù)對齊到自然對齊的方案。
在這點上,不言而喻,訪問單個字節(jié)(u8或者char)永遠不會造成非對齊,因為所有的內(nèi)存地址是可以被1整除的。
在一個相關(guān)主題上,考慮到上述注意事項,你可以觀察到,你可以對結(jié)構(gòu)中的字段重新排序,以便將字段放置會插入填充的位置,從而減少結(jié)構(gòu)實例的整體駐留內(nèi)存大小。 的最佳布局
上面的例子是::
對于自然對齊方案,編譯器只需在結(jié)構(gòu)末尾添加一個字節(jié)的填充。添加此填充是為了滿足這些結(jié)構(gòu)的數(shù)組的對齊約束。
另一個值得考慮的點是在一個結(jié)構(gòu)類型中__ attribute__((packed))的使用。這個特定于 GCC 的屬性告訴編譯器永遠不要在結(jié)構(gòu)中插入任何填充,當您想使用 C 結(jié)構(gòu)來表示一些固定順序排列的數(shù)據(jù)時非常有用。
您可能會認為,在訪問不滿足體系結(jié)構(gòu)對齊要求的字段時,使用此屬性很容易導(dǎo)致未對齊的訪問。 然而,同樣,編譯器知道對齊約束,并將生成額外的指令來以不會導(dǎo)致未對齊訪問的方式執(zhí)行內(nèi)存訪問。 當然,與非壓縮情況相比,額外的指令顯然會導(dǎo)致性能損失,因此只有在避免結(jié)構(gòu)填充很重要時才應(yīng)使用壓縮屬性。
會造成非對齊訪問的代碼
考慮到上述情況,讓我們進入一個可能導(dǎo)致未對齊內(nèi)存訪問的函數(shù)的真實示例。 以下來自 include/linux/etherdevice.h 的函數(shù)是一個優(yōu)化的例程,用于比較兩個以太網(wǎng) MAC 地址的相等性:
bool ether_addr_equal(const u8 *addr1, const u8 *addr2){#ifdef CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESSu32 fold = ((*(const u32 *)addr1) ^ (*(const u32 *)addr2)) |((*(const u16 *)(addr1 + 4)) ^ (*(const u16 *)(addr2 + 4)));return fold == 0;#elseconst 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}在上面的函數(shù)中,當硬件具有高效的非對齊訪問能力時,這段代碼沒有問題。 但是當硬件無法訪問任意邊界上的內(nèi)存時,對 a[0] 的引用會導(dǎo)致從地址 addr1 開始的內(nèi)存中讀取 2 個字節(jié)(16 位)。
Think about what would happen if addr1 was an odd address such as 0x10003.
(Hint: it’d be an unaligned access.)
想想如果addr1是一個奇數(shù)地址比如0x10003,會發(fā)生什么(提示:這是一個非對齊訪問)。
盡管上述函數(shù)存在潛在的未對齊訪問問題,但無論如何它都包含在內(nèi)核中,但被理解為只能在 16 位對齊地址上正常工作。 由調(diào)用者來確保這種對齊或根本不使用此功能。 這種對齊不安全的功能仍然很有用,因為它對于可以確保對齊的情況來說是一個不錯的優(yōu)化,在以太網(wǎng)網(wǎng)絡(luò)環(huán)境中幾乎所有場景都是如此。
下面是一些可能導(dǎo)致未對齊訪問的代碼示例:
void myfunc(u8 *data, u32 value) {[...]*((u32 *) data) = cpu_to_le32(value);[...] }每當 data 參數(shù)指向一個不能被 4 整除的地址時,此代碼將導(dǎo)致未對齊的訪問。
總之,您可能會遇到未對齊訪問問題的 2 個主要場景包括:
避免非對齊訪問
避免未對齊訪問的最簡單方法是使用 <asm/unaligned.h> 頭文件提供的 get_unaligned() 和 put_unaligned() 宏。
回到之前可能導(dǎo)致未對齊訪問的代碼示例:
void myfunc(u8 *data, u32 value) {[...]*((u32 *) data) = cpu_to_le32(value);[...] }為了避免非對齊內(nèi)存訪問,你可以像下面一樣重寫:
void myfunc(u8 *data, u32 value) {[...]value = cpu_to_le32(value);put_unaligned(value, (u32 *) data);[...] }get_unaligned() 宏的工作原理類似。 假設(shè) ‘data’ 是一個指向內(nèi)存的指針并且您希望避免未對齊的訪問,它的用法如下:
u32 value = get_unaligned((u32 *) data);這些宏適用于任何長度的內(nèi)存訪問(不僅僅是上面例子中的 32 位)。請注意,與對齊內(nèi)存的標準訪問相比,使用這些宏訪問未對齊的內(nèi)存在性能方面可能代價高昂。
如果使用此類宏不方便,另一種選擇是使用 memcpy(),其中源或目標(或兩者)的類型為 u8* 或 unsigned char*。由于此操作的逐字節(jié)性質(zhì),避免了未對齊的訪問。
對齊與網(wǎng)絡(luò)
在需要對齊負載的架構(gòu)上,網(wǎng)絡(luò)要求 IP 標頭在四字節(jié)邊界上對齊以優(yōu)化 IP 堆棧。 對于常規(guī)以太網(wǎng)硬件,使用常量 NET_IP_ALIGN。 在大多數(shù)架構(gòu)上,這個常量的值為 2,因為正常的以太網(wǎng)頭是 14 字節(jié)長,所以為了獲得正確的對齊,需要 DMA 到一個可以表示為 4*n + 2 的地址。這里一個值得注意的例外是 powerpc 它將 NET_IP_ALIGN 定義為 0,因為未對齊地址的 DMA 可能非常昂貴,并且使未對齊加載的成本相形見絀。
對于某些無法 DMA 到未對齊地址(如 4*n+2 或非以太網(wǎng)硬件)的以太網(wǎng)硬件,這可能是一個問題,然后需要將傳入的幀復(fù)制到對齊的緩沖區(qū)中。 因為這在可以進行非對齊訪問的架構(gòu)上是不必要的,所以代碼可以像這樣依賴于 CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS:
#ifdef CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESSskb = original skb #elseskb = copy skb #endif譯注:
其他非對齊訪問資料:談?wù)剝?nèi)存對齊
總結(jié)
- 上一篇: jQuery字体自动翻动
- 下一篇: 微信小程序基于vant的自定义底部导航栏