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机制是一种I/O多路复用技术,它可以同时监视多个文件描述符,当某个文件描述符就绪(一般是读写就绪)时,就会通知程序进行相应的操作。
优点:select机制的优点是可以同时处理多个连接,避免了大量的线程或进程切换,提高了系统的并发性能。
在Linux系统中,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覆盖原来的超时时间。
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。
问题1:select函数最大文件描述(maxfd)有什么作用?
问题2:select优缺点有哪些?
优点:
缺点:
问题3:select为什么会有1024文件描述符限制?
问题4:select有哪些设计缺陷?
epoll机制效率高,适用于高并发场景,所以epoll机制广泛用于各种开源项目。
select机制编程简单,如果对高并发没有需求,那么很多人会选择select机制做多路IO请求处理。
poll机制既没有高并发能力,编程也并不简单,所以使用场景比其它两种要少。
poll机制的使用需要调用系统调用poll()函数,该函数会阻塞进程直到有文件描述符就绪或者超时。
poll()函数的参数是一个pollfd结构体数组,每个结构体中包含了一个文件描述符和该文件描述符所关注的事件类型。
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。
问题1:poll的优缺点?
优点:
缺点:
问题2:poll和select的区别?
poll和select底层实现非常相似,分析poll和select内核源码会发现二者之间很多地方都复用了相同的代码。
poll可以说是select的加强版,poll优化了select一些设计缺陷:
不过很可惜,即使poll对select做了很多优化,依然没有改变轮询方式,也没有改变selec执行效率低的本质问题。
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。
epoll可以理解为event poll,它是一种事件驱动的I/O模型,可以用来替代传统的select和poll模型。epoll的优势在于它可以同时处理大量的文件描述符,而且不会随着文件描述符数量的增加而降低效率。
epoll的接口和工作模式相对于select和poll更加简单易用,因此在高并发场景下被广泛使用。
epoll的实现机制是通过内核与用户空间共享一个事件表,这个事件表中存放着所有需要监控的文件描述符以及它们的状态,当文件描述符的状态发生变化时,内核会将这个事件通知给用户空间,用户空间再根据事件类型进行相应的处理。
socket等待队列用于在socket接收到数据后添加就绪epoll事件节点和唤醒eventpoll等待队列项。
socket收到数据后,唤醒socket等待队列项,并执行等待队列项注册的回调函数ep_poll_callback,ep_poll_callback函数将就绪epoll事件节点添加至就绪队列,并唤醒eventpoll等待队列项。
eventpoll等待队列用于阻塞当前进程,用于epoll_wait未检测到就绪epoll事件节点的情况。
epoll_wait检测就绪队列是否有epoll事件节点,没有epoll事件节点,则使用等待队列将当前进程挂起,后续ep_poll_callback函数会唤醒当前进程。
就绪队列用于存储就绪epoll事件节点,用户通过epoll_wait函数获取就绪epoll事件节点。
红黑树用于存储通过epoll_ctl函数注册的epoll事件节点。
int epoll_create(int size);
## 功能:
epoll_create函数用于创建epoll文件。
## 参数:
size:目前内核还没有实际使用,只要大于0就行。
## 返回值:
成功:返回epoll文件描述符。
失败:返回-1,并设置errno。
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。
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:返回就绪事件个数。
问题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模式 | 只触发一次,减少用户态和内核态切换,减少了资源消耗。 | 一次触发无法保证读取到完整数据。 |
问题2:epoll为什么高效?
问题3:epoll采用阻塞方式是否影响性能?
epoll机制本身也是阻塞的,当epoll_wait未检测到epoll事件时,会出让CPU,阻塞进程,这种阻塞是非常有必要的,如果不及时出让CPU会浪费CPU资源,导致其他任务无法抢占CPU,只要epoll机制能够在检测到epoll事件后,及时唤醒进程处理,并不会影响epoll性能。
问题4:socket采用阻塞还是非阻塞?
socket采用非阻塞方式。
epoll机制属于IO多路复用机制,这种机制的特点是一个进程处理多路IO请求,如果socket设置成阻塞模式会存在以下几个问题: