IO复用的总结及一些问题

  在前面的文章中,分别介绍了常用的三种实现IO多路复用的函数:selectpollepoll今天对主要是对这三个函数的总结,以及对一些io复用的问题的总结。

总结

系统调用 select poll epoll
事件集合 用户通过3个参数(readset、writeset,exceptset)分别传入感兴趣的可读、可写及异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪事件。这使得用户每次调用select都要重置这3个参数 同一处理所有事件类型,因此只需要一个事件集参数(struct pollfd *fds)。用户通过fd.events传入感兴趣的事件,内核通过修改pollfd.revents反馈其中就绪的事件 内核通过一个事件表直接管理用户感兴趣的所有事件。因此每次调用epoll_wait时,无需反复传入用户感兴趣的事件。Epoll_wait系统调用的参数events仅用来反馈就绪的事件
应用程序索引就绪文件描述符的时间复杂度 O(n) O(n) O(1)
最大支持的文件描述符数 一般有最大值限制:1024 65535 系统允许打开的最大文件描述符数目(cat/proc/sys/fs/file-max) 65535 系统允许打开的最大文件描述符数目(cat/proc/sys/fs/file-max)
工作模式 LT LT 支持ET高效模式
内核实现和工作效率 采用轮询的方式来检测就绪事件,算法的时间复杂度为O(n) 采用轮询的方式来检测就绪事件,算法的时间复杂度为O(n) 采用回调的方式来检验就绪事件,算法的事件复杂度为O(1)

注意
  ①、65535,限制的是返回的文件描述符的最大个数,而不是服务器允许连接的最大个数,因为服务器连接时,消耗的是文件描述符而不是端口号;
  ②、端口号是unsigned short int 类型的,所以端口号的范围是[0,65536]。绑定-1端口的话,其实绑定的是最大的端口号,因为-1的无符号数表示的就是最大值。当我们绑定0号端口的话,系统随机返回一个可用的端口号;
  65535个端口限制与意义:  

对于客户端来说,是指对外发起不同连接的最大个数;
对于服务器来说,是指对外提供不同监听的最大个数。

  ③、select监听的文件描述符之所以是1024,是因为select采用用户态和内核态的双层轮询,耗cpu所以不能太大,以2^n存储,1024正好合适。
  
Q1:IO复用是同步还是异步?
  关于同步和异步的概念,以及IO模型,不了解的可以参考IO模型。同步和异步是针对消息的通知机制而言,同步是需要主动等待消息通知,而异步则是被动接收消息通知,通过回调、通知、状态等方式来被动获取消息。IO多路复用中(以select为例)阻塞到select函数调用时,用户进程是主动等待并调用select函数获取数据就绪状态消息,并且其进程状态为阻塞。所以,把IO多路复用归为同步阻塞模式。

Q2:epoll的EPOLLONESHOT事件?
  epoll模型的ET模式一般来说只触发一次,然而在并发程序中一个socket上的事件还是有可能被触发多次,例如:当epoll_wait已经检测到socket描述符fd1就绪,并通知一个线程去处理fd1的就绪书剑,在处理过程中该fd1又有新的数据可读,会唤醒其他线程对fd1进行操作,那么就出现了两个工作线程同时处理同一个socket的情况,这就会引发数据的不一致性等问题。应该是socket在任意连接的时候都只能被同一个线程去处理。当然,我们可以通过使用epoll的EPOLLONESHOT事件来实现。
  对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多只触发其上注册的一个可读,可写,异常事件,且只触发一次.除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONESHOT事件. 所以当一个线程去处理某个socket的时候,其他线程是没有机会来操纵该socket的。当该线程处理完socket上的事件后,应该重置该socket上的EPOLLONESHOT事件,这样,当该socket下次可读时,EPOLLIN事件可以被触发,从而让其它的工作线程可以去处理该socket。

Q3:为什么说epoll比select和poll高效?
  ①、select/poll返回时,内核会直接修改出传入事件集合,用于标示哪些事件就绪。所以每次调用时都要传递监控的所有socket给select/poll系统调用,也就是说要将用户态的socket列表拷贝到内核态,当监听的文件描述符数量上万的话每次都要拷贝几十几百KB的内存到内核态,效率很低。而调用epoll_wait时不用每次调用都向内核拷贝事件描述信息,它在内核中维护一个事件表,并提供一个独立的系统调用epoll_ctl来控制往其中添加 删除 修改事件。这样每次epoll_wait调用都直接从该内核事件表中取得用户注册的事件,而无须反复从用户空间读取这些事件文件描述符就会被注册到内核事件中,只有当有新的socket需要监听时,才会从用户空间拷贝。
  ②、select和poll采用的是轮询的机制,每次都需要将扫描一遍所有的文件描述符,并从其中找出就绪的文件描述符返回给用户。这样的话时间复杂度是O(n)。而epoll_wait则不同,它采用的是回调的机制。内核维护了一个双链表,用户存储发生的事件;当内核检测到就绪的文件描述符后,就会触发回调函数,将就绪的文件描述符对应的事件插入到内核就绪链表中。当epoll_wait调用时,仅仅观察这个list链表里有没有数据。有数据就返回,没有数据就sleep,等到超时t时间到后即使链表没数据也返回。它的时间复杂度是O(1)。
  ③、select和poll只能工作在低效的LT模式下,epoll支持高效的ET模式,在这种模式下,可以减少epoll_wait的调用。并且epoll还支持EPOLLONESHOT事件,该事件能进一步的减少可读、可写、异常等事件被触发的次数。
  
  但是,当活动连接数比较多的情况下,epoll并不一定比select和poll高效,因为在这个时候,回调函数被触发的比较频繁,需要不断的压栈出栈操作。epoll用于处理大量链接中,少量活动连接的情况时比较高效。

猜你喜欢

转载自blog.csdn.net/z_ryan/article/details/80910610