构建高性能站web站点第三章服务器并发处理能力(3)

3.5 持久连接

持久连接(Keep-Alivve)有时候也称为长连接,它本身是TCP通信的一种普通方式,即在一次TCP连接中持续发送多份数据而不断开连接,与它相反的方式称为短连接,也就是建立连接后发送一份数据便断开,然后再次建立连接发磅下一份数据,周而复始。一般而言,是否采用持久连接,完全 取决于应用的特点和需要。从性能的角度看,建立TCP连接操作本身便是一荐不小的开销,所以在允许的情况下,连接次数越少,越有利于性能的提升。

在Web应用层通信中,由于HTTP的无状态特性,使得HTTP通信毫不依赖于TCP长连接,长久以来大家习惯了“一次性”的HTTP通信,即一次TCP连接处理一个HTTP请求。然而回归TCP传输层,长连接带来的好处显而易见,它对于密集型的图片或网页等数据请求处理有着明显的加速作用。HTTP/1.1对长连接有了完整的定义,同时,基于标准化的协议。很多浏览器和Web服务器也都纷纷提供了对于长连接的支持。

可以想象,HTTP长连接实施需要浏览器和Web服务器的共同协作,缺一不可。一方面,浏览器需要保持一个TCP连接并重复利用,不断地发送多个请求,另一方面,服务器不能过早地主动关闭连接。要实现这一点并不难,目前的浏览器普遍支持长连接,表现在其发出的HTTP请求数据头中包含关于长连接的声明,如下所示:

Connection:Keep-Alive

这种声明的含义在于告诉服务器,“如果可以的话,请让我重用这个连接”。言下之意就是告诉服务器不要在处理完当前请求后马上关闭连接。

同时,在Web服务器上也要打开长连接的支持,幸运的是,目前所有主流的Web服务器软件都支持长连接,比如在Apache 2.2.11中,长连接的支持默认为开启状态,当然你也可以通过以下方法关闭:

KeepAlive Off

当我们不希望浏览器和Web服务器使用长连接方式时,我们可以关闭服务器的长连接支持,这也是唯一的办法,因为我们无权去修改每个用户的浏览器设置。

对于长连接的有效使用,其关键的一点在于长连接超时时间的设置,即长连接在什么时候被关闭呢?这个设置同是出现在浏览器和Web器上,因为双方都可以主动关闭,对于IE7,默认的超时时间为1分钟,你也可以通过修改注册表来修改超时时间。对于Web服务哭喊 ,一般会提供超时时间的配置参数,比如在Apache中,可以通过httpd.conf中的如下参数进行配置:

KeepAliveTemeout 30

以上的命令设置超时时间为30秒,而在默认情况下,Apache将其设置为5秒。值得注意的是,浏览器和Web服务器各自的超时时间设置不一定一致,所以在实际运行中,是以最短的超时时间为准。

了解这些内容后,我们来看看由浏览器发起的一个支持长连接的HTTP请求:

 

服务器的HTTP响应头如下 所示:

在以上请求和响应数据头中,粗体部分体现了长连接的支持和超时时间的设置。

我们前面说过,在请求大量小文件的时候,长连接的有效使用可以减少大量重新建立连接的开销,有效加速性能,ab的启动选项参数中有对长连接的支持,也就是模拟浏览器发起支持长连接的HTTP请求。我们用ab来试试Apache的prefork模式在长连接下的表示。

首先,我们不使用长连接,请求一个497K字节的图片,ab测试结果如下所示:

 

Server Software:        Apache/2.4.6
Server Hostname:        localhost
Server Port:            80

Document Path:          /test.gif
Document Length:        508335 bytes

Concurrency Level:      100
Time taken for tests:   50.302 seconds
Complete requests:      100000
Failed requests:        0
Write errors:           0
Total transferred:      50859800000 bytes
HTML transferred:       50833500000 bytes
Requests per second:    1987.99 [#/sec] (mean)
Time per request:       50.302 [ms] (mean)
Time per request:       0.503 [ms] (mean, across all concurrent requests)
Transfer rate:          987392.53 [Kbytes/sec] received

根据前面的多项测试,这次的结果看起来在我们的预料之中,下面我们使用长连接模式进行测试,结果如下所示:

 

Server Software:        Apache/2.4.6
Server Hostname:        localhost
Server Port:            80

Document Path:          /test.gif
Document Length:        508335 bytes

Concurrency Level:      100
Time taken for tests:   44.453 seconds
Complete requests:      100000
Failed requests:        0
Write errors:           0
Keep-Alive requests:    99029
Total transferred:      50863357301 bytes
HTML transferred:       50833500000 bytes
Requests per second:    2249.55 [#/sec] (mean)
Time per request:       44.453 [ms] (mean)
Time per request:       0.445 [ms] (mean, across all concurrent requests)
Transfer rate:          1117377.66 [Kbytes/sec] received

从结果中可以看出,吞吐率有所提高,使用了长连接的请求为99029个,也就是说这些请求没有专门建立连接,而是重用了已经建立好的连接。

我们再来看看Nginx在长连接情况下的表现,这次我们使用strace的系统调用统计功能,可以直观地进行时间对比。首先我位不使用长连接,对Nginx从压力测试开始到结束进行跟踪,结题如下所示:

[root@localhost ~]# strace -c -p 7180
strace: Process 7180 attached
^Cstrace: Process 7180 detached
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 30.82    0.435927          22     20000           close
 10.96    0.155047          16     10000           write
  9.03    0.127687          13     10000           open
  7.94    0.112270          11     10000           sendfile
  7.84    0.110837          11     10000           accept4
  7.19    0.101639          10     10000           writev
  6.01    0.085048           9     10000           recvfrom
  5.43    0.076839           8     10002           epoll_wait
  5.34    0.075538           8     10000           epoll_ctl
  4.85    0.068564           7     10000           setsockopt
  4.60    0.065047           7     10000           fstat
------ ----------- ----------- --------- --------- ----------------
100.00    1.414443                120002           total

然后,我们使用胀连接方式对其进行同样条件的压力测试,结果如下所示:

[root@localhost ~]# strace -c -p 7180
strace: Process 7180 attached
^Cstrace: Process 7180 detached
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 25.87    0.246954          12     19922           setsockopt
 13.85    0.132204          13     10000           write
 12.61    0.120373          12     10000           open
 11.40    0.108816          11     10000           sendfile
 10.50    0.100209          10     10000           writev
  9.27    0.088521           9     10177           close
  8.89    0.084901           8     10099           recvfrom
  6.72    0.064134           6     10000           fstat
  0.55    0.005228          22       241           epoll_wait
  0.22    0.002114          12       177           accept4
  0.14    0.001324           7       177           epoll_ctl
------ ----------- ----------- --------- --------- ----------------
100.00    0.954778                 90793           total

可以看出,在长连接情况下,总的系统调用数减少了一半左右,其中socket的accetp()和close()的减少量在我们的意料之中,注意它们消耗的时间,比不使用长连接的时候减少了很多,这在整体时间中占有不小的比例。

对于以上测试方法,长连接超时时间的不同设置带来的性能差异不容易测试,因为不论Web服务器的超时时间为多少,ab总是在发完所有请求后关闭所有连接,所以不存在超时的情况,那么超时时间的长短究竟对Web服务器性能有何影响呢?不难想象,对于Apache这样的多进程模型来说,如果长连接超时时间过长,比如60秒,那么即便是浏览器没有任何请求,而Apache仍然维持着连接该浏览器的子进程。一旦并发用户数较多,那么Apache将维持着大量的空闲进程,严重影响了服务器性能。另一方面,对于使用多线程的轻量级Web服务器(如Nginx),长连接超时时间过长同样也不是一件好事,回为有时由于超时时间过长导致资源无效占有而引发的损失已经超过了由于重复连接所造成的损失,这的确得不偿失。

事实上,持久连接无处不在,除了以上在HTTP通信中的应用,它还可以应用在任何涉及TCP通信的应用中,比如MySQL数据访问时的持久连接以及连接池,这部分内容我们会在后面的详细探讨。总之不论如何应用,持久连接的动机很简单,那就是尽量减少连接次数,尽量重用连接通道。

3.6 I/O模型

有人说,比特天生就是用来被复制的,数据的生命意义便在于/输出,的确,计算机的重要工作之一便是负责各种设备的数据输入/输出,也就是I/O(In/Out)操作。

事实上,I/O操作根据设备的不同分为很多种类型,比如内存I/O、网络I/O、磁盘I/O、对于内存I/O,一般我们在讨论web站点性能时很少提及,因为相比于岳两种I/O操作,内存I/O的速度已经足够快了,前面我们介绍过网络数据传输的瓶颈往往在于带宽最低的交换节点。类似地,计算机性能的瓶颈往往并不在内存I/O本身。

对于网络I/O和磁盘I/O,它们的速度要慢很多,尽管使用RAID磁盘陈列可以通过并行磁盘访问来回忆磁盘I/O速度,购买大量独享网络带宽以及使用高带宽网络适配器可以提高网络I/O的速度。但问题在于,这些I/O操作需要由内核系统调用来完成,同是系统调用显然需要CPU来调度,而CPU的速度毫不疑问是非常快的,这就便利CPU不得不浪费宝贵的时间来等待慢速I/O操作。

尽管我们通过多进程等方式来充分利用究竟的CPU资源,但我们还是希望能够让CPU花费够少的时间在I/O操作的调度上,这样就可以腾出更多的CPU时间来完成更多的I/O操作,事实上,如何让高速的CPU和慢速的I/O设备更好的协调工作,这是从现代计算机诞生到现在一直在探讨的话题,很多技术和策略都围绕它们展开。

针对本章Web服务器并发处理能力的讨论范畴,我们所关注的I/O操作主要是网络数据的接收和发送,以及磁盘文件的访问,我们将其归纳为多种模型,称为I/O模型,它们的本质区别便在于CPU的参与方式。

PIO与DMA

在介绍I/O模型之前,有必要简单地说说慢速I/O设备和内存之间的数据传输方式。

我们拿磁盘来说,很早以前,磁盘和内存之间的数据传输是需要CPU控制的,也就是说如果我们读取磁盘谁的到内存中,数据要经过CPU存储转发,这种方式称为PIO.显然这种方式非常不合理,需要占用大量的CPU时间来读取文件,造成文件访问时系统几乎停止响应。

后来,DMA(直接内存访问,Direct Memory Access)取代了PIO,它可以不经过CPU而直接进行磁盘和内存的数据交换。在DMA模式下,CPU只需要向DMA控制器下达指令,让 DMA控制器来处理数据的传送即可,DMA控制器通过系统总线来传输数据,传磅完毕再通知CPU,这样就在很大程度上降低了CPU占有率,大大节省了系统资源,而它的传输速度与PIO的差异其实并不十分明显,因为这主要取决于慢速设备的速度。

可以肯定的是,PIO模式的计算机我们现在已经很少见到了。

同步阻塞I/O

说到阻塞,首先得说说I/O等待。造成等待的原因非常多,比如Web服务器在等待用户的访问,这便是等待,因为它不知道谁会来访问,所以只能等。随后,当某个用户通过浏览器发请求,Web服务器与该浏览器建立TCP连接后,又要等待用户发出HTTP请求数据,用户请求数据在网络上传输需要时间,进入服务器接收缓冲区队列以被复制到进程地址空间都需要时间,另外,假如浏览器和Web服务器采用HTTP长连接模式,那么在超时关闭连接之前,服务器还要等待浏览器发磅其他的请求,这也是I/O等待。

可见,I/O等待是不可避免的,那么既然有了等待,就会有阻塞,但是注意,我们说的阻塞是指当前发起I/O操作的进程被阻塞,产不是CPU被阻塞。事实上没有什么能让CPU阻塞的,CPU只知道拼命地计算,对于阻塞一无所知。

另外,“同步”的概念在这里显得并不那么重要,只是为了和后面的异步I/O加以区分,我们在介绍异步事件和异步I/O的时候再来探讨同步和异步的区别。但是需要说明的是,对于磁盘文件的访问,也有一个所谓的“同步”的选项,即使用O_SYNC标志打开文件,在规范情况下,对磁盘文件调用read()将阻塞进程,一直到数据被复制到进程用户态内存空间,而对磁盘文件调用write()则不同,它会在数据被复制到内核缓冲区后立即返回。如果使用O_SYNC标志打开文件,则对写文件操作产生影响,它便利write()必须等待数据真正写入磁盘后才返回。

同步阻塞I/O是指当进程调用某些涉及I/O操作的系统调用或库函数时,比如accept()、send()、recv()等,进程便暂停下来,等待I/O操作完成后再继续运行。这是一种简单而有效的I/O模型,它可以和多进程结合起来有效地利用VPU资源,但是其代价就是多进程的大量内存开销。

比如在Apache-prefork 模型中,某个子进程在等待请求时,进程阻塞在accept()调用,我们用stracce进程跟踪,如下所示:

[root@localhost ~]# strace -p 8274
strace: Process 8274 attached
accept4(4, 

为了简单说明同步阻塞I/O以及和其他I/O模型区别,我们举个有意思的例子。比如你去逛街,逛着逛着有点饿了,这时你看到商场里有个小吃城,就去一个小店买一碗面条,交了钱,可面条做起来得需要时间,你也没知道什么时候可以做好,没办法,只好坐在那里等,等面条做好后吃完再继续逛街。显然,这里的吃面便是I/O操作,它要等待厨师做面条,还要等待自己把面条吃完。

同步非阻塞I/O

在同步阻塞/O中,进程实际上等待的时间可能包括两部分,一个 等待数据的就绪,另一个是等待数据的复制,对于网络 I/O来说,前者的时间可能要更长一些。

与此不同的是,同步非阻塞I/O的调用不会等待数据的就绪,如果数据不可读或者不可写,它会立即告诉进程。比如我们使用非阻塞recv()接收网络数据的时候,如果网卡缓冲区中没有可接收的数据,函数就及时返回,告诉进程没有数据可读了。相比于阻塞I/O,这种非阻塞I/O结合反复轮询来尝试数据是否就绪,防止进程被阻塞,最大的好处便在于可以在一个进程里同时处理多个I/O操作。

但正是由于需要进程执行多次的轮询来查看数据是否就绪,这花费了大量的CPU时间,便利进程处于忙碌等待状态。

回到买面条的故事中,假如你不甘心坐着等面条做好,想去顺便逛逛街,可以担心面条做好后没有及时领取,所以在逛一会跑回去看看面条是否做好,往返了很多次,最后虽然及时地吃上了面条,但是却累得气喘吁吁。

非阻塞I/O一般只针对网络I/O有效,我们只要在socket的选项设置中使用O_NONBLOCK即可,这样对于该socket的send()或recv()采用非阻塞方式。值得注意的是,对于磁盘I/O,非阻塞I/O并不产生效果。

多路I/O就绪通知

在实际应用中,特别是Web服务器,同时处理大量的文件描述符是必不可少的,但是使用同步非阻塞I/O显然不是最佳的选择,在这种模型下,我们知道如果服务器要同时接收多个TCP连接的数据,就必须轮流对每个socket调用接收数据的方法,比如recv()。不管这些socket有没有可以接收的数据,都要询问一遍,假如大部分socket并没有数据可以接收,那么进程便会浪费很多CPU时间用于检查这些socket,这显然不是我们所希望看到的。

多路I/O就绪通知的出现,提供了对大量文件描述符就绪检查的高性能方案,它允许进程通过一种方法来同时监视所有文件描述符,并可以快速获取所有就绪的文件描述符,然后只针对这些文件描述符进行数据访问。

回到买面条的故事中,假如你不止买了一份面条,还在其他几个小吃店买了饺子、粥、馅饼等,因为一起逛街的朋友看到你的面条后也饿了。这些东西都需要时间来制作,在同步非阻塞I/O模型中,你要轮流不停地云各个小吃店询问进度,痛苦不堪,现在引入多路I/O就绪通知后,小吃城管理处给大厅安装了一块电子屏幕,以后所有小吃店的食物做好后,都会显示在屏幕上,这可真是个好消息,你只需要间隔性地看看大屏幕就可以了,了许你还可以同时逛逛附近的商店,在不远处也可以看到大屏幕。

需要注意的是,I/O就绪通知只是帮助我们快速获得就绪的文件描述符,当得知数据就绪后,就访问数据本身而言,仍然需要选择阻塞或非阻塞的访问方式,一般我们选择非阻塞方式,以防止任何意外的等待阻塞整个进程,比如有时就绪通知只代表一个内核的提示,也许此时文件描述符尚未真正准备好或者已经被客户端关闭连接。

由于平台和历史等原因,多路I/O就绪通知有很多不同的实现,在检查大量文件描述符时的性能也存在一定的差异。说到这里,我们有必要简单介绍一下UNIX家族的历史,这有助于我们更加深刻地了解这些依赖于平台的技术。

记住,最早的UNXI诞生于贝尔实验室,在随后的十年,UNIX在学术机构和大型企业中被广泛应用。贝尔实验室的AT&T收购了西方电子公司的研究部门后设立的一个独立实体,AT&T这时以廉价的许可将UNIX的源代码授权给一些研究机构和大学,供它们研究和教学使用,这便利很多机构在此基础上对UNIX进行了完善和扩展,促进了UNIX的发展,同时产生了一些新变种版本,其中最著名的变种之一便是由加州大学Berkeley分校开发的BSD,这一变种对UNIX有着重大贡献和深远影响。

由于BSD开始被很多企业广泛采用,不久之后,AT&The意识到了UNIX的商业价值,开始停止源代码授权,同是为了统一混乱的UNIX版本,AT&T综合了其他大学和企业开发的各种UNIX,开发了UNIX System V Release 1。

这时候BSD已经非常成熟,比如select就诞生于这时候的 4.2 BSD,而TCP/IP则诞生于4.1BSD,它的代码也成为以后几乎所有操作系统TCP/IP实现代码前辈,包括Windows。BSD不断增大的影响力张于引起了AT&T的关注,AT&T将BSD告上了法庭,一场源代码版权官司开始了,一直持续到AT&T将UNIX实验项目卖给Novell。

Novell采取了开明的态度,它允许BSD自由开发,但前提是必须删除来自AT&T的代码,就这样,BSD诞生了全新的版本4.4BSD lite,这个版本不存在法律问题,所以它成为很多现代自由版UNIX的基础,它们和UNIX以及Linux一起,共同成为UNIIX大家族的成员。

BSD在发展中也逐渐衍生出3个主要的分支:FreeBSD、OpenBSD和netBSD。

在此后的几十年中,UNIX仍在不断变化,其版本所有者不断变更,授权者的数量也在增加。有很多大公司在取得了UNIX的授权之后,开发了自己的UNIX产品,比如IBM的AIX、HP的HPUX、Sun的Solars、AGI的iRIX,以及Apple的MacOS。

正是在Unix之间的利益纷争及其商业化运行的背景下,乱世出英雄,Linux诞生了,它天生便吸取了UNIX的教训,在版本问题上直接采用自由的GPL开源许可,这种开发精神使得Linux就像当年的BSD一样迅速发展,同时又由于GPL的使用,很好地规避了变种并存的情况。

这真是一部充满斗争的历史,它深深影响到几十年后的我们,本章介绍的技术基本都来自于那个时代,所以当你被很多相似的实现方法搞得眼花缭乱时,想想那也许只是多年前的一个历史错误。

以下的实现方法,我们只侧重介绍其优势和局限性,目的在于真正了解它们在何种场景下表现出色,至于如何使用它们来编写代码,超出了本书的讨论范围,你可以查看手册或者相关书籍。

select

select最早于1983年出现在4.2BSD中,它通过一个select()系统调用来监视包含多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程可以获得这些文件描述符而进行后续的读写操作。

select目前几乎在所有的平台上都支持,其良好的跨平台支持也是它的一个优点,事实上从现在看来,这也 它所剩下不多的优点之一。

select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。所以,假如使用了select的服务器已经维持了1024个连接,那么你的请求可能会拒绝。

另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大TCP连接处于非活跃状态,但调用select()会对所有select进行一次线性扫描,所以这也是浪费了一定的开销。

poll

poll在1996年诞生于System V Release 3,显然UNIX不愿意直接沿用BSD的select,而是自己重新实现了一遍,这和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。

poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

另外,select()和poll()将就绪的文件描述符告诉进程后,如果进程没有对其进行I/Or操作,那么下次调用select()或poll()(的时候将再次报告这些文件描述符,的以它们一般不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)。

SIGIO

Linux 2.4 提供了SIGIO,它通过实时信号(Real Time Signal)来实现select/poll的通知方法,但是它们的不同在于,select/poll告诉我们哪些文件描述符是就绪的,一直到我们读写它之前,每次select/poll都会告诉我们:而SIGIO则是告诉我们哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发(Edge Triggered)。SIGIO几乎是Linux 2.4下性能最好的多路I/O就就绪通知方法。

但是,SIGIO也存在一些缺点,在SIGIO机制中,代表事件的信号由内核中的事件队列来维护,信号按照顺序进行通知,这可能导致当一个信号到达的时候,该事件已经过期,它所描述的文件描述符已经被关闭。另一方面,事件队列是有长度限制的,无论你设置多大的上限,总有可能被事件装满,这就很容易发生事件丢失,所以这时候需要采用其他方法来弥补损失。

/dev/poll

Sun在Solaris中提供了新的实现方法,它使用了虚拟人/dev/poll设备,你可以将要监视的文件描述符数组写入这个设备,然后通过ioctl()来等待事件通知。当ioctl()返回就绪的文件描述符后,你可以从/dev/poll中读取所有就绪的文件描述符数组,这点类似于SIGIO,节省了扫描所有文件描述符的开销。

在Linux下有很多方法可以实现类似/dev/poll的设备,但是都没有提供直接的内核支持,这些方法在服务器负载较大时性能不够稳定。

/dev/epoll

随后,名为/dev/epoll的设备以补丁的形式出现在Linux2.4上,它提供了类似/dev/poll的功能,而且增加了内存映射(mmap)技术,在一定程度上提高了性能,后面会详细介绍内存映射的内容。

但是,/dev/epoll仍然只是一个补丁,Linux 2.4并没有半它的实现加入内核。

epoll

直到Linux 2.6才出现了内核直接支持的实现方法,那就是epoll,这几乎具备了之前所说的一切优点,被公认为Linux 2.6下性能最好的多路I/O就绪通知方法。

epoll可以同时支持水平触发和边缘触发,理论上边缘触发的性能要更高一些,但是代码实现相当复杂,因为任何意外的丢失事件都会造成请求处理错误。在默认情况下epoll采用水平触发。如果要使用边缘触发,则需要在事件注册时增加EPOLLET选项,在lighttpd的epoll模型代码(src/fdevent_linux_sysepoll.c)中,可以看到它注释掉了EPOLLET,并没有使用边缘触发方式,如下所示:

ep.events |= EPOLLERR| EPOLLHUP | /* | EPOLLET */;

而在Nginx的epoll模型代码(src/event/modules/ngx_epoll_module.c)中,可以看到它使用了边缘触发,如下所示:

ee.events = EPOLLIN|EPOLLOUT|EPOLLET;

另外,epoll同样中告知那些就绪的文件描述符,而且当我们调用epoll_wait()获取就绪文件描述符时,返回的并不是实际的描述符,而是一个代表就绪描述符的值,你只支柱根去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(nmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。

另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册每个文件描述符,一旦某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

回到买面条的故事中,虽然有了电子屏幕,但是显示的内容是所有餐品的状态,包括正在制作的和已经做好的,这显然给你造成阅读上的麻烦,就好像select/poll每次返回所有监视的文件描述符一样。如果能够只显示做好的餐品,那该多好,随后小吃城管理处改进了大屏幕,实现了这一点,这就像/dev/poll一样只告知就绪的文件描述符。在显示做好的餐点时,如果是显示一次,而不管你有没有看到,这就相当于边缘触发,而如果在你领取餐点之前,每次都显示,就相当于水平触发。

但尽管这样,一旦你走得比较远,就还得花时间走到小吃城去看电子屏幕,能不能让你理加轻松地获得通知呢?管理处这次采用了手机短信通知的方法,你只要到管理处注册后,便可以在餐点就绪时及时收到短信通知,这类似于epoll的事件机制。

kqueue

FreeBSD中实现了kqueue,它像epoll个一样可以设置水平触发或边缘触发,同是kqueue还可以用来监视磁盘文件和目录,但是它的API在很多平台都不支持,而且文档相当匮乏。kquere和epoll的性能非常接近。

对于以上介绍的多种实现方法,在Linux平台的Web服务器中,我们常用的主要有select、poll、SIGIOI、epoll,尤其是在Linux2.6中,epoll成为首要选择。

也许你没用特别留意,在安装一些Web服务器软件的时候,configure过程中会检查当前系统支持的多路I/O就绪的多路I/O就绪通知方法,比如Nginx在configure时,其中一部分内容如下所示:

+rt signals found

checkig for epoll ...found

checking for poll() ...fofund

checking for /dev/poll ... not found

checking for kqueue ... not found

在本章随后关于并发策略的内容中,我们会将这些多路I/O就绪通知方法应用在其中,并进行测试对比。

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值