當前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__ ( " lockn" " cmpxchgl %2,%1n" " sete %0n" : "=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 10pthread_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非常困難。
在某個線程中改變用戶標識,不能將此變化應用於進程中的所有線程。
簡言之,每個線程看起來都像是一個單獨的進程(其行為的某些方面也是如此)。