將 Win32 C/C++ 應用程式遷移到 POWER 上的 Linux,第 1 部分: 程序、執行緒和共享記憶體服務 (轉載)
概述
有很多方式可以將 Win32 C/C++ 應用程式移植和遷移到 pSeries 平臺。您可以使用免費軟體或者第三方工具來將 Win32 應用程式程式碼移到 Linux。在我們的方案中,我們決定使用一個可移植層來抽象系統 API 呼叫。可移植層將使我們的應用程式具有以下優勢:
- 與硬體無關。
- 與作業系統無關。
- 與作業系統上版本與版本間的變化無關。
- 與作業系統 API 風格及錯誤程式碼無關。
- 能夠統一地在對 OS 的呼叫中置入效能和 RAS 鉤子(hook)。
由於 Windows 環境與 pSeries Linux 環境有很大區別,所以進行跨 UNIX 平臺的移植比進行從 Win32 平臺到 UNIX 平臺的移植要容易得多。這是可以想到的,因為很多 UNIX 系統都使用共同的設計理念,在應用程式層有非常多的類似之處。不過,Win32 API 在移植到 Linux 時是受限的。本文剖析了由於 Linux 和 Win32 之間設計的不同而引發的問題。
初始化和終止
在 Win2K/NT 上,DLL 的初始化和終止入口點是 _DLL_InitTerm 函式。當每個新的程序獲得對 DLL 的訪問時,這個函式初始化 DLL 所必需的環境。當每個新的程序釋放其對 DLL 的訪問時,這個函式為那個環境終止 DLL。當您連結到那個 DLL 時,這個函式會自動地被呼叫。對應用程式而言,_DLL_InitTerm 函式中包含了另外一個初始化和終止例程。
在 Linux 上,GCC 有一個擴充套件,允許指定當可執行檔案或者包含它的共享物件啟動或停止時應該呼叫某個函式。語法是 __attribute__((constructor))
或 __attribute__((destructor))
這些函式的 C 原型是:
|
|
程序服務
Win32 程序模型沒有與 fork()
和 exec()
直接相當的函式。在 Linux 中使用 fork()
呼叫總是會繼承所有內容,與此不同, CreateProcess()
接收用於控制程序建立方面的顯式引數,比如檔案控制代碼繼承。
CreateProcess API 建立一個包含有一個或多個在此程序的上下文中執行的執行緒的新程序,子程序與父程序之間沒有關係。在 Windows NT/2000/XP 上,返回的程序 ID 是 Win32 程序 ID。在 Windows ME 上,返回的程序 ID 是除去了高位(high-order bit)的 Win32 程序 ID。當建立的程序終止時,所有與此程序相關的資料都從記憶體中刪除。
為了在 Linux 中建立一個新的程序, fork()
系統呼叫會複製那個程序。新程序建立後,父程序和子程序的關係就會自動建立,子程序預設繼承父程序的所有屬性。Linux 使用一個不帶任何引數的呼叫建立新的程序。 fork()
將子程序的程序 ID 返回給父程序,而不返回給子程序任何內容。
Win32 程序同時使用控制代碼和程序 ID 來標識,而 Linux 沒有程序控制代碼。
Win32 | Linux |
CreateProcess | fork() execv() |
TerminateProcess | kill |
ExitProcess() | exit() |
GetCommandLine | argv[] |
GetCurrentProcessId | getpid |
KillTimer | alarm(0) |
SetEnvironmentVariable | putenv |
GetEnvironmentVariable | getenv |
GetExitCodeProcess | waitpid |
建立程序服務
在 Win32 中, CreateProcess()
的第一個引數指定要執行的程式,第二個引數給出命令列引數。CreateProcess 將其他程序引數作為引數。倒數第二個引數是一個指向某個 STARTUPINFORMATION 結構體的指標,它為程序指定了標準的裝置以及其他關於程序環境的啟動資訊。在將 STARTUPINFORMATION 結構體的地址傳給 CreateProcess 以重定向程序的標準輸入、標準輸出和標準錯誤之前,您需要設定這個結構體的 hStdin、hStdout 和 hStderr 成員。最後一個引數是一個指向某個 PROCESSINFORMATION 結構體的指標,由被建立的程序為其新增內容。程序一旦啟動,它將包含建立它的程序的控制代碼以及其他內容。
|
在 Linux 中,程序 ID 是一個整數。Linux 中的搜尋目錄由 PATH 環境變數(exec_path_name)決定。 fork()
函式建立父程序的一個副本,包括父程序的資料空間、堆和棧。 execv()
子例程使用 exec_path_name 將呼叫程序當前環境傳遞給新的程序。
這個函式用一個由 exec_path_name 指定的新的程序映像替換當前的程序映像。新的映像構造自一個由 exec_path_name 指定的正規的、可執行的檔案。由於呼叫的程序映像被新的程序映像所替換,所以沒有任何返回。
|
終止程序服務
在 Win32 程序中,父程序和子程序可能需要單獨訪問子程序所繼承的由某個控制代碼標識的物件。父程序可以建立一個可訪問而且可繼承的副本控制代碼。Win32 示例程式碼使用下面的方法終止程序:
- 使用 OpenProcess 來獲得指定程序的控制代碼。
- 使用 GetCurrentProcess 獲得其自己的控制代碼。
- 使用 DuplicateHandle 來獲得一個來自同一物件的控制代碼作為原始控制代碼。
如果函式成功,則使用 TerminateThread 函式來釋放同一程序上的主執行緒。然後使用 TerminateThread 函式來無條件地使一個程序退出。它啟動終止並立即返回。
|
在 Linux 中,使用 kill 子例程傳送 SIGTERM 訊號來終止特定程序(processId)。然後呼叫設定 WNOHANG 位的 waitpid 子例程。這將檢查特定的程序並終止。
|
程序依然存在服務
Win32 OpenProcess 返回特定程序(processId)的控制代碼。如果函式成功,則 GetExitCodeProcess 將獲得特定程序的狀態,並檢查程序的狀態是否是 STILL_ACTIVE。
|
在 Linux 中,使用 kill 子例程傳送通過 Signal
引數指定的訊號給由 Process
引數(processId)指定的特定程序。Signal 引數是一個 null 值,會執行錯誤檢查,但不傳送訊號。
|
執行緒模型
執行緒 是系統分配 CPU 時間的基本單位;當等待排程時,每個執行緒保持資訊來儲存它的“上下文”。每個執行緒都可以執行程式程式碼的任何部分,並共享程序的全域性變數。
構建於 clone()
系統呼叫之上的 LinuxThreads 是一個 pthreads 相容執行緒系統。因為執行緒由核心來排程,所以 LinuxThreads 支援阻塞的 I/O 操作和多處理器。不過,每個執行緒實際上是一個 Linux 程序,所以一個程式可以擁有的執行緒數目受核心所允許的程序總數的限制。Linux 核心沒有為執行緒同步提供系統呼叫。Linux Threads 庫提供了另外的程式碼來支援對互斥和條件變數的操作(使用管道來阻塞執行緒)。
對有外加 LinuxThreads 的訊號處理來說,每個執行緒都會繼承訊號處理器(如果派生這個執行緒的父程序註冊了一個訊號處理器的話。只有在 Linux Kernel 2.6 和更高版本中支援的新特性才會包含 POSIX 執行緒支援,比如 用於 Linux 的 Native POSIX Thread Library(NPTL)。
執行緒同步、等待函式、執行緒本地儲存以及初始化和終止抽象是執行緒模型的重要部分。在這些之下,執行緒服務只負責:
- 新執行緒被建立,threadId 被返回。
- 通過呼叫 pthread_exit 函式可以終止當前的新執行緒。
Win32 | Linux |
_beginthread | pthread_attr_init pthread_attr_setstacksize pthread_create |
_endthread | pthread_exit |
TerminateThread | pthread_cancel |
GetCurrentThreadId | pthread_self |
執行緒建立
Win32 應用程式使用 C 執行期庫,而不使用 Create_Thread API。使用了 _beginthread 和 _endthread 例程。這些例程會考慮任何可重入性(reentrancy)和記憶體不足問題、執行緒本地儲存、初始化和終止抽象。
Linux 使用 pthread 庫呼叫 pthread_create()
來派生一個執行緒。
threadId 作為一個輸出引數返回。為建立一個新執行緒,要傳遞一組引數。當新執行緒被建立時,這些引數會執行一個函式。stacksize 用作新執行緒的棧的大小(以位元組為單位),當新執行緒開始執行時,實際的引數被傳遞給函式。
指定執行緒程式(函式)
進行建立的執行緒必須指定要執行的新執行緒的啟動函式的程式碼。啟動地址是 threadproc 函式(帶有一個單獨的引數,即 threadparam)的名稱。如果呼叫成功地建立了一個新執行緒,則返回 threadId。Win32 threadId 的型別定義是 HANDLE。Linux threadId 的型別定義是 pthread_t。
- threadproc
- 要執行的執行緒程式(函式)。它接收一個單獨的 void 引數。
- threadparam
- 執行緒開始執行時傳遞給它的引數。
設定棧大小
在 Win32 中,執行緒的棧由程序的記憶體空間自動分配。系統根據需要增加棧的大小,並在執行緒終止時釋放它。在 Linux 中,棧的大小在 pthread 屬性物件中設定;pthread_attr_t 傳遞給庫呼叫 pthread_create()
。
|
終止執行緒服務
在 Win32 中,一個執行緒可以使用 TerminateThread 函式終止另一個執行緒。不過,執行緒的棧和其他資源將不會被收回。如果執行緒終止自己,則這樣是可取的。在 Linux 中,pthread_cancel 可以終止由具體的 threadId 所標識的執行緒的執行。
Win32 | Linux |
TerminateThread((HANDLE *) threadId, 0); | pthread_cancel(threadId); |
執行緒狀態
在 Linux 中,執行緒預設建立為可合併(joinable)狀態。另一個執行緒可以使用 pthread_join()
同步執行緒的終止並重新獲得終止程式碼。可合併執行緒的執行緒資源只有在其被合併後才被釋放。
Win32 使用 WaitForSingleObject()
來等待執行緒終止。
Linux 使用 pthread_join 完成同樣的事情。
Win32 | Linux |
unsigned long rc; rc = (unsigned long) WaitForSingleObject (threadId, INIFITE); | unsigned long rc=0; rc = pthread_join(threadId, void **status); |
結束當前執行緒服務的執行
在 Win32 中,使用 _endthread()
來結束當前執行緒的執行。在 Linux 中,推薦使用 pthread_exit()
來退出一個執行緒,以避免顯式地呼叫 exit 例程。在 Linux 中,執行緒的返回值是 retval,可以由另一個執行緒呼叫 pthread_join()
來獲得它。
Win32 | Linux |
_endthread(); | pthread_exit(0); |
獲得當前執行緒 ID 服務
在 Win32 程序中,GetCurrentThreadId 函式獲得進行呼叫的執行緒的執行緒識別符號。Linux 使用 pthread_self()
函式來返回進行呼叫的執行緒的 ID。
Win32 | Linux |
GetCurrentThreadId() | pthread_self() |
Win32 | Equivalent Linux code |
Sleep (50) | struct timespec timeOut,remains; timeOut.tv_sec = 0; timeOut.tv_nsec = 500000000; /* 50 milliseconds */ nanosleep(&timeOut, &remains); |
Win32 SleepEx 函式掛起 當前執行緒,直到下面事件之一發生:
- 一個 I/O 完成回撥函式被呼叫。
- 一個非同步過程呼叫(asynchronous procedure call,APC)排隊到此執行緒。
- 最小超時時間間隔已經過去。
Linux 使用 sched_yield 完成同樣的事情。
Win32 | Linux |
SleepEx (0,0) | sched_yield() |
共享記憶體服務
共享記憶體允許多個程序將它們的部分虛地址對映到一個公用的記憶體區域。任何程序都可以向共享記憶體區域寫入資料,並且資料可以由其他程序讀取或修改。共享記憶體用於實現程序間通訊媒介。不過,共享記憶體不為使用它的程序提供任何訪問控制。使用共享記憶體時通常會同時使用“鎖”。
一個典型的使用情形是:
- 某個伺服器建立了一個共享記憶體區域,並建立了一個共享的鎖物件。
- 某個客戶機連線到伺服器所建立的共享記憶體區域。
- 客戶機和伺服器雙方都可以使用共享的鎖物件來獲得對共享記憶體區域的訪問。
- 客戶機和伺服器可以查詢共享記憶體區域的位置。
Win32 | Linux |
CreateFileMaping, OpenFileMapping | mmap shmget |
UnmapViewOfFile | munmap shmdt |
MapViewOfFile | mmap shmat |
建立共享記憶體資源
Win32 通過共享的記憶體對映檔案來建立共享記憶體資源。Linux 使用 shmget/mmap 函式通過直接將檔案資料合併入記憶體來訪問檔案。記憶體區域是已知的作為共享記憶體的段。
檔案和資料也可以在多個程序和執行緒之間共享。不過,這需要程序或執行緒之間同步,由應用程式來處理。
如果資源已經存在,則 CreateFileMapping()
重新初始化共享資源對於程序的約定。如果沒有足夠的空閒記憶體來處理錯誤的共享資源,此呼叫可能會失敗。 OpenFileMapping()
需要共享資源必須已經存在;這個呼叫只是請求對它的訪問。
在 Win32 中,CreateFileMapping 不允許您增加檔案大小,但是在 Linux 中不是這樣。在 Linux 中,如果資源已經存在,它將被重新初始化。它可能被銷燬並重新建立。Linux 建立可以通過名稱訪問的共享記憶體。 open()
系統呼叫確定對映是否可讀或可寫。傳遞給 mmap()
的引數必須不能與 open()
時請求的訪問相沖突。 mmap()
需要為對映提供檔案的大小(位元組數)。
對 32-位核心而言,有 4GB 虛地址空間。最前的 1 GB 用於裝置驅動程式。最後 1 GB 用於核心資料結構。中間的 2GB 可以用於共享記憶體。當前,POWER 上的 Linux 允許核心使用 4GB 虛地址空間,允許使用者應用程式使用最多 4GB 虛地址空間。
Win32 | Linux |
PAGE_READONLY | PROT_READ |
PAGE_READWRITE | (PROT_READ | PROT_WRITE) |
PAGE_NOACCESS | PROT_NONE |
PAGE_EXECUTE | PROT_EXEC |
PAGE_EXECUTE_READ | (PROT_EXEC |PROT_READ) |
PAGE_EXECUTE_READWRITE | (PROT_EXEC | PROT_READ | PROT_WRITE) |
要獲得 Linux 共享記憶體的分配,您可以檢視 /proc/sys/kernel 目錄下的 shmmax、shmmin 和 shmall。
在 Linux 上增加共享記憶體的一個示例:
|
下面是建立共享記憶體資源的 Win32 示例程式碼,以及相對應的 Linux nmap 實現。
|
刪除共享記憶體資源
為銷燬共享記憶體資源,munmap 子例程要取消被對映檔案區域的對映。munmap 子例程只是取消對 mmap 子例程的呼叫而建立的區域的對映。如果某個區域內的一個地址被 mmap 子例程取消對映,並且那個區域後來未被再次對映,那麼任何對那個地址的引用將導致給程序發出一個 SIGSEGV 訊號。
Win32 | 等價的 Linux 程式碼 |
UnmapViewOfFile(token->location); CloseHandle(token->hFileMapping); | munmap(token->location, token->nSize); close(token->nFileDes); remove(token->pFileName); free(token->pFileName); |
結束語
本文介紹了關於初始化和終止、程序、執行緒及共享記憶體服務從 Win32 API 到 POWER 上 Linux 的對映。這絕對沒有涵蓋所有的 API 對映,而且讀者只能將此資訊用作將 Win32 C/C++ 應用程式遷移到 POWER Linux 的一個參考。