當前UNIX上的企業級軟件大部分都是為了迎合大公司的商務需要。因而它必須支持新出現的技術,並能順應迅速發展的市場潮流,比如強大而靈活的Linux操作系統的大量使用。由於這種軟件大部分是大型的、多線程的而且是多進程的,所以將其移植到Linux面臨著挑戰。通過本文,可以獲得在把某個企業級軟件真正移植到Linux的過程中得到的清單和建議。 當前商務IT行為的一個實際情形是,很多組織正在將IT轉移到Linux,因為它具備了作為系統平台的靈活性與穩定性。另一個實際情形是,捨棄現有企業級軟件的代價過於高昂。這兩種情形經常同時出現,但關鍵是要解決它們。 將企業級軟件移植到Linux可能會面臨很多有趣的挑戰。每一個步驟都必須要小心--從做出設計選擇,到獲得可用的構建系統,再到最終得到要在Linux上執行的針對特定系統的代碼。 本文基於我在RHEL和SLES發行版本中(在Intel和IBM eServer zSeries體系結構上運行C應用程序)所獲得的經驗,但是這些經驗同樣適用於其他發行版本。我將討論一些在Linux上運行您的應用程序的計劃和需要考慮的技術問題,包括以下內容: * 獲得可用的構建系統。 * 確定可行的操作環境。 * 盡量減少在多種體系結構上構建產品所投入的精力(Linux需要得到那些體系結構的支持)。 * 確定特定體系結構的變化,比如互斥鎖定(mutex locking)。 * 使用新的編譯器,盡可能為多種體系結構維持一個詳盡的通用代碼基(code base)。 * 確定IPC機制。 * 選擇合適的線程模型。 * 按Linux特定的指導方針改變安裝和包裝方式。 * 確定信號選項。 * 選擇解析器工具,比如lex/yacc。 * 做出全局化選擇。 獲得可用的構建系統 支持多個平台的產品通常要求指定將要運行產品的具體操作系統。這種通用代碼通常保存在源目錄結構的獨立代碼組成部分中。 例如,特定於操作系統的代碼規劃可能是類似這樣: src/operating_system_specific_code_component/aix(用於AIX)。 src/operating_system_specific_code_component/solaris(用於Solaris)。 src/operating_system_specific_code_component/UNIX(用於其他種類的Unix)。 下圖從更為“圖形化”的角度展示了特定於操作系統的代碼規劃。
圖1 代碼組織規劃 獲得Linux構建系統 首先,您應該為特定於Linux的代碼創建一個目錄,並將來自某個平台的文件置於其中。當您為Linux引入了一個新目錄後,規劃可能類似這樣: src/operating_system_specific_code_component/linux(用於Linux) 然後這將讓我們得到一個類似如下的新的代碼規劃。
圖2 新的代碼組織規劃 通常,應用程序的大部分代碼通用於所有種類的Unix,也可用於Linux。經驗表明,對於特定於Linux的代碼,首先選擇特定於Solaris的文件可以最小化向Linux移植特定於平台的代碼所需的精力。 然後,修改makefile並引入特定於Linux的條目: 對將要使用的編譯器的定義 * 程序庫路徑 * 線程庫路徑 * 編譯器標記 * 包含文件路徑 * 預處理程序標記 * 需要的所有其他內容 源文件中的很多改動與包含文件路徑的修改有關系。例如,要使用變量errno,需要明確地包含。 在不直接包含特定於體系結構的包含文件(而是包含推薦文件)的所有地方,都必須要小心。例如,就像在中所提及的: #ifndef _DLFCN_H # error "Never use directly; include instead." #endif 您應該小心地使用指示符-Dlinux 或者單詞“linux”。Linux上的預處理程序將單詞“linux”翻譯為數字1。例如,如果文件中有一個/home/linux路徑,而且使用cpp來對此文件進行預處理,則輸出文件中的路徑將是/home/1。為了避免發生這種替換,預處理程序指示符可以是類似這樣的:/lib/cpp -traditional -Ulinux。 通用編譯命令 程序員通常所使用的編譯器是gcc。典型的編譯命令行可能類似這樣:gcc -fPIC -D_GNU_SOURCE -ansi -O2 -c -I。-fPIC幫助生成位置無關代碼,等價於Solaris上的-fPIC。-ansi等價於Solaris上的-Xa。 對於共享對象,典型的鏈接時間指示符應該是gcc -fPIC -shared -o -L-l。-shared等價於Solaris上的-G。 對於擁有入口點的可重定位對象,典型的指示符可能是gcc -fPIC -shared -o -e entry_point -L-l。 在開始選擇最佳操作環境之前,我將先分析在其他體系結構上編譯代碼所涉及的問題。 他體系結構上的編譯 另一個需要考慮的重要事項是,程序員應該能夠讓代碼盡可能容易地在其他體系結構上編譯。構建系統應該為涉及的每種體系結構准備單獨的定義文件。例如,用於x86體系結構的編譯器指示符應該有-DCMP_x86標記,用於某些特定於pSeries服務器上的Linux的代碼應該有-DCMP_PSERIES指示符。對於在x86體系結構的系統上進行的編譯,具體構建定義文件中的編譯命令行類似如下: gcc -fPIC -D_GNU_SOURCE -ansi -O2 -c -I -DCMP_x86 而下面的編譯命令行用於在pSeries體系結構上進行的編譯: gcc -fPIC -D_GNU_SOURCE -ansi -O2 -c -I-DCMP_PSERIES。 -CMP_x86和-CMP_PSERIES都是用戶定義標記,當程序的特定於Linux的代碼將要使用特定於體系結構的代碼時都要使用它們。我的經驗是,大部分用於Linux的應用程序代碼都是與體系結構無關的,特定於體系結構的代碼出現在需要編寫匯編代碼的地方。例如,如果您要使用比較(compare)和交換(swap)指令的實現來開發鎖,那麼您將要使用特定於體系結構的代碼。 代碼的安排應該使得在代碼規劃中特定於Linux的目錄內不存在特定於體系結構的子目錄。為什麼?因為Linux已經為屏蔽體系結構細節做出了很多工作,應用程序的程序員通常不應該關心應用程序將要在哪種體系結構之上去編譯。目標應該是,以最少的精力,對代碼、代碼規劃和makefile文件進行最少的修改,就可以令為特定體系結構所編寫的程序在其他體系結構上被編譯。通過避免在linux目錄中出現特定於體系結構的子目錄,可以大大簡化makefile文件。 linux子目錄中的源文件中可能會有帶有預處理程序指示符的代碼形式,如下: #ifdef CMP_x86 #elif CMP_PSERIES #else #error No code for this architecture in __FILE__ #endif 確定可行的操作環境 計劃步驟的關鍵是確定應用程序要移植到Linux的哪個發行版本。您應該確保計劃移植的程度所需要的所有軟件都可用。例如,可能不能為Linux2.6發行版本發布某個中間件產品,因為在大部分典型配置中所使用的一個關鍵的第三方數據庫在那個發行版本上不能用。最初提供的產品或者應用程序可能不得不改為基於Linux2.4發行版本。 應用程序交互所需要的某些軟件,也可能並不是在應用程序所面向的所有發行版本和體系結構上都可用。對所選操作環境的可行性進行仔細研究。 需要考慮的另一個問題是,應用程序是32位的還是64位的,它是否要與其他也以32位或64位模式運行的第三方軟件共存。 特定於體系結構的變化 應用程序中特定於體系結構的代碼通常局限於少數地方。在本節我將考慮一些示例。 確定字節次序(endian-ness) 程序員不必擔心是為何種體系結構編寫代碼。Linux在/usr/include/endian.h中給出了確定字節次序的途徑。您可以使用下面的典型代碼片斷來確定操作環境是big-endian還是little-endian;您可以方便地設置具體的標記。 /* Are we big-endian? */ #include #if __BYTE_ORDER == __LITTLE_ENDIAN #define MY_BIG_ENDIAN #elif __BYTE_ORDER == __BIG_ENDIAN #undef MY_BIG_ENDIAN #endif 確定棧指針 可以編寫內聯程序集(inline assembly)來確定棧指針。 int get_stack(void **StackPtr) { *StackPtr = 0; #ifdef CMP_x86 __asm__ __volatile__ ("movl %%esp, %0": "=m" (StackPtr) ); #else #error No code for this architecture in __FILE__ #endif return(0); } 實現比較與交換 這裡是為Intel體系結構實現比較與交換的一個示例。 bool_t My_CompareAndSwap(IN int *ptr, IN int old, IN int new) { #ifdef CMP_x86 unsigned char ret; /* Note that sete sets a 'byte' not the Word */ __asm__ __volatile__ ( " lock\n" " cmpxchgl %2,%1\n" " sete %0\n" : "=q" (ret), "=m" (*ptr) : "r" (new), "m" (*ptr), "a" (old) : "memory"); return ret; #else #error No code for this architecture in __FILE__ #endif } 選擇IPC機制 可選的進程間通信(interprocess communication)(IPC)機制--用於應用程序間通信和數據共享的機制--通常包括使用信號、編寫可加載的內核擴展或者使用進程共享的互斥體(mutex)和條件變量。 信號是最容易實現的,但是在多線程環境中必須要小心,因為派生的所有線程都具有類似的信號掩碼(signal mask)。在進程結構的建模中,通常應該只有一個線程來處理信號,否則信號就可能被發送到任意的線程,導致結果可能會不可預知。線程可能是由不受應用程序控制的其他參與實體在進程中派生的,這個應用程序可能不能控制它們的信號掩碼。出於這個原因,信號可能並不是在大型多線程應用程序中進行IPC 的流行方式。例如,在應用程序服務器中運行的應用程序可以派生它們自己的線程,可以捕獲對應用程序服務器進程有實際意義的信號。 內核擴展不容易編寫,可能不能方便地在支持Linux的多種體系結構間進行移植。 隨著POSIX draft 10標准的出台,以及它在Linux 2.6 上的實現的可用,進程共享互斥體(互斥對象:允許多個程序共享同一資源但不能同時使用的程序)和條件變量成為在多進程環境中實現IPC機制的合適選擇。此機制要求建立共享內存,在其中存放互斥體和條件變量,所有進程都有對這些結構體的共同的引用。 選擇線程模型 正要移植到Linux的某些老應用程序非常有可能是基於pthreads draft 4。最新版本的Linux支持pthreads draft 10,所以需要小心地對調用進行適當的映射。如果應用程序使用了某些基於第三方實現的異常處理機制(比如,DCE提供的TRY-CATCH宏),那麼程序員需要確保那些異常處理代碼也與pthreads draft 10相兼容。 下表中是從draft 4到10所發生的變化的示例。 表1 從pthreads draft 4到10調用所發生的變化 pthreads draft 4 pthreads draft 10 pthread_setcancel(CANCEL_ON) pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL) pthread_setcancel(CANCEL_OFF) pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL) pthread_setasynccancel(CANCEL_ON) pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL) pthread_setasynccancel(CANCEL_OFF) pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, NULL) pthread_getspecific(key, value) *value = pthread_getspecific(&key) 可選的線程模型包括native Linux threads和Native POSIX Thread Library(NPTL)實現,後者為Linux中的線程提供了與POSIX兼容的實現。Linux內核(從2.5版本起)已經得到修改,具備了POSIX兼容支持。在SLES9中NPTL是可用的。Red Hat已經為RHEL3(它基於Linux2.4 內核)反向移植(backport)了NPTL支持。RHEL 3既支持NPTL也支持native Linux threads。可以通過設置環境變量LD_ASSUME_KERNEL=2.4.1切換到native Linux threads,但是很多提供商都已經將他們的軟件移植到了支持NPTL的RHEL3上。 使用native Linux threads的主要障礙在於以下方面: 子線程SIGCHILD會送到線程而不是進程(非POSIX行為)。 getpid()被破壞--獲得構成進程的一組線程的pid非常困難。 在某個線程中改變用戶標識,不能將此變化應用於進程中的所有線程。 簡言之,每個線程看起來都像是一個單獨的進程(其行為的某些方面也是如此)。 內核方面也有問題: 擁有成百上千線程的進程會令/proc文件系統幾乎不能使用。每一個線程都表現為一個獨立的進程。 信號實現的問題主要在於缺少內核支持。SIGSTOP等專用信號必須由內核來為所有線程處理。 實現同步原語時對信號的錯誤使用會帶來更多問題。發送信號不是確保同步的明智方法。 另一方面,使用NPTL: 在最新的發行版本中(Linux 2.6以後),信號問題似乎已經被解決。現在,信號可以作為一個整體發送給進程。 已經實現了futex(快速用戶空間互斥體(fast userspace mutex)是在Linux上實現鎖定和構建semaphores和POSIX mutexes等高層次鎖定抽象的基本工具),它能令調用者在內核中等待並可以被顯式地喚醒。這樣,PTHREAD_PROCESS_SHARED和進程間POSIX同步原語可以被實現,而且現在可用。 它使用1:1模型(每個用戶級線程有一個底層的內核線程),並且可以搶占(內核線程可以被搶占)。 它適用於極消耗I/O和CPU的應用程序。 其目標是實現百分之百POSIX兼容。 基於各個發行版本的改進,使用支持NPTL的Linux版本通常是明智的。 文件系統,使用參數,棧 在移植工作中,我的小組發現了許多變化多樣的事情,由於它們相對較為簡單,在此我將集中進行介紹。 對文件系統的支持 如果您的應用程序需要使用記錄日志和寫入數據文件等工具,那麼,相對於原始的I/O,基於文件系統的支持更便於安裝、配置和管理。 系統使用參數 好像不存在收集參數信息(比如內存堆的使用)的直接的系統調用。要確定此類參數,需要利用/proc文件系統的支持。 Stackwalk 當前,只有在Intel體系結構上支持pstack等調用;在其他體系結構上的支持還在開發的過程中。要通過程序進行棧的追蹤,程序員可能不得不使用ABI定義為尚未得到支持的體系結構去實現他們自己的版本。 另一個選擇是使用基於gdb的腳本來獲得棧的信息。產品的高可維護性通常要用到棧的信息。gdb更為標准化,可跨不同體系結構和發行版本使用。 內核映射和共享內存段的使用 如果應用程序使用共享內存段,那麼必須要小心地設置共享內存段的起始位置,除非用戶想依賴系統所提供的初始地址。另外,不同的體系結構具有不同的內存映射支持;共享內存可用的區域也可能不同。 例如,在Intel體系結構中,每個進程將底部四分之三的地址空間分配給用戶區域;頂部部分分配給內核。這意味著任何Linux進程所占的全部內存最多可達2GB(390)或者3GB(Intel)。這個總數必須包括文本、數據和棧段,再加上所有的共享內存區域。在Linux/390上,用於共享內存的區域從0x50000000開始,並且必須在0x7fffa000之前結束。如果您想要在應用程序將要支持的所有體系結構上保持通用的起始地址,那麼在確定那個地址之前必須考慮所有的體系結構。 信號 與其他Unix平台相比,信號--發送啟動和停止某個傳輸或者其他操作的控制信號--並沒有太大的不同,只是信號號碼可能不一樣,或者有些信號在某些發行版本(RHEL AS 3)上不可用,比如SIGEMT。(要獲得關於Solaris和Linux之間信號區別的詳細資料,請參閱參考資料部分的參考文獻。) 配置內核參數 程序員可能會被要求去調整內核的某些參數,以使得應用程序能夠在運行期進行調整。如果是那樣,則需要考慮的一些重要參數包括threads-max(每個進程的最大線程數)、shmmax、 msgmax等等。在/proc/sys/kernel中配置參數列表。可以使用/sbin/sysctl系統調用來配置這些參數。如果您正在移植某個大型的多線程應用程序,那麼threads-max參數可能會尤其重要。 lex/yacc等解析器工具 要做好准備,您在AIX或者Solaris上所編寫的語法某些部分可能不能直接在Linux上使用。例如,yylineno(一個不正式的lex掃描器內部變量)等某些變量默認情況下可能不能直接在Linux上使用。下面的代碼片斷可以檢查yylineno是否得到了直接的支持。打開一個名為a.l的文件(其內容如下): %{ %} %% %% 然後輸入 lex a.l。在 lex.yy.c 中搜索“yylineno”。如果那個變量不可用,有兩種可能的支持 yylineno 的解決方案,即在 Linux 中為 lex 使用 -l 選項(換句話說,執行 lex -l a.l),或者將代碼修改為如下所示: %{ %} %option yylineno %% %% 某些發行版本(比如SLES 9)默認並沒有附帶yacc,但附帶bison。如果需要yacc,可能得去下載它。 全局化問題 在Linux上某些代碼頁的命名可能會不同。例如,AIX上的IBM-850在Linux上可能另外命名為ibm850,ISO8859-1可能被另外命名為ISO-8859-1。如果應用程序消息編錄依賴於這些代碼頁中的某些,而且需要代碼頁轉換,則可能必須修改腳本(使用iconv工具可以完成)。在Linux上,ja_Jp、en_US等大部分常見的位置都可用。 安全性考慮 在新的發行版本中(RHEL AS 3),基於套接字的通信默認得到了保護,所以,如果您正在實現基於IP的服務器,其進程要監聽某個特定的端口,那麼您應該向iptables中添加新的服務。iptables用於建立、維護和檢查Linux內核中的IP數據包過濾器規則表。 例如,首先您可能必須添加一個新的鏈,比如/sbin/iptables -N RH-Firewall-1-INPUT,然後在那個鏈中添加新的服務,像這樣:iptables -I RH-Firewall-1-INPUT -s 0/0 -i eth0 -m state --state NEW -p TCP --dport 60030 -j ACCEPT(新的目標端口 60030被映射到/etc/services中的某個服務)。 定位安裝的軟件包和變量數據 遵循Linux社區所提出的打包(packaging)忠告是一個好主意--這些忠告有助於防止/opt中的代碼和/var中的應用程序數據出現混亂。這些忠告建議將提供商與軟件包名稱包含在存放代碼和數據的位置。作為說明,考慮下面的示例。 假設IBM開發了一個名為“abc”的應用程序。那個軟件包理論上應該安裝在/opt/ibm/abc中。相關的數據應該位於/var/opt/ibm/abc中,而不是簡單的位於/var中。 測試 將一個新產品移植到新的平台,需要對那個產品進行詳盡的測試。需要特別注意的方面包括進程間通信、打包、系統間通信(AIX與Linux,或者Solaris與Linux之間的客戶機-服務器)、一致存儲(出於字節次序的因素)、數據變換格式,等等。 外部文檔可能會發生變化,所以應該進行徹底的文檔復查。 移植到Linux測試用例需要依照開發工作適當地階段化。在開始進行完全產品測試之前,應該先測試一些可交付使用的中間產品。這將幫助您在產品開發的早期階段找出問題。(要深入了解高效通用測試方法,請參閱參考資料部分關於XP的參考文獻。) 結束語 在本文中我簡單涉及了移植的不同階段,包括針對不同OS范圍的設計選擇、創建合適的目錄結構、創建構建系統、完成代碼修改,以及測試。我也強調了那些需要集中精力關注的地方,比如信號、共享內存、互斥體和條件變量、線程以及特定於體系結構的變化。本文基於在向Linux 2.6-based 系統中移植大型的多線程應用程序時所獲得的實際經驗,我希望這個清單能幫助您節省時間和精力。 每次移植的細節都會發生變化,但是我所列舉的基本概念(結合下面的參考文獻中的材料)將使您更容易地完成移植的過程。
來源:賽迪網