文章目录
Linux IO模型是指Linux操作系统中用于实现输入输出的一种机制。
Linux IO模型主要分为五种:阻塞IO、非阻塞IO、IO复用、信号驱动IO和异步IO。
- 阻塞IO是最常见的IO模型,当用户进程发起一个IO请求后,内核会一直等待,直到IO操作完成并返回结果。在此期间,用户进程会被阻塞,无法进行其他操作。
- 非阻塞IO是在阻塞IO的基础上进行改进的一种IO模型。当用户进程发起一个IO请求后,内核会立即返回一个错误码,表示IO操作还未完成。用户进程可以继续进行其他操作,随后再通过轮询的方式来查询IO操作是否完成。
- IO复用是指通过select、poll、epoll等系统调用来监听多个文件描述符的IO事件。当某个文件描述符就绪时,内核会通知用户进程进行IO操作。相比于阻塞IO和非阻塞IO,IO复用可以同时监听多个文件描述符,提高了IO效率。
- 信号驱动IO是指用户进程通过signal或sigaction系统调用来注册一个信号处理函数,当IO操作完成时,内核会向用户进程发送一个SIGIO信号,用户进程在信号处理函数中进行IO操作。相比于阻塞IO和非阻塞IO,信号驱动IO可以避免用户进程被阻塞,提高了IO效率。
- 异步IO是指当用户进程发起一个IO请求后,内核会立即返回,表示IO操作已经开始。当IO操作完成后,内核会通知用户进程,用户进程在此时才进行IO操作。相比于其他IO模型,异步IO可以避免用户进程被阻塞,提高了IO效率。
本篇文章要学习的是IO复用的三种模式。
- select、poll、epoll三种IO复用模式对比
对比项 select poll epoll 事件对象存储方式 位图 链表+数组 红黑树 底层实现 轮询:每次调用需要从内存拷贝全部事件到用户空间 轮询:每次调用需要从内存拷贝全部事件到用户空间 回调通知方式,每次调用只需从内存拷贝就绪事件到用户空间 最大连接数 1024 理论上无上限(由系统资源池决定) 理论上无上限(由系统资源池决定) 是否适用于高并发场景 否。随着连接数增加,性能线性下降。 否。随着连接数增加,性能线性下降。 是。随着连接数增加,性能无明显递减。 编程难度 低 中 高
- epoll机制效率高,适用于高并发场景,所以epoll机制广泛用于各种开源项目。
- select机制编程简单,如果对高并发没有需求,那么很多人会选择select机制做多路IO请求处理。
- poll机制既没有高并发能力,编程也并不简单,所以使用场景比较少。
彻底弄懂IO:https://blog.csdn.net/qq_41822345/article/details/124653958
相关链接1:https://mp.weixin.qq.com/s/ZuqW-SCPZpydjYTl6EvL8g
相关链接2:https://mp.weixin.qq.com/s/BFbnF1YU1jJXHjc7zYT18w
相关链接3:https://mp.weixin.qq.com/s/Tmr49E8vya1cK4un2OXmQQ
相关链接4:https://mp.weixin.qq.com/s/2jlr4BvUlLHBbyDu672PMg
一、Select
1、select简介
-
概念:select机制是一种I/O多路复用技术,它可以同时监视多个文件描述符,当某个文件描述符就绪(一般是读写就绪)时,就会通知程序进行相应的操作。
-
优点:select机制的优点是可以同时处理多个连接,避免了大量的线程或进程切换,提高了系统的并发性能。
2、select实现原理
在Linux系统中,select机制可以通过select函数来实现。select函数的参数包括需要监视的文件描述符集合、超时时间等。当select函数返回时,程序可以通过遍历文件描述符集合来确定哪些文件描述符已经就绪,然后进行相应的操作。
- 下面这张原理图把select主要活动轨迹都进行了描述。
-
select核心实现原理是位图,select总共有三种位图,分别为读,写,异常位图,用户程序预先将socket文件描述符注册至读,写,异常位图,然后通过select系统调用轮询位图中的socket的读,写,异常事件。
- select位图为1024比特位图,通过整型数组模拟而成。
- select位图数组长度为16,每个数组元素为8字节,一个字节为8比特,位图大小=16 * 8 * 8 = 1024比特。
- select位图每个比特对应一个文件描述符数值。
-
select底层通过轮询方式获取读,写,异常位图中注册的socket文件事件,如果检测到有socket文件处于就绪状态,则会将socket对应的事件设置到输出位图,等所有位图中的socket都被轮询完,会统一将输出位图通过copy_to_user函数复制到输入位图,并且覆盖掉输入位图注册信息(也就是用户初始化的位图被内核修改)。
-
select轮询完所有位图,如果未检测到任何socket文件处于就绪状态,根据超时时间确定是否返回或者阻塞进程。
-
socket检测到读,写,异常事件后,会通过注册到socket等待队列的回调函数poll_wake将进程唤醒,唤醒的进程将再次轮询所有位图。
-
select返回时会将剩余的超时时间通过copy_to_user覆盖原来的超时时间。
3、select编程
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
## 功能:
select函数是Linux系统中的一种I/O多路复用机制,它可以同时监视多个文件描述符。
## 参数:
nfds:最大文件描述符+1。
readfds:读文件描述符集合,可设置为NULL。
writefds:写文件描述符集合,可设置为NULL。
exceptfds:异常文件描述符集合,可设置为NULL。
timeout:超时时间,设置为NULL为阻塞模式,
## 返回值:
成功:返回检测到的文件描述符数量。
失败:返回-1,设置errno。
超时:返回0。
- select编码流程
4、select常见问题
问题1:select函数最大文件描述(maxfd)有什么作用?
- select使用1024比特位图监测最多1024个文件描述符,然而实际的情况很少会到达1024文件描述符上限,使用maxfd可以避免每次都轮询1024个文件描述符,从而提高轮询效率。
- maxfd通常设置为已打开最大文件描述符+1,目的是为了保证位图中每个文件描述符都被轮询到。
问题2:select优缺点有哪些?
优点:
- select支持多种文件描述符类型,包括socket、标准输入输出、管道、FIFO等。
- select是跨平台的,可以在多种操作系统上使用。
缺点:
- select的效率不高,每次调用select都需要将所有的文件描述符从用户态复制到内核态,这个过程比较耗时。
- select返回后需要遍历所有的文件描述符,找到就绪的文件描述符,这个过程也比较耗时。
- select支持的文件描述符数量有限,通常是1024个。
问题3:select为什么会有1024文件描述符限制?
- 进程默认打开最大文件描述符为1024(次要原因)。
- select采用轮询的方式获取读,写,异常事件,如果需要轮询的文件描述符比较多的话,select执行效率会非常低(个人观点)。
- select是通过一个整型数组来模拟位图,整型数组长度和元素大小已经固定,只能模拟出1024比特位图,使用select如果超过1024文件描述符的限制,可能会导致内存越界和其他未知问题(真正原因)。
问题4:select有哪些设计缺陷?
- select最大文件描述符为1024,所以无法满足高并发应用场景,高并发场景请使用epoll机制。
- select采用轮询的方式获取读,写,异常事件,轮询的方式效率低,不管文件事件是否就绪,都需要去做检测。
- select位图设置和获取采用覆盖方式,用户输入的读,写,异常位图会被内核修改,编程非常容易出错,可参考select实现原理图分析,所以每次调用select之前都要重新设置位图。
- select执行完后会返回剩余超时时间,剩余超时时间会覆盖原来的超时时间,导致超时机制异常。可参考select实现原理图分析,所以每次调用select之前都要重新设置超时时间。
二、poll
epoll机制效率高,适用于高并发场景,所以epoll机制广泛用于各种开源项目。
select机制编程简单,如果对高并发没有需求,那么很多人会选择select机制做多路IO请求处理。
poll机制既没有高并发能力,编程也并不简单,所以使用场景比其它两种要少。
1、poll简介
- 概念:poll是一种I/O多路复用机制,它可以同时监视多个文件描述符,当其中任意一个文件描述符就绪时,就会通知程序进行相应的读写操作。
- poll机制与select机制类似,但是poll没有最大文件描述符数量的限制,因此在文件描述符数量较大时,poll的效率会更高。
2、poll实现原理
poll机制的使用需要调用系统调用poll()函数,该函数会阻塞进程直到有文件描述符就绪或者超时。
poll()函数的参数是一个pollfd结构体数组,每个结构体中包含了一个文件描述符和该文件描述符所关注的事件类型。
- 下面这张原理图把poll主要活动轨迹都进行了描述。
- 用户将想要监听的socket文件绑定struct pollfd对象,并注册监听事件至struct pollfd对象events成员,监听多个socket文件使用struct pollfd数组。
- 用户通过struct pollfd数组注册poll事件至poll_list链表,poll_list链表单个元素可以存储固定数量的struct pollfd对象。
- poll系统调用采用轮询方式获取socket事件信息,一次poll调用需完成整个poll_list链表轮询工作,轮询socket的过程中会创建socket等待队列项,并加入socket等待队列(用于socket唤醒进程)。如果检测到socket处于就绪状态,将socket事件保存在struct pollfd对象的revents成员。
- poll系统调用完成一次轮询后,如果检测到有socket处于就绪状态,则将poll_list链表所有的struct pollfd通过copy_to_user拷贝至用户struct pollfd数组。如果未检测到有socket处于就绪状态,根据超时时间确定是否返回或者阻塞进程。
- socket检测到读,写,异常事件后,会通过注册到socket等待队列的回调函数poll_wake将进程唤醒,唤醒的进程将再次轮询poll_list链表。
3、poll编程
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
## 功能:
poll函数是Linux系统中的一种I/O多路复用机制,它可以同时监视多个文件描述符。
## 参数:
fds:监听事件结构体数组。【这个结构体包含三个属性:1、fd: 监听文件描述符。2、events:监听事件集合,用于注册监听事件。3、revents:返回事件集合,用于存储返回事件。】
nfds:监听事件结构体数组长度。
timeout:等于-1:一直阻塞。等于0:立即返回。大于0:等待超时时间,单位毫秒。
## 返回值【与select返回值一致】:
成功:返回检测到的文件描述符数量。
失败:返回-1,设置errno。
超时:返回0。
- poll编码流程
4、poll常见问题
问题1:poll的优缺点?
优点:
- poll没有1024最大文件描述符限制。
- poll监视事件(events)和返回事件(revents)分离,简化编程。
缺点:
- 采用轮询方式获取就绪文件描述符,效率低,和select一样。
- 每次调用poll都需要把所有文件描述符从内核空间复制到用户空间。
- 虽然poll没有1024最大文件描述符限制,但是注册的文件描述符越多,poll效率越低。
问题2:poll和select的区别?
poll和select底层实现非常相似,分析poll和select内核源码会发现二者之间很多地方都复用了相同的代码。
poll可以说是select的加强版,poll优化了select一些设计缺陷:
- poll不受1024最大文件描述符限制,poll采用poll_list链表方式存储输入和输出事件,理论上可以不受最大文件描述符限制。
- poll传入的是struct pollfd数组,并指定了数组长度,可以减少无效的轮询,提高轮询效率。
- poll监视事件(events)和返回事件(revents)分离,每次调用poll不需要重新设置struct pollfd对象。
- poll返回时不会返回剩余超时时间,用户不需要当心超时出现异常**。**
不过很可惜,即使poll对select做了很多优化,依然没有改变轮询方式,也没有改变selec执行效率低的本质问题。
三、epoll
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。
1、epoll简介
-
epoll可以理解为event poll,它是一种事件驱动的I/O模型,可以用来替代传统的select和poll模型。epoll的优势在于它可以同时处理大量的文件描述符,而且不会随着文件描述符数量的增加而降低效率。
-
epoll的接口和工作模式相对于select和poll更加简单易用,因此在高并发场景下被广泛使用。
2、epoll实现原理
epoll的实现机制是通过内核与用户空间共享一个事件表,这个事件表中存放着所有需要监控的文件描述符以及它们的状态,当文件描述符的状态发生变化时,内核会将这个事件通知给用户空间,用户空间再根据事件类型进行相应的处理。
- 下面这张原理图把epoll主要活动轨迹都进行了描述。
- socket等待队列
socket等待队列用于在socket接收到数据后添加就绪epoll事件节点和唤醒eventpoll等待队列项。
socket收到数据后,唤醒socket等待队列项,并执行等待队列项注册的回调函数ep_poll_callback,ep_poll_callback函数将就绪epoll事件节点添加至就绪队列,并唤醒eventpoll等待队列项。
- eventpoll等待队列
eventpoll等待队列用于阻塞当前进程,用于epoll_wait未检测到就绪epoll事件节点的情况。
epoll_wait检测就绪队列是否有epoll事件节点,没有epoll事件节点,则使用等待队列将当前进程挂起,后续ep_poll_callback函数会唤醒当前进程。
- 就绪队列
就绪队列用于存储就绪epoll事件节点,用户通过epoll_wait函数获取就绪epoll事件节点。
- 红黑树
红黑树用于存储通过epoll_ctl函数注册的epoll事件节点。
3、epoll编程
- ①、创建epoll文件
int epoll_create(int size);
## 功能:
epoll_create函数用于创建epoll文件。
## 参数:
size:目前内核还没有实际使用,只要大于0就行。
## 返回值:
成功:返回epoll文件描述符。
失败:返回-1,并设置errno。
- ②、增加,删除,修改epoll事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
## 功能:
epoll_ctl函数用于增加,删除,修改epoll事件,epoll事件会存储于内核epoll结构体红黑树中。
## 参数:
epfd:epoll文件描述符。
op:操作码【EPOLL_CTL_ADD:插入事件、EPOLL_CTL_DEL:删除事件、EPOLL_CTL_MOD:修改事件】
fd:epoll事件绑定的套接字文件描述符。
events:epoll事件结构体。
## 返回值:
成功:返回0。
失败:返回-1,并设置errno。
- epoll事件处理原则:epoll_wait获取epoll事件 = 注册epoll事件 & 就绪epoll事件
- ③、 epoll事件就绪
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
## 功能:
epoll_wait用于监听epoll事件。
## 参数:
epfd:epoll文件描述符。
events:epoll事件数组。
maxevents:epoll事件数组长度。
timeout:超时时间,【小于0:一直等待;等于0:立即返回;大于0:等待超时时间返回,单位毫秒。
## 返回值:
小于0:出错。
等于0:超时。
大于0:返回就绪事件个数。
- epoll编码流程
4、epoll常见问题
问题1:LT模式和ET模式区别?
-
LT模式又称水平触发,ET模式又称边缘触发。
-
LT模式只不过比ET模式多执行了一个步骤,就是当epoll_wait获取完就绪队列epoll事件后,LT模式会再次将epoll事件添加到就绪队列。
-
LT模式多了这样一个步骤会让LT模式调用epoll_wait时会一直检测到epoll事件,直到socket缓冲区数据清空为止。
-
ET模式则只会在缓冲区满足特定情况下才会触发epoll_wait获取epoll事件。
模式 | EPOLLIN | EPOLLOUT |
---|---|---|
LT模式 | 首次触发,socket接收缓冲区检测到数据。一直触发,直到socketi接收缓存为空。 | 首次触发,socket发送缓冲区有空间可发送数据。一直触发,直到socket发送缓冲区无空间发送数据。 |
ET模式 | 首次触发,socket接收缓冲区检测到数据。 | 首次触发,socket发送缓冲区有空间发送数据。 |
- LT模式和ET模式优缺点。
模式 | 优点 | 缺点 |
---|---|---|
LT模式 | 通过频繁触发保证数据完整性。 | 频繁的用户态和内核态切换消耗大量系统资源。 |
ET模式 | 只触发一次,减少用户态和内核态切换,减少了资源消耗。 | 一次触发无法保证读取到完整数据。 |
问题2:epoll为什么高效?
- eventpoll等待队列机制,当就绪队列没有epoll事件时主动让出CPU,阻塞进程,提高CPU利用率。
- socket等待队列机制,只有接收到数据时才会将epoll事件插入就绪队列,唤醒进程获取epoll事件。
- 红黑树提高epoll事件增加,删除,修改效率。
- 任务越多,进程出让CPU概率越小,进程工作效率越高,所以epoll非常适合高并发场景。
问题3:epoll采用阻塞方式是否影响性能?
epoll机制本身也是阻塞的,当epoll_wait未检测到epoll事件时,会出让CPU,阻塞进程,这种阻塞是非常有必要的,如果不及时出让CPU会浪费CPU资源,导致其他任务无法抢占CPU,只要epoll机制能够在检测到epoll事件后,及时唤醒进程处理,并不会影响epoll性能。
问题4:socket采用阻塞还是非阻塞?
socket采用非阻塞方式。
epoll机制属于IO多路复用机制,这种机制的特点是一个进程处理多路IO请求,如果socket设置成阻塞模式会存在以下几个问题:
- 一个进程同一时间只能处理一个socket数据,如果socket被阻塞,那么该进程无法处理其他的socket数据,严重影响了性能。
- 阻塞的本质是进程状态和上下文的切换,频繁的阻塞会把让CPU一直处于上下文切换的状态,导致CPU瞎忙。