转:连接 ---  from 喜悦村

http://www.phpx.com/viewthread.php?tid=259222&highlight=


@阻塞、非阻塞

什么样的操作是阻塞的,什么样的操作是非阻塞的呢?举一个银行汇款的例子:我们到银行去汇款给别人,是把钱交给银行服务窗口,或者是塞进存款机,银行服务 员或存款机系统通知我们成功了,我们就离开了。并不是等待钞票真的到了对方的手上而什么也不干。那么这次操作就是非阻塞的,一直等待对方反馈的傻瓜就是阻 塞的。

阻塞、非阻塞通常应用于IO操作上(Socket的操作方式和文件IO是一样的)。那么如果对于一个文件描述符的IO操作,IO函数只是将消息放进去而不 必等待对方真的接收成功,程序转而去继续做别的事情,这次操作就是非阻塞的。如果IO函数必须等待对方的成功反馈,这时程序只是在等待什么也不做,这次操 作就是阻塞的。

对于TCP的send函数来说,如果这个描述符是非阻塞的,send()只将消息放进它的缓冲区中,就直接返回实际放进缓冲区的字节数,如果是阻塞的,它会一直等待,直到所有数据都安全送达对方,返回实际发送的字节数(这个时候缓冲区可能已经空了)。

那么,TCP协议本身会保证缓冲区内的数据安全有序地送达,如果传送失败,协议会有三次重发,当对方正确收到数据并ACK回来时,协议才会认为真正成功, 并清空缓冲区中的相应数据。所以如果消息长度总是小于缓冲区大小,对于TCP协议来说,非阻塞方式有着重要的意义,它会节省很多“等待”的时间来做别的, 尤其是面向一些慢速连接(如公共互联网、手机客户端等)时,这对提升程序的处理能力是有好处的。

UDP协议下,就不这么简单了。由于UDP协议本身并不保证连接有效,也不会确认消息正确发送,所以简单地把连接描述符置为非阻塞是一种不负责的做法。因 为它只是一次性地把数据“发送”出去就再也不管了,为了保证安全有效,我们还要做很多的工作,自己来编写消息反馈和拼装。更有一点要注意,UDP是不会保 证若干数据包的顺序到达的,所以我们可以看见memcached的UDP实现,是开辟了一个很大的SEND_BUFFER,争取一次性把数据放进去。 UDP有它灵活高效的特点,但是为了保证安全稳定,需要付出更多的实现代价。

@同步、异步
好多人会把阻塞 / 非阻塞与同步 / 异步混淆。阻塞和非阻塞是套接字层面上,而同步异步是操作系统本身概念。同步表示当一个操作被调用时,在操作真正结束时,调用不会返回。而异步表示调用返 回时操作者并不会立即获取调用结果,而是需要在之后通过消息、回调等方式获取,操作实际在“后台”进行了。同样举银行的例子:

好多人一起去银行,过去的方式是排队,一个挨着一个,在排到你之前,你跑了,再回来只能重新排,一定要等到排到自己了,才可以办业务,这种方式就是同步的。同步与是否阻塞无关,因为好多人都在排队。

现在的方式,是进门在排号机拿一个小纸条,上面会写有顺序号,以及前面有多少人在排队,然后就可以坐下等。当轮到你的时候,银行会主动通知你,你再去办理 业务,这就是“消息”和“回调”。这同样和是否阻塞无关,因为如果你觉得前面只有一两个人,就可以等,如果前面有四五十人,按照目前中国的银行营业厅办事 效率,大可去附近吃个饭,如果前面有五六千人,直接回家就好了……这就是异步操作。

@IO复用
著名的c10k,就是为了解决单个服务器的高并发量问题。之前的阻塞 / 非阻塞、同步 / 异步概念都是为了最终解释IO模型。

我们来看各种级别的IO模型:

1、阻塞IO
也就是之前贴出的echo server源代码,它的实现流程如下:(图非原创,是某位大神画的,膜拜……)


应用程序调用recvfrom(),转入内核,内核有两个过程:wait for data和copy data from kernel to user,直到copy结束后,操作才会返回。这就好像100多人都在银行排队,第一个人交了钱,营业员把钱收走,转给另一个人,另一个人反馈了(打电 话?)说收到钱了,第一个人才离开,第二个人开始办理。整个过程从营业员收完钱之后包括他自己在内,银行里的所有人都在等,这是最没有效率的方式。

2、非阻塞IO
营业员稍微聪明了一些:


转入内核之后,在第一个过程wait for data成功后,recvfrom返回了。也就是营业员收到钱之后就告诉第一个人可以回去了,接着办理第二个人的业务。这样,至少营业员没有闲着。整个状态就是在轮询是不是有人交了钱。

3、IO multiplexing
这个时候,顾客也聪明了:


这是最常见的IO复用模式了,select和poll都是这种。所谓“复用”,就是可以同时处理多个套接字。银行有了排号机,会主动通知该谁了。 select是一种阻塞操作,当没有fd有数据时候(没有空闲窗口),会有个人一直看着,直到有了可用资源。不过至少大家不用排队了,而是可以随便坐在哪 里,或者到门口抽烟。

4、SIGIO


基于信号的IO复用模型,只有*NIX系统才支持,它的进步之处在于,select不再阻塞了,而是由系统注册的信号回调函数来通知,有一点像计时器了。 这个时候,银行不需要大堂经理去一直看着是否有窗口空闲了,直接装了个提示牌和喇叭。当一个营业员办完了业务,按一下身边的按钮,就触发了一条信号,提示 准备好服务下一个用户了,这是主动的。

5、AIO(Asynchronous IO)


以上四种,在内核的copy data from kernel touser都会阻塞,不过到了aio,则是完全非阻塞的了。对应的就是Windows的IOCP(完成端口)和posix_aio*,不过在*NIX系 统上,AIO只是一个设想,从来没有变成现实。在这个时代,只要把钱交给银行,并告知要汇给什么人,就可以走了。成功之后银行会主动通知,不需要排队,也 不需要等候。网银时代到来了……

可以看出以上5种模型,越往后阻塞越少,可以同时处理的能力越强。从中也可以解释为什么IIS的静态响应效率普遍好于apache,因为IOCP的模型比select要“高级”。

@select & poll
我们从最初级的“复用”开始,就是select / poll,这是非常经典的复用模式,著名的Apache就是这种模式的。对比上面的echoserver代码,select的不同是在accept之后不 再是直接等待数据,而是直接等待下一个accept。另外的select()来对已经连接的fd轮询,直到寻找到有数据可读的fd,并返回。

由此可以看出select()的问题,在于它的效率是线性的,也就是O(n),它的轮询规模会随着同时连接的fd数上涨,而且每个进程的select()不能同时处理1024(FD_SETSIZE)个以上的fd(所以apache使用了多进程)。

为了解决fd上限问题,提出了/dev/poll,它不再有FD_SETSIZE的限制,但是实现方式和select()类似。

@epoll & kqueue
从select / poll的实现方式中可以看出,如果有一个非常大的fd集合,而同时又只有少量处于活动状态,应用程序会耗费大量的CPU时间用于轮询空闲的fd。 linux从2.5.44开始加入了epoll支持,BSD系统也加入了kqueue,其实它们应该属于3、4级之间的一种实现方式。kqueue的实现 方式和epoll很类似,所以下面直来说说epoll。epoll实现了对文件描述符的“开关”设定,也就是回调机制,当一个fd准备就绪之后,它会主动 去触发回调,通知用户程序自己已经就绪,而不再需要轮询。这样它的代价就是恒定的O(1),也就是与fd总数无关。所以epoll可以同时处理非常大的 fd集合。

不过,当几乎所有的fd都是活动状态时(这在高速网络中很少会有),epoll的性能并不会比select / poll高多少。

@一致性接口
我们做开发的时候,尤其是跨平台的通用系统,经常会需要一个通用的接口,它会自动根据操作系统来选择最适合的IO模型,而对于开发者来说是透明的。我们只 需要知道,我有多少多少钱要汇给谁谁,别的事情比如怎么个汇法,银行在哪就不用管了,接口会去完成。这类接口有很多,比如重型的ACE,和轻量级的 libevent(这个很著名吧,虽然大部分web程序员是通过安装memcached才知道有这么个东西却不知道它是什么)、libev(与 libevent类似,实现效率比libevent要高)等。

使用ACE的典型例子就是Mongos服务器(流行的WOW服务模拟器),而libevent的实例不用说就是著名的memcached了,有兴趣的可以 去读它的源代码,我的Comoro第一个版本也是基于libevent的,之后的corelib使用的是原生epoll,因为我并不需要支持linux以 外的系统。另:已经有人把libevent扩展成了支持IOCP,所以有了windows版的memcached,不过由于内存分配实现方式的不 同,windows版的memcached并不稳定。

========================= 我是华丽的分割线 =========================

IOCP是优秀的,也是成熟的(它比epoll早好多),不过大部分的大型服务器都是基于*NIX系统的,只有一些经典的微软私有服务和一些游戏服务器是 基于IOCP的,所以我们下面不去理会孤芳自赏的IOCP,主要讲述一下epoll。下面的代码来自corelib,也就是我的新版本的comoro的网 络实现基础,我尽量做足够的文字描述,其中涉及了很多算法和数据结构相关的概念,有问题的可以去翻书。

所有实现基于epoll和pthread,不使用第三方库如libevent。如需测试,请准备2.6+内核的linux系统。

首先来熟悉一下epoll的三个调用,也是仅有的三个基本调用(成功的东西总是很简单):

int epoll_create(),创建一个epoll句柄,其实也是一个fd。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event), 对一个epoll句柄epfd操作。op是操作方式,fd是操作数(也是一个文件句柄),events是事件属性。操作包括EPOLL_CTL_ADD, 添加一个句柄(fd)到epoll中;EPOLL_CTL_MOD,修改句柄fd的监听方式;EPOLL_CTL_DEL,从epoll中移除句柄fd。 看看,比数据库操作还简单。

事件属性包括两个部分:event.data.fd就是当前需要监听的句柄,event.events是需要监听的事件类型集合,事件类型包 括:EPOLLIN——读事件;EPOLLOUT——写事件;EPOLLRDHUP——客户端半关闭;EPOLLPRI——高优先级读事 件;EPOLLERR——连接错误,这个事件是不需要特别指定的,epoll总是会发现连接错误;EPOLLHUP——客户端主动关闭,这个事件也不需要 被特别指定;EPOLLONESHOT——一次性使用,当一次事件触发之后,epoll不会再理会这个fd,除非使用EPOLL_CTL_MOD修改了监 听事件;EPOLLET——边缘触发,下面会详细描述它。

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout), 等待epoll事件发生。epfd是需要检查的epoll句柄,events是结果暂存数组,maxevents是可以同时返回的事件最大 数,timeout是等待事件。与其说是“等待”不如说是“检查”,因为epoll是主动触发的,epoll_wait的实现也与select()的阻塞 遍历不同,它只是检查一下当前集合中有哪些fd触发了开关。不过,从形式上我们还是经常让它的timeout为-1,这样如果epoll中没有fd活 动,epoll_wait会阻塞,直到有了活跃分子。当然我们也可以把timeout设为0,让它“点到为止”,不过如果是在无限循环中使用 timeout为0的epoll_wait,又有大部分的fd处于非活动状态,所需要付出的代价和select类似,甚至更高,因为它是每次“遍历”所有 fd,一直这样不停地遍历下去。

epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式。LT模式下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作,而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无 论fd中是否还有数据可读。所以在ET模式下,read一个fd的时候一定要把它的buffer读光,也就是说一直读到read的返回值小于请求值,或者 遇到EAGAIN错误。

下面了解一下目前大型服务器流行的程序模型:
  • 单线程单进程:早期的memcached。不过单线程单进程的高性能服务器必然是事件驱动的(epoll / kqueue / IOCP)
  • 单线程多进程:经典的Apache prefork就是这种
  • 多线程单进程:分为动态线程和静态线程池,简单地使用动态线程来实现复用的大型服务器由于性能问题已经不多了,静态线程池的是目前的主流方式,如新的memcached。
  • 多线程多进程:Nginx就是这种结构,可以启动多个进程,每个进程有固定数目的子线程,并且每个线程都是独立的事件驱动循环。这是目前最高效的方式了。
具 体使用何种方式来开发服务器程序,要根据需求来决定。通常来说,对于WebServer这种上下文无关的应用,更适合创建多个工作进程,而像游戏服务器这 类,由于用户之间有密切的关系,所以进程分离会带来很大的进程间通讯负担。另外,使用多进程或多线程在现时有一个更重要的意义就是充分利用多CPU和多核 资源,而多进程比多线程可以更有效地利用多个处理器核心。

Apache prefork是早期的一种经典结构,它又一个父进程fork出多个子进程来“等待”连接(不是有连接到了才fork,所以叫做prefork),所以系 统中总是有几个空闲的进程来等待操作,直到进程数达到上限。父进程只负责接受连接请求,子进程负责处理HTTP请求,通常来说,子进程在处理一定数量的请 求之后会“自杀”,再fork出一个新的来。这种模型是很稳定的,不过多进程程序如果处理得不好,在进程数很高且负载很大的情况下容易发生“惊群”(多个 进程同时被唤醒)。

后来在Apache2中,添加了新的模式,比如Worker,也就是多进程多线程模式。这样每个进程的处理工作看起来是“并行”的了,但是为了稳定性,很多大网站还是继续在使用prefork。

在Apache2.2中,终于不再完全依赖select(),提供了event方式(事件驱动)。Apache的发展是稳定的,保守的,这也是它所追求的方向。

而Nginx则不同,它所追求的就是性能。所以它以激进的方式使用了多线程多进程的epoll。而memcached由于上下文相关性(数据共享),所以使用了单进程静态线程池。

corelib同样使用的是单进程的静态线程池。所谓静态,就是在应用启动时候就已经创建好了若干数目的工作线程,且数目不会再改变。主线程会以轮询的方 式把操作分配给工作线程,以达到最好的系统资源利用状态。一般来说,worker线程数应该和CPU核心数成整倍关系,这也算是最简单的一种“多核”开发 吧~~
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值