分类:
2009-10-26 10:02:42
将 Win32 C/C++ 应用程序迁移到 POWER 上的 Linux,第 1 部分: 进程、线程和共享内存服务 |
|
级别: 初级
Nam Keung (), 高级程序员
Chakarat Skawratananond (), pSeries Linux 技术顾问
2005 年 1 月 06 日
本文的内容是 Win32 API(特别是进程、线程和共享内存服务)到 POWER 上 Linux 的映射。本文可以帮助您确定哪种映射服务最适合您的需要。作者向您详细介绍了他在移植 Win32 C/C++ 应用程序时遇到的 API 映射。
有很多方式可以将 Win32 C/C++ 应用程序移植和迁移到 pSeries 平台。您可以使用免费软件或者第三方工具来将 Win32 应用程序代码移到 Linux。在我们的方案中,我们决定使用一个可移植层来抽象系统 API 调用。可移植层将使我们 的应用程序具有以下优势:
由于 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))
。
这些基本上与构造函数及析构函数相同,可以替代 glibc 库中的 _init 和 _fini 函数。
这些函数的 C 原型是:
void __attribute__ ((constructor)) app_init(void); |
Win32 sample |
|
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 结构体的指针,由被创建的进程为其添加内容。
进程一旦启动,它将包含创建它的进程的句柄以及其他内容。
Win32 example |
在 Linux 中,进程 ID 是一个整数。Linux 中的搜索目录由 PATH 环境变量(exec_path_name)决定。
fork()
函数建立父进程的一个副本,包括父进程的数据空间、堆和栈。
execv()
子例程使用 exec_path_name 将调用进程当前环境传递给新的进程。
这个函数用一个由 exec_path_name 指定的新的进程映像替换当前的进程映像。新的映像构造自一个由 exec_path_name 指定的正规的、可执行的文件。由于调用的进程映像被新的进程映像所替换,所以没有任何返回。
Equivalent Linux code |
在 Win32 进程中,父进程和子进程可能需要单独访问子进程所继承的由某个句柄标识的对象。父进程可以 创建一个可访问而且可继承的副本句柄。Win32 示例代码使用下面的方法终止进程:
如果函数成功,则使用 TerminateThread 函数来释放同一进程上的主线程。然后使用 TerminateThread 函数 来无条件地使一个进程退出。它启动终止并立即返回。
Win32 sample code |
在 Linux 中,使用 kill 子例程发送 SIGTERM 信号来终止特定进程(processId)。 然后调用设置 WNOHANG 位的 waitpid 子例程。这将检查特定的进程并终止。
Equivalent Linux code |
Win32 OpenProcess 返回特定进程(processId)的句柄。如果函数成功,则 GetExitCodeProcess 将获得特定进程的状态,并检查进程的状态是否是 STILL_ACTIVE。
Win 32 sample |
在 Linux 中,使用 kill 子例程发送通过
Signal
参数指定的信号给
由
Process
参数(processId)指定的特定进程。Signal 参数是一个
null 值,会执行错误检查,但不发送信号。
Equivalent Linux code |
|
线程 是系统分配 CPU 时间的基本单位;当等待调度时,每个线程保持信息来保存它的“上下文”。每个 线程都可以执行程序代码的任何部分,并共享进程的全局变量。
构建于
clone()
系统调用之上的 LinuxThreads 是一个 pthreads 兼容线程系统。
因为线程由内核来调度,所以 LinuxThreads 支持阻塞的 I/O 操作和多处理器。不过,每个线程实际上是一个
Linux 进程,所以一个程序可以拥有的线程数目受内核所允许的进程总数的限制。Linux 内核没有为线程同步提供
系统调用。Linux Threads 库提供了另外的代码来支持对互斥和条件变量的操作(使用管道来阻塞线程)。
对有外加 LinuxThreads 的信号处理来说,每个线程都会继承信号处理器(如果派生这个线程的父进程 注册了一个信号处理器的话。只有在 Linux Kernel 2.6 和更高版本中支持的新特性才会包含 POSIX 线程支持,比如 用于 Linux 的 Native POSIX Thread Library(NPTL)。
线程同步、等待函数、线程本地存储以及初始化和终止抽象是线程模型的重要部分。在这些之下,线程服务只 负责:
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。
在 Win32 中,线程的栈由进程的内存空间自动分配。系统根据需要增加栈的大小,并在线程终止时释放它。
在 Linux 中,栈的大小在 pthread 属性对象中设置;pthread_attr_t 传递给库调用
pthread_create()
。
Win32 sample |
在 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); |
在 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 函数挂起 当前线程,直到下面事件之一发生:
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 上增加共享内存的一个示例:
echo 524288000 > /proc/sys/kernel/shmmax |
下面是创建共享内存资源的 Win32 示例代码,以及相对应的 Linux nmap 实现。
Win32 sample code |
为销毁共享内存资源,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 的一个参考。