I/O多路复用详解

I/O多路复用详解
2010年07月09日
  转自:http://konglingchun.is-programmer.com/posts/12146 .html 要想完全理解I/O多路复用,需先要了解I/O模型:
  一、五种I/O模型
  1、阻塞I/O模型
  最流行的I/O模型是阻塞I/O模型,缺省情形下,所有套接口都是阻塞的。我们以数据报套接口为例来讲解此模型(我们使用UDP而不是TCP作为例子的原因在于就UDP而言,数据准备好读取的概念比较简单:要么整个数据报已经收到,要么还没有。然而对于TCP来说,诸如套接口低潮标记等额外变量开始活动,导致这个概念变得复杂)。
  进程调用recvfrom,其系统调用直到数据报到达且被拷贝到应用进程的缓冲区中或者发生错误才返回,期间一直在等待。我们就说进程在从调用recvfrom开始到它返回的整段时间内是被阻塞的。
  2、非阻塞I/O模型
  进程把一个套接口设置成非阻塞是在通知内核:当所请求的I/O操作非得把本进程投入睡眠才能完成时,不要把本进程投入睡眠,而是返回一个错误。也就是说当数据没有到达时并不等待,而是以一个错误返回。
  3、I/O复用模型
  调用select或poll,在这两个系统调用中的某一个上阻塞,而不是阻塞于真正I/O系统调用。 阻塞于select调用,等待数据报套接口可读。当select返回套接口可读条件时,调用recevfrom将数据报拷贝到应用缓冲区中。
  4、信号驱动I/O模型
  首先开启套接口信号驱动I/O功能, 并通过系统调用sigaction安装一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据报准备好被读时,就为该进程生成一个 SIGIO信号。随即可以在信号处理程序中调用recvfrom来读数据报,井通知主循环数据已准备好被处理中。也可以通知主循环,让它来读数据报。
  5、异步I/O模型
  告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核拷贝到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的主要区别是:
  信号驱动I/O:由内核通知我们何时可以启动一个I/O操作,
  异步I/O模型:由内核通知我们I/O操作何时完成。
  二、I/O复用的典型应用场合:
  1、当客户处理多个描述字(通常是交互式输入和网络套接口)时,必须使用I/O复用。
  2、如果一个服务器要处理多个服务或者多个协议(例如既要处理TCP,又要处理UDP),一般就要使用I/O复用。
  三、支持I/O复用的系统调用
  目前支持I/O复用的系统调用有select、pselect、poll、epoll:
  1、select函数
  该函数允许进程指示内核等待多个事件中的任何一个发生,并仅在有一个或多个事件发生或经历一段指定的时间后才唤醒它。
  格式为: 6 返回:就绪描述字的正数目,0-超时,-1-出错
  我们从该函数的最后一个参数开始介绍,它告知内核等待所指定描述字中的任何一个就绪可花多少时间。其timeval结构用于指定这段时间的秒数和微秒数。
  struct timeval{
  long tv_sec; //seconds
  long tv_usec; //microseconds
  };
  这个参数有三种可能:
  (1)永远等待下去:仅在有一个描述字准备好I/O时才返回。为此,我们把该参数设置为空指针。
  (2)等待一段固定时间:在有一个描述字准备好I/O时返回,但是不超过由该参数所指向的timeval结构中指定的秒数和微秒数。
  (3)根本不等待:检查描述字后立即返回,这称为轮询。为此,该参数必须指向一个timeval结构,而且其中的定时器值必须为0。
  中间的三个参数readset、writeset和exceptset指定我们要让内核测试读、写和异常条件的描述字。如果我们对某一个的条件不感兴趣, 就可以把它设为空指针。struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符,可通过以下四个宏进行设置:
  void FD_ZERO(fd_set *fdset); //清空集合
  void FD_SET(int fd, fd_set *fdset); //将一个给定的文件描述符加入集合之中
  void FD_CLR(int fd, fd_set *fdset); //将一个给定的文件描述符从集合中删除
  int FD_ISSET(int fd, fd_set *fdset); // 检查集合中指定的文件描述符是否可以读写 ?
  目前支持的异常条件只有两个:
  (1)某个套接口的带外数据的到达。
  (2)某个已置为分组方式的伪终端存在可从其主端读取的控制状态信息。
  第一个参数maxfdp1指定待测试的描述字个数,它的值是待测试的最大描述字加1(因此我们把该参数命名为maxfdp1),描述字0、1、2...maxfdp1-1均将被测试。
  一个应用select的例子: 注:本章内容摘自<Unix 网络编程>第六章。 2、pselect函数
  pselect函数是由POSIX发明的,如今许多Unix变种都支持它。 6 返回:就绪描述字的个数,0-超时,-1-出错
  pselect相对于通常的select有两个变化:
  1、pselect使用timespec结构,而不使用timeval结构。timespec结构是POSIX的又一个发明。
  struct timespec{
  time_t tv_sec; //seconds
  long tv_nsec; //nanoseconds
  };
  这两个结构的区别在于第二个成员:新结构的该成员tv_nsec指定纳秒数,而旧结构的该成员tv_usec指定微秒数。
  2、pselect函数增加了第六个参数:一个指向信号掩码的指针。该参数允许程序先禁止递交某些信号,再测试由这些当前被禁止的信号处理函数设置的全局变量,然后调用pselect,告诉它重新设置信号掩码。
  关于第二点,考虑下面的例子,这个程序的SIGINT信号处理函数仅仅设置全局变量intr_flag并返回。如果我们的进程阻塞于select调用,那 么从信号处理函数的返回将导致select返回EINTR错误。然而调用select时,代码看起来大体如下: 问题是在测试intr_flag和调用select之间如果有信号发生,那么要是select永远阻塞,该信号就会丢失。有了pselect后,我们可以如下可靠地编写这个例子的代码: 在测试intr_flag变量之前,我们阻塞SIGINT。当pselect被调用时,它先以空集(zeromask)取代进程的信号掩码,再检查描述 字,并可能进入睡眠。然而当pselect函数返回时,进程的信号掩码又被重置为调用pselect之前的值(即SIGINT被阻塞)。
  3、poll函数
  poll函数起源于SVR3,最初局限于流设备。SVR4取消了这种限制,允许poll工作在任何描述字上。poll提供的功能与select类似,不过在处理流设备时,它能够提供额外的信息。 4 返回:就绪描述字的个数,0-超时,-1-出错
  第一个参数是指向一个结构数组第一个元素的指针。每个数组元素都是一个pollfd结构,用于指定测试某个给定描述字fd的条件。
  struct pollfd{
  int fd; //descriptor to check
  short events; //events of interest on fd
  short revents; //events that occurred on fd
  };
  要测试的条件由events成员指定,而返回的结果则在revents中存储。常用条件及含意说明如下: poll函数可用的测试值
  常量 说明
  POLLIN 普通或优先级带数据可读
  POLLRDNORM 普通数据可读
  POLLRDBAND 优先级带数据可读
  POLLPRI 高优先级数据可读
  POLLOUT 普通数据可写
  POLLWRNORM 普通数据可写
  POLLWRBAND 优先级带数据可写
  POLLERR 发生错误
  POLLHUP 发生挂起
  POLLNVAL 描述字不是一个打开的文件
  注意:后三个只能作为描述字的返回结果存储在revents中,而不能作为测试条件用于events中。
  第二个参数nfds是用来指定数组fdarray的长度。
  最后一个参数timeout是指定poll函数返回前等待多长时间。它的取值如下: 一个使用poll的网络程序例子: 022 #endif /*即使都包含这两个文件,编译器也找不到,不知何解。索性自己定义了。*/
  074 client[0].events = POLLIN; /*将测试条件设置成普通或优先级带数据可读,此处书中为POLLRDNORM,
  075 但是怎么也编译不过去 ,编译器就是找不到,所以就临时改成了POLLIN这个条件,
  118 if( errno== ECONNRESET) //如果连接断开,就关闭连接,并设当前描述符不可用
  126 elseif(n == 0) //如果数据读取完毕,关闭连接,设置当前描述符不可用
  注:本章内容摘自<Unix 网络编程>第六章。 4、epoll 在linux的网络编程中,很长的一段时间都在使用select来做事件触发。然而select逐渐暴露出了一些缺陷,使得linux不得不在新的内核中 寻找出替代方案,那就是epoll。其实,epoll与select原理类似,只不过,epoll作出了一些重大改进,即:
  a、当它们所监听的集合中有状态发生改变时,select需要循环检查整个集合,才能确定那个文件描述符状态发生改变,进而进行操作;而epoll在添加 文件描述符到集合时,已经绑定了该文件描述符的对应函数,因此,当该文件描述符状态改变时,不需要循环查询整个集合,因而将复杂度由0(n)将为o (1),性能得到几何量级的提高,尤其是在大量连接的情况下。
  b、再有,select所监听的描述字最大数目是有一定限制的,由FD_SETSIZE设置,通常是1024。对于那些需要支持的上万连接数目的web服 务器来说显然是太少了,尽管可以通过修改头文件再重编译内核来扩大这个数目,不过资料也同时指出这样会带来网络效率的下降。(详细请参考:epoll 相对于poll的优点 )
  epoll的接口非常简单,一共就三个函数:
  (1)、int epoll_create(int size);
  创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。需要注意的是,当创建好epoll句柄后,epoll本身就占用一个fd值,所以用完后必须调用close()关闭,以防止fd被耗尽。
  (2)、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  epoll的事件注册函数,第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
  EPOLL_CTL_ADD:注册新的fd到epfd中;
  EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
  EPOLL_CTL_DEL:从epfd中删除一个fd;
  第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事件,struct epoll_event结构如下:
  struct epoll_event{
  __uint32_t events; //epoll events
  epoll_data_t data; //user data variable
  };
  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表示已超时。 令人高兴的是,2. 6内核的epoll比其2. 5开发版本的/ dev/ epoll简洁了许多,所以,大部分情况下,强大的东西往往是简单的。唯一有点麻烦是epoll有2种工作方式: LT和ET。
  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确认。 注:以上参考epoll精髓 一文。
  一个epoll的例子: 051 ("消息接收失败!错误代码是%d,错误信息是'%s'\n",
  069 * 科技站在巨人的肩膀上进步更快!感谢有开源前辈的贡献!
  编译此程序用命令:
  gcc -Wall epoll-server.c -o server
  运行此程序需要具有管理员权限!
  sudo ./server 7838 1
  通过测试这一个服务器可能同时处理10000 -3 = 9997 个连接!
  如果这是一个在线服务系统,那么它可以支持9997人同时在线,比如游戏、聊天等。
  参考网址:http://zhoulifa.bokee.com/6081520.html
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值