alignment fault

什麼是對齊異常?

簡單來說,當CPU訪問內存地址時,如果發現訪問的地址是不對齊的,硬件(部分)就會自動觸發對齊異常。對齊即要求被訪問的地址滿足其數據類型的位寬要求,比如要訪問一個4字節int型的數據,但是提供的地址不是4字節對齊的,那就是不對齊了。也就是說要訪問的數據的位寬長度是多少,那麼訪問的地址就必須是按這個位寬長度對齊的。如果是char類型的,那就沒有沒有對齊要求了。

爲什麼在部分硬件上出現?

部分CPU硬件支持非對齊訪問,典型的就是X86,X86硬件會自動處理非對齊訪問情況,對軟件透明,代價是犧牲效率。硬件處理簡單來說就是通過多次訪存操作,結合拼接(或拆分)操作實現,比如要讀取一個4字節的int型數據,當地址在2字節的邊界時,則需要進行兩次內存讀取操作,將邊界前後的兩個4字節的數據讀取出來,然後取出其中的部分數據進行拼接,才能得到想要的數據,X86中,這些操作雖然都是由硬件自動完成,但是相對於對齊的數據訪問來說,其性能損失也是非常明顯的。

部分CPU“部分支持”非對齊訪問,典型的就是ARM,其“單指令”操作支持非對齊,但“羣指令”操作(SIMD)則不支持(必須對齊訪問)。

部分CPU硬件不支持對齊訪問,但通過軟件支持。典型的就是部分mips架構,其通過內核中對alignment fault異常處理流程中進行處理,比如將非對齊的數據訪問,通過多次訪存操作和拼接操作來處理,也可以使用類似memcpy的方式來處理,當然代價是更嚴重的性能損失。

ARM架構內核中也有類似的處理分支,可以通過相關的配置來控制其處理方式。

定位方法

內核中的手段

Linux內核中有alignment=啓動參數和/proc/cpu/alignment參數,用於控制出現alignment fault時默認的處理行爲,具體定義如下:

alignment= [KNL,ARM]
Allow the default userspace alignment fault handler
behaviour to be specified. Bit 0 enables warnings,
bit 1 enables fixups, and bit 2 sends a segfault.
該參數由3位組成,第一位控制是否打印warning,第二位控制是否通過軟件修復,第3位控制是否觸發段錯誤。

通常,在出現alignment fault時,需要分析定位原因,而不能簡單的通過內核的fixup或者忽略,由於由此帶來的性能損耗是非常大的,當然如果您的環境中不在乎性能,那就另當別論了。

所以,通常在分析定位alignment fault異常時,需要設置bit0和bit2,即:

echo 5 > /proc/cpu/alignment
如此設置後,在出現alignment fault時,就能在messages中有較詳細的打印,同時,正常情況下(除非禁用了),如果是出現在用戶態,還會有core文件生成,如果是出現在內核態,則會除非die(),最終觸發panic和kdump,生成vmcore,便於後續的深入分析。

分析思路

此類問題的具體分析思路爲:

在蒐集到core(或vmcore)文件後,使用gdb(或crash)工具進行分析。
確認出現問題的PC指針值
確認PC指針處觸發問題的指令
確認PC指針處對應的具體代碼行
分析代碼邏輯,確認是否有可能導致出現對齊異常的代碼編寫問題,比如不同的指針類型直接的轉換,或者是結構體中padding問題。
爲什麼出現?

我們瞭解,計算機中,CPU是通過總線訪問內存的,而alignment fault正式總線控制器返回給CPU core的。不同的硬件,總線控制器的實現和配置不同,導致不同硬件上,對於非對齊訪問的表現也不同,前面也做了說明。

那爲什麼部分CPU至今仍堅持不支持非對齊訪問呢,最主要的原因肯定是性能問題了。如之前所說,非對齊訪問帶來的性能損耗是相當明顯的,在目前主流的計算機體系下,其是一個明顯的性能損失點,這也是性能調優過程中需要重點關注的點,特別是當CPU自身性能不濟時,需要尤其關注,這就對程序猿們提出了更高的要求。

由於alignment fault對性能的影響,所以很多CPU中,會將此類問題當做一種異常上報,目的就是告訴用戶:這裏有性能隱患了,雖然我可能爲您修復,但需要您的關注,建議您修正代碼,以提升性能。

由什麼引起?

出現alignment fault問題,通常是用戶編寫的代碼導致。估計很多程序猿在編寫代碼(特別是c/c++代碼)時,從未考慮過這樣的問題,那是因爲多數可能都在X86架構下的進行代碼開發,而且沒有考慮過代碼的移植性,如前面所說X86硬件會自動處理非對齊問題,用戶感知不到,但這種情況下,由此帶來的性能損耗,用戶可能也關注不到了。另一方面,部分情況下,編譯器也會自動做padding處理(如對結構體的自動填充對齊),這也進一步讓程序猿們減少了對alignment fault的關注。

最常見的可能導致alignment fault的代碼編寫方式如:

指針轉換:將低位寬類型的指針轉換爲高位寬類型的指針,如:將char * 轉爲int ,或將void 轉爲結構體指針。這類操作是導致alignment fault的最主要的來源,在分析定位問題時,需要特別關注。對於出現異常卻又必須這樣使用的場景,對這類轉換後的指針進行訪問時,如果不能確認其對應的地址是對齊的,則應該使用memcpy訪問(memcpy方式不存在對齊問題)。另外,建議轉換後立即使用,不要將其傳遞到其他函數和模塊,防止擴展,帶來潛在的問題。

使用packed屬性或者編譯選項。這樣的操作會關閉編譯器的自動填充功能,從而使結構體中各個字段緊湊排列,如果排列時未處理好對齊,則可能導致alignment fault。一些場景下(內核中也較常見)確實需要用戶自行緊湊排列結構體,可節省空間(在內存資源稀缺的場景下,很有用),此時需要特別關注對齊問題,建議通過填充的方法儘量對齊,如此可能會導致空間浪費,但是會提升訪問性能,典型的“以空間換時間”的思路。如果對空間有強烈要求,而可以接受性能損失,也可以不考慮對齊,不做padding,但在訪問這些結構體的數據時,需要全部使用memcpy的方式。

解決方案

通常,對於alignment fault有如下幾種處理方法,不同的方法對性能影響不同,如下按性能從高到低描述:

程序猿保證對齊

這是最理想的解決方案,沒有性能損失(但可能會有一定的空間浪費),對程序猿們的要求也比較高,但確實非常非常有必要。

寫代碼時需要記住:數據地址應該至少對齊到與訪問寬度相同的水平。即:1字節訪問無需對齊,2字節訪問需要地址能被2整除,4字節訪問需要地址能被4整除,8字節訪問需要地址能被8整除。

另一方面,主流編譯器通常會自動的通過填充pad來輔助處理對齊問題,程序猿們編程代碼時,通常只需要關注:儘量將數據寬度大的字段(也即較長的double/longlong型變量)放到結構體的前面即可,如此,數據寬度較小的字段無需編譯器補齊,從而可以節約內存。

此外,寄存器寬度也對對齊有影響,通常情況下,寄存器寬度即代表了最高的對齊要求,例如32位的arm,在載入8字節的數據(如longlong型)時,也只需要4字節對齊。另外需要注意一些cpu的擁有SIMD指令,這些指令對應的寄存器寬度往往要遠大於cpu自己的核心寄存器,因而也會有更高的對齊要求。

還有,雖然memcpy(或memset)的方式不要求對齊,但對於device類型(linux內核中分配內存時指定)的內存,部分架構下(如ARM64)不能使用memcpy(或memset)的方式訪問,否則也會出現alignment fault,具體案例請參考我的另一篇文章。

硬件處理

如前所述,一些CPU硬件自身已經支持非對齊訪問,並且在多數情況下能夠支持“快速非對齊訪問”。這裏的“快速”,指的是使用單個不對齊訪問指令快於(拆分不對齊訪問後產生的)兩條對齊訪問指令的情況。在這樣的硬件下,我們通常無需在軟件層面上做出特殊的佈置和調整。但是,對於性能有要求的代碼,還是需要慎重考慮是否添加pad來消除不對齊,因爲到目前爲止不對齊訪問仍然明顯的慢於對齊訪問。

編譯器或代碼拆分

當cpu不能進行快速不對齊訪問時,爲了提高代碼執行效率,應該在軟件層面拆分不對齊訪問指令。

現代的編譯器通常都有對不對齊訪問的特殊處理(例如gcc中的munaligned-access等選項)。當編譯器檢查到不對齊的訪問(並且對應的目標硬件不支持快速不對齊訪問時),會自動生成拆分訪問的代碼。但是,需要特別注意,編譯器在編譯時無法獲知所有指針的地址信息,因此不能完全對齊對齊問題。

代碼拆分,需要程序猿自行將不對齊的內存訪問拆分成對齊的變量訪問。例如一個int x指針,當我們知道x只是雙字節對齊時,就要將其拆分兩個short,如果不知道其地址的對齊情況,可以先對該指針地址進行4或者2的求餘再來決定如何拆分,當然,這種情況下,也可以直接調用memcpy進行拷貝。在現代編譯器中memcpy等常用函數已經被編譯器高度優化了,其實現邏輯和我們前面手寫代碼是完全一樣的。

內核處理

如前面描述,Linux內核(部分架構,如ARM)自身提供了對alignment fault的異常處理機制,對於內核來說alignment fault被當做一種異常來處理,類似於缺頁異常,通常,該異常由硬件觸發(x86等硬件自動處理的架構不會觸發此類異常),內核捕獲後進行相應處理,比如進行一些fixup操作,如果修復成功,則萬事大吉,用戶感知不到(但性能上損耗嚴重),如果不能修復,則進行後續處理,大致流程爲:

判斷是內核態還是用戶態觸發
如果是用戶態,則給用戶進程發送Sigbus信號,用戶進程收到信號後觸發coredump,蒐集core文件。
如果是內核態,則直接進入die()流程,最終觸發panic和kdump,蒐集vmcore

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章