从头盘一盘多路复用(1)epoll

404 阅读4分钟

Redis、Nginx、Tomcat、JavaNio、Netty无一都用到了多路复用提高性能,然而这一部分涉及到操作系统网卡等内核或者硬件的知识,导致我的理解一直是云里雾里的,所以我就死磕一定要弄明白这个东西,不然不是没办法实现我买房收租家里蹲的梦想了!今儿就先从根儿盘一盘select、poll、epoll这些到底是个啥玩意儿。

我是看了这个系列文章理了自己的思路:

1. socket基本接收流程

    1. 网卡收到网线传来的数据之后,会将数据通过硬件电路传输,将数据会存入内存中的某个地址中,以供操作系统读取。
    1. 在数据写入内存之后,网卡向cpu发出一个中断信号(这个中断信号cpu会优先响应,运行一段针对网卡的中断程序,去相应网卡的中断信号)。
    1. 当有请求来的时候,其实是会创建一个socket对象,这个socket对象类似于文件描述符,会交给文件系统管理(所以可以通过查看进程占有的文件描述符来看socket是否过多),socket中有发送缓冲区、接收缓冲区、等待列表。连接建立后,监听的socketserver的进程,从内核空间的的工作队列中移动到socket的等待队列中,阻塞监听进程。等待数据的到来。

2. recv

使用recv方法,在socket接收到数据之后,操作系统就会将socket的等待队列中等进程移除,重新放入内核空间的工作队列中,这时候调用recv方法,就会返回接收到的数据。

recv的缺点就是他是单线程的,他只能监视一个socket,处理完一个socket连接之后,才能处理下一个socket连接。

3. select

select的模型能够处理多个socket的监视,他其实也就是监视多个socket,但是处理也都是在同一个线程里处理的。

他的实现的思路其实也是比较简单的,直接用一个数组存放所有的socket,然后一旦有任何一个socket的接收到数据了之后,就通知监听的进程,然后进程会遍历所有的socket,读取数据处理。

调用recv和调用select的对应的中断程序内容是不一样的,当有数据接收时,网卡会出发不同的中断程序,将监听进程唤醒加入到内核的工作队列中,并且把所有socket中的等待队列的监听进程移除。

select的模型实现了监听多个socket,但是这里有性能的损耗:

  1. 调用select的时候,将监听进程添加到所有的socket的等待队列,这是一次遍历。
  2. 当有任何一个socket中有数据来时,需要把监听程序从所有的socket等待队列中移除,这是第二次遍历。
  3. 当监听进程被cpu执行,需要遍历所有的socket,去判定哪一个socket接收到了数据,这是第三次遍历。

所以根据上面说法,会有三次遍历,所以同时监听的socket数量有上限,不能过大。

4. epoll

epoll解决了select的不停的遍历的过程,当然也是有对应的中断程序。只不过是中断程序的操作对象不一样。

这里通过epoll_create()让内核创建了一个eventpoll的对象,也就是上面的epfd,这个也是交给文件系统管理的,里面也有一个等待队列。

这个eventpoll对象里面,维护了一个等待队列,存放所有需要监视的socket。

当程序运行到epoll_wait时,需要获取socket中的数据,这个时候就会将进程阻塞等待数据到来,这时候会把进程引用添加到eventpoll的等待队列中,将进程从工作队列中移出。

当有某一socket接收到了传来的数据,会发送中断给cpu,cpu处理中断程序,中断程序一方面会将socket添加到就绪队列中,一方面会唤醒eventpoll等待队列的进程,让进程再次进入工作队列中执行,因为有了就绪队列,所以程序直接就能知道哪些socket已经就绪,可以直接读取数据,不需要再遍历所有socket。

所以epoll通过eventpoll的对象,分离了socket列表的维护和进程的阻塞。

  • 通过创建socket的监视列表和就绪列表,避免了遍历所有的socket,进程可以直接拿就绪队列的socket直接读取数据。
  • 通过创建统一的等待队列,避免了进程需要添加到所有的socket的等待队列中。