2011年2月16日 星期三

撰寫跨平台程式需注意的 I/O 和 Offset 問題

Standard
對於檔案操作,坊間已經有太多 Hello World 程式示範過無數次,往往運用簡單的 lseek() 、read() 和 write() 已滿足大多數需求。但在 ARM 與 x86 架構之間,便有些微妙的差異,讓 Hello World 級的範例程式處處失效。的確,很少人真的動手寫一支程式去處理大型檔案,更少人會將這類程式移植跨平台使用。

若發現原本在 x86 上可正常使用 write()/pwrite() 等方式寫入資料,但交叉編譯(Cross-Compile)至 ARM 後,始終無法如預期讀寫資料,那意味著你可能掉進 offset 的陷阱了。

說起來這問題牽涉到 off_t 變數形態的記憶體使用長度,並可分為三個面向探討『Kernel』、『glibc』、『應用程式(Application)』。理論上 Runtime 時,三方對檔案操作的 offset 定義應該是要一樣,但事實上不然,只有在 x86 平台上比較統一。在 ARM 等這類平台,由於各家廠商在製作 BSP 各自定義,各程式和往往預設使用上不盡相同,更主要原因是因為多數應用程式都是從 x86 移殖過來,和 ARM 平台上 32-bit 的預設處理方式會格格不入。

在 x86 架構上(測試環境為 Debian Sid、 Intel Platform),通常 off_t 被定義為 unsigned long long (無號最長整數),總長度是為 8 Bytes(因長度等同 off64_t,下文將 64-bit 的 off_t 以 off64_t 稱之),在此環境下的 gcc+glibc 會自動使用 off64_t 形態做為 offset 處理參數,當然在編譯程式時也會自動使用相對應的處理函數,實作和定義可參考 unistd.h。

而在 ARM 的環境上,卻不一定如此,在某些平台和 BSP 中,off_t 預設就會被定為 32-bit 長度,會造成一些可能發生的問題。

一個例子,若是有寫過 FUSE(Filesystem in User Space) 程式,並移植到 ARM 的經驗,就有機會發現系統上 off_t 定義的大混亂。起因是 libfuse 使用 off64_t 與 kernel/Application 溝通,但若是系統編譯環境預設使用 off_t,我們寫的 filesystem 程式便會不正常。原因在於 lseek()、write()、pwrite() 和 pread() 系列函數只吃 off_t 而不是 off64_t,但同樣的 code ,此問題不會發生在多數 x86 系統上(因為編譯時預設是使用 off64_t 和 lseek64()、write64()、pwrite64() 和 pread64())。

解決這問題有兩種做法,一種是在編譯代入 D_FILE_OFFSET_BITS=64 或程式中定義 __USE_FILE_OFFSET64,另一種就是將所有 pwrite()/pread() 等函式強制改為 pwrite64()/pread64() 等處理 64-bit offset 的函式。