将 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))。 这些基本上与构造函数及析构函数相同,可以替代 glibc 库中的 _init 和 _fini 函数。

这些函数的 C 原型是:

void __attribute__ ((constructor)) app_init(void);
void __attribute__ ((destructor)) app_fini(void);

				Win32 sample
_DLL_InitTerm(HMODULE modhandle, DWORD fdwReason, LPVOID lpvReserved)
{	
    WSADATA			Data;	
    switch (fdwReason)	
    {		
        case DLL_PROCESS_ATTACH:		
        if (_CRT_init() == -1)			
           return 0L;               
          /* start with initialization code */		
          app_init();		
          break;	
    case DLL_PROCESS_DETACH:		
         /* Start with termination code*/               
         app_fini();		
         _CRT_term();		
         break;      
    …..	
    default:		
          /* unexpected flag value - return error indication */		
            return 0UL;	
    }	return 1UL;		/* success */
}
      

进程服务

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
PROCESS_INFORMATION    procInfo;
STARTUPINFO	        startupInfo;
typedef DWORD          processId;
char                   *exec_path_name
char                   *_cmd_line;
        GetStartupInfo( &startupInfo );     // You must fill in this structure
if( CreateProcess( exec_path_name,  // specify the executable program
                       _cmd_line,   // the command line arguments
                       NULL,       // ignored in Linux
                       NULL,       // ignored in Linux
                       TRUE,       // ignored in Linux
                       DETACHED_PROCESS | HIGH_PRIORITY_CLASS,
                       NULL,       // ignored in Linux
                       NULL,       // ignored in Linux
               &startupInfo,
                    &procInfo))
        *processId = procInfo.dwProcessId;
else
{
        *processId = 0;
          return RC_PROCESS_NOT_CREATED;
}	
      

在 Linux 中,进程 ID 是一个整数。Linux 中的搜索目录由 PATH 环境变量(exec_path_name)决定。 fork() 函数建立父进程的一个副本,包括父进程的数据空间、堆和栈。 execv() 子例程使用 exec_path_name 将调用进程当前环境传递给新的进程。

这个函数用一个由 exec_path_name 指定的新的进程映像替换当前的进程映像。新的映像构造自一个由 exec_path_name 指定的正规的、可执行的文件。由于调用的进程映像被新的进程映像所替换,所以没有任何返回。

				Equivalent Linux code
#include <stdlib.h>
#include <stdio.h>
int    processId;
char  *exec_path_name;
char  *	cmd_line ;
cmd_line = (char *) malloc(strlen(_cmd_line ) + 1 );
if(cmd_line == NULL)
         return RC_NOT_ENOUGH_MEMORY;
         	
strcpy(cmd_line, _cmd_line);
if( ( *processId = 
        fork() ) == 0 )		// Create child
 {	
         char		*pArg, *pPtr;
         char		*argv[WR_MAX_ARG + 1];
         int		 argc;
         if( ( pArg = strrchr( exec_path_name, '/' ) ) != NULL )
                pArg++;
         else
                pArg = exec_path_name;
         argv[0] = pArg;
         argc = 1;
         
         if( cmd_line != NULL && *cmd_line != '\0' )
         {
                  
               pArg = strtok_r(cmd_line, " ", &pPtr);
               
               while( pArg != NULL )
               {
                              argv[argc] = pArg;
                              argc++;
                              if( argc >= WR_MAX_ARG )
                              break;
                              pArg = strtok_r(NULL, " ", &pPtr);
                }
         }
         argv[argc] = NULL;
         
         execv(exec_path_name, argv);
         free(cmd_line);
         exit( -1 );
}
else if( *processId == -1 )
{
           *processId = 0;
           free(cmd_line);
           return RC_PROCESS_NOT_CREATED;
}
      

终止进程服务

在 Win32 进程中,父进程和子进程可能需要单独访问子进程所继承的由某个句柄标识的对象。父进程可以 创建一个可访问而且可继承的副本句柄。Win32 示例代码使用下面的方法终止进程:

  • 使用 OpenProcess 来获得指定进程的句柄。
  • 使用 GetCurrentProcess 获得其自己的句柄。
  • 使用 DuplicateHandle 来获得一个来自同一对象的句柄作为原始句柄。

如果函数成功,则使用 TerminateThread 函数来释放同一进程上的主线程。然后使用 TerminateThread 函数 来无条件地使一个进程退出。它启动终止并立即返回。

				Win32 sample code
if( thread != (HANDLE) NULL )
{
    HANDLE	thread_dup;
    if( DuplicateHandle( OpenProcess(PROCESS_ALL_ACCESS, TRUE,  processId),
                                     thread,
                                     GetCurrentProcess(),
                                     &thread_dup,  //Output
                                     0,
                                     FALSE,
                                     DUPLICATE_SAME_ACCESS ))
      {
            TerminateThread( thread_dup, 0);
       }
}
TerminateProcess(
        OpenProcess(PROCESS_ALL_ACCESS, TRUE, 
        processId), 
   (UINT)0 );
      

在 Linux 中,使用 kill 子例程发送 SIGTERM 信号来终止特定进程(processId)。 然后调用设置 WNOHANG 位的 waitpid 子例程。这将检查特定的进程并终止。

				Equivalent  Linux code
pid_t	nRet;
int	status;
kill( processId, SIGTERM );
nRet = waitpid( processId, &status, WNOHANG);  //Check specified
   process is terminated
      

进程依然存在服务

Win32 OpenProcess 返回特定进程(processId)的句柄。如果函数成功,则 GetExitCodeProcess 将获得特定进程的状态,并检查进程的状态是否是 STILL_ACTIVE。

				Win 32 sample
HANDLE      nProc;
DWORD       dwExitCode;
nProc = OpenProcess(PROCESS_ALL_ACCESS, TRUE, processId);
if ( nProc != NULL)
{
     GetExitCodeProcess( nProc, &dwExitCode );
     if (dwExitCode == STILL_ACTIVE )
            return RC_PROCESS_EXIST;
     else
            return RC_PROCESS_NOT_EXIST;
}
else
     return RC_PROCESS_NOT_EXIST;
      

在 Linux 中,使用 kill 子例程发送通过 Signal 参数指定的信号给 由 Process 参数(processId)指定的特定进程。Signal 参数是一个 null 值,会执行错误检查,但不发送信号。

				Equivalent Linux code
if ( kill ( processId, 0 ) == -1 && errno == ESRCH ) // No process can 
                                       be found
                             // corresponding to processId
         return RC_PROCESS_NOT_EXIST;
else
        return RC_PROCESS_EXIST;
      

线程模型

线程 是系统分配 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 sample
int		hThrd;
DWORD          dwIDThread;
unsigned       stacksize;
void           *thrdparam; //parameter to be passed to the thread when it 
                                  //begins execution
HANDLE         *threadId;
if( stacksize < 8192 )
     stacksize = 8192;
else
     stacksize = (stacksize/4096+1)*4096;
     
     hThrd = _beginthread( thrdproc,    // Definition of a thread entry 
                                                                  //point
                                NULL,
                                stacksize,
                                thrdparam);
if (hThrd == -1)
     return RC_THREAD_NOT_CREATED);
     *threadId = (HANDLE) hThrd;
__________________________________________________________________     
                          
        Equivalent Linux code
                                                            
#include <pthread.h>
pthread_t   *threadId;
void         thrdproc  (void *data);  //the thread procedure (function) to 
                                               //be executed. 
                      //It receives a single void parameter
void        *thrdparam; //parameter to be passed to the thread when it 
                               //begins execution
pthread_attr_t  attr;
int             rc = 0;
if (thrdproc == NULL || threadId == NULL)
     	return RC_INVALID_PARAM);
     	
if (rc = pthread_attr_init(&attr))	
     return RC_THREAD_NOT_CREATED);  // EINVAL, ENOMEM
      
if (rc = pthread_attr_setstacksize(&attr, stacksize))
     return RC_THREAD_NOT_CREATED);   // EINVAL, ENOSYS
     
if (rc = pthread_create(threadId, &attr, (void*(*)(void*))thrdproc, 
   thrdparam))
     return RC_THREAD_NOT_CREATED);      // EINVAL, EAGAIN
      

终止线程服务

在 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 Sleep 函数的时间段的单位是毫秒,可以是 INFINITE,在这种情况下线程将永远不会再重新开始。 Linux sleep 函数类似于 Sleep,但是时间段以秒来计。要获得毫秒级的精度,则使用 nanosleep 函数来提供同样的服务。

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()

共享内存服务

共享内存允许多个进程将它们的部分虚地址映射到一个公用的内存区域。任何进程都可以向共享内存区域写入数据, 并且数据可以由其他进程读取或修改。共享内存用于实现进程间通信媒介。不过,共享内存不为使用它的进程提供 任何访问控制。使用共享内存时通常会同时使用“锁”。

一个典型的使用情形是:

  1. 某个服务器创建了一个共享内存区域,并建立了一个共享的锁对象。
  2. 某个客户机连接到服务器所创建的共享内存区域。
  3. 客户机和服务器双方都可以使用共享的锁对象来获得对共享内存区域的访问。
  4. 客户机和服务器可以查询共享内存区域的位置。

共享内存映射表

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

最大共享内存增加到 500 MB。

下面是创建共享内存资源的 Win32 示例代码,以及相对应的 Linux nmap 实现。

				Win32 sample code
typedef struct
 {
   // This receives a pointer within the current process at which the 
   // shared memory is located.
   // The same shared memory may reside at different addresses in other
   // processes which share it.
       void *	location;
       HANDLE	hFileMapping;
}mem_shared_struct, *mem_shared, *token;
mem_shared_struct   *token;
if ((*token = (mem_shared) malloc(sizeof(mem_shared_struct))) == NULL)     
     return RC_NOT_ENOUGH_MEMORY;
     
if (newmode == new_shared_create)
   (*token)->hFileMapping = CreateFileMapping((HANDLE) 0xFFFFFFFF, NULL,
                 PAGE_READWRITE,
                 0,
                (DWORD) size,
                 (LPSTR) name);
 else
      (*token)->hFileMapping = OpenFileMapping(FILE_MAP_ALL_ACCESS,
                 FALSE,
                 (LPSTR) name);
if ((*token)->hFileMapping == NULL)
{
      free( *token );
      return RC_SHM_NOT_CREATED );
}
(*token)->location = MapViewOfFile((*token)->hFileMapping,
                                      FILE_MAP_READ | FILE_MAP_WRITE, 
                                      0, 0, 0);
if ((*token)->location == NULL)
{
      CloseHandle((*token)->hFileMapping);
            free(*token);
            return RC_OBJECT_NOT_CREATED;
}
____________________________________________________________________
                     
        Equivalent Linux code
                                  
typedef struct
{    	
      void    *location;
      int	 nFileDes;	
      cs_size	 nSize;	
      char	 *pFileName;
}mem_shared_struct, *mem_shared, token;
mode_t	mode=0;
int	flag=0;
int	i, ch='\0';
char   name_buff[128];
if (newmode == new_shared_create)
                  flag = O_CREAT;
else if (newmode != new_shared_attach)	
                  return RC_INVALID_PARAM;
                  
if ((*token = (mem_shared) malloc(sizeof(mem_shared_struct))) == NULL)
               return RC_NOT_ENOUGH_MEMORY;
               
strcpy(name_buff, "/tmp/" );
strcat(name_buff, name );
if(((*token)->pFileName = malloc(strlen(name_buff)+1)) == NULL )
{     
      free(*token);
      return RC_NOT_ENOUGH_MEMORY;
}
mode |= S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP;
flag |= O_RDWR;
if(newmode == new_shared_create)    
      remove(name_buff);
      
if(((*token)->nFileDes = open(name_buff, flag, mode)) < 0)
{    
      free((*token)->pFileName);
      free(*token);
      return RC_OBJECT_NOT_CREATED;
}
if(newmode == new_shared_create)
{    
      lseek((*token)->nFileDes, size - 1, SEEK_SET);
      write((*token)->nFileDes, &ch, 1);
}
if(lseek((*token)->nFileDes, 0, SEEK_END) < size)
{	
         free((*token)->pFileName);
         free(*token);
         return RC_MEMSIZE_ERROR;
}
(*token)->location = mmap( 0, size,
                  PROT_READ | PROT_WRITE,
                  MAP_VARIABLE | MAP_SHARED, 
                      (*token)->nFileDes,
                                  0);
                                  
if((int)((*token)->location) == -1)
{   
       free((*token)->pFileName);
       free(*token);    
       return RC_OBJECT_NOT_CREATED;
}
(*token)->nSize = size;strcpy((*token)->pFileName, name_buff);
      

删除共享内存资源

为销毁共享内存资源,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 的一个参考。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值