多进程模型和Slect模型服务器介绍

来源:互联网 发布:视频音量调节软件 编辑:程序博客网 时间:2024/05/16 16:02

本科的时候写过 TCP 后台服务器,当时是好奇心的驱使,学的程度只是熟悉了 API,这次再 Socket 服务器,不仅对后台服务器架构有了更深了理解,对 Linux 系统也有了更深的理解。

一共花了 4 天时间,写了 2 个 TCP 网络服务器,一个是多进程版本,一个连接进程,用连接进程 Fork 读写进程操作连接的读写;另一个是使用了 Select 的双进程版本,相对第一个版本,进程量少了,资源消耗少了, 网络连接吞吐量大了,不过编程复杂度更高,而且在我实现的版本里需要进程之间能够传递文件描述符的支持,写这个服务器花了挺长时间。


首先,我们可以实现一个简单的服务器版本,这个服务先接受连接,然后在读写数据。这是我大学时候为了熟悉 API 实现过的版本,当时也只为了 2 个终端之间的连接。这里有个最大的问题,由于读写操作是阻塞的,当一个连接如果没有结束,服务器没法接受另外一个客户端的连接。于是产生了第一种应对多连接的服务器模型,用一个父进程做连接进程,接受客户发起的新连接,用阻塞的方式接受,可以避免 CPU 忙等的情况,接受到连接手,通过 fork 创建一个新进程,新进程用于处理该连接的读写操作。同时在线的用 N 个连接,服务器端就有 N 个读写进程。

多进程服务器模型

多进程版本思路清晰,逻辑简单,也符合服务器和客户之间的连接关系。不过,多个进程的方式占用了大量的系统资源。
  • Fork 需要内核处理,占用内核时间。
  • 新进程要有独立的进程空间,随着 N 增大,需要的空间会越来越多。
  • 多个读写进程之间的切换,需要耗费系统时间。
看出进程的毛病,我们可以可以把多进程的改成多线程,让资源消耗的更少,不用复制进程的空间,存储空间,以及执行流切换时间的时间降低。


多线程确实已经非常不错了,我想应该已经能满足一部分中等规模网站的需求了吧。但是可以看见,多个执行流依然有切换代价,虽然线程比进程好很多,但是还是有切换代价,另外系统调度到的线程并不一定是可以读写的线程,如果调度到未准备好的线程,这个调度就是白白浪费时间,在线程这里的执行也是浪费时间。最后一点,也是用一些软件不用多线程的一个重要原因,多线程有“一损俱损”的代价,由于线程之间共享了进程内几乎所有的资源,一代一个线程搞坏了进程内某个信息,或者崩溃,都可能导致整个进程的崩溃,在这里就是一个连接坏了,整个服务器都崩了。

第二个实现的模型是使用了 Selet 的服务器模型,Select 是 APUE 在 15 章高级 I/O 中讲到的知识,Select 是 I/O 多路 复用技术,是轮询的思路(跟网络里的某型只是是不是很像呢~),能够一次轮询出可以读写的文件,触发方式是水平触发(与 Epoll 的区别),可以设置成阻塞也可以非阻塞。一旦能够确定可读写 socket,我们就丢开了阻塞的包袱,完全可以把所有连接都放到一个进程里来维护。这里我是用双进程实现的,一个连接进程,另一个读写进程,逻辑也非常清晰。

使用了 Select 模型后,服务器值剩下两个进程,连接进程已经是最优了,浪费的时间只有 Select 轮询的时间,其他的时间都是在与客户端进行读写,比之间的版本有了不少的提升。这个模型唯一的毛病就是 Select 轮询的时间优点长,因为是轮询所以浪费了不少时间在询问没有准备好的读写连接上,下一步优化就在于优化轮询的时间上。

----------------------------------------------------------- 以上都是理论-------------------------------------------

理解了第一个模型的工作方式,代码还是能很快完成的,注意:使用 ntohs 转换 TCP 的字节序和本地字节序,另外一个是服务器端 bind 的 socket 是监听描述符,accpet 获得才是连接描述符。

实现第二个模型颇费了一些时间,其中核心的问题就是由于连接 Socket 是在读写进程之后创建的,所以需要进程之间传递 socket(文件描述符),这里可以变通换成多线程就没有问题了,但是我不想,既然想好了就这么做下去,除非是在做不下去。个人认为传递描述符是挺重要的一个问题,毕竟 Unix 中所有设备都是文件,能够传递描述符意味了可以传递设备的管理权,否则进程之间只能传递信息。不过虽然看起来重要,好像实际上并没有很多这样的需求,程序员可以选择直接 open 一下设备就好了。

简单描述一下如何传递文件描述符。请看下图,描述符只是进程内的信息,买个描述符记录了进程打开文件表的索引位置,所以如果要传递文件描述符要内核在当前进程内新打开一个打开文件表项,然后将指针指向传递过来的文件位置。所以,不能简单地认为传递一个整数就可以传递文件描述符了。

APUE 在 17 章第四节有专门介绍如何在进程见传递描述符的,书中介绍了两种方法,一种是 Stream,另一种用 Unix domain socket 中的 sendmsg 进行传递。然后我就一个一个来呗,调用的方式是ioctl(fd, I_SENDFD, &sendfd),先用的管道,不行,然后又用 Unix domain 消息,还是不行,总是出现 "Inappropriate ioctl for device" , man 一下 ioctl 会发现这个错误并不在文档的 ioctl 说明的错误之内。不知道什么原因,于是在网上找了一圈,搜到一部分信息,原来是 Linux 抛弃了 Stream ,而用 ioctl 传递描述符需要有 Stream 的支持,所以出现了上述错误,(吐槽两句,既然抛弃了还留着参数是几个意思??)。

关于传递文件的网上资源:
http://www.zhihu.com/question/35156527
http://stackoverflow.com/questions/2358684/can-i-share-a-file-descriptor-to-another-process-on-linux-or-are-they-local-to-t
http://stackoverflow.com/questions/11355552/ioctl-giving-invalid-argument#
http://fixunix.com/unix/84093-streams-pipes-ioctl-i_sendfd.html

之后用 sendmsg 中的辅助数据进行传输,类型设为 SCM_RIGHTS ,调试过程还算顺利,有个问题也是需要重点突出来的,就是一开始我写的 sendmsg 和 recvmsg 一传输数据程序就自动退出,没有任何错误提示,通过打印 $? ,得到结果是 50 ,是一个奇葩的什么锁问题。调试一下,发现是 struct msghdr 中的辅助数据msg_control 只能是用栈空间变量,如果 malloc 出来的变量就会出现错误,应该数据所占空间有关, malloc并增加 struct 内部使用空间,猜想这里出了问题(有知道的,请留言~)
正确定义如下:

Union {
struct cmsghdr cm;
char data[CMSG_SPACE(sizeof(int))];
} cmsg;

另外在读写进程里 Select 还要设成非阻塞,否则没法接受新连接。
还有 (connfd = accept(sockfd, (sockaddr *)&connAddr, &connAddrLen)) > 0) ,加红的括号一定要有,
否则由于 = 优先级低于 > , connfd 始终等于 1 。


代码请戳:多进程器服务器,Select 服务器

0 0
原创粉丝点击