Windows IPC 应用移植到 Linux,第1部分:进程和线程 - Go语言中文社区

Windows IPC 应用移植到 Linux,第1部分:进程和线程


内容

简介:  随着开发者将原本普遍的 Windows® 应用迁移到 Linux™ 平台,正在进行的向开源迁移的浪潮有可能引发极大的 移植问题。这个由三部分构成的系列文章提供一个映射指南,并附有例子,能够简化从 Windows 到 Linux 的转变。第 1 部分介绍了进程和线程。

当前,很多全球商务和服务都正在趋于开源 —— 业界的所有主要参与者都在争取实现此目标。这一趋势催生了一个重要的 迁移模式:为不同平台(Windows、OS2、Solaris 等)维持的现有产品将被移植到开放源代码的 Linux 平台。

很多应用程序在设计时并未考虑到需要将它们移植到 Linux。这有可能使移植成为一件痛苦的事情,但并非绝对如此。 本系列文章的目的是,帮助您将涉及到 IPC 和线程原语的复杂应用程序从 Windows 迁移到 Linux。我们与您分享迁移 这些关键应用程序的经验,包括要求线程同步的多线程应用程序以及要求进程间同步的多进程应用程序。

简言之,可以将此系列文章看作是一个映射文档 —— 它提供了与线程、进程和进程间通信元素(互斥体、信号量等等)相关 的各种 Windows 调用到 Linux 调用的映射。我们将那些映射分为三个部分:

  • 第 1 部分涉及的是进程和线程。
  • 第 2 部分处理的是信号量与事件。
  • 第 3 部分涵盖了信号量、关键区域和等待函数。

进程

Windows 中和 Linux 中的基本执行单位是不同的。在 Windows 中,线程是基本执行单位,进程是一个容纳线程的容器。

在 Linux 中,基本执行单位是进程。Windows API 所提供的功能可以直接映射到 Linux 系统调用:

表 1. 进程映射

WindowsLinux类别
CreateProcess() 
CreateProcessAsUser()
fork() 
setuid() 
exec() 
可映射
TerminateProcess()kill()可映射
SetThreadpriority() 
GetThreadPriority()
Setpriority() 
getPriority()
可映射
GetCurrentProcessID()getpid()可映射
Exitprocess()exit()可映射
Waitforsingleobject() 
Waitformultipleobject()
GetExitCodeProcess()
waitpid() 
Using Sys V semaphores, Waitforsingleobject/multipleobject 
不能实现
与上下文相关
GetEnvironmentVariable 
SetEnvironmentVariable
getenv() 
setenv()
可映射

“类别”一列(解释了本文中所使用的分类结构)表明了 Windows 结构是否 可映射 或者 与上下文相关

  • 如果可映射,则 Windows 结构可以映射到特定的 Linux 结构(需要仔细检查类型、参数、返回代码等)。Windows 和 Linux 结构都提供了类似的功能。
  • 如果是与上下文相关,则 Linux 中可能有(也可能没有)相应于给定的 Windows 结构的结构,或者 Linux 可能有不只一个提供 类似功能的结构。无论是哪种情况,都要根据应用程序上下文才能确定要使用哪个特定的 Linux 结构。

创建进程

在 Windows 中,您可以使用 CreateProcess() 来创建一个新的进程。 CreateProcess() 函数创建一个新的进程及其主线程,如下:

BOOL CreateProcess(
 LPCTSTR lpApplicationName,                  // name of executable module
  LPTSTR lpCommandLine,                      // command line string
  LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD
  LPSECURITY_ATTRIBUTES lpThreadAttributes,  // SD
  BOOL bInheritHandles,                      // handle inheritance option
  DWORD dwCreationFlags,                     // creation flags
  LPVOID lpEnvironment,                      // new environment block
  LPCTSTR lpCurrentDirectory,                // current directory name
  LPSTARTUPINFO lpStartupInfo,               // startup information
  LPPROCESS_INFORMATION lpProcessInformation // process information
)

bInheritHandles 确定了子进程是否要继承父进程的句柄。lpApplicationName 和 lpCommandLine 给出了将要被 启动的进程的名称与路径。lpEnvironment 定义了进程可使用的环境变量。

在 Linux 中,exec* 家族函数使用一个新的进程映像取代当前进程映像(如下所示):

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int  execle(const  char  *path,  const  char  *arg  , ..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

exec* 的这些版本只是内核函数 execve() (int execve(const char *filename, char *const argv [], char *const envp[])) 的各种调用接口。在这里,argv 是包含有参数 list 的指针,envp 是包含有环境变量列表(主要是 key=value 对)的指针。

它必须与 fork() 命令一起使用,所以父进程和子进程都在运行: pid_t fork(void)fork() 会创建 一个子进程,与父进程相比只是 PID 和 PPID 不同;实际上,资源利用设为 0。

默认情况下,exec() 继承父进程的组和用户 ID,这就使得它会依赖于父进程。可以使用 以下方法来改变:

  • 设置指定程序文件的 set-uid 和 set-gid 位
  • 使用 setpgid() 和 setuid() 系统调用

CreateProcessAsUser() 函数与 CreateProcess() 类似,只是 新进程是在用户通过 hToken 参数描述的安全上下文中运行。在 Linux 中,没有与此函数惟一对应的函数,但是可以使用下面的逻辑来实现对它的复制:

  • 使用 fork() 创建一个具有新的 PID 的子进程
  • 使用 setuid() 切换到那个新的 PID
  • 使用 exec() 将现有进程改变为将要执行的进程

终止进程

在 Windows 中,您可以使用 TerminateProcess() 强制终止一个运行中的进程。

BOOL TerminateProcess(
  HANDLE hProcess, // handle to the process
  UINT uExitCode   // exit code for the process
);

这个函数终止运行中的进程及其相关线程。只是在非常极端的场合才会使用这个函数。

在 Linux 中,您可以使用 kill() 来强行杀死一个进程: int kill(pid_t pid, int sig)。这个系统调用会终止 id 为 PID 的进程。 您也可以使用它向任何组或者进程发出信号。

使用等待函数

在子进程依赖于父进程的情况下,您可以在父进程中使用等待函数来等待子进程的终止。在 Windows 中,您可以使用WaitForSingleObject() 函数调用来实现此功能。

您可以使用 WaitForMultipleObject() 函数来等待多个对象。

DWORD WaitForMultipleObjects(
  DWORD nCount,             // number of handles in array
  CONST HANDLE *lpHandles,  // object-handle array
  BOOL bWaitAll,            // wait option
  DWORD dwMilliseconds      // time-out interval
);

您可以向对象句柄数组(object-handle array)中填充很多需要等待的对象。根据 bWaitALL 选项,您既可以等待所有对象被信号通知,也可以等待其中任意一个被信号通知。

在这两个函数中,如果您想等待有限的一段时间,则可以在第二个参数中指定时间间隔。如果您想无限制等待,那么使用 INFINITE作为 dwMilliseconds 的值。将 dwMilliseconds 设置为 0 则只是检测对象的状态并返回。

在 Linux 中,如果您希望无限期等待进程被杀死,则可以使用 waitpid()。在 Linux 中,使用 waitpid() 调用无法等待限定的时间。

在这段代码中:pid_t waitpid(pid_t pid, int *status, int options)waitpid() 会无限期等待子进程的终止。在 Windows 和 Linux 中,等待函数 会挂起当前进程的执行,直到它完成等待,不过,在 Windows 中可以选择在指定的时间后退出。使用 System V 信号量,您可以实现类似于 WaitForSingleObject() 和 WaitForMultipleObject() 的限时等待或者 NO WAIT 功能,在本系列的第 2 部分中将讨论此内容。本系列的第 3 部分将深入讨论等待函数。

退出进程

退出进程指的是优雅(graceful)地退出进程,并完成适当的清除工作。在 Windows 中,您可以使用 ExitProcess() 来执行此操作。

VOID ExitProcess(
  UINT uExitCode   // exit code for all threads
);

ExitProcess() 是在进程结束处执行的方法。这个函数能够干净地停止进程。包括调用所有 链接到的动态链接库(DLL)的入口点函数,给出一个值,指出这个进程正在解除那个 DLL 的链接。

Linux 中与 ExitProcess() 相对应的是 exit()void exit(int status);

exit() 函数会令程序正常终止,并将 &0377 状态值返回给父进程。 C 语言标准 规定了两个定义(EXIT_SUCCESS 和EXIT_FAILURE), 可以被传递到状态参数,以说明终止成功或者不成功。

环境变量

每个进程都拥有关联到它的一组环境,其中主要是 name=value 对,指明进程可以访问的各种环境变量。尽管我们可以在 创建进程时指定环境,不过也有特定函数可以在进程创建后设置和获得环境变量。

在 Windows 中,您可以使用 GetEnvironmentVariable() 和 SetEnvironmentVariable() 来获得和设置环境变量。

DWORD GetEnvironmentVariable(
  LPCTSTR lpName,  // environment variable name
  LPTSTR lpBuffer, // buffer for variable value
  DWORD nSize      // size of buffer
);

如果成功,则此函数返回值缓存的大小,如果指定的名称并不是一个合法的环境变量名,则返回 0。 SetEnvironmentVariable() 函数为当前进程设置指定的环境变量的内容。

BOOL SetEnvironmentVariable(
  LPCTSTR lpName,  // environment variable name
  LPCTSTR lpValue  // new value for variable
);

如果函数成功,则返回值非零。如果函数失败,则返回值为零。

在 Linux 中,getenv() 和 setenv() 系统调用提供了相应的 功能。

char *getenv(const char *name);
int setenv(const char *name, const char *value, int overwrite);

getenv() 函数会在环境列表中搜索与名称字符串相匹配的字符串。这个函数会返回一个 指向环境中的值的指针,或者如果不匹配则返回 NULL。setenv() 函数将变量名和值添加 到环境中,如果那个名称并不存在。如果环境中已经存在那个名称,而且如果 overwrite 非零,则它的值会被修改为 value。如果 overwrite 为零,则 name 的值不会被改变。 如果成功,则 setenv() 会返回零,如果环境中空间不足,则返回 -1。

例子

下面的例子解释了我们在本节中讨论的内容。


清单 1. Windows 进程代码
				
//Sample Application that explain process concepts
//Parameters Declaration/Definition
int TimetoWait;
STARTUPINFO si;
PROCESS_INFORMATION pi;
LPTSTR lpszCurrValue,LPTSTR lpszVariable;
TCHAR tchBuf[BUFSIZE];
BOOL fSuccess;
if(argc > 2)
{
    printf("InvalidArgument");
    ExitProcess(1); //Failure
}
//Get and display an  environment variable PATH
lpszCurrValue = ((GetEnvironmentVariable("PATH",tchBuf, BUFSIZE) > 0) ? tchBuf : NULL);
lpszVariable = lpszCurrValue;
//Display the environment variable
while (*lpszVariable)
    putchar(*lpszVariable++);
putchar('n');
//Initialise si and pi
ZeroMemory( &si, sizeof(si) );
si.cb = sizeof(si);
ZeroMemory( &pi, sizeof(pi) );
//Create a childProcess
if( !CreateProcess( NULL,             // No module name (use command line).
                    "SomeProcess",    // Command line.
                    NULL,             // Process handle not inheritable.
                    NULL,             // Thread handle not inheritable.
                    FALSE,            // Set handle inheritance to FALSE.
                    0,                // No creation flags.
                    NULL,             // Use parent's environment block.
                    NULL,             // Use parent's starting directory.
                    &si,              // Pointer to STARTUPINFO structure.
                    &pi )             // Pointer to PROCESS_INFORMATION structure.
                        )
{
    printf( "CreateProcess failed." );
}
// Wait until child process exits.
if(argc == 2)
{
    TIMEOUT = atoi(argv[1]);
    ret = WaitForSingleObject( pi.hProcess, TIMEOUT );
    if(ret == WAIT_TIMEOUT)
    {
        TerminateProcess(pi.hProcess);
    }
}
else
{
    WaitForSingleObject( pi.hProcess, INFINITE );
    ...
}
    ExitProcess(0); //Success


清单 2. 相应的 Linux 进程代码
				
#include <stdlib.h>
int main(int argc,char *argv[])
{
    //Parameters Declaration/Definition
    char PathName[255];
    char *Argptr[20];
    int rc;
    char *EnvValue,*lpszVariable;
    if(argc > 1)
    {
        printf(" Wrong parameters !!");
        exit(EXIT_FAILURE);
    }
    //Get and display an  environment variable PATH
    EnvValue = getenv("PATH");
    if(EnvValue == NULL)
    {
        printf("Invalid environment variable passed as param !!");
    }else
    {
        lpszVariable = EnvValue;
        while (*lpszVariable)
            putchar(*lpszVariable++);
        putchar('n');
    }
    rc = fork(); //variable rc's value on success would be process ID in the parent
                 //process, and 0 in the child's thread of execution.
    switch(rc)
    {
        case -1:
            printf("Fork() function failed !!");
            ret = -1;
            break;
        case 0:
            printf("Child process...");
            setpgid(0,0);  //Change the parent grp ID to 0
        ret = execv(PathName,Argptr); // there are other flavours of exec available,
                                      // u can use any of them based on the arguments.
        if(ret == -1)
        {
            kill(getpid(),0);
        }
        break;
         default:
             // infinitely waits for child process to die
             Waitpid(rc,&status,WNOHANG);
             //Note RC will have PID returned since this is parent process.
             break;
    }
    exit(EXIT_SUCCESS);
}

线程

在 Windows 中,线程是基本的执行单位。在进程的上下文中会有一个或多个线程在运行。调度代码在内核中实现。 没有单独的“调度器(scheduler)”模块或例程。

Linux 内核使用的是进程模型,而不是线程模型。Linux 内核提供了一个轻量级进程框架来创建线程;实际的线程在用户 空间中实现。在 Linux 中有多种可用的线程库(LinuxThreads、NGPT、NPTL 等等)。本文中的资料基于 LinuxThreads 库,不过这里的资料也适用于 Red Hat 的 Native POSIX Threading Library(NPTL)。

本节描述 Windows 和 Linux 中的线程。内容涵盖了创建线程、设置其属性以及修改其优先级。

表 2. 线程映射

WindowsLinux类别
CreateThreadpthread_create 
pthread_attr_init 
pthread_attr_setstacksize 
pthread_attr_destroy
可映射
ThreadExitpthread_exit可映射
WaitForSingleObjectpthread_join 
pthread_attr_setdetachstate 
pthread_detach
可映射
SetPriorityClass 
SetThreadPriority
setpriority 
sched_setscheduler 
sched_setparam 

pthread_setschedparam 
pthread_setschedpolicy 
pthread_attr_setschedparam 
pthread_attr_setschedpolicy
与上下文相关

创建线程

在 Windows 中,您可以使用 CreateThread() 来创建线程,创建的线程在调用进程的 虚拟地址空间中运行。

HANDLE CreateThread(
  LPSECURITY_ATTRIBUTES lpThreadAttributes,     // SD
  SIZE_T dwStackSize,                           // initial stack size
  LPTHREAD_START_ROUTINE lpStartAddress,        // thread function
  LPVOID lpParameter,                           // thread argument
  DWORD dwCreationFlags,                        // creation option
  LPDWORD lpThreadId                            // thread identifier
);

lpThreadAttributes 是指向线程属性的指针,决定了线程句柄是否能由子进程继承。

Linux 使用 pthread 库调用 pthread_create() 来派生线程:

int pthread_create (pthread_t *thread_id, pthread_attr_t *threadAttr,
                    void * (*start_address)(void *), void * arg);

注意:在 Windows 中,受可用虚拟内存的限制,一个进程可以创建的线程数目是有限的。默认情况下, 每个线程有一兆栈空间。因此,您最多可以创建 2,028 个线程。如果您减小默认栈大小,那么可以创建更多线程。 在 Linux 中,使用 ULIMIT -a(limits for all users)可以获得每个用户可以创建的线程 的最大数目,可以使用 ULIMIT -u 来修改它,不过只有在登录时才可以这样做。 /usr/Include/limit.h 和 ulimit.h 下的头文件定义了这些内容。您可以修改它们并重新编译内核,以使其永久生效。 对于 POSIX 线程限制而言,local_lim.h 中定义的 THREAD_THREADS_MAX 宏定义了数目的上限。

指定线程函数

CreateThread() 中的 lpStartAddress 参数是刚 创建的线程要执行的函数的地址。

pthread_create() 库调用的 start_address 参数是 刚创建的线程要执行的函数的地址。

传递给线程函数的参数

在 Windows 中,系统调用 CreateThread() 的参数 lpParameter 指定了要传递给刚创建的线程的参数。它指明了将要传递给新线程的数据条目的地址。

在 Linux 中,库调用 pthread_create() 的参数 arg 指定了将要传递给新线程的参数。

设置栈大小

在 Windows 中,CreateThread() 的参数 dwStackSize 是将要分配给新 线程的以字节为单位的栈大小。栈大小应该是 4 KB 的非零整数倍,最小为 8 KB。

在 Linux 中,栈大小在线程属性对象中设置;也就是说,将类型为 pthread_attr_t 的 参数 threadAttr 传递给库调用pthread_create()。 在设置任何属性之前,需要通过调用 pthread_attr_init() 来初始化这个对象。 使用调用pthread_attr_destroy() 来销毁属性对象:

int pthread_attr_init(pthread_attr_t *threadAttr);
int pthread_attr_destroy(pthread_attr_t *threadAttr);

注意,所有 pthread_attr_setxxxx 调用都有与 pthread_xxxx 调用(如果有)类似的功能,只是您只能在线程创建之前使用pthread_attr_xxxx,来更新将 要作为参数传递给 pthread_create 的属性对象。同时,您在创建线程之后的任意时候都可以使用pthread_xxxx

使用调用 pthread_attr_setstacksize() 来设置栈大小: int pthread_attr_setstacksize(pthread_attr_t *threadAttr, int stack_size);

退出线程

在 Windows 中,系统调用 ExitThread() 会终止线程。 dwExitCode 是线程的返回值,另一个线程通过调用 GetExitCodeThread() 就可以得到它。

VOID ExitThread(
  DWORD dwExitCode   // exit code for this thread
);

Linux 中与此相对应的是库调用 pthread_exit()。 retval 是线程的返回值,可以在另一个线程中通过调用 pthread_join() 来获得它: int pthread_exit(void* retval);

线程状态

在 Windows 中,没有保持关于线程终止的显式线程状态。不过,WaitForSingleObject() 让线程能够显式地等待进程中某个指定的或者非指定的线程终止。

在 Linux 中,默认以可连接(joinable)的状态创建线程。在可连接状态中,另一个线程可以同步这个线程的终止, 使用函数pthread_join() 来重新获得其终止代码。可连接的线程只有在被连接后 才释放线程资源。

Windows 使用 WaitForSingleObject() 来等待某个线程终止:

DWORD WaitForSingleObject(
  HANDLE hHandle,
  DWORD dwMilliseconds
);

其中:

  • hHandle 是指向线程句柄的指针。
  • dwMilliseconds 是以毫秒为单位的超时值。 如果这个值被设置为 INFINITE,则它会无限期地阻塞进行调用的线程/进程。

Linux 使用 pthread_join() 来完成同样的功能: int pthread_join(pthread_t *thread, void **thread_return);

在分离的状态中,线程终止后线程资源会立即被释放。通过对线程属性对象调用 pthread_attr_setdetachstate() 可以设置分离状态: int pthread_attr_setdetachstate (pthread_attr_t *attr, int detachstate);。 以可连接状态创建的线程,稍后可以被转为分离状态,方法是使用 pthread_detach() 调用:int pthread_detach (pthread_t id);

改变优先级

在 Windows 中,线程的优先级由其进程的优先级等级以及进程优先级等级中的线程优先级层次决定。 在 Linux 中,线程本身就是一个执行单位,有其自己的优先级。它与其进程的优先级没有依赖关系。

在 Windows 中,您可以使用 SetPriorityClass() 来设置特定进程的优先级等级:

BOOL SetPriorityClass(
  HANDLE hProcess,         // handle to the process
  DWORD dwPriorityClass    // Priority class
);

dwPriorityClass 是进程的优先级等级,它可以设置为下列值中的任意一个:

  • IDLE_PRIORITY_CLASS
  • BELOW_NORMAL_PRIORITY_CLASS
  • NORMAL_PRIORITY_CLASS
  • ABOVE_NORMAL_PRIORITY_CLASS
  • HIGH_PRIORITY_CLASS
  • REALTIME_PRIORITY_CLASS

一旦设置了进程的优先级等级,就可以使用 SetThreadPriority() 在进程的 优先级等级内部设置线程的优先级层次:

BOOL SetThreadPriority(
  HANDLE hThread,
  int nPriority
);

nPriority 是线程的优先级值,它被设置为下列之一;

  • THREAD_PRIORITY_ABOVE_NORMAL 将优先级设置为比优先级等级高 1 级。
  • THREAD_PRIORITY_BELOW_NORMAL 将优先级设置为比优先级等级低 1 级。
  • THREAD_PRIORITY_HIGHEST 将优先级设置为比优先级等级高 2 级。
  • THREAD_PRIORITY_IDLE 为IDLE_PRIORITY_CLASSBELOW_NORMAL_PRIORITY_CLASS 版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
    原文链接:https://blog.csdn.net/user_920/article/details/8077945
    站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2021-06-14 06:19:08
  • 阅读 ( 869 )
  • 分类:Linux

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢