select、poll和epoll

select、poll和epoll

对select、poll、epoll了解得不多,下面是从《构建高性能Web站点》摘录下来的介绍,等以后真正接触到select、poll和epoll方面的开发再详细写一下使用上的区别。

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

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

select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。

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

poll
poll在1986年诞生于System V Release 3,它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制。

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

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

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

epoll可以同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。

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

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

http://blog.endlesscode.com/2010/03/27/select-poll-epoll-intro/

select/poll写法的不同

在 Unix/Linux下写TCP/IP服务器最常用的就是select/poll这两个函数。我的理解是,select是多路选择的意思,而poll是选举、轮询的意思(有区别吗OTL)。我查了一下,发现IBM有个写得很好的例子,于是整理一下,写成一个C文件,然后进行对比,发现:函数参数不同,以及判断是否有新连接和新的输入数据的方式不一样(废话,被拖走...),如下所示(用cygwin的gcc4编译)

Makefile如下:
  1. # 在CYGWIN下gcc4编译,用Unix换行  
  2. CC = gcc  
  3. RM = rm -f  
  4.    
  5. all : select.exe poll.exe  
  6.    
  7. select.exe : test.c  
  8.      $(CC) -o $@ $<   
  9.    
  10. poll.exe : test.c  
  11.      $(CC) -o $@ $< -DUSE_POLL  
  12.    
  13. clean:  
  14.      $(RM) *.o *.exe  
复制代码
比较而言用select写会简单一点,用poll会稍微繁琐一点(不过比起多线程实现来说这些都算简单了OTL)
附件:  您需要登录才可以下载或查看附件。没有帐号?加入到“JS堂”
欢迎来到 JS堂,这是一个面向 JavaScript 的开源社区。

TOP

 

有谁知道在SOCKET编程中,select()函数的作用

Select在Socket编程中还是比较重要的,可是对于初学Socket的人来说都不太爱用Select写程序,他们只是习惯写诸如connect、 accept、recv或recvfrom这样的阻塞程序(所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件的发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回)。可是使用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。下面详细介绍一下!

Select的函数格式(我所说的是Unix系统下的伯克利socket编程,和windows下的有区别,一会儿说明):
  1. int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);
复制代码
先说明两个结构体:

第一,struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄,这可以是我们所说的普通意义的文件,当然Unix下任何设备、管道、FIFO等都是文件形式,全部包括在内,所以毫无疑问一个socket就是一个文件,socket句柄就是一个文件描述符。fd_set集合可以通过一些宏由人为来操作,比如清空集合 FD_ZERO(fd_set *),将一个给定的文件描述符加入集合之中FD_SET(int ,fd_set *),将一个给定的文件描述符从集合中删除FD_CLR(int ,fd_set*),检查集合中指定的文件描述符是否可以读写FD_ISSET(int ,fd_set* )。一会儿举例说明。

第二,struct timeval是一个大家常用的结构,用来代表时间值,有两个成员,一个是秒数,另一个是毫秒数。

具体解释select的参数:

int maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数的值无所谓,可以设置不正确。

fd_set *readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。

fd_set *writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。

fd_set *errorfds同上面两个参数的意图,用来监视文件错误异常。

struct timeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态,第一,若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;第二,若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;第三,timeout的值大于0,这就是等待的超时时间,即 select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。

返回值:

负值:select错误 正值:某些文件可读写或出错 0:等待超时,没有可读写或错误的文件

在有了select后可以写出像样的网络程序来!举个简单的例子,就是从网络上接受数据写入一个文件中。

例子:
  1. main()

  2. {
  3.     
  4.     int sock;
  5.     
  6.     FILE *fp;
  7.     
  8.     struct fd_set fds;
  9.     
  10.     struct timeval timeout={3,0}; //select等待3秒,3秒轮询,要非阻塞就置0
  11.     
  12.     char buffer[256]={0}; //256字节的接收缓冲区
  13.     
  14.     /* 假定已经建立UDP连接,具体过程不写,简单,当然TCP也同理,主机ip和port都已经给定,要写的文件已经打开
  15.     
  16.     sock=socket(...);
  17.     
  18.     bind(...);
  19.     
  20.     fp=fopen(...); */
  21.     
  22.     while(1)
  23.     
  24.     {
  25.         
  26.         FD_ZERO(&fds); //每次循环都要清空集合,否则不能检测描述符变化
  27.         
  28.         FD_SET(sock,&fds); //添加描述符
  29.         
  30.         FD_SET(fp,&fds); //同上
  31.         
  32.         maxfdp=sock&gt;fp?sock+1:fp+1; //描述符最大值加1
  33.         
  34.         switch(select(maxfdp,&fds,&fds,NULL,&timeout)) //select使用
  35.         
  36.         {
  37.             
  38.             case -1: exit(-1);break; //select错误,退出程序
  39.             
  40.             case 0:break; //再次轮询
  41.             
  42.             default:
  43.             
  44.             if(FD_ISSET(sock,&fds)) //测试sock是否可读,即是否网络上有数据
  45.             
  46.             {
  47.                 
  48.                 recvfrom(sock,buffer,256,.....);//接受网络数据
  49.                 
  50.                 if(FD_ISSET(fp,&fds)) //测试文件是否可写
  51.                 
  52.                 fwrite(fp,buffer...);//写入文件
  53.                 
  54.                 buffer清空;
  55.                 
  56.             }// end if break;
  57.             
  58.         }// end switch
  59.         
  60.     }//end while
  61.     
  62. }//end main
复制代码
select()的机制中提供一fd_set的数据结构,实际上是一long类型的数组,
每一个数组元素都能与一打开的文件句柄(不管是Socket句柄,还是其他
文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,
当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执
行了select()的进程哪一Socket或文件可读,下面具体解释:

#include <sys/types.h>
#include <sys/times.h>
#include <sys/select.h>

int select(nfds, readfds, writefds, exceptfds, timeout)
int nfds;
fd_set *readfds, *writefds, *exceptfds;
struct timeval *timeout;

ndfs:select监视的文件句柄数,视进程中打开的文件数而定,一般设为呢要监视各文件
中的最大文件号加一。
readfds:select监视的可读文件句柄集合。
writefds: select监视的可写文件句柄集合。
exceptfds:select监视的异常文件句柄集合。
timeout:本次select()的超时结束时间。(见/usr/sys/select.h,
可精确至百万分之一秒!)

当readfds或writefds中映象的文件可读或可写或超时,本次select()
就结束返回。程序员利用一组系统提供的宏在select()结束时便可判
断哪一文件可读或可写。对Socket编程特别有用的就是readfds。
几只相关的宏解释如下:

FD_ZERO(fd_set *fdset):清空fdset与所有文件句柄的联系。
FD_SET(int fd, fd_set *fdset):建立文件句柄fd与fdset的联系。
FD_CLR(int fd, fd_set *fdset):清除文件句柄fd与fdset的联系。
FD_ISSET(int fd, fdset *fdset):检查fdset联系的文件句柄fd是否
可读写,>0表示可读写。
(关于fd_set及相关宏的定义见/usr/include/sys/types.h)

这样,你的socket只需在有东东读的时候才读入,大致如下:
  1. ...
  2. int sockfd;
  3. fd_set fdR;
  4. struct timeval timeout = ..;
  5. ...
  6. for(;;) {
  7. FD_ZERO(&fdR);
  8. FD_SET(sockfd, &fdR);
  9. switch (select(sockfd + 1, &fdR, NULL, &timeout)) {
  10. case -1:
  11. error handled by u;
  12. case 0:
  13. timeout hanled by u;
  14. default:
  15. if (FD_ISSET(sockfd)) {
  16. now u read or recv something;
  17. /* if sockfd is father and
  18. server socket, u can now
  19. accept() */
  20. }
  21. }
  22. }
复制代码
所以一个FD_ISSET(sockfd)就相当通知了sockfd可读。
至于struct timeval在此的功能,请man select。不同的timeval设置
使使select()表现出超时结束、无超时阻塞和轮询三种特性。由于
timeval可精确至百万分之一秒,所以Windows的SetTimer()根本不算
什么。你可以用select()做一个超级时钟。

FD_ACCEPT的实现?依然如上,因为客户方socket请求连接时,会发送
连接请求报文,此时select()当然会结束,FD_ISSET(sockfd)当然大
于零,因为有报文可读嘛!至于这方面的应用,主要在于服务方的父
Socket,你若不喜欢主动accept(),可改为如上机制来accept()。

至于FD_CLOSE的实现及处理,颇费了一堆cpu处理时间,未完待续。

-- 
讨论关于利用select()检测对方Socket关闭的问题:

仍然是本地Socket有东东可读,因为对方Socket关闭时,会发一个关闭连接通知报文,会马上被select()检测到的。关于TCP的连接(三次握手)和关闭(二次握手)机制,敬请参考有关TCP/IP的书籍。

不知是什么原因,UNIX好象没有提供通知进程关于Socket或Pipe对方关闭的信号,也可能是cpu所知有限。总之,当对方关闭,一执行recv()或read(),马上回返回-1,此时全局变量errno的值是115,相应的sys_errlist[errno]为"Connect refused"(请参考/usr/include/sys/errno.h)。所以,在上篇的for(;;)...select()程序块中,当有东西可读时,一定要检查recv()或read()的返回值,返回-1时要作出关断本地Socket的处理,否则select()会一直认为有东西读,其结果曾几令cpu伤心欲断针脚。不信你可以试试:不检查recv()返回结果,且将收到的东东(实际没收到)写至标准输出...在有名管道的编程中也有类似问题出现。具体处理详见拙作:发布一个有用的Socket客户方原码。

至于主动写Socket时对方突然关闭的处理则可以简单地捕捉信号SIGPIPE并作
出相应关断本地Socket等等的处理。SIGPIPE的解释是:写入无读者方的管道。
在此不作赘述,请详man signal。

以上是cpu在作tcp/ip数据传输实验积累的经验,若有错漏,请狂炮击之。

唉,昨天在hacker区被一帮孙子轰得差点儿没短路。ren cpu(奔腾的心) z80

补充关于select在异步(非阻塞)connect中的应用,刚开始搞socket编程的时候
我一直都用阻塞式的connect,非阻塞connect的问题是由于当时搞proxy scan
而提出的呵呵通过在网上与网友们的交流及查找相关FAQ,总算知道了怎么解决这一问题.同样用select可以很好地解决这一问题.大致过程是这样的:

1.将打开的socket设为非阻塞的,可以用fcntl(socket, F_SETFL, O_NDELAY)完成(有的系统用FNEDLAY也可).

2.发connect调用,这时返回-1,但是errno被设为EINPROGRESS,意即connect仍旧在进行还没有完成.

3.将打开的socket设进被监视的可写(注意不是可读)文件集合用select进行监视,
如果可写,用getsockopt(socket, SOL_SOCKET, SO_ERROR, &error, sizeof(int));来得到error的值,如果为零,则connect成功.

在许多unix版本的proxyscan程序你都可以看到类似的过程,另外在solaris精华区->编程技巧中有一个通用的带超时参数的connect模块.
欢迎来到 JS堂,这是一个面向 JavaScript 的开源社区。

TOP

 

epoll精髓

在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。
相比于select,epoll最大的好处在于它不会随着监听fd数目的增长而降低效率。因为在内核中的select实现中,它是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。并且,在linux/posix_types.h头文件有这样的声明:
  1. #define __FD_SETSIZE    1024
复制代码
表示select最多同时监听1024个fd,当然,可以通过修改头文件再重编译内核来扩大这个数目,但这似乎并不治本。

epoll的接口非常简单,一共就三个函数:
1. int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:

  • EPOLL_CTL_ADD:注册新的fd到epfd中;
  • EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
  • EPOLL_CTL_DEL:从epfd中删除一个fd;

第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
  1. struct epoll_event {
  2.   __uint32_t events;  /* Epoll events */
  3.   epoll_data_t data;  /* User data variable */
  4. };
复制代码
events可以是以下几个宏的集合:

  • EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
  • EPOLLOUT:表示对应的文件描述符可以写;
  • EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
  • EPOLLERR:表示对应的文件描述符发生错误;
  • EPOLLHUP:表示对应的文件描述符被挂断;
  • EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
  • EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

--------------------------------------------------------------------------------------------

从man手册中,得到ET和LT的具体描述如下

EPOLL事件有两种模型:

  • Edge Triggered (ET)
  • Level Triggered (LT)

假如有这样一个例子:

  • 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符
  • 这个时候从管道的另一端被写入了2KB的数据
  • 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作
  • 然后我们读取了1KB的数据
  • 调用epoll_wait(2)......

Edge Triggered 工作模式:
如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。

  • 基于非阻塞文件句柄
  • 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read()时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。

Level Triggered 工作模式
相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。

然后详细解释ET, LT:

LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.
ET(edge-triggered) 是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once), 不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认(这句话不理解)

在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。(未测试)

另外,当使用epoll的ET模型来工作时,当产生了一个EPOLLIN事件后,
读数据的时候需要考虑的是当recv()返回的大小如果等于请求的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取 :
  1. while(rs)
  2. {
  3.   buflen = recv(activeevents.data.fd, buf, sizeof(buf), 0);
  4.   if(buflen < 0)
  5.   {
  6.     // 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区已无数据可读
  7.     // 在这里就当作是该次事件已处理处.
  8.     if(errno == EAGAIN)
  9.      break;
  10.     else
  11.      return;
  12.    }
  13.    else if(buflen == 0)
  14.    {
  15.      // 这里表示对端的socket已正常关闭.
  16.    }
  17.    if(buflen == sizeof(buf)
  18.      rs = 1;   // 需要再次读取
  19.    else
  20.      rs = 0;
  21. }
复制代码
还有,假如发送端流量大于接收端的流量(意思是epoll所在的程序读比转发的socket要快),由于是非阻塞的socket,那么send()函数虽然返回,但实际缓冲区的数据并未真正发给接收端,这样不断的读和发,当缓冲区满后会产生EAGAIN错误(参考man send),同时,不理会这次请求发送的数据.所以,需要封装socket_send()的函数用来处理这种情况,该函数会尽量将数据写完再返回,返回 -1表示出错。在socket_send()内部,当写缓冲已满(send()返回-1,且errno为EAGAIN),那么会等待后再重试.这种方式并不很完美,在理论上可能会长时间的阻塞在socket_send()内部,但暂没有更好的办法.
  1. ssize_t socket_send(int sockfd, const char* buffer, size_t buflen)
  2. {
  3.   ssize_t tmp;
  4.   size_t total = buflen;
  5.   const char *p = buffer;

  6.   while(1)
  7.   {
  8.     tmp = send(sockfd, p, total, 0);
  9.     if(tmp < 0)
  10.     {
  11.       // 当send收到信号时,可以继续写,但这里返回-1.
  12.       if(errno == EINTR)
  13.         return -1;

  14.       // 当socket是非阻塞时,如返回此错误,表示写缓冲队列已满,
  15.       // 在这里做延时后再重试.
  16.       if(errno == EAGAIN)
  17.       {
  18.         usleep(1000);
  19.         continue;
  20.       }

  21.       return -1;
  22.     }

  23.     if((size_t)tmp == total)
  24.       return buflen;

  25.     total -= tmp;
  26.     p += tmp;
  27.   }

  28.   return tmp;
  29. }
复制代码
欢迎来到 JS堂,这是一个面向 JavaScript 的开源社区。

TOP

 

Windows完成端口与Linux epoll技术简介

http://blog.csdn.net/guyun_shine/archive/2009/03/10/3974807.aspx

WINDOWS完成端口编程
1、基本概念
2、WINDOWS完成端口的特点
3、完成端口(Completion Ports )相关数据结构和创建
4、完成端口线程的工作原理
5、Windows完成端口的实例代码
Linux的EPoll模型
1、为什么select落后
2、内核中提高I/O性能的新方法epoll
3、epoll的优点
4、epoll的工作模式 
5、epoll的使用方法
6、Linux下EPOll编程实例

总结WINDOWS完成端口编程
摘要:开发网络程序从来都不是一件容易的事情,尽管只需要遵守很少的一些规则;创建socket,发起连接,接受连接,发送和接受数据。真正的困难在于:让你的程序可以适应从单单一个连接到几千个连接乃至于上万个连接。利用Windows平台完成端口进行重叠I/O的技术和Linux在2.6版本的内核中引入的EPOll技术,可以很方便在在在Windows和Linux平台上开发出支持大量连接的网络服务程序。本文介绍在Windows和Linux平台上使用的完成端口和EPoll模型开发的基本原理,同时给出实际的例子。本文主要关注C/S结构的服务器端程序,因为一般来说,开发一个大容量,具可扩展性的winsock程序一般就是指服务程序。

1、基本概念
设备---windows操作系统上允许通信的任何东西,比如文件、目录、串行口、并行口、邮件槽、命名管道、无名管道、套接字、控制台、逻辑磁盘、物理磁盘等。绝大多数与设备打交道的函数都是CreateFile/ReadFile/WriteFile等。所以我们不能看到**File函数就只想到文件设备。与设备通信有两种方式,同步方式和异步方式。同步方式下,当调用ReadFile函数时,函数会等待系统执行完所要求的工作,然后才返回;异步方式下,ReadFile这类函数会直接返回,系统自己去完成对设备的操作,然后以某种方式通知完成操作。
重叠I/O----顾名思义,当你调用了某个函数(比如ReadFile)就立刻返回做自己的其他动作的时候,同时系统也在对I/0设备进行你要求的操作,在这段时间内你的程序和系统的内部动作是重叠的,因此有更好的性能。所以,重叠I/O是用于异步方式下使用I/O设备的。 重叠I/O需要使用的一个非常重要的数据结构OVERLAPPED。

2、WINDOWS完成端口的特点
Win32重叠I/O(OverlappedI/O)机制允许发起一个操作,然后在操作完成之后接受到信息。对于那种需要很长时间才能完成的操作来说,重叠IO机制尤其有用,因为发起重叠操作的线程在重叠请求发出后就可以自由的做别的事情了。在WinNT和Win2000上,提供的真正的可扩展的I/O模型就是使用完成端口(CompletionPort)的重叠I/O.完成端口---是一种WINDOWS内核对象。完成端口用于异步方式的重叠I/0情况下,当然重叠I/O不一定非使用完成端口不可,还有设备内核对象、事件对象、告警I/0等。但是完成端口内部提供了线程池的管理,可以避免反复创建线程的开销,同时可以根据CPU的个数灵活的决定线程个数,而且可以让减少线程调度的次数从而提高性能其实类似于WSAAsyncSelect和select函数的机制更容易兼容Unix,但是难以实现我们想要的“扩展性”。而且windows的完成端口机制在操作系统内部已经作了优化,提供了更高的效率。所以,我们选择完成端口开始我们的服务器程序的开发。

  • 发起操作不一定完成,系统会在完成的时候通知你,通过用户在完成端口上的等待,处理操作的结果。所以要有检查完成端口,取操作结果的线程。在完成端口上守候的线程系统有优化,除非在执行的线程阻塞,不会有新的线程被激活,以此来减少线程切换造成的性能代价。所以如果程序中没有太多的阻塞操作,没有必要启动太多的线程,CPU数量的两倍,一般这样来启动线程。
  • 操作与相关数据的绑定方式:在提交数据的时候用户对数据打相应的标记,记录操作的类型,在用户处理操作结果的时候,通过检查自己打的标记和系统的操作结果进行相应的处理。
  • 操作返回的方式:一般操作完成后要通知程序进行后续处理。但写操作可以不通知用户,此时如果用户写操作不能马上完成,写操作的相关数据会被暂存到到非交换缓冲区中,在操作完成的时候,系统会自动释放缓冲区。此时发起完写操作,使用的内存就可以释放了。此时如果占用非交换缓冲太多会使系统停止响应。

3、完成端口(Completion Ports )相关数据结构和创建
其实可以把完成端口看成系统维护的一个队列,操作系统把重叠IO操作完成的事件通知放到该队列里,由于是暴露“操作完成”的事件通知,所以命名为“完成端口”(COmpletionPorts)。一个socket被创建后,可以在任何时刻和一个完成端口联系起来。
完成端口相关最重要的是OVERLAPPED数据结构
  1. typedef struct _OVERLAPPED { 
  2.     ULONG_PTR Internal;//被系统内部赋值,用来表示系统状态 
  3.     ULONG_PTR InternalHigh;// 被系统内部赋值,传输的字节数 
  4.     union { 
  5.         struct { 
  6.             DWORD Offset;//和OffsetHigh合成一个64位的整数,用来表示从文件头部的多少字节开始 
  7.             DWORD OffsetHigh;//操作,如果不是对文件I/O来操作,则必须设定为0 
  8.         }; 
  9.         PVOID Pointer; 
  10.     }; 
  11.     HANDLE hEvent;//如果不使用,就务必设为0,否则请赋一个有效的Event句柄 
  12. } OVERLAPPED, *LPOVERLAPPED;
复制代码
下面是异步方式使用ReadFile的一个例子
  1. OVERLAPPED Overlapped; 
  2. Overlapped.Offset=345; 
  3. Overlapped.OffsetHigh=0; 
  4. Overlapped.hEvent=0; 
  5. //假定其他参数都已经被初始化 
  6. ReadFile(hFile,buffer,sizeof(buffer),&dwNumBytesRead,&Overlapped);
复制代码
这样就完成了异步方式读文件的操作,然后ReadFile函数返回,由操作系统做自己的事情,下面介绍几个与OVERLAPPED结构相关的函数 
等待重叠I/0操作完成的函数
  1. BOOL GetOverlappedResult (
  2. HANDLE hFile,
  3. LPOVERLAPPED lpOverlapped,//接受返回的重叠I/0结构
  4. LPDWORD lpcbTransfer,//成功传输了多少字节数
  5. BOOL fWait //TRUE只有当操作完成才返回,FALSE直接返回,如果操作没有完成,通过调//用GetLastError ( )函数会返回ERROR_IO_INCOMPLETE 
  6. );
复制代码

宏HasOverlappedIoCompleted可以帮助我们测试重叠I/0操作是否完成,该宏对OVERLAPPED结构的Internal成员进行了测试,查看是否等于STATUS_PENDING值。

一般来说,一个应用程序可以创建多个工作线程来处理完成端口上的通知事件。工作线程的数量依赖于程序的具体需要。但是在理想的情况下,应该对应一个CPU创建一个线程。因为在完成端口理想模型中,每个线程都可以从系统获得一个“原子”性的时间片,轮番运行并检查完成端口,线程的切换是额外的开销。在实际开发的时候,还要考虑这些线程是否牵涉到其他堵塞操作的情况。如果某线程进行堵塞操作,系统则将其挂起,让别的线程获得运行时间。因此,如果有这样的情况,可以多创建几个线程来尽量利用时间。
应用完成端口:
    创建完成端口:完成端口是一个内核对象,使用时他总是要和至少一个有效的设备句柄进行关联,完成端口是一个复杂的内核对象,创建它的函数是:
  1. HANDLE CreateIoCompletionPort( 
  2.     IN HANDLE FileHandle, 
  3.     IN HANDLE ExistingCompletionPort, 
  4.     IN ULONG_PTR CompletionKey, 
  5.     IN DWORD NumberOfConcurrentThreads 
  6. );
复制代码

通常创建工作分两步:
第一步,创建一个新的完成端口内核对象,可以使用下面的函数:
  1. HANDLE CreateNewCompletionPort(DWORD dwNumberOfThreads) 

  2.           return CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,NULL,dwNumberOfThreads); 
  3. };
  4.    
复制代码
第二步,将刚创建的完成端口和一个有效的设备句柄关联起来,可以使用下面的函数:
  1. bool AssicoateDeviceWithCompletionPort(HANDLE hCompPort,HANDLE hDevice,DWORD dwCompKey) 

  2.           HANDLE h=CreateIoCompletionPort(hDevice,hCompPort,dwCompKey,0); 
  3.           return h==hCompPort; 
  4. };
复制代码


说明 
  • CreateIoCompletionPort函数也可以一次性的既创建完成端口对象,又关联到一个有效的设备句柄
  • CompletionKey是一个可以自己定义的参数,我们可以把一个结构的地址赋给它,然后在合适的时候取出来使用,最好要保证结构里面的内存不是分配在栈上,除非你有十分的把握内存会保留到你要使用的那一刻。
  • NumberOfConcurrentThreads通常用来指定要允许同时运行的的线程的最大个数。通常我们指定为0,这样系统会根据CPU的个数来自动确定。创建和关联的动作完成后,系统会将完成端口关联的设备句柄、完成键作为一条纪录加入到这个完成端口的设备列表中。如果你有多个完成端口,就会有多个对应的设备列表。如果设备句柄被关闭,则表中自动删除该纪录。

4、完成端口线程的工作原理

完成端口可以帮助我们管理线程池,但是线程池中的线程需要我们使用_beginthreadex来创建,凭什么通知完成端口管理我们的新线程呢?答案在函数GetQueuedCompletionStatus。该函数原型:
  1. BOOL GetQueuedCompletionStatus( 
  2.     IN HANDLE CompletionPort, 
  3.     OUT LPDWORD lpNumberOfBytesTransferred, 
  4.     OUT PULONG_PTR lpCompletionKey, 
  5.     OUT LPOVERLAPPED *lpOverlapped, 
  6.     IN DWORD dwMilliseconds 
  7. );
复制代码

这个函数试图从指定的完成端口的I/0完成队列中抽取纪录。只有当重叠I/O动作完成的时候,完成队列中才有纪录。凡是调用这个函数的线程将被放入到完成端口的等待线程队列中,因此完成端口就可以在自己的线程池中帮助我们维护这个线程。完成端口的I/0完成队列中存放了当重叠I/0完成的结果----一条纪录,该纪录拥有四个字段,前三项就对应GetQueuedCompletionStatus函数的2、3、4参数,最后一个字段是错误信息dwError。我们也可以通过调用PostQueudCompletionStatus模拟完成了一个重叠I/0操作。 

当I/0完成队列中出现了纪录,完成端口将会检查等待线程队列,该队列中的线程都是通过调用GetQueuedCompletionStatus函数使自己加入队列的。等待线程队列很简单,只是保存了这些线程的ID。完成端口会按照后进先出的原则将一个线程队列的ID放入到释放线程列表中,同时该线程将从等待GetQueuedCompletionStatus函数返回的睡眠状态中变为可调度状态等待CPU的调度。所以我们的线程要想成为完成端口管理的线程,就必须要调用GetQueuedCompletionStatus函数。出于性能的优化,实际上完成端口还维护了一个暂停线程列表,具体细节可以参考《Windows高级编程指南》,我们现在知道的知识,已经足够了。完成端口线程间数据传递线程间传递数据最常用的办法是在_beginthreadex函数中将参数传递给线程函数,或者使用全局变量。但是完成端口还有自己的传递数据的方法,答案就在于CompletionKey和OVERLAPPED参数。

CompletionKey被保存在完成端口的设备表中,是和设备句柄一一对应的,我们可以将与设备句柄相关的数据保存到CompletionKey中,或者将CompletionKey表示为结构指针,这样就可以传递更加丰富的内容。这些内容只能在一开始关联完成端口和设备句柄的时候做,因此不能在以后动态改变。

OVERLAPPED参数是在每次调用ReadFile这样的支持重叠I/0的函数时传递给完成端口的。我们可以看到,如果我们不是对文件设备做操作,该结构的成员变量就对我们几乎毫无作用。我们需要附加信息,可以创建自己的结构,然后将OVERLAPPED结构变量作为我们结构变量的第一个成员,然后传递第一个成员变量的地址给ReadFile函数。因为类型匹配,当然可以通过编译。当GetQueuedCompletionStatus函数返回时,我们可以获取到第一个成员变量的地址,然后一个简单的强制转换,我们就可以把它当作完整的自定义结构的指针使用,这样就可以传递很多附加的数据了。太好了!只有一点要注意,如果跨线程传递,请注意将数据分配到堆上,并且接收端应该将数据用完后释放。我们通常需要将ReadFile这样的异步函数的所需要的缓冲区放到我们自定义的结构中,这样当GetQueuedCompletionStatus被返回时,我们的自定义结构的缓冲区变量中就存放了I/0操作的数据。CompletionKey和OVERLAPPED参数,都可以通过GetQueuedCompletionStatus函数获得。
线程的安全退出
       很多线程为了不止一次的执行异步数据处理,需要使用如下语句:
  1. while (true)
  2. {
  3.        ......
  4.        GetQueuedCompletionStatus(...); 
  5.         ......
  6. }
复制代码

那么如何退出呢,答案就在于上面曾提到的PostQueudCompletionStatus函数,我们可以用它发送一个自定义的包含了OVERLAPPED成员变量的结构地址,里面包含一个状态变量,当状态变量为退出标志时,线程就执行清除动作然后退出。

5、Windows完成端口的实例代码:
  1. DWORD WINAPI WorkerThread(LPVOID lpParam)
  2. {
  3.     ULONG_PTR *PerHandleKey;
  4.     OVERLAPPED *Overlap;
  5.     OVERLAPPEDPLUS *OverlapPlus, *newolp;
  6.     DWORD dwBytesXfered;
  7.     while (1) {
  8.     ret = GetQueuedCompletionStatus(hIocp, &dwBytesXfered, (PULONG_PTR) & PerHandleKey, &Overlap, INFINITE);
  9.     if (ret == 0) {
  10. // Operation failed
  11.         continue;
  12.     }
  13.     OverlapPlus = CONTAINING_RECORD(Overlap, OVERLAPPEDPLUS, ol);
  14.     switch (OverlapPlus->OpCode) {
  15.     case OP_ACCEPT:
  16. // Client socket is contained in OverlapPlus.sclient
  17. // Add client to completion port
  18.         CreateIoCompletionPort((HANDLE) OverlapPlus->sclient, hIocp, (ULONG_PTR) 0, 0);
  19. // Need a new OVERLAPPEDPLUS structure
  20. // for the newly accepted socket. Perhaps
  21. // keep a look aside list of free structures.
  22.         newolp = AllocateOverlappedPlus();
  23.         if (!newolp) {
  24. // Error
  25.         }
  26.         newolp->s = OverlapPlus->sclient;
  27.         newolp->OpCode = OP_READ;
  28. // This function divpares the data to be sent
  29.         PrepareSendBuffer(&newolp->wbuf);
  30.         ret = WSASend(newolp->s, &newolp->wbuf, 1, &newolp->dwBytes, 0, &newolp.ol, NULL);
  31.         if (ret == SOCKET_ERROR) {
  32.         if (WSAGetLastError() != WSA_IO_PENDING) {
  33. // Error
  34.         }
  35.         }
  36. // Put structure in look aside list for later use
  37.         FreeOverlappedPlus(OverlapPlus);
  38. // Signal accept thread to issue another AcceptEx
  39.         SetEvent(hAcceptThread);
  40.         break;
  41.     case OP_READ:
  42. // Process the data read 
  43. // Repost the read if necessary, reusing the same
  44. // receive buffer as before
  45.         memset(&OverlapPlus->ol, 0, sizeof(OVERLAPPED));
  46.         ret = WSARecv(OverlapPlus->s, &OverlapPlus->wbuf, 1, &OverlapPlus->dwBytes, &OverlapPlus->dwFlags, &OverlapPlus->ol, NULL);
  47.         if (ret == SOCKET_ERROR) {
  48.         if (WSAGetLastError() != WSA_IO_PENDING) {
  49. // Error
  50.         }
  51.         }
  52.         break;
  53.     case OP_WRITE:
  54. // Process the data sent, etc.
  55.         break;
  56.     }            // switch
  57.     }                // while
  58. }                // WorkerThread
复制代码


查看以上代码,注意如果Overlapped操作立刻失败(比如,返回SOCKET_ERROR或其他非WSA_IO_PENDING的错误),则没有任何完成通知时间会被放到完成端口队列里。反之,则一定有相应的通知时间被放到完成端口队列。更完善的关于Winsock的完成端口机制,可以参考MSDN的Microsoft PlatFormSDK,那里有完成端口的例子。访问http://msdn.microsoft.com/library/techart/msdn_servrapp.htm可以获得更多信息。

Linux的EPoll模型
Linux 2.6内核中提高网络I/O性能的新方法-epoll I/O多路复用技术在比较多的TCP网络服务器中有使用,即比较多的用到select函数。

1、为什么select落后
首先,在Linux内核中,select所用到的FD_SET是有限的,即内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数,在我用的2.6.15-25-386内核中,该值是1024,搜索内核源代码得到:
include/linux/posix_types.h:#define __FD_SETSIZE         1024
也就是说,如果想要同时检测1025个句柄的可读状态是不可能用select实现的。或者同时检测1025个句柄的可写状态也是不可能的。其次,内核中实现select是用轮询方法,即每次检测都会遍历所有FD_SET中的句柄,显然,select函数执行时间与FD_SET中的句柄个数有一个比例关系,即select要检测的句柄数越多就会越费时。当然,在前文中我并没有提及poll方法,事实上用select的朋友一定也试过poll,我个人觉得select和poll大同小异,个人偏好于用select而已。

2、内核中提高I/O性能的新方法epoll
epoll是什么?按照man手册的说法:是为处理大批量句柄而作了改进的poll。要使用epoll只需要这三个系统调用:epoll_create(2), epoll_ctl(2), epoll_wait(2)。当然,这不是2.6内核才有的,它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)

Linux2.6内核epoll介绍
先介绍2本书《The Linux NetworkingArchitecture--Design and Implementation of Network Protocols in theLinux Kernel》,以2.4内核讲解LinuxTCP/IP实现,相当不错.作为一个现实世界中的实现,很多时候你必须作很多权衡,这时候参考一个久经考验的系统更有实际意义。举个例子,linux内核中sk_buff结构为了追求速度和安全,牺牲了部分内存,所以在发送TCP包的时候,无论应用层数据多大,sk_buff最小也有272的字节.其实对于socket应用层程序来说,另外一本书《UNIX Network Programming Volume1》意义更大一点.2003年的时候,这本书出了最新的第3版本,不过主要还是修订第2版本。其中第6章《I/OMultiplexing》是最重要的。Stevens给出了网络IO的基本模型。在这里最重要的莫过于select模型和AsynchronousI/O模型.从理论上说,AIO似乎是最高效的,你的IO操作可以立即返回,然后等待os告诉你IO操作完成。但是一直以来,如何实现就没有一个完美的方案。最著名的windows完成端口实现的AIO,实际上也是内部用线程池实现的罢了,最后的结果是IO有个线程池,你应用也需要一个线程池......很多文档其实已经指出了这带来的线程context-switch带来的代价。在linux平台上,关于网络AIO一直是改动最多的地方,2.4的年代就有很多AIO内核patch,最著名的应该算是SGI那个。但是一直到2.6内核发布,网络模块的AIO一直没有进入稳定内核版本(大部分都是使用用户线程模拟方法,在使用了NPTL的linux上面其实和windows的完成端口基本上差不多了)。2.6内核所支持的AIO特指磁盘的AIO---支持io_submit(),io_getevents()以及对DirectIO的支持(就是绕过VFS系统buffer直接写硬盘,对于流服务器在内存平稳性上有相当帮助)。

所以,剩下的select模型基本上就是我们在linux上面的唯一选择,其实,如果加上no-blocksocket的配置,可以完成一个"伪"AIO的实现,只不过推动力在于你而不是os而已。不过传统的select/poll函数有着一些无法忍受的缺点,所以改进一直是2.4-2.5开发版本内核的任务,包括/dev/poll,realtime signal等等。最终,DavideLibenzi开发的epoll进入2.6内核成为正式的解决方案

3、epoll的优点

<1>支持一个进程打开大数目的socket描述符(FD)
select最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
<2>IO效率不随FD数目增加而线性下降
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在os内核。在一些benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率,相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idleconnections模拟WAN环境,epoll的效率就远在select/poll之上了。
<3>使用mmap加速内核与用户空间的消息传递。
这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。而如果你想我一样从2.5内核就关注epoll的话,一定不会忘记手工mmap这一步的。
<4>内核微调
这一点其实不算epoll的优点了,而是整个linux平台的优点。也许你可以怀疑linux平台,但是你无法回避linux平台赋予你微调内核的能力。比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么可以在运行时期动态调整这个内存pool(skb_head_pool)的大小--- 通过echoXXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函数的第2个参数(TCP完成3次握手的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的NAPI网卡驱动架构。

4、epoll的工作模式
令人高兴的是,2.6内核的epoll比其2.5开发版本的/dev/epoll简洁了许多,所以,大部分情况下,强大的东西往往是简单的。唯一有点麻烦是epoll有2种工作方式:LT和ET。

  • LT(leveltriggered)是缺省的工作方式,并且同时支持block和no-blocksocket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.
  • ET(edge-triggered)是高速工作方式,只支持no-blocksocket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(onlyonce),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。

epoll只有epoll_create,epoll_ctl,epoll_wait 3个系统调用,具体用法请参考http://www.xmailserver.org/linux-patches/nio-improve.html ,在http://www.kegel.com/rn/也有一个完整的例子,大家一看就知道如何使用了Leader/follower模式线程pool实现,以及和epoll的配合。

5、 epoll的使用方法
首先通过create_epoll(intmaxfds)来创建一个epoll的句柄,其中maxfds为你epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,之后的所有操作将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。之后在你的网络主循环里面,每一帧的调用epoll_wait(int epfd, epoll_event events, int maxevents, int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写了。基本的语法为: 
  1. nfds = epoll_wait(kdpfd, events, maxevents, -1);
复制代码

其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait这个函数操作成功之后,epoll_events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。最后一个timeout是epoll_wait的超时,为0的时候表示马上返回,为-1的时候表示一直等下去,直到有事件范围,为任意正整数的时候表示等这么长的时间,如果一直没有事件,则范围。一般如果网络主循环是单独的线程的话,可以用-1来等,这样可以保证一些效率,如果是和主逻辑在同一个线程的话,则可以用0来保证主循环的效率。
epoll_wait范围之后应该是一个循环,遍利所有的事件: 
  1. for (n = 0; n < nfds; ++n) {
  2.     if (events[n].data.fd == listener) {    //如果是主socket的事件的话,则表示有新连接进入了,进行新连接的处理。 
  3.     client = accept(listener, (struct sockaddr *) &local, &addrlen);
  4.     if (client < 0) {
  5.         perror("accept");
  6.         continue;
  7.     }
  8.     setnonblocking(client);    // 将新连接置于非阻塞模式 
  9.     ev.events = EPOLLIN | EPOLLET;    // 并且将新连接也加入EPOLL的监听队列。 
  10.     注意,这里的参数EPOLLIN | EPOLLET ? ??没有设置对写socket ? ??监听,如果有写操作的话,这个时候epoll ? ??不会返回事件的,如果要对写操作也监听的话,应该是EPOLLIN | EPOLLOUT | EPOLLET ev.data.fd = client;
  11.     if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) {
  12. // 设置好event之后,将这个新的event通过epoll_ctl加入到epoll的监听队列里面,这里用EPOLL_CTL_ADD来加一个新的 epoll事件,通过EPOLL_CTL_DEL来减少一个epoll事件,通过EPOLL_CTL_MOD来改变一个事件的监听方式。 
  13.         fprintf(stderr, "epoll set insertion error: fd=%d0, 
  14.                                 client); 
  15.                         return -1; 
  16.                     } 
  17.                 } 
  18.                 else // 如果不是主socket的事件的话,则代表是一个用户socket的事件,则来处理这个用户socket的事情,比如说read(fd,xxx)之类的,或者一些其他的处理。 
  19.                     do_use_fd(events[n].data.fd); 
  20. }
复制代码

对,epoll的操作就这么简单,总共不过4个API:epoll_create, epoll_ctl, epoll_wait和close。 
如果您对epoll的效率还不太了解,请参考我之前关于网络游戏的网络编程等相关的文章。

以前公司的服务器都是使用HTTP连接,但是这样的话,在手机目前的网络情况下不但显得速度较慢,而且不稳定。因此大家一致同意用SOCKET来进行连接。虽然使用SOCKET之后,对于用户的费用可能会增加(由于是用了CMNET而非CMWAP),但是,秉着用户体验至上的原则,相信大家还是能够接受的(希望那些玩家月末收到帐单不后能够保持克制...)。这次的服务器设计中,最重要的一个突破,是使用了EPOLL模型,虽然对之也是一知半解,但是既然在各大PC网游中已经经过了如此严酷的考验,相信他不会让我们失望,使用后的结果,确实也是表现相当不错。在这里,我还是主要大致介绍一下这个模型的结构。

6、Linux下EPOll编程实例
EPOLL模型似乎只有一种格式,所以大家只要参考我下面的代码,就能够对EPOLL有所了解了,代码的解释都已经在注释中:
  1. while (TRUE) {
  2.     int nfds = epoll_wait(m_epoll_fd, m_events, MAX_EVENTS, EPOLL_TIME_OUT);        //等待EPOLL时间的发生,相当于监听,至于相关的端口,需要在初始化EPOLL的时候绑定。
  3.     if (nfds <= 0)
  4.         continue;
  5.     m_bOnTimeChecking = FALSE;
  6.     G_CurTime = time(NULL);
  7.     for (int i = 0; i < nfds; i++) {
  8.         try {
  9.             if (m_events[i].data.fd == m_listen_http_fd)        //如果新监测到一个HTTP用户连接到绑定的HTTP端口,建立新的连接。由于我们新采用了SOCKET连接,所以基本没用。
  10.             {
  11.             OnAcceptHttpEpoll();
  12.             } else if (m_events[i].data.fd == m_listen_sock_fd)        //如果新监测到一个SOCKET用户连接到了绑定的SOCKET端口,建立新的连接。
  13.             {
  14.             OnAcceptSockEpoll();
  15.             } else if (m_events[i].events & EPOLLIN)        //如果是已经连接的用户,并且收到数据,那么进行读入。
  16.             {
  17.             OnReadEpoll(i);
  18.             }

  19.             OnWriteEpoll(i);        //查看当前的活动连接是否有需要写出的数据。
  20.         }
  21.         catch(int) {
  22.             PRINTF("CATCH捕获错误n");
  23.             continue;
  24.         }
  25.     }
  26.     m_bOnTimeChecking = TRUE;
  27.     OnTimer();                        //进行一些定时的操作,主要就是删除一些短线用户等。
  28. }
复制代码


其实EPOLL的精华,也就是上述的几段短短的代码,看来时代真的不同了,以前如何接受大量用户连接的问题,现在却被如此轻松的搞定,真是让人不得不感叹,对哪。

总结
Windows完成端口与Linux epoll技术方案是这2个平台上实现异步IO和设计开发一个大容量,具可扩展性的winsock程序指服务程序的很好的选择,本文对这2中技术的实现原理和实际的使用方法做了一个详细的介绍。
欢迎来到 JS堂,这是一个面向 JavaScript 的开源社区。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值