epoll

recv 阻塞

  • 操作系统维护有 运行进程队列

  • 创建一个socket后,这个socket包括数据发送和接收的缓冲区、进程等待队列

  • 当调用recv(...) 时,当前进程从运行队列 改变到 该socket的等待队列,不会阻塞 cpu,同时等待着收到数据后,重新进入运行队列

  • 当socket接收到数据后,操作系统将该socket等待队列上的进程重新放回到工作队列,该进程变成运行状态,继续执行代码。也由于socket的接收缓冲区已经有了数据,recv可以返回接收到的数据。

网卡接收网络数据

  1. 网卡收到网线传输来的网络数据,会先写到内存里

  2. 把数据写入到内存后,网卡向cpu发出一个中断信号,通知cpu有数据到达,cpu执行中断程序。(ps: 中断的优先级是很高的)

  3. 此处的中断程序主要有两项功能:

    1. 先将网络数据写入到对应socket的接收缓冲区里面

    2. 再唤醒进程,重新将进程放入工作队列

  4. 操作系统如何知道网络数据对应于哪个socket?因为一个socket对应着一个端口号,而网络数据包中包含了ip和端口的信息,内核可以通过端口号找到对应的socket

  5. 如何同时监视多个socket的数据?select、poll、epoll。

select

流程

  1. 设置所有需要监听的 socket fd

  2. 先遍历一遍,如果某个 socket 有数据,直接返回,不会阻塞。

  3. 如果都没有数据,把当前进程加入到设置的 各个socket的等待队列,把进程从运行队列移除,当前进程阻塞。

  4. 当任何一个socket收到数据后,中断程序将唤起进程,将进程从所有socket的等待队列中移除,加入到工作队列里面。

  5. 当进程A被唤醒后,它知道至少有一个socket接收了数据,但还不知道具体是哪个。程序还需遍历一遍socket列表,就可以得到就绪的socket。

缺点

  • 每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个fds列表传递给内核,有一定的开销

  • 因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,默认只能监视1024个socket。

  • 进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次。

epoll

  • epoll要解决的就是 select中的问题,减少遍历,保存就绪socket

流程

  1. epoll初始化用epoll_create创建一个event_poll对象,这个对象有就绪列表(双向链表),红黑树,等待列表。就绪列表存放就绪的socket,红黑树存放所有正在监听的socket引用,等待列表放正在等待的进程。

  2. 用epoll_ctl添加或删除所要监听的socket,内核会将eventpoll添加到所有设置的socket的等待队列中。

  3. 当socket收到数据后,中断程序会操作eventpoll对象(而不是直接操作进程,这是和select的重要区别),给eventpoll的就绪列表添加socket引用

  4. eventpoll对象相当于是socket和进程之间的中介,socket的数据接收并不直接影响进程,而是通过改变eventpoll的就绪列表来改变进程状态。

  5. 当程序执行到epoll_wait时,

    1. 如果就绪队列已经引用了socket,那么epoll_wait直接返回。

    2. 如果就绪队列为空,把进程放到eventpoll的等待队列,阻塞进程。

  6. 当socket接收到数据,中断程序一方面修改就绪队列,另一方面唤醒eventpoll等待队列中的进程,进程再次进入运行状态。

参考:

罗培羽 https://zhuanlan.zhihu.com/p/63179839arrow-up-right

最后更新于