Windows服务端程序向Linux移植经验总结

  前面做了一些Windows服务端程序向Linux服务端程序的移植工作,还是有些收获的,这里整理记录一下,将工作内容和细节方面的东西供大家参考。
  需要说明几点:这里的移植是真的移植,而不是考虑跨平台开发型的;这里的移植是针对无界面形式的程序,主要是服务端程序;那啥wine、mono的太大,就不考虑了。
WINDOWS LINUX

一、数据类型定义

1.1 数据类型的差异

  Windows和Linux的数据类型定义完全是两个风格,而且Windows的类型定义喜欢用大写字母,Windows的数据类型主要是定义在windef.h这个头文件里面的,可以将简单的数据类型按照目标机器翻译过来,当然也有些定义比较的复杂,不过很多却是没用的,可以安全的删掉。

1.2 函数接口的差异

  Windows也是支持POSIX标准的,所以大多数的底层函数还是可以用的,但是某些函数的名字和参数签名等还是有些差异(比如stricmp/strcasecmp),如果要在这个头文件里面处理,可以内联封装加进去。

二、进程间的通信

2.1 创建线程

  Windows使用CreateThread创建线程,而Linux通常使用pthread线程库来实现多线程。

2.2 多线程以及进程间同步

  在Windows上面,除了CriticalSection是仅限线程间的同步之外,其他Mutex、Event、filemap,在使用的时候,只要设定了lpName,那么就可以在其他进程中用这个名字打开,就算是进程间的同步和通信了,否则的话就是线程间的同步。
  不仅仅在这里,其实进程间通信Linux一般都有POSIX和SYS V两套接口,实际经验和感受来说POSIX的接口更加习惯好用一些。

Windows Linux(线程) Linux(进程)
CriticalSection pthread_mutex -
Mutex semaphore semaphore
Event semaphore semaphore
filemap shm、mmap shm、mmap

  shm和mmap其实都差不多,但是如果有些数据不像映射到硬盘文件系统上(比如处于保密安全考虑),那么就推荐使用shm来实现。

三、定时器

  在Windows下面,调用SetTimer创建定时器的时候可以指定ID,一个进程中可以创建很多个定时器,但是在Linux下就没有这么方便了。alarm/sleep都是用SIGALRM来实现的,而且代码没法异步执行;timer_create可以指定发送的SIGNUM,然后可以利用调用sigaction提供的参数来区别各个定时器,当然是个候选的方式;同时我找到的网络还有推荐的是使用timerfd+epoll来实现,本库就是按照这个实现封装来模拟SetTimer的操作。
  看似后面两者都可以实现多定时器,实际还是有些差异,前者设定信号处理函数,但是信号处理函数是异步的,所以在这种情况下所做的事情有限,而后者使用一个线程专门检测执行信号处理函数,算是一个进程同步上下文,虽然精度有所欠缺(比如只能按顺序检测没法按照真正时间排序),但是使用更加方便安全。

四、网络IO复用

4.1 数据和函数定义细节差异

  Windows针对自己的异步IO添加了一系列的宏,比如WSAEFAULT、WSANOTINITIALISED等,以及WSAGetLastError等函数(这个直接用errno模拟了)。

4.2 网络IO复用

  虽然都是事件驱动的网络IO复用技术,但是算是两者网络风格最大的不同了。

4.2.1 Windows平台

  Windows采用完成端口(Completion Port)的方式,一般的操作步骤是:
  (1) 采用CreateIoCompletionPort创建完成端口;
  (2) 创建bind、listen服务端套接字;
  (3) 根据服务器的CPU数量,创建一定数目的工作线程,然后在每个工作线程中调用GetQueuedCompletionStatus等待完成事件;
  (4) 上面函数返回,说明操作系统IO操作已经完成,就可以直接操作数据了。

4.2.2 Linux平台

  Linux有经典的select、poll,以及后面增强型的epoll方式,目前epoll有很多优点(侦听socket数目多、效率高等),所以没有特殊利用就用epoll吧,epoll的操作步骤是:
  (1) 创建服务器侦听socket并实现bind、listen等操作;
  (2) 设置socket为O_NONBLOCK模式;
  (3) 创建epoll_event结构,首先将listen的socket加入到侦听当中;
  (4) 进入event_loop中,调用epoll_wait,如果listen socket有请求,就accept得到链接的n_socket,并把这个n_socket再次添加到epoll侦听中;
  (5) 以后每次epoll_wait返回,就两种情况:要么listen socket有新连接请求,要么之前连接并侦听的socket有数据到达可供读取。
  内部的实现细节暂不讨论,其中最大的区别就是,Windows的完成端口用起来十分简单,注册之后,你提供数据存储的地址后就等待,一旦返回,说明数据已经接受好并放到指定的位置了,用户只需要专注数据处理了;Linux繁杂但是灵活,而且epoll可以设置Level/Edge触发,应对不同网络负载情况。
  注意:其实现在看来,两者都归并为异步IO,Windows的完成端口是异步非阻塞的,比如接受程序调用IO操作之后就直接返回,执行其它事务了,而别的线程检测到IO完成标志后执行相应操作;而Linux的IO复用确是异步阻塞的,当监听的套接字都不可用的时候,这些函数将处于一直阻塞的状态(体现在epoll_wait的阻塞)。

五、其他方面

5.1 调试技术

  有时候程序写多了,莫名其妙的Segmentfault,如果没有调试信息会让人一脸萌逼不知所措。针对这类情况,可以:
  (1) 编译的时候添加-g调试参数,产生函数符号;
  (2) 系统启动coredump支持,这样产生fatal错误的时候,会生成错误转储,用gdb可以调试之;
  (3) 发生段错误会产生SIGSEGV信号,可以在程序开始的时候,将这个信号处理函数挂靠在特定的处理函数中,在函数中使用backtrace_symbols来回溯调用链。

5.2 单链表

  Linux内核有个大名鼎鼎的list_head,可以封装在数据结构中使用,十分方便。但是这东西默认是内核态的,所以做了一点点修改,就可以在用户态方便的使用了。还要的RBTree也从内核中移植出来了,可以保证高性能的访问需求。
  当然,如果不是洁癖的要C的话,还是可以考虑用C++,因为C++标准库和Boost库封装了大量的容器类和算法类,虽然编译出来的可执行程序显得比较的臃肿,不过大多数情况下总比你自己造出的轮子要稳定可靠的多。

5.3 编码问题

  Windows系统下默认编码是GBK,Linux平台下默认编码是UTF8,虽然对于一般的程序源文件编码类型问题不大,最多是注释看起来是乱码而已,但是对于源代码中使用了字符串明文的情况,这个差异就会比较大。
  我移植的程序是做自然语言处理部分的,代码中包含了大量的中文字符串明文,同时程序中还有很多算法也是基于字符串是GBK编码方式假设的。所以移植的时候,就全部保留了源代码文件GBK编码模式,这样程序处理逻辑就可以沿用原来的部分,但是在访问文件系统的时候还需要注意将文件路径编码为UTF8的模式,否则会告之文件和目录找不到的错误。
  程序的编码沿用了传统的GNU libiconv库。Boost包含了一个Boost.locale的库,其中包含各种编码、常规字符和宽字符的互转,虽然表象使用起来方便,但是没有相关文档,就没有使用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static int charset_convert(const char *from_charset, const char *to_charset,
const char *inbuf, char *outbuf, size_t outlen) {
iconv_t icd;
size_t inlen = strlen(inbuf);
size_t outleft = outlen;
memset(outbuf, 0, outlen);

if ((iconv_t)-1 == (icd = iconv_open(to_charset, from_charset)))
return -1;

if ((size_t)-1 == iconv(icd, (char**)&inbuf, &inlen, &outbuf, &outleft)) {
iconv_close(icd);
return -1;
}

iconv_close(icd);
return (int)(outlen - outleft);
}

int gbk2utf(const std::string& str_gbk, std::string& store) {
size_t outlen = str_gbk.size() * 2 + 1;

char *outbuf = (char *)malloc(outlen);
if (!outbuf)
return -1;

int ret = charset_convert("GBK", "UTF-8", str_gbk.c_str(), outbuf, outlen);
if (ret == -1) {
free(outbuf);
return -1;
}

store = std::string(outbuf);
free(outbuf);
return ret;
}

  此外,如果需要查看程序的输出,默认的UTF8终端可能会显示乱码,必须设置终端的输出字符为GBK编码才可以。汉字的编码由旧到新是GB2312、GBK、GB18030,越新的标准支持的汉字符集越多,而且编码方式都是向下兼容的,GB2312现在来说收录的字符偏少,但GBK已经满足绝大部分需要了。
  如果原来程序是多个部分的,移植目标是生成一个大项目的时候,要记住使用命名空间进行包装,否则可能会产生潜在的名字冲突问题,常量定义尽量使用const代替define,否则名字空间也保护不了你!

5.4 程序执行路径

  很多程序的配置文件路径、程序输出位置等都是相对于可执行程序路径的,在程序部署的位置千差万别的时候,代码中能获取当前执行程序的路径往往是十分有用的。在Windows中,通过::GetModuleFileName这个库函数就可以得到,而Linux中可以通过getpid()获取当前进程的pid,然后访问proc文件系统得到可执行程序的路径信息,具体来说:

1
2
3
4
snprintf(szProcFilePath, sizeof(szProcFilePath), "/proc/%d/exe", (int)getpid());
memset(szPath, 0, sizeof(szPath));
readlink(szProcFilePath, szPath, sizeof(szPath)-1);
*(strrchr(szPath, '/')+1) = 0;

  移植的代码已经托管到st_utils了,欢迎大家测试指正。同时由于工作的时间比较的旧,很多参考文献不能一一列出了,如原作者有需求,请联系我加上!

本文完!

参考文献