简单概述
select,poll,epoll都是用来实现IO多路复用的机制,在Linux网络模型中对应着IO复用模型Unix上的IO模型
select:最大支持1024个文件描述符,在描述符较多情况下性能较差,水平触发
poll:poll与select基本相同,只是没有文件描述符的限制,水平触发
epoll:文件描述符为系统上限,在描述符较多情况下性能较好,同时支持水平与边缘触发
水平触发与边缘触发
水平触发:只要文件描述符关联的读内核缓冲区非空,有数据可以读取,就一直发出可读信号进行通知,当文件描述符关联的内核写缓冲区不满,有空间可以写入,就一直发出可写信号进行通知,如果系统中有大量不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率
边缘触发:当文件描述符关联的读内核缓冲区由空转化为非空的时候,则发出可读信号进行通知,当文件描述符关联的内核写缓冲区由满转化为不满的时候,则发出可写信号进行通知。如果这次没有把数据全部读写完(如读写缓冲区太小),它不会再次通知你,直到该文件描述符上出现第二次可读写事件才会通知你,这种模式比水平触发效率高,系统不会充斥大量不关心的就绪文件描述符
使用Linux epoll模型的水平触发模式,当socket可写时,会不停的触发socket可写的事件,如何处理?当需要向socket写数据时,将该socket加入到epoll等待可写事件。接收到socket可写事件后,调用write或send发送数据,当数据全部写完后, 将socket描述符移出epoll列表,这种做法需要反复添加和删除。边缘模式就可以直接搞定,并不需要用户层程序的补丁操作。
select
Linux将进程分类为两个队列,阻塞队列和工作队列。网络应用程序调用linux read读取数据时,如果没有数据就一直阻塞。假设当进程执行到read时,A进程就从工作队列中移到该socket的等待队列中,A进程就被阻塞。阻塞期间,如果来数据了,中断处理就会将数据装入接收队列,然后唤醒A进程,移动到工作队列中。
read阻塞原理:进程分为“运行”和“等待”等几种状态。当进程 A 执行到创建 socket 的语句时,操作系统会创建一个由文件系统管理的 socket 对象。这个 socket 对象包含了发送缓冲区、接收缓冲区与等待队列等成员。当程序执行到 read 时,操作系统会将进程 A 从工作队列移动到该 socket 的等待队列中,那么进程A就不会被执行,也不会占用 CPU 资源。当 socket 接收到数据后,操作系统将该 socket 等待队列上的进程重新放回到工作队列,该进程变成运行状态
如何从阻塞队列移到工作队列:网卡将数据写入内存后,网卡产生一个中断,处理器立即停止它正在做的事,然后跳转到内存中预定义的中断程序开始执行。
但是每个read方法只能监控一个socket,select的原理就是使用一个数据存放socket,如果数组(代码中写死了数组长度为1024)中的所有socket都没有数据,进程就被挂起,直到有socket收到数据,唤醒进程。
假如程序同时监视如下图的 sock1、sock2 和 sock3 三个 socket,那么在调用 select 之后,操作系统把进程 A 分别加入这三个 socket 的等待队列中。
当任何一个 socket 收到数据后,中断程序将唤起进程,所谓唤起进程,就是将进程从所有的等待队列中移除,加入到工作队列里面
当进程 A 被唤醒后,它知道至少有一个 socket 接收了数据。程序只需遍历一遍 socket 列表,就可以得到就绪的 socket。
缺点:每次调用select都需要将进程加入到所有要监控的socket的等待队列中,每次唤醒都要从所有的队列中移除;被唤醒后,进程并不知道哪些socket有数据,需要遍历。
epoll
epoll将等待队列和阻塞进程分开了,使用epoll_ctl维护等待队列,使用epoll_wait阻塞进程,epoll内部维护了一个就绪队列,收到数据的socket直接加入就绪队列,当 epoll 监听的 socket 状态发生改变(变为可读或可写)时,就会把就绪的 socket 添加到就绪队列中,当进程被唤醒,只要获取就绪队列就能知道哪些socket收到数据了。epoll内部使用红黑树保存所有监听的socket,添加和查找元素的时间复杂度为 O(log n)
当某个进程调用 epoll_create 方法时,内核会创建一个 eventpoll 对象。eventpoll 对象也是文件系统中的一员,和 socket 一样,它也会有等待队列。
rdllist: 保存已经就绪的文件列表。
rbr: 使用红黑树来管理所有被监听的文件。红黑树节点是epitem
创建 epoll 对象后,可以用 epoll_ctl 添加或删除所要监听的 socket。以添加 socket 为例,如果通过 epoll_ctl 添加 sock1、sock2 和 sock3 的监视,内核会将 eventpoll 添加到这三个 socket 的等待队列中。
当 socket 收到数据后,中断程序会给 eventpoll 的“就绪列表”添加 socket 引用。如下图展示的是 sock2 和 sock3 收到数据后,中断程序让 rdlist 引用这两个 socket。
eventpoll 对象相当于 socket 和进程之间的中介,socket 的数据接收并不直接影响进程,而是通过改变 eventpoll 的就绪列表来改变进程状态。
当 socket 接收到数据,中断程序一方面修改 rdlist,另一方面唤醒 eventpoll 等待队列中的进程,进程 A 再次进入运行状态(如下图)。也因为 rdlist 的存在,进程 A 可以知道哪些 socket 发生了变化。
网友评论