网络编程Epoll/Poll/Select详解

在这些开源项目中都有使用(redis、memcached/libevent、nginx)

Linux 文件描述符

  • linux中一切都是文件(普通文件、目录文件、链接文件、设备文件。。。)

  • 其中文件描述符fd(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,其值是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行 I/O 操作的系统调用都通过文件描述符。

  • 例如命令(g++ lots_of_errors 2>&1 | head) 其中2>&1中的 2 就是表示的「标准错误」,1 就是「标准输出」(0是「标准输入」),中间的 & 表示后面跟的数字是文件描述符而不是一个文件(不然所有的「标准错误」就重定向到一个名为 1 的文件中了)。

Select(1983)

I/O 多路复用这个概念被提出来以后, select 是第一个实现,其缺点为:

  • 描述符的数量限制,能监控的文件描述符最大为 FD_SETSIZE(宏定义以前是1024),对于连接数很多的场景就无法满足;

  • 每次调用 select 都需要从用户空间把描述符集合拷贝到内核空间,当描述符集合变大之后,用户空间和内核空间的内存拷贝会导致效率低下;

  • 每次调用 select 都需要在内核线性遍历文件描述符的集合,当描述符增多,效率低下。

Poll(1997)

  • 它能解决 select 对文件描述符数量有限制的问题,但是依然不能解决线性遍历以及用户空间和内核空间的低效数据拷贝问题。

Epoll(2002)

随着互联网的发展c10k问题出现,select/poll无法满足,这时候epoll登场了:

  • 1.针对文件描述符数量的限制,其实 poll 已经解决了,poll 使用的是链表的方式管理 socket 描述符,但不够高效,如果有百万级别的连接需要管理,如何快速的插入和删除就变得很重要,于是 epoll 采用了红黑树的方式进行管理,这样能保证在添加和删除 socket 时,有 O(log(n)) 的复杂度;

  • 2.针对用户态和内核态对文件描述符集合的拷贝,其实对于 select 来说,由于这个集合是保存在用户态的,所以当调用 select 时需要屡次的把这个描述符集合拷贝到内核空间。epoll 直接把这个集合放在内核空间进行管理,epoll 在内核空间创建了一颗红黑树,应用程序直接把需要监控的 socket 对象添加到这棵树上,直接从用户态到内核态了,而且后续也不需要再次拷贝了;【那些说用了共享内存mmap的都是瞎说】

  • 3.针对socket就绪后内核线性遍历文件描述符集合的问题,与 select 不同,epoll 使用了一个双向链表来保存就绪的 socket,这样当活跃连接数不多的情况下,应用程序只需要遍历这个就绪链表就行了,而 select 没有这样一个用来存储就绪 socket 的东西,导致每次需要线性遍历所有socket,以确定是哪些 socket 就绪了。这里需要注意的是,这个就绪链表保存活跃链接,数量是较少的,是需要从内核空间拷贝到用户空间。【那些说用了共享内存mmap的都是瞎说】

内核收包路径

  1. 网卡收到包

  2. 触发cpu中断信号 && 网卡通过DMA(direct memory access)特性把数据包存放到内存某处/通常是ringBuffer

  3. cpu收到中断信号后调用中断处理程序(网卡驱动程序)处理数据包

  4. cpu调用NAPI处理数据包,经过一系列内核代码,数据包来到内核协议栈

  5. 经过协议栈网络层/传输层

  6. 通过协议头信息找到对应的socket(内核维护了srcip,srcport,dstip,dstport -> socket的映射关系)

  7. 把数据包放到这个 socket 的接收队列(接收缓冲区)中,准备通知应用程序,socket 就绪

从 socket 到应用程序

  1. socket结构包含一个等待队列结构(包括同步和异步等待两个队列,存放的是关注这个 socket 上的事件的进程,等待队列中的进程会处于阻塞状态,异步等待队列中的进程不会阻塞);

  2. 当 socket 就绪后(接收缓冲区有数据),那么就会 wake up 等待队列中的进程,通知进程 socket 上有事件,可以开始处理了;

  3. 对于epoll稍有差异,socket 在添加到这棵 epoll 树上时,会在这个 socket 的 wait queue 里注册一个回调函数,当有事件发生的时候再调用这个回调函数(而不是唤醒进程);

  4. 这个回调函数会把这个 socket 添加到 eventpoll 实例中的就绪链表上,也就是 rdllist 上,并唤醒 epoll_wait,通知 epoll 有 socket 就绪,并且已经放到了就绪链表中;

  5. epoll_wait 内会调用到 ep_send_events_proc 这个函数,这个函数是用来把就绪链表中的内容复制到用户空间,向应用程序通知事件。【那些说用了共享内存mmap请看看源码】

Accept的惊群效应

什么是惊群,如果一个 socket 上有多个进程在同时等待事件,当事件触发后,内核可能会唤醒多个或者所有在等待的进程,然而只会有一个进程成功获取该事件,其他进程都失败,这种情况就叫惊群,会一定程度浪费 cpu,影响性能。

  • accept 事件属于可读事件的一种,当 socket 有可读事件达到后,epoll_wait 获取到就绪的 socket,应用程序开始处理可读事件,如果这个 socket 的 fd 等于 listen() 的 fd,说明有新连接到达,(server)开始调用 accept() 处理连接。

  • accept() 返回的新的 socket 对象,对应与 client 的一个新的连接,应用程序需要把这个新的 socket 对象注册到 epoll 红黑树上,并且添加关心的事件(EPOLLIN/EPOLLOUT…),然后开始 epoll 循环。

  • 对于 accept() 来说,通常我们会使用多线程或者多进程的方式来监听同一个 listen fd,此时,就很可能发生惊群效应。

epool水平触发LT/边沿触发ET

(水平触发)与 ET(边沿触发)是电子信号里面的概念。比如:event = EPOLLIN | EPOLLLT,将 event 设置为 EPOLLIN 与水平触发。只要 event 为 EPOLLIN 时就能不断调用 epoll 回调函数。比如: event = EPOLLIN | EPOLLET,event 如果从 EPOLLOUT 变化为 EPOLLIN 的时候,就会触发,在此情形下,变化只发生一次,故只调用一次 epoll 回调函数。关于水平触发与边沿触发放在 epoll 回调函数执行的时候,如果为 EPOLLET(边沿触发),与之前的 event 对比,如果发生改变则调用 epoll 回调函数,如果为 EPOLLLT(水平触发),则查看 event 是否为 EPOLLIN, 即可调用 epoll 回调函数。

(639条消息) 网络编程Epoll/Poll/Select详解_~一叶、的博客-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值