阻塞I/O至I/O多路复用

阻塞I/O指进程发起调用后会被挂起(阻塞),直到收到数据再返回。如果调用一直不返回,进程就一直被挂起。因此,使用阻塞I/O需要利用多线程来处理多个文件描述符。

引入非阻塞I/O的原因为:多线程切换有一定的开销。非阻塞I/O不会被挂起,调用时立即返回成功或错误,因此,可以在单线程里轮询多个文件描述符是否就绪。

引入I/O多路复用的原因是:非阻塞I/O每次发起系统调用,只能检查一个文件描述符是否就绪;当文件描述符较多时,系统调用成本高。I/O多路复用,可以通过一次系统调用,检查多个文件描述符状态。这也是I/O多路复用的主要优点,相比于非阻塞I/O,在文件描述符较多场景下,减少系统调用的开销。

  • I/O多路复用相当于把遍历所有文件描述符,并通过非阻塞I/O查看其是否就绪的过程从用户线程移到了内核中,由内核负责轮询。

进程通过select/poll/epoll系统调用发起I/O多路复用,这些系统调用默认是同步阻塞的:如果传入的多个文件描述符中,有描述符就绪,则返回就绪的文件描述符;否则如果所有文件描述符都未就绪,就阻塞调用进程,直到某个描述符就绪,或阻塞时长超过设置的timeout后,再返回。(I/O多路复用内部检查每个文件描述符的就绪状态时采用非阻塞I/O)

  • timeout参数为NULL时,无限阻塞直到某个描述符就绪;
  • timeout参数为0时,立即返回,不阻塞。

为什么I/O多路复用内部使用非阻塞I/O

I/O多路复用内部会遍历集合中每个文件描述符,判断其是否就绪;

for(auto& fd : read_set){
if(readable(fd)){ //判断fd是否就绪
count++;
FDSET(fd, &res_rset); //将fd添加到就绪集合
}
} return count; //返回就绪描述符个数(select)

这里readable()就是一个非阻塞I/O调用。若在这使用阻塞I/O,那么当fd未就绪时,select会阻塞在这个文件描述符上,无法检查下一个描述符。

select

主旨思想:

  1. 首先构造一个关于文件描述符的列表,将要监听的文件描述符添加到该列表中。
  2. 调用一个系统函数,监听该列表中的文件描述符,直到这些描述符中的一个或多个进行I/O操作时,该函数才返回。
    • 此函数是阻塞的
    • 函数对文件描述符检测的操作由内核完成
  3. 在返回时,它会告诉进程有多少描述符要进行I/O操作。
int select(int nfds, fd_set *restrict readfds,
fd_set *restrict writefds,
fd_set *restrict errorfds,
struct timeval *restrict timeout);

readfds,writefds,errorfds是三个文件描述符集合。select会遍历每个集合的前nfds个描述符,分别找到可读取,可写入,发生错误的描述符,统称“就绪”描述符。用找到的子集替换参数中的对应集合,返回所有就绪描述符的总数。

timeout参数为调用select时的阻塞时长。若所有文件描述符均未就绪,则阻塞调用进程,直到某个描述符就绪或阻塞时长超过设置的timeout后返回。timeout设为NULL,无限阻塞直到某个描述符就绪;timeout设为0,立即返回,不阻塞。

fd_set文件描述符集合

由于文件描述符fd是一个从0开始的无符号整数,所以可用fd_set的二进程每一位来表述一个文件描述符。某一位为1,则已经就绪。当设fd_set长度为1字节时,则一个fd_set变量最大可以表示8个文件描述符。返回fd_set = 00100100时,表示第3,6文件描述符已就绪。

fd_set的API:

#include <sys/select.h>
int FD_ZERO(int fd, fd_set *fdset); //令fd_set全部bit置0
int FD_CLR(int fd, fd_set *fdset); //令fd_set某一bit置0
int FD_SET(int fd, fd_set *fdset); //令fd_set某一bit置1
int FD_ISSET(int fd, fd_set *fdset); //检测fd_set某一bit是否为1

select使用示例

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h> int main(void){
while(1){
//声明一个fd_set类型的变量readFDs
fd_set readFDs;
//调用FD_ZERO,将readFDs所有bit置0
FD_ZERO(&readFDs); int fd;
//调用FD_SET,将readFDs感兴趣的bit置1,表示要监听这几个文件描述符
for(fd = minFD; fd < maxFD; fd++)
FD_SET(fd, &readFDs); //将readFDs传给select,调用select
int rc = select(maxFD + 1, &readFDs, NULL, NULL, NULL);
//select会将readFDs中就绪的bit置1,未就绪的置0,并返回就绪的描述符数量 //当select返回后,调用FD_ISSET检测给定位是否为1,表示对应描述符是否就绪
int fd;
for(fd = minFD; fd < maxFD; fd++)
if(FD_ISSET(fd, &readFDs))
processFD(fd);
}
}

select的缺点

  1. 性能开销大:

    • 调用select时陷入内核,这时需将参数中的fd_set从用户空间拷贝到内核空间
    • 内核需遍历传递进来的所有fd_set每一bit,不管它们是否就绪;不断轮询所有fd集合,直到存在就绪,期间可能存在睡眠和唤醒多次交替。
  2. 同时监听的文件描述符数量太少。受限于sizeof(fd_set),在编译内核时就已经确定并无法更改,一般为1024,不同操作系统不一样。
  3. fds集合不能重用,每次都需要重置。

poll

poll的主旨思想大致与select相同。poll在用户态通过数组方式传递文件描述符,在内核态会转为链表方式存储,没有最大数量限制。

int poll(struct pollfd *fds,
nfds_t nfds, int timeout
);

poll通过pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制;其中的fd表示一个个文件描述符,events表示注册的事件,而revents表示实际发生的事件,由内核来填充;由此pollfd数组只需要被初始化一次。

poll相较于select的优点

  1. 由于通过pollfd数组向内核传递事件表,所以没有文件描述符个数大小的限制
  2. 由于内部存在events和revents变量,因此pollfd数组只需初始化一次

poll的缺点

解决了上述select缺点的2和3,但没有解决1。

epoll

epoll为对select和poll的改进,解决了上述I/O多路复用函数的缺点。epoll有以下特点:

  • 使用红黑树存储文件描述符集合
  • 使用队列存储就绪的文件描述符
  • 每个文件描述符只需在添加时被传入一次;通过事件改变文件描述符状态

    epoll模型不同于select和poll,它使用三个函数:epoll_create,epoll_ctlepoll_wait

epoll_create

int epoll_create(int size);

epoll_create会创建一个epoll实例,同时返回一个引用该实例的文件描述符。

返回的文件描述符仅仅指向对应的epoll实例,并不表示真实的磁盘节点。其它API例如epoll_ctl,epoll_wait会使用这个文件描述符来操作相应的epoll实例。

当创建好epoll句柄后,就会占用一个fd值,在linux可以查看/proc/进程id/fd/。所以在使用完epoll后,必须调用close(epfd)关闭对应的文件描述符,否则可能导致fd被耗尽。当指向同一个epoll实例的所有文件描述符都被关闭后,操作系统会销毁这个epoll实例。

epoll内部存储:

  • 监听列表:所有要监听的文件描述符(红黑树)
  • 就绪列表:所有就绪文件描述符(双向链表)

epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll_ctl会监听文件描述符fd上发生的event事件。

参数说明:

  • epfd即epoll_create返回的文件描述符
  • fd表示要监听的目标文件描述符
  • event表示要监听的事件(可读,可写,发送错误…)
  • op表示要对fd执行的操作,分为:
    • EPOLL_CTL_ADD:为fd添加一个监听事件event
    • EPOLL_CTL_MOD:更改与文件描述符fd关联的事件event(event为一个结构体变量,相当于event本身没变,更改了其内部字段的值)
    • EPOLL_CTL_DEL:删除fd所有监听事件,此情况event参数无用

epoll_ctl会将文件描述符fd添加到epoll实例的监听列表里,同时为fd设置一个回调函数,并监听事件event。当fd上发生相应事件时,会调用回调函数,将fd添加到epoll实例的就绪队列中。

epoll_wait

int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);

此函数为epoll模型的主函数,其功能相当于select。

参数说明:

  • epfd即epoll_create返回的文件描述符,指向一个epoll实例
  • events是一个数组,保存就绪状态的文件描述符,其空间由调用者负责申请
  • maxevents指定events大小
  • timeout类似于select中的timeout。若设为-1,一直阻塞,直到有文件描述符就绪;设为0,会立即返回

    返回值表示events中存储的就绪描述符个数,最大不超过maxevents

epoll的优点

首先epoll是对select和poll的改进,所以就避免了“性能开销大”和“文件描述符数量少”两个缺点。

  • 文件描述符数量少:select使用整型数组存储文件描述符集合;epoll使用红黑树存储,数量大
  • 性能开销大:epoll_ctl中为每个文件描述符指定了回调函数,并在就绪时将其加入到就绪列表,因此epoll不像select那样遍历每个文件描述符来判断是否就绪,只需要判断就绪列表为空即可。这样在没有文件描述符就绪时,epoll能更早让出系统资源。[O(n)->O(1)]
  • 另外,epoll对于每个描述符,只需在epoll_ctl传递一次,之后epoll_wait无需再次传递;不像每次调用select都需要向内核拷贝所有要监听的描述符集合,大大提高效率。

水平触发(LT) 边缘触发(ET)

select与poll只支持LT;epoll支持LT和ET。(信号驱动的I/O也是边缘触发)

  1. 水平触发(Level Trigger):当文件描述符就绪时,会触发通知,若用户程序没有一次性把数据读/写完毕,下次还会发出可读/可写信号进行通知。
  • 假设委托内核检测读(read)事件->检测fd的读缓冲区
  • 读缓冲区有数据->epoll检测到后会给用户通知
    • a.用户不读数据,数据一直在缓冲区,epoll一直通知
    • b.用户只读了一部分数据,epoll会通知
    • c.缓冲区数据读完了,不通知
  1. 边缘触发(Edge Trigger):当且仅当描述符从未就绪变为就绪时,通知一次,之后不再通知。
  • 假设委托内核检测读(read)事件->检测fd的读缓冲区
  • 读缓冲区有数据->epoll检测到后会给用户通知
    • a.用户不读数据,数据一直在缓冲区,epoll下次检测时就不通知了
    • b.用户只读了一部分数据,epoll不通知
    • c.缓冲区数据读完了,不通知

区别:边缘触发的效率更高,减少事件被重复触发的次数,函数不会返回大量用户程序可能不需要的文件描述符。

边缘触发要使用非阻塞I/O

  • 每次调用read系统调用读取数据时,最多只能读取缓冲区大小的字节数;若某个描述符一次性收到的数据超过了缓冲区的大小,那么需对其read多次才能全部读取完毕

select的LT模式可以使用阻塞I/O或非阻塞I/O

  • select可以使用阻塞I/O(上文select示例就是用的阻塞I/O)。通过select获取到所有的可读文件描述符后,遍历每个文件描述符,read一次数据

    • 由于这些文件描述符均可读,因此即使read是阻塞I/O,也一定可读到数据,不会一直阻塞
    • select采用LT模式,因此若第一次read没有读取完全部数据,那么下次调用select时依然会返回这个文件描述符,再次read直到读完
  • select也可以使用非阻塞I/O。当遍历某个可读文件描述符时,使用for循环调用read多次,直到读取完所有数据为止(返回EWOULDBLOCK)。这样虽然会多一个read调用,但可以减少select的次数

epoll的ET模式必须使用非阻塞I/O

  • 在epoll的ET模式下,只会在文件描述符可读/可写状态发生切换时,才会收到操作系统的通知

    • 因此,若使用epoll的ET模式,在收到通知时,必须使用非阻塞I/O,并且必须循环调用read或write多次,直到返回EWOULDBLOCK为止,然后再调用epoll_wait等待操作系统下一次通知
    • 若没有一次性读/写完所有数据,那么在操作系统看来这个文件描述符的状态未改变,将不再发起通知,调用epoll_wait会使得该文件描述符一直等待下去,服务端也会一直等待客户端的响应,业务流程无法走完
    • ET模式的好处是每次调用epoll_wait都是有效的—-已保证数据全部读写完毕,等待下次通知。而在LT模式下,若调用epoll_wait时数据未读/写完毕,会直接返回,再次通知。因此ET模式能显著减少事件被触发的次数
    • 显然,ET模式需要循环读/写一个文件描述符的所有数据。若使用阻塞I/O,那么一定会在最后一次调用(没有数据可读/写)时阻塞,导致无法正常结束

三者对比

  • select:调用开销大(需要拷贝集合);集合大小有限制;需要遍历整个集合找到就绪的描述符(只支持LT模式)
  • poll:poll采用数组(内核用链表)方式存储文件描述符集合,没有最大存储数量限制,且只需对数组初始化一次;其它与select无区别(LT)
  • epoll:调用开销小(无需拷贝);集合大小无限制;采用回调机制,不需要遍历整个集合(支持LT和ET模式)
  • select和poll在用户态维护文件描述符集合,因此每次将完整集合拷贝给内核
  • epoll由操作系统内核维护文件描述符集合(epoll_event结构体),因此只需在创建时传入文件描述符

适用场景

  • select和poll:连接数较少且均十分活跃,由于epoll需要很多回调,这两者可能性能更佳
  • epoll:连接数较多且有较多不活跃的连接,epoll效率比其它两者高很多

参考资料

select,poll和epoll使用场景和区别的更多相关文章

  1. 聊聊select, poll 和 epoll

    聊聊select, poll 和 epoll 假设项目上需要实现一个TCP的客户端和服务器从而进行跨机器的数据收发,我们很可能翻阅一些资料,然后写出如下的代码. 服务端 void func(int s ...

  2. Linux下select, poll和epoll IO模型的详解

    http://blog.csdn.net/tianmohust/article/details/6677985 一).Epoll 介绍 Epoll 可是当前在 Linux 下开发大规模并发网络程序的热 ...

  3. I/O复用中的 select poll 和 epoll

    I/O复用中的 select poll 和 epoll: 这里有一些不错的资料: I/O多路复用技术之select模型: http://blog.csdn.net/nk_test/article/de ...

  4. (转)Linux下select, poll和epoll IO模型的详解

    Linux下select, poll和epoll IO模型的详解 原文:http://blog.csdn.net/tianmohust/article/details/6677985 一).Epoll ...

  5. linux select poll and epoll

    这里以socket文件来阐述它们之间的区别,假设现在服务器端有100 000个连接,即已经创建了100 000个socket. 1 select和poll 在我们的线程中,我们会弄一个死循环,在循环里 ...

  6. Select,poll,epoll复用

    Select,poll,epoll复用 1)select模块以列表的形式接受四个参数,分别是可读对象,可写对象,产生异常的对象,和超时设置.当监控符对象发生变化时,select会返回发生变化的对象列表 ...

  7. Linux中select poll和epoll的区别

    在Linux Socket服务器短编程时,为了处理大量客户的连接请求,需要使用非阻塞I/O和复用,select.poll和epoll是Linux API提供的I/O复用方式,自从Linux 2.6中加 ...

  8. [转载] select, poll和epoll的区别

    源地址:http://sheepxxyz.blog.163.com/blog/static/61116213201022003513530/ 随着2.6内核对epoll的完全支持,网络上很多的文章和示 ...

  9. select,poll 和 epoll ??

    其实所有的 I/O 都是轮询的方法,只不过实现的层面不同罢了. 其中 tornado 使用的就是 epoll 的. selec,poll 和 epoll 区别总结 基本上 select 有 3 个缺点 ...

  10. select poll和 epoll

    select .poll.epoll 都是多路io复用的机制,i/o多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知乡音的程序进行相应的读写操作.但s ...

随机推荐

  1. FPGA MIG调试bug(一)

    目标器件:复旦微FPGA:JFM7K325T8FCBGA676(对标Xilinx Kintex-7系的XC7K325T) 工程背景:板载4片DDR(AllianceMemory_DDR3L_8G_AS ...

  2. C++程序设计实验四 继承

    程序源码: #include <iostream> #include <typeinfo> // definitation of Graph class Graph { pub ...

  3. input输入框限制输入

    <input type="password" value="" id="pwd" class="Rectangle-1188 ...

  4. VUE学习-元素过渡

    单元素过渡 appear 初始渲染 通过 appear attribute 设置节点在初始渲染的过渡 appear + css <transition appear appear-class=& ...

  5. asp.net.core学习笔记1:swagger的使用和webapi接收Jobject对象

    环境:asp.net.core 3.1 (一觉醒来官方已经不推荐3.0了,于是没有任何core经验,也只能开始了3.1的开发学习) 由于现有项目前后端分离.微服务化日趋流行,所以上手不采用web应用( ...

  6. ubuntu 安装SVN

    s1: [sudo apt-get install subversion] s2:本地新建一个文件夹如SVN,在该文件夹下打开终端输入命令[svn co https://xxx/svn/xxx --u ...

  7. vscode plugin - jenkins jack使用方法

    Jenkins jack用于使vscode可直连jenkins,能推送代码至对应jenkins job进行代码测试 一.vscode安装jenkins jack 二.连接jenkins 根据提示输入j ...

  8. lowcodeEngine 组件面板的拖拽功能

    设计器和渲染器处在不同的 Frame 渲染器以单独的 iframe 嵌入,xxx-simulator-renderer 通过和 host进行通信来和设计器打交道,比如点击渲染画布任意一个位置,需要能计 ...

  9. 题解 UVA10859 【Placing Lampposts】

    交了N次,重构一次代码终于过了..... 题意:一片森林,1.输出占领所有边需要的最小的路灯个数 2.输出两端点均被占领的边的条数 3.只有一端被占领的边的条数 还是比较简单的 开始的时候思路不够清晰 ...

  10. PADS生成CAM文档(Gerber)

    CAM   一个正常的CAM文档应包括n+8层,n指的是层数,8指的是: 顶层丝印层,Silkscreen TOP 底层丝印层,Silkscreen BOTTOM 顶层组焊层,Solder Mask ...