1 現象描述
表面現象是人機程式死機,所有功能不能正常工作。
這個問題已經出現過多次,這次終於花了一周時間把它搞明白了,後面總結了跟蹤過程及分析並給出了3種解決方案。
2 問題跟蹤2.1 跟蹤步驟1——top命令查看線程狀態
狀態如,cpu佔用率hmi-848線程已經達到95%。
2.2 跟蹤步驟2——strace工具載入848線程
輸出資訊如,可以看到系統在不停的調用futex,並且調用出錯(參數無效)。
Process 848 attached - interrupt to quit SYS_333(0x2, 0x80, 0x2, 0, 0x80) = 1620625728 <0.000000> futex(0x8016dc66, FUTEX_WAIT_PRIVATE, 2, NULL) = -1 EINVAL (Invalid argument) <0.000000> SYS_335(0x2, 0x2, 0x2, 0, 0x80) = 2 <0.001000> SYS_333(0x2, 0x80, 0x2, 0, 0x80) = 1620625728 <0.001000> futex(0x8016dc66, FUTEX_WAIT_PRIVATE, 2, NULL) = -1 EINVAL (Invalid argument) <0.000000> SYS_335(0x2, 0x2, 0x2, 0, 0x80) = 2 <0.000000> SYS_333(0x2, 0x80, 0x2, 0, 0x80) = 1620625728 <0.000000> futex(0x8016dc66, FUTEX_WAIT_PRIVATE, 2, NULL) = -1 EINVAL (Invalid argument) <0.000000> |
2.3 跟蹤步驟3——開啟map檔案尋找0x8016dc66地址相關資訊
果然找到了0x8016dc66地址,如。該地址對應著glbsem.c中一個全域變數gs_util_mutex,在glbsem.c找到該變數其定義為“pthread_mutex_t
gs_util_mutex”,為一個線程鎖。
根據上面分析得出結論1:
鎖(或解鎖)時出現了錯誤,並且介面沒有返回一致在調用futex。
根據該結論進行分析,對於線程鎖的操作符合規範沒有問題,進而得出推測1:
導致出錯的原因可能是gs_util_mutex的空間遭到破壞或加鎖(或解鎖)的庫函數實現有錯誤。
2.4 跟蹤步驟4——確認gs_util_mutex空間有沒有被破壞
懷疑別人之前先確認一下自己,還是先看看自己的gs_util_mutex空間有沒有被破壞。開啟map檔案,看看與0x8016dc66相鄰地址是些什麼,可以看,仔細分析周圍這些變數操作會不會破壞gs_util_mutex資料,當我看到gs_currSemFreeFile相關操作時眼前一亮(哈哈!問題終於解決了),代碼。第8行條件不滿足fname得不到初始化之後在第27行將fname中內容複寫到gs_currSemFreeFile,此時複製的是什麼內容長度是多少就是個不確定的了,這就可能會破壞gs_util_mutex資料。再仔細分析除此之外沒發現其它問題。
ST_VOID gs_free_semx (SD_CONST ST_CHAR *srcFile, ST_INT srcLineNum) { ST_CHAR fname[SLOG_MAX_FNAME+1] ; ST_INT idx; if (gs_track) { if (gs_debug_sel & GS_LOG_FLOW) { if (srcFile != NULL) slogTrimFileName (fname, srcFile); else strcpy (fname, "Unknown"); } idx = gs_currSemOwnerIndex; --gs_currSemOwnerIndex; if (gs_currSemOwnerIndex == -1) { GLBSEM_LOG_CFLOW0 (" The semaphore should now be free"); } else if (gs_currSemOwnerIndex >= 0 && gs_currSemOwnerIndex < MAX_SEM_NEST_TRACK) { GLBSEM_LOG_CFLOW2 (" File %s, Line %d now has the semaphore", gs_currSemOwnerFile[gs_currSemOwnerIndex], gs_currSemOwnerLine[gs_currSemOwnerIndex]); strcpy (gs_currSemFreeFile[idx], fname); gs_currSemFreeLine[idx] = srcLineNum; if (strcmpi (gs_currSemFreeFile[idx], gs_currSemOwnerFile[idx])) { GLBSEM_LOG_ERR2 ("Possible problem: %s (%d)", gs_currSemOwnerFile[idx], gs_currSemOwnerLine[idx]); } } else if (gs_currSemOwnerIndex >= 0 && gs_currSemOwnerIndex >= MAX_SEM_NEST_TRACK) { GLBSEM_LOG_CFLOW0 (" Nested too deep to track"); } else if (gs_currSemOwnerIndex < -1) { GLBSEM_LOG_ERR0 ("GLBSEM gs_free_semx error: Sem track index negative"); gs_currSemOwnerIndex = -1; } } gs_mutex_free (&gs_glb_mutex); } |
2.5 跟蹤步驟5——修改並驗證
上面代碼第3行修改為“ST_CHAR fname[SLOG_MAX_FNAME+1] = {0};”並編譯驗證。令人沮喪的是問題依然存在。
2.6 跟蹤步驟6——編寫簡單代碼驗證線程鎖
按照原程式中線程鎖的使用編寫驗證代碼,結果程式跑了24小時也沒什麼問題,這下可是丈二和尚摸不著頭了。難道真的不是線程鎖的問題?
2.7 跟蹤步驟7——理理思路從頭再來
還是從最初的資訊切入吧,對就從“futex(0x8016dc66, FUTEX_WAIT_PRIVATE, 2, NULL) = -1 EINVAL (Invalid argument)”入手。閱讀了futex的協助文檔、相關的文章以及對應源碼,當根據參數分析futex的時候發現:根據第2個參數futex會調用get_futex_key,而get_futex_key中有如下代碼:
if (unlikely((address % sizeof(u32)) != 0))
return -EINVAL;
這兩行代碼的意思是address對4求餘不為0就返回EINVAL (Invalid
argument),address又恰恰是futex的第1個參數0x8016dc66(即gs_util_mutex的地址),恰恰0x8016dc66不能被4整除。嗯,這就是問題的所在了!
結論2:線程鎖變數的首地址必須是4的倍數。
2.8 跟蹤步驟8——再次使用步驟6的代碼驗證結論2
開啟步驟6的map檔案發現線程鎖變數的首地址是4的倍數,修改源檔案並編譯確保線程鎖變數的首地址不是4的倍數,運行程式。果然不一會兒就卡死了。
3 原理分析3.1 應用背景
線程屬性設定為PTHREAD_MUTEX_RECURSIVE
3.2 失效機理3.2.1 線程鎖基於futex
futex的使用手冊有如下描述:
(註:參考於http://www.kernel.org/doc/man-pages/online/pages/man2/futex.2.html)
long futex(u32 __user *uaddr, int op, u32 val, ktime_t *timeout,
u32 __user *uaddr2, u32 val2, u32 val3)
The uaddr argument needs to point to an aligned integer which stores the
counter. The operation to execute is passed via the op argument, along with a
value val
3.2.2 libc中線程鎖的對齊機制
線程鎖的屬性定義如下,其為一聯合體,成員__align用於保障聲明該類型變數時變數首地址是sizeof(long int)的整數倍。(註:參考於libc2.10.1)
typedef union
{
struct __pthread_mutex_s
{
int __lock;
unsigned int __count;
int __owner;
/* KIND must stay at this position in the structure to maintain
binary compatibility. */
int __kind;
unsigned int __nusers;
__extension__ union
{
int __spins;
__pthread_slist_t __list;
};
} __data;
char __size[__SIZEOF_PTHREAD_MUTEX_T];
long int __align;
} pthread_mutex_t;
M68k預設使用兩位元組對齊,普通gcc編譯器預設使用4位元組對齊,即使用普通gcc編譯器時__align成員保證了線程鎖屬性變數的首地址是4的倍數,而使用m68k編譯時間__align成員就不能保證線程鎖屬性變數的首地址是4的倍數了(實際是2的倍數)。
3.2.3 參數錯誤問什麼會引起cpu佔用率高
假設有兩個線程pthr1和pthr2共用一個鎖,當pthr1已經加鎖還未解鎖時切換至pthr2運行,pthr2此時加鎖則會調用futex,futex繼續調用futex_wait試圖阻塞pthr2,utex_wait在調用get_futex_key時返回了參數無效,而pthread_mutex_lock的實現又沒有恰當處理futex的傳回值,發現lock的值沒有變化就進入到futex調用的死迴圈了,線程而的cpu佔用率一下就高了。
4 解決方案
1、
從飛思卡爾擷取libc等所有庫的源碼,自己編譯4位元組對齊的庫,這樣應用也能按4位元組對齊進行編譯了。這樣可以解決這個問題,還可以解決訊號量等對齊的問題。(強烈建議按這種方式解決,因為我們不知道和對齊相關的問題還有多少潛藏在我們的應用中)
2、
我們自己封裝線程鎖變數建立及銷毀介面、使用malloc和free為線程鎖變數分配和釋放空間,可以保證其首地址是按4直接對齊的。
3、
聲明線程鎖變數使用__attribute((aligned (4))),以保證其首地址按4位元組對齊。