前言
开发网络服务器,最核心的难题就是如何高效处理大量的客户端并发连接。传统方案是每来一个客户端就创建一条线程,但连接数量暴涨之后,大量线程会带来巨大的内存开销与CPU调度压力,服务器极易过载。Linux下一共存在4种IO模型,IO多路复用是实现高并发的主流技术,select、poll、epoll都属于多路复用,其中epoll性能最为突出,Nginx、Redis这类高性能服务底层都依赖epoll。本篇梳理4类IO模型,讲解多路复用核心原理以及epoll整套工作逻辑,帮助理解TCP并发服务器底层。
一、TCP并发:多线程模型
最简单的并发思路:每接入一个新客户端,新建一条独立线程专门负责和客户端通信。
局限:线程池只能缓解问题,无法支撑上万规模的海量连接,这时就要依靠IO多路复用。
二、Linux四种IO模型
1. 阻塞IO
当进程发起IO调用,如果没有就绪数据,操作系统直接将进程挂起,让出CPU资源,不会消耗CPU;等到数据准备完成之后,调用才会返回。
- 优点:CPU利用率高,无数据时不占用CPU。
- 缺点:一条线程同一时刻只能处理一个文件描述符,想要处理大量连接就需要大量线程。
2. 非阻塞IO
把文件描述符设置成非阻塞模式。调用读写接口,没有数据的时候不会阻塞,直接返回错误。应用程序需要不停循环调用系统接口,轮询检查是否有数据到来。
- 优点:单线程可以管理多个fd。
- 缺点:轮询会持续占用CPU,即便没有数据也要不断执行代码,CPU占用很高,实际项目很少单独使用。
3. 信号驱动IO
属于异步IO的雏形。
缺点:信号机制不可靠,事件容易丢失;业务写在信号处理函数极易产生bug,几乎不会在正式项目使用。
4. IO多路复用
核心思想:单线程,委托内核同时监视一大批文件描述符。只要集合中任意一个fd事件就绪,函数就返回,用户程序再去处理已经就绪的fd。
代表实现:select、poll、epoll。
select
select接收一组文件描述符集合,集合内任意fd产生事件,select就解除阻塞返回。
select会直接修改传入的fd集合:会清除掉没有事件发生的fd,集合里面只会保留就绪的fd。所以每一轮循环,都必须重新装填fd集合。
配套操作宏:
- FD_ZERO:清空整个fd集合
- FD_SET:把指定fd加入集合
- FD_CLR:将指定fd从集合移除
- FD_ISSET:判断fd是否还在就绪集合内
select函数参数解读:
返回值含义:
- 大于0:已经就绪的文件描述符数量;
- -1:调用出错;
- 0:超时,全程没有任何事件产生。
select的固有短板
三、epoll:Linux高性能多路复用
epoll是Linux独有的多路复用实现,解决select的性能缺陷。内核内部维护红黑树,用来存储所有需要监听的文件描述符。一共三个核心接口:epoll_create、epoll_ctl、epoll_wait。
1. epoll_create
功能:创建epoll实例,在内核生成事件监听表。
- size:早期用于提示内核事件表预估大小,新版Linux中该参数已经被忽略。
- 返回值:成功返回epoll自身的文件描述符epfd;失败返回-1。
2. epoll_ctl
专门操作epoll内核事件表,实现添加、修改、删除监听事件。
参数:
- EPOLL_CTL_ADD:向内核事件表新增fd与监听事件
- EPOLL_CTL_MOD:修改已经注册fd的监听事件
- EPOLL_CTL_DEL:从事件表删除fd
返回:成功返回0,失败返回-1。
epoll_event结构:内部包含事件标记events,以及联合体data。data.fd用来保存文件描述符,epoll_wait返回事件的时候,会原样把data返回给用户,业务依靠这个fd定位就绪连接。
常用事件标志:
- EPOLLIN:读事件,接收缓冲区有可读数据;新客户端连接到来也会触发该事件;
- EPOLLOUT:写事件,发送缓冲区有空余空间,可以向外发送数据;
- EPOLLET:开启边缘触发模式;默认是水平触发LT。
重要坑点:调用epoll_ctl添加fd的时候,event.data.fd必须和第三个传入参数fd保持一致,否则epoll_wait拿到错误fd,业务逻辑完全错乱。
3. epoll_wait
阻塞等待IO事件,内核将就绪事件拷贝到用户空间数组。
参数:
返回值:
- 大于0:本次就绪的fd数量nready,循环只遍历0 ~ nready-1,不要遍历整个数组;
- -1:接口调用出错;
- 0:超时,没有任何事件发生。
四、LT水平触发 与 ET边缘触发
LT水平触发(默认模式)
只要缓冲区还有未读完的数据,每一次epoll_wait都会上报该事件。就算本次没有读完全部数据,下一轮epoll_wait依旧会通知。编码简单,不容易丢失事件。
ET边缘触发 EPOLLET
只有IO状态发生改变的那一瞬间,才通知一次。例如新数据到达,只会触发一次通知;如果本次没有把缓冲区全部读完,剩余数据不会再产生事件通知。
ET模式必须把socket设置为非阻塞,循环读取直到读到EAGAIN,一次性读完缓冲区全部内容。性能更高,但编码复杂度上升。
五、epoll TCP服务器完整业务流程
- 如果就绪fd是监听sockfd:调用accept获取新客户端confd,将confd加入epoll监听;
- 如果是普通客户端confd:执行read读取数据,处理业务逻辑;
- 捕获EPOLLHUP、EPOLLERR事件:代表客户端断开连接,把fd从epoll删除,调用close关闭文件描述符。
六、开发高频踩坑总结
小结
IO多路复用本质是单线程委托内核批量监控大量文件描述符。
- select实现简单,但是性能上限低,存在fd数量限制;
- epoll依靠内核红黑树、事件回调机制,在海量连接场景性能优异,是Linux网络开发必须掌握的技术。
注意:多路复用只是优化IO等待过程,内部read/write依旧属于同步IO,并不是真正的异步IO。
网硕互联帮助中心



评论前必须登录!
注册