将Win32 C/C++应用程序迁移到Linux-进程、线程和共享内存 -------- 转
本文的内容是 Win32 API(特别是进程、线程和共享内存服务)到 POWER 上 Linux 的映射。本文可以帮助您确定哪种映射服务最适合您的需要。作者向您详细介绍了他在移植 Win32 C/C++ 应用程序时遇到的 API 映射。
概述
有很多方式可以将 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))
。这些基本上与构造函数及析构函数相同,可以替代 glibc 库中的 _init 和 _fini 函数。
这些函数的 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() |
sleep 服务
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 的一个参考。
特别声明
IBM、eServer 和 pSeries 是 IBM Corporation 在美国和/或其它国家或地区的商标。
UNIX 是 The Open Group 在美国和其它国家或地区的注册商标。
Microsoft 和 Windows 是 Microsoft Corporation 在美国和/或其它国家或地区的商标或注册商标。
所有其他商标和注册商标是它们相应公司的财产。
此出版物/说明是在美国完成的。IBM 可能不在其他国家或地区提供在此讨论的产品、程序、服务或特性,而且信息可能会不加声明地加以修改。有关您当前所在区域的产品、程序、服务和特性的信息,请向您当地的 IBM 代表咨询。任何对 IBM 产品、程序、服务或者特性的引用并非意在明示或暗示只能使用 IBM 的产品、程序、服务或者特性。只要不侵犯 IBM 的知识产权,任何同等功能的产品、程序、服务或特性,都可以代替 IBM 产品、程序、服务或特性。
涉及非 IBM 产品的信息可从这些产品的供应商、其出版说明或其他可公开获得的资料中获取,并不构成 IBM 对此产品的认可。非 IBM 价目及性能数字资源取自可公开获得的信息,包括供应商的声明和供应商的全球主页。 IBM 没有对这些产品进行测试,也无法确认其性能的精确性、兼容性或任何其他关于非 IBM 产品的声明。有关非 IBM 产品性能的问题应当向这些产品的供应商提出。
有关非 IBM 产品性能的问题应当向这些产品的供应商提出。IBM 公司可能已拥有或正在申请与本说明中描述的内容有关的各项专利。提供本说明并未授予用户使用这些专利的任何许可。您可以用书面方式将许可查询寄往: IBM Director of Licensing IBM Corporation North Castle Drive Armonk, NY 10504-1785 U.S.A。所有关于 IBM 未来方向或意向的声明都可随时更改或收回,而不另行通知,它们仅仅表示了目标和意愿而已。联系您本地的 IBM 办公人员或者 IBM 授权的转销商,以获得特定的 Statement of General Direction 的全文。
本说明中所包含的信息没有提交给任何正式的 IBM 测试,而只是“按原样”发布。虽然 IBM 可能为了其在特定条件下的精确性而已经对每个条目进行了检查,但不保证在其他地方可以获得相同的或者类似的结果。使用此信息或者实现这里所描述的任何技术是客户的责任,取决于客户评价并集成它们到客户的操作环境的能力。尝试为他们自己的环境而修改这些技术的客户,这样做所带来的风险由他们自行承担。
参考资料
- 您可以参阅本文在 developerWorks 全球站点上的 英文原文。
作者简介 Nam Keung 是一名高级程序员,他曾致力于 AIX 通信开发、AIX 多媒体、SOM/DSOM 开发和 Java 性能方面的工作。他目前的工作包括帮助 ISV 进行应用程序设计、部署应用程序、性能调优和关于 pSeries 平台的教育。他从 1987 年起就是 IBM 的程序员了。您可以通过 [email protected] 与 Nam 联系。 |
|