Migrating Win32 C/C++ applications to Linux on POWER, Part 1: Process, thread

来源:互联网 发布:jquery数组长度 编辑:程序博客网 时间:2024/05/01 16:13

http://www.ibm.com/developerworks/systems/library/es-MigratingWin32toLinux.html


This article covers Win32 API mapping, particularly process, thread, and shared memory services to Linux on POWER. The article can help you decide which of the mapping services best fits your needs. The author takes you through the APIs mapping he faced while porting a Win32 C/C++ application.

Overview

There are many ways to port and migrate from the Win32 C/C++ applications to the pSeries platform. You can use freeware or 3rd party tools to port the Win32 application code to move to Linux. In our scenario, we decided to use a portability layer to abstract the system APIs call. A portability layer will offer our application the following benefits:

  • Independence from the hardware
    • Independence from the operating system
    • Independence from changes introduced from release to release on operating systems
  • Independence from operating system API styles and error codes
  • Ability to uniformly place performance and RAS hooks on calls to the OS

Because the Windows environment is quite different from the pSeries Linux environment, porting across UNIX platforms is considerably easier than porting from the Win32 platform to the UNIX platform. This is expected, as many UNIX systems share a common design philosophy and provide a lot of similarities at the application layer. However, the Win32 APIs are in limited in the task of porting to Linux. This article identifies issues due to differences in design between Linux and Win32.



Initialization and termination

On Win2K/NT, the initialization and termination entry point for a DLL is the _DLL_InitTerm function. When each new process gains access to the DLL, this function initializes the necessary environment for the DLL. When each new process frees its access to the DLL, this function terminates the DLL for that environment. This function is called automatically when you link to the DLL. For applications, an additional initialization and termination routine is included in the _DLL_InitTerm function.

On Linux, the GCC has an extension that allows specifying that a function should be called when the executable or shared object containing it is started up or finished. The syntax is __attribute__((constructor)) or __attribute__((destructor)). These are basically the same as constructors and destructors to replace the _init and _fini functions from the glibc library.

The C prototypes for these functions are:

void __attribute__ ((constructor)) app_init(void);void __attribute__ ((destructor)) app_fini(void);
Win32 sample _DLL_InitTerm(HMODULE modhandle, DWORD fdwReason, LPVOID lpvReserved){    WSADATAData;    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 */}

Process service

The Win32 process model has no direct equivalents to fork() and exec(). Rather than always inherit everything as is done in Linux with the fork() call, the CreateProcess() accepts explicit arguments that control aspects of the process creation, such as file handle inheritance.

The CreateProcess API creates a new process containing one or more threads that run in the context of the process, and there is no relationship between the child and parent processes. On Windows NT/2000/XP, the returned process ID is the Win32 process ID. On Windows ME, the returned process ID is the Win32 process ID with the high-order bit stripped off. When a created process terminates, all the data associated with the process is erased from the memory.

To create a new process in Linux, the fork() system call duplicates the process. After a new process is created, a parent and child relationship is automatically established and the child process inherits all the attributes from the parent process by default. Linux creates a new process using a single system call with no parameters. The fork() returns the child's process ID to the parent and none to the child.

Win32 processes are identified both by handles and process IDs, whereas Linux has no process handles.

Process mapping tableWin32LinuxCreateProcessfork()
execv()TerminateProcesskillExitProcess()exit()GetCommandLineargv[]GetCurrentProcessIdgetpidKillTimeralarm(0)SetEnvironmentVariableputenvGetEnvironmentVariablegetenvGetExitCodeProcesswaitpid

Create process service

In Win32, the first argument of CreateProcess() specifies the program to run, and the second argument provides the command line arguments. CreateProcess takes other process parameters as arguments. The second-to-last argument is a pointer to a STARTUPINFORMATION structure, which specifies the standard devices for the process and other start up information about the process environment. You need to set the hStdin, hStdout, and hStderr members of STARTUPINFORMATION structure before passing its address to CreateProcess to redirect standard input, standard output, and standard error of the process. The last argument is a pointer to a PROCESSINFORMATION structure, which is filled up by created processes. Once the process starts, it will contain, among other things, the handle to the created process.

Win32 example PROCESS_INFORMATION    procInfo;STARTUPINFO        startupInfo;typedef DWORD          processId;char                   *exec_path_namechar                   *_cmd_line; GetStartupInfo( &startupInfo );     // You must fill in this structureif( 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;}

In Linux, the processId is an integer. The search directories in Linux are determined by the PATH environment variable (exec_path_name).The fork() function makes a copy of the parent, including the parent's data space, heap, and stack. The execv() subroutine uses the exec_path_name to pass the calling process current environment to the new process.

This function replaces the current process image with a new process image specified by exec_path_name. The new image is constructed from a regular, executable file specified by exec_path_name. No return is made because the calling process image is replaced by the new process image.

Equivalent Linux code #include #include 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;}

Terminate process service

In the Win32 process, the parent and child processes may require different access to an object identified by a handle that the child inherits. The parent process can create a duplicate handle with the desired access and inheritability. The Win32 sample code uses the following scenario to terminate a process:

  • Use OpenProcess to get the handle of the specified process
  • Use GetCurrentProcess for its own handle
  • Use DuplicateHandle to obtain the handle from the same object as the original handle

If the function succeeds, use the TerminateThread function to release the primary thread on the same process. The TerminateProcess function is then used to unconditionally cause a process to exit. It initiates termination and returns immediately.

Win32 sample code if( thread != (HANDLE) NULL ){    HANDLEthread_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 );

In Linux, use the kill subroutine to send the SIGTERM signal to terminate the specified process (processId). Then call the waitpid subroutine with the WNOHANG bit set. This checks the specified process and is terminated.

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

Process still exists service

The Win32 OpenProcess returns the handle to the specified process (processId). If the function succeeds, the GetExitCodeProcess will retrieve the status of the specified process and checks whether the process status is 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;

In Linux, use the kill subroutine to send the signal specified by the Signal parameter to the process specified by the Process parameter (processId). The Signal parameter is a null value, the error checking is performed, but no signal is sent.

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;

Thread model

A thread is a basic entity to which the system allocates CPU time; each thread maintains information to save its "context" while waiting to be scheduled. Each thread can execute any part of the program code and share global variables of the process.

LinuxThreads is a pthreads compatible thread system built on top of the clone() system call. Because threads are scheduled by the kernel, LinuxThreads supports blocking I/O operations and multiprocessors. However, each thread is actually a Linux process, so the number of threads a program can have is limited to the total number of processes allowed by the kernel. The Linux kernel does not provide system calls for thread synchronization. The Linux Threads library provides additional code to support operations on mutex and condition variables (using pipes to block threads).

For signal handling when coupled with the LinuxThreads, each thread inherits a signal handler if one was registered by a parent process that spawned the thread. Only the new features supported in Linux Kernel 2.6 and higher will include the improved POSIX threading support, such as the Native POSIX Thread Library for Linux (NPTL).

Important parts of the thread model are the thread synchronization, wait functions, thread local storage, and initialization and termination abstraction. Below, the thread services are only addressed as:

  • A new thread is created, and the threadId is returned
  • The current new thread can be terminated by invoking the pthread_exit function
Thread mapping tableWin32Linux_beginthreadpthread_attr_init
pthread_attr_setstacksize
pthread_create_endthreadpthread_exitTerminateThreadpthread_cancelGetCurrentThreadIdpthread_self

Thread Creation

The Win32 application uses the C runtime libraries instead of using the Create_Thread APIs. The routines of _beginthread and _endthread are used. These routines take care of any reentrancy and memory leak problems, thread local storage, initialization, and termination abstraction.

Linux uses the pthread library call pthread_create() to spawn a thread.

The threadId is returned as an output parameter. A set of parameters are passed for creating a new thread. The arguments execute a function when a new thread is created. The stacksize argument is used as the size, in bytes, of the new thread's stack, and the actual argument is to be passed to the function when the new thread starts executing.

Specify the thread procedure (function)

The creating thread must specify the starting function of the code that the new thread is to execute. The starting address is the name of the threadproc function with a single argument, threadparam. If the call succeeds in creating a new thread, the threadId is returned. The typedef for Win32 threadId is a HANDLE. The typedef for Linux threadId is pthread_t.

threadprocThe thread procedure (function) to be executed. It receives a single void parameter.threadparamThe parameter to be passed to the thread when it begins execution.

Set the stack size

In Win32, the thread stack is allocated automatically in the memory space of the process. The system increases the stack as needed and frees it when the thread terminates. In Linux, the stack size is set in the pthread attributes object; the pthread_attr_t is passed to the library call pthread_create().

Win32 sample inthThrd;DWORD          dwIDThread;unsigned       stacksize;void           *thrdparam; //parameter to be passed to the thread when it                                   //begins executionHANDLE         *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_t   *threadId;void         thrdproc  (void *data);  //the thread procedure (function) to                                                //be executed.                       //It receives a single void parametervoid        *thrdparam; //parameter to be passed to the thread when it                                //begins executionpthread_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

In Win32, one thread can terminate another thread with the TerminateThread function. However, the thread's stack and other resources will not be deallocated. This is recommended if the thread terminates itself. In Linux, the pthread_cancel method terminates execution of the thread identified by the specified threadId.

Terminate thread serviceWin32LinuxTerminateThread((HANDLE *) threadId, 0);pthread_cancel(threadId);

Thread state

In Linux, threads are created in joinable state by default. Another thread can be synchronize with the thread's termination and recover its termination code using the function pthread_join(). The thread resources of the joinable thread are released only after it is joined.

Win32 uses WaitForSingleObject() to wait for a thread to terminate.

Linux uses pthread_join to do the same.

Win32Linuxunsigned long rc;

rc = (unsigned long) WaitForSingleObject (threadId, INIFITE);unsigned long rc=0;

rc = pthread_join(threadId,
void **status);

In Win32, _endthread() is used to end the execution of the current thread. In Linux, it is recommended to use pthread_exit() to exit a thread to avoid implicitly calling the exit routine. In Linux, the retval is the return value of the thread, and it can be retrieved from another thread by calling pthread_join().

End the execution of current thread serviceWin32Linux_endthread();pthread_exit(0);

In the Win32 process, The GetCurrentThreadId function retrieves the thread identifier of the calling thread. Linux uses the pthread_self() function to return the calling thread's ID.

Get the current thread ID serviceWin32LinuxGetCurrentThreadId()pthread_self()

The time period for the Win32 Sleep function is in milliseconds and can even be INFINITE, in which case the thread will never resume. The Linux sleep function is similar to Sleep, but the time periods are measured in seconds. To obtain the millisecond resolution, use the nanosleep function to provide the same service.

Sleep serviceWin32Equivalent Linux codeSleep (50)struct timespec timeOut,remains;

timeOut.tv_sec = 0;
timeOut.tv_nsec = 500000000; /* 50 milliseconds */

nanosleep(&timeOut, &remains);

The Win32 SleepEx function suspends the current thread until one of the following occurs:

  • An I/O completion callback function is called
  • An asynchronous procedure call (APC) is queued to the thread
  • The minimum time-out interval elapses

Linux uses the sched_yield to do the same thing.

Win32LinuxSleepEx (0,0)sched_yield()

Shared memory service

Shared memory allows multiple processes to map a portion of their virtual address to a common memory region. Any process can write data to a shared memory region, and the data are readable and modified by other processes. The shared memory is used to implement an interprocess communication media. However, shared memory does not provide any access control for processes that use it. It is a common practice to use "locks" along with shared memory.

A typical usage scenario is:

  1. A server creates a shared memory region and sets up a shared lock object
  2. A client can attach the shared memory region created by the server
  3. Both the client and server can use the shared lock object to get access to the shared memory region
  4. The client and server can query the location of the shared memory resource
Shared memory mapping tableWin32LinuxCreateFileMaping,
OpenFileMappingmmap
shmgetUnmapViewOfFilemunmap
shmdtMapViewOfFilemmap
shmat

Create a shared memory resource

Win32 creates a shared memory resource by shared memory-mapped files. Linux uses the shmget/mmap function to access files by directly incorporating file data into memory. The memory areas are known as the shared memory segments.

Files or data can also be shared among multiple processes or threads. But, this requires synchronization between these processes or threads and its handling is up to the application.

The CreateFileMapping() reinitializes the commitment of the shared resource to the process if the resource already exists. The call can fail if there is insufficient memory free to handle the erroneous shared resource. OpenFileMapping() indicates the shared resource must already exist; this call is merely requesting access to it.

In Win32, the CreateFileMapping does not allow you to grow the file size, but that's not the case in Linux. In Linux, if the resource already exists, it will be re-initialized. It may be destroyed and recreated. Linux creates the shared memory that is accessed using a name. The open() system call determines whether the mapping is readable or writable. The parameters passed to mmap() must not conflict with the access requested during open(). The mmap() needs to supply the size of the file as the number of bytes to map.

For 32-bit kernels, there are 4GB virtual address spaces. The first 1 GB is for device drivers. The last 1 GB is for kernel data structures. The 2GB in the middle can be used for shared memory. Currently, Linux on POWER allows 4GB for the kernel and up to 4GB virtual address space for user applications.

Mapping memory access protection bitsWin32LinuxPAGE_READONLYPROT_READPAGE_READWRITE(PROT_READ | PROT_WRITE)PAGE_NOACCESSPROT_NONEPAGE_EXECUTEPROT_EXECPAGE_EXECUTE_READ(PROT_EXEC |PROT_READ)PAGE_EXECUTE_READWRITE(PROT_EXEC | PROT_READ | PROT_WRITE)

To find out the allocation of the Linux shared memory, you can look at shmmax, shmmin and shmall under the /proc/sys/kernel directory.

An example to increase shared memory on Linux is:

echo 524288000 > /proc/sys/kernel/shmmax

The maximum shared memory is increased to 500 MB.

Below is the Win32 sample code, and the equivalent of the Linux mmap implementation, to create a shared memory resource.

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;       HANDLEhFileMapping;}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_tmode=0;intflag=0;inti, 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);

To destroy a shared memory resource, the munmap subroutine unmaps a mapped file region. The munmap subroutine only unmaps regions created from calls to the mmap subroutine. If an address lies in a region that is unmapped by the munmap subroutine and that region is not subsequently mapped again, any reference to that address will result in delivery of a SIGSEGV signal to the process.

Delete a shared memory resourceWin32Equivalent Linux codeUnmapViewOfFile(token->location);

CloseHandle(token->hFileMapping);munmap(token->location, token->nSize);

close(token->nFileDes);

remove(token->pFileName);

free(token->pFileName);

Conclusion

This article covers the Win32 APIs mapping to Linux on POWER regarding the initialization and termination, process, thread, and shared memory services. This is by no means to have a full coverage of all APIs mapping and readers can only be used this information as a reference to migrate to Win32 C/C++ application to POWER Linux.

Special Notices

IBM, eServer and pSeries are registered trademarks or trademarks of the IBM Corporation in the United States or other countries or both.

UNIX is a registered trademark of The Open Group in the United States and other countries.

Microsoft and Windows are trademarks or registered trademarks of Microsoft Corporation, in the United States, other countries, or both.

All other trademarks and registered trademarks are the properties of their respective companies.

This publication/presentation was produced in the United States. IBM may not offer the products, programs, services or features discussed herein in other countries, and the information may be subject to change without notice. Consult your local IBM business contact for information on the products, programs, services, and features available in your area. Any reference to an IBM product, program, service, or feature is not intended to state or imply that only IBM's product, program, service, or feature may be used. Anyfunctionally equivalent product, program, service, or feature that does not infringe on IBM's intellectual property rights may be used instead.

Information concerning non-IBM products was obtained from a supplier of these products, published announcement material, or other publicly available sources and does not constitute an endorsement of such products by IBM. Sources for non-IBM list prices and performance numbers are taken from publicly available information, including vendor announcements and vendor worldwide homepages. IBM has not tested these products and cannot confirm the accuracy of performance, capability, or any other claims related to non-IBM products. Questions on the capability of non-IBM products should be addressed to the supplier of those products.

Questions on the capabilities of non-IBM products should be addressed to suppliers of those products. IBM may have patents or pending patent applications covering subject matter in this presentation. Furnishing this presentation does not give you any license to these patents. Send license inquiries, in writing, to IBM Director of Licensing, IBM Corporation, New Castle Drive, Armonk, NY 10504-1785 USA. All statements regarding IBM's future direction and intent are subject to change or withdrawal without notice, andrepresent goals and objectives only. Contact your local IBM office or IBM authorized reseller for the full text of a specific Statement of General Direction.

The information contained in this presentation has not been submitted to any formal IBM test and is distributed "AS IS." While each item may have been reviewed by IBM for accuracy in a specific situation, there is no guarantee that the same or similar results will be obtained elsewhere. The use of this information or the implementation of any techniques described herein is a customer responsibility and depends on the customer's ability to evaluate and integrate them into the customer's operational environment. Customers attempting to adapt these techniques to their own environments do so at their own risk.

<script>window._bd_share_config={"common":{"bdSnsKey":{},"bdText":"","bdMini":"2","bdMiniList":false,"bdPic":"","bdStyle":"0","bdSize":"16"},"share":{}};with(document)0[(getElementsByTagName('head')[0]||body).appendChild(createElement('script')).src='http://bdimg.share.baidu.com/static/api/js/share.js?v=89860593.js?cdnversion='+~(-new Date()/36e5)];</script>
阅读(161) | 评论(0) | 转发(0) |
0

上一篇:Migrate Win32 C/C++ application to Linux on POWER, Part 2: Mutexes

下一篇:select、poll、epoll之间的区别总结

相关热门文章
  • test123
  • 编写安全代码——小心有符号数...
  • 使用openssl api进行加密解密...
  • 一段自己打印自己的c程序...
  • sql relay的c++接口
  • linux dhcp peizhi roc
  • 关于Unix文件的软链接
  • 求教这个命令什么意思,我是新...
  • sed -e "/grep/d" 是什么意思...
  • 谁能够帮我解决LINUX 2.6 10...
给主人留下些什么吧!~~
评论热议
0 0