Linux----五种IO模型与非阻塞IO

张开发
2026/4/10 22:26:05 15 分钟阅读

分享文章

Linux----五种IO模型与非阻塞IO
这里写目录标题font colorFF00FF1. IO的理解font colorFF00FF2. 五种IO模型font colorFF00FF3. 非阻塞IOfont colorFF00FF4. selectfont colorFF00FF4.1 select参数font colorFF00FF4.2 readfds输入输出参数font colorFF00FF4.3 select的返回值font colorFF00FF5. pollfont colorFF00FF5.1 poll函数接口font colorFF00FF6. epollfont colorFF00FF6.1 epoll的接口font colorFF00FF6.2 epoll的原理font colorFF00FF6.3 epoll的优点(和select的缺点对应)font colorFF00FF7. poll vs select vs epoll (以读为例)font colorFF00FF8. Reactor1. IO的理解1. IOinputoutputIO比较慢为什么为什么访问外设会比较慢网络通信不是网卡吗也是与外设交互以TCP为例accept时会获得一个文件描述符但是IO了你想读就一定有数据吗如果没有数据进程就要阻塞等数据来了才能把数据从接收缓冲区拷贝到应用层当然write也要等待传输层缓冲区不为满才能继续写入IO 等拷贝外设让我们等的时间太久了所以说IO比较慢高效IO什么叫高效IO呢单位时间内减少IO中等待的比重2. 五种IO模型以钓鱼的例子说明这里的鱼要放到桶里钓鱼 等 调1. 张三专注钓鱼鱼漂不动张三不动 -阻塞IO2. 李四不会因为鱼没有上钩卡在鱼漂上 -非阻塞IO3. 王五通过让鱼上钩反向通知我- 信号驱动IO4. 赵六拉了一卡车的鱼竿(50只鱼竿) -多路复用多路转接5. 田七我是喜欢吃鱼发起钓鱼小李去钓鱼了 -异步IO1. 张三没有鱼就在那里一直等除此之外什么都不做2. 李四没有鱼可以做一些其它事情边做事情边等等鱼漂动了再放下手里的事去钓3. 王五王五的鱼竿上绑了铃铛王五一直在做其它的事这个铃铛响了他才去钓4. 赵六50只鱼竿都在钓鱼哪个鱼漂动了就去哪个鱼竿钓5. 田七田七是个大老板让自己的下属小李去钓鱼自己回家歇息1. 钓拷贝6. 人物进程7. 河OS8. 桶用户缓冲区9. 鱼数据10.铃铛信号11. 鱼竿文件描述符1. 阻塞 vs非阻塞阻塞因为IO条件不具备阻塞会卡住直到条件就绪非阻塞检测到IO条件不具备出错返回不同等待方式不同非阻塞效率高如果这两个进程只有一个文件描述符只对一个文件进行读写那它们的IO效率是一样的如果多个文件肯定就是非阻塞效率高了因为多个文件不可能同时都没数据或者缓冲区内的数据都满了非阻塞可以对其它文件进行IO但是只对一个文件进行读写也可以说非阻塞的效率高但是这个效率高表示的是非阻塞做了更多其它的事情不是IO效率这几个钓鱼佬谁钓鱼的效率最高赵六它只要抬头看就基本上会有鱼上钩或者刚钓完一个鱼另一个鱼竿的鱼漂又动了单位时间内等的比重就会很低王五有没有等待呢王五只是不需要检测鱼漂是否动了因为鱼漂动了铃铛会响通知他但是他参与钓的过程阻塞非阻塞信号驱动多路复用 - 同步IO只要有人参与了IO不管是[拷贝等]就是同步的同步IO vs 异步IOIO 等拷贝凡是参与IO等或拷贝的任意一个或多个阶段 - 同步IO否则叫发起IO或者IO工作流和你的工作流无关 - 异步IO这里的同步和线程同步没有任何关系3. 非阻塞IO⼀个文件描述符默认都是阻塞IOfcntl#includeunistd.h#includefcntl.hintfcntl(intfd,intcmd,.../* arg */);1. 这个函数的作用可以把一个文件描述符设置为非阻塞1. 使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图)放到fl里面2. 再使用F_SETFL将文件描述符设置回去3. 设置回去的同时加上⼀个O_NONBLOCK参数表示把文件描述符设置为非阻塞#includeiostreamusing namespace std;#includefcntl.h#includeunistd.hvoidSetNonBlock(intfd){intflfcntl(fd,F_GETFL);if(fl0){perror(fcntl);return;}fcntl(fd,F_SETFL,fl|O_NONBLOCK);}intmain(){SetNonBlock(0);charbuffer[1024];while(true){ssize_tnread(0,buffer,sizeof(buffer)-1);if(n0){coutbufferendl;}elseif(n0){if(errnoEAGAIN||errnoEWOULDBLOCK){std::cout数据没有准备好...std::endl;// C也有语言级输出缓冲区sleep(1);continue;}elseif(errnoEINTR){//因为收到信号可能会执行自定义捕捉方法再回来时跳过read导致进程不在read阻塞了此时进程可能会退出这里继续阻塞不要退出continue;}else{// read读取出错了break;}}else{// Linux中ctrl d标识输入结束read返回值是0类似读到文件结尾break;}}return0;}这里的错误码会被设置表示为什么出错fcntl的返回值有三种情况1. 如果返回值大于0表示有数据可以之间读取到2. 如果返回值等于0表示没有数据可读了类似读到文件结尾read直接退出打印出n的结果是03. 如果返回值0并不代表read读取出错分为三种情况根据错误码被设置的不同结果不同4. select1. IO 等 拷贝read/write/recv/send/recvfrom/sendto这些接口一次等待传入一个fdselect一次等待传入的多个fd一旦多个fd有任意一个或者多个fd就绪了select会通知上层告诉调用方哪些fd可以IO了select就是通过等待多个fd的一种就绪事件通知机制什么叫可读底层有数据读事件就绪什么叫可写底层有空间写事件就绪对于fd一般默认读事件不就续写事件是默认就绪的因为最开始的fd接收缓冲区和发送缓冲区都是空的fd_set内核提供用户的数据结构一次可以向fd_set添加多个fd这个数据结构是位图用户发给内核内核发给用户表示你让我关心的0号和7号下标的fd已经可以IO了可以IO比特位置为14.1 select参数1. nfdsmaxfd1比如你添加了017这三个文件描述符那么这个参数就应该设置为82. timout时长1. 设置为NULL不设置时长一直阻塞着直到有fd就绪通知上层一直不退2. 假设timout设置为50设置时长为5s和0微秒timeout时间内阻塞等待超时就非阻塞返回假设这里在2秒时有一个fd就绪了此时返回值就是3秒返回的是剩余时间的大小3. 假设timeout设置为00设置时长为0s和0微秒非阻塞不管有没有数据就绪都直接返回4.2 readfds输入输出参数1. 输入的时候1. 用户告诉内核你要帮我关心哪些fd上的读事件2. 比特位的位置表示fd编号比特位的内容表示是否关心2. 返回的时候1. 内核告诉用户你让我关心的哪些fd上面的读事件已经就绪了2. 比特位的位置表示fd编号比特位的内容表示是否就绪1. 位图是输入输出的所以这个位图一定会频繁的变更3. 位图有多少个比特位决定了select最多能关心多少个fdfd_set是一个系统提供的数据类型(struct)fd_set大小固定证明select能同时等待多少个fd有上限我这里是1024个比特位4. 如果把fd添加到readfds集合中表示该fd只关心读事件这个只字告诉内核你只需要帮我关心fd的读事件1. 如果同时关心读写把fd添加到 readfds 和 writefds 里2. 如果先关心读再关心写先把fd添加到readfds返回后再把fd添加到writefds3. 如果关心读写异常把fd添加到readfdswritefds和 exceptfds 里4.3 select的返回值1. 大于0是几就表示有fd就绪了2. 等于0超时了在timout时间内没有fd就绪3. 小于0select报错1. 服务器刚启动时默认只有一个fdaccept的本质是阻塞IOaccept关心的是listenfd的读事件因为accept只是获取连接相当于读取连接就是listenfd的读事件将listenfd添加到select函数中让select帮我关心读事件连接到来 读事件就绪读事件没就绪之前1. 非阻塞一直轮询2. 阻塞设置了时间就等间隔时间到了再轮询3. nullptr一直阻塞直到读事件就绪voidFD_CLR(intfd,fd_set*set);// 把fd从fd_set集合中移除位图对应位设为0intFD_ISSET(intfd,fd_set*set);// 判断fd是否在fd_set集合中voidFD_ZERO(fd_set*set);// 清空集合把位图的所有比特位置0voidFD_SET(intfd,fd_set*set);//把fd加入fd_set集合位图对应位位置1select的特点1. 可监控的文件描述符个数取决于sizeof(fd_set)的值我这边服务器上sizeof(fd_set)128每bit表示一个文件描述符我服务器上支持的最大文件描述符是128*810242. 将fd加入select监控集的同时还要再使用一个数据结构array保存放到select监控集中的fd1. 用于再select返回后array作为源数据和fd_set进行FD_ISSET判断2. select返回后会把以前加入的但并无事件发生的fd清空则每次开始select前都要重新从array取得fd逐一加入(但是要先FD_ZERO)扫描array的同时取得fd最大值maxfd用于select的第一个参数3. 每次调用select都需要把fd集合从用户态拷贝到内核态select缺点1. 每次调用select都需要手动设置fd集合从接口使用角度来说也非常不便2. 每次调用select都需要在内核遍历传递进来的所有fd这个开销在fd很多时也很大内核也要遍历fd否则怎么知道哪个文件描述符的读事件就绪呢3. select支持的文件描述符数量太小5. poll5.1 poll函数接口#includepoll.hintpoll(structpollfd*fds,nfds_tnfds,inttimeout);// pollfd结构structpollfd{intfd;/* file descriptor */shortevents;/* requested events */shortrevents;/* returned events */};|只要有一个对应位为1结果就是1两个对应位都为1结果才是1事件就是宏一个整数1. 第一个参数可以理解为指向数组起始元素地址的指针2. 第二个参数表示数组的长度3. 第三个参数ms为单位这是-1表示永久阻塞剩下的和select一样1. 结构体内第一个成员fd2. 结构体内第二个成员位图结构events|POLLIN表示用户告诉内核我要关心POLLIN事件起始肯定要把events和revents都设置为0然后events | POLLIN表示把对应的比特位设置为1表示关心3. 结构体内第三个成员位图结构revents | POLLIN内核告诉用户你要让我关心的fd上面的events事件就绪了起始肯定要把events和revents都设置为0然后revents POLLIN表示内核是否把revents里POLLIN对应的比特位置为1了如果revents里对应的比特位为1它们才为1表示就绪返回值小于0表示出错• 返回值等于0表示poll函数等待超时• 返回值大于0表示poll由于监听的文件描述符就绪而返回返回值和select一样POLLIN和POLLOUT分别对应读写事件1. poll的作用和定位poll只负责等一次可以等待多个fd事件就绪就可以对上层进行通知跟select一样2. 调用的时候fd events 有效用户告诉内核你要帮我关心fd上的events事件3. poll成功返回时fd revents有效内核告诉用户你要让我关心的events事件已经就绪了poll解决了select的什么问题1. poll输入输出参数分离了所以poll不用再向select那样进行参数重置了2. poll等待的fd个数没有上限如果fd-1在内核中内核不关心这类fd的eventspoll的优点不同于select使用三个位图来表示三个fdset的方式poll使用一个pollfd的指针实现1. pollfd结构包含了要监视的events和就绪的revents不再使用select“参数-值”传递的方式接使用比select更方便poll并没有最大数量限制(但是数量过大后性能也是会下降)poll的缺点1. poll中监听的文件描述符数目增多时和select函数⼀样poll返回后需要轮询pollfd来获取就绪的描述符(循环)2. 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态因此随着监视的描述符数量的增长其效率也会线性下降因为要循环每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中(特点)6. epollepoll核心定位基于对多个fd等待的就绪事件通知机制事件就绪就可以对上层进行通知跟selectpoll一样按照man手册的说法是为处理大批量句柄而作了改进的poll6.1 epoll的接口1. 创建一个epoll模型intepoll_create(intsize);1. 从linux2.6.8之后size参数是被忽略的2. 用完之后必须调用close()关闭3. 返回值是一个文件描述符2. 用户告诉内核你要帮我关心哪一个fd上面的event事件intepoll_ctl(intepfd,intop,intfd,structepoll_event*event);op1. EPOLL_CTL_ADD 注册新的fd到epfd中2. EPOLL_CTL_MOD 修改已经注册的fd的监听事件3. EPOLL_CTL_DEL 从epfd中删除⼀个fdevents可以是以下几个宏的集合1. EPOLLIN 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)2. EPOLLOUT表示对应的文件描述符可以写3.EPOLLET将EPOLL设为边缘触发模式3. 内核通知用户你让我关心的fd们上面哪些事件已经就绪intepoll_wait(intepfd,structepoll_event*events,intmaxevents,inttimeout);返回值和timeout(ms)跟select和poll一样events理解为数组maxevents设置数组的大小6.2 epoll的原理1. 当我们调用 epoll_create时OS会把我们创建1. 红黑树2. 就绪队列3. 回调队列调用epoll_ctl时就是创建一个红黑树结点然后把fd和events设置进去一个数据结构对象可以属于多种数据结构这个epoll_create的返回值是epfdepfd可以找到文件描述符表找到struct filestruct file内部有有一个private_data的指针指向eventpolleventpoll内部有红黑树和就绪队列和等待队列而红黑树和就绪队列的内部结点都是epitem这个结构体里面维护红黑树和就绪队列的结构所以为什么每个接口都要带epfd就是为了找到就绪队列和红黑树对他们进行操作这个回调队列里的base指针可以指向每个红黑树结点然后一旦fd就绪回调队列就执行回调方法把红黑树结点连接到就绪队列里回调方法在传输层的tcp和udp的socket里面有回调指针在那里进行回调然后epoll直接从就绪队列里获取revents和fd怎么看待就绪队列1. 就绪队列epoll的本质就是一个基于事件就绪的生产者消费者模型epoll接口是线程安全的上图也可以看到内核源代码里有锁和信号量2. 获取就绪事件如果用户定义的缓冲区大小不够了怎么办没事数据没拿完就绪队列继续保存着把你拿完数据的队列的结点删除下次可以继续从就绪队列里拿还没拿完的数据3. epoll_ctl完成的事情1. 在红黑树中插入节点2. 向底层回调注册回调方法OS会严格按照0下标开始依次拷贝(就绪队列的数据到用户数组)保存就绪事件和fd(保存在就绪队列里)就是把存0号fd的队列的就绪事件和fd拷贝到0号数组下标内部应用层处理就绪事件的时候都是OS严格按照文件描述符和下标对应拷贝的处理的都是就绪的基本不需要非法检测6.3 epoll的优点(和select的缺点对应)1. 接口使用方便虽然拆分成了三个函数但是反而使用起来更方便高效不需要每次循环都设置关注的文件描述符也做到了输入输出参数分离开2. 数据拷贝轻量只在合适的时候调用EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)3. 事件回调机制避免使用遍历而是使用回调函数的方式将就绪的文件描述符结构加入到就绪队列中epoll_wait返回直接访问就绪队列就知道哪些文件描述符就绪这个操作时间复杂度O(1)即使文件描述符数目很多效率也不会受到影响4. 没有数量限制文件描述符数目无上限7. poll vs select vs epoll (以读为例)1. select支持的文件描述符数量太小poll和epoll没有限制2. 用户和内核都会修改select的同一个读事件位图会变化用户要关心01237这四个fd1000 1111OS告诉用户一个fd都没有就绪把位图修改为全0但是用户要继续关心01237这四个fd1000 1111所以要重新循环遍历辅助数组重新设置位图和fd的最大值2. poll的结构体的两个成员(short events, short revents)不像select对同一个位图修改所有poll不会出现这种问题2. epoll两个数据结构来完成的(红黑树就绪队列)更不会出现这种问题3. poll也就是解决了select的位图问题还有fd不足问题其它和select问题一样4. select和poll都是用辅助数组存储fd的所以每次都要循环遍历数组中fd是否0数组默认设置的fd为-1而epoll用的内核红黑树存储直接调用EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中无需遍历5. epoll使用事件回调机制避免使用遍历而是使用回调函数的方式将就绪的文件描述符结构加入到就绪队列中然后把epoll_wait返回直接访问就绪队列就知道哪些文件描述符就绪这个操作时间复杂度O(1)即使文件描述符数目很多效率也不会受到影响而select和poll则需要遍历数组找到数组为-1的值把新获取的文件描述符覆盖掉-1才可以8. Reactorepoll如果事件就绪但是我们不进行处理epoll会一直通知我为什么1. 如果有事件就绪但是没有处理或者是没来的急及处理EPOLL模型会一直通知我们我们把这个模式叫 LT模式EPOLL模型的两种模式1. LT模式水平触发(默认)2. ET模式边缘触发如何理解水平触发和边缘触发(以读为例)1. LT模式只要传输层接收缓冲区有报文就要一直通知上层上层可以不读完因为上层知道你下次还会通知上层2. ET模式1. 传输层接收缓冲区只有从无到有从有到多2. 也就是从网络拿到的报文导致传输层接收缓冲区数据变化的时候才会通知上层3. 即便上层把数据拿走了一部分后来再也没有新增了ET再也不通知上层了倒逼上层必须收到通知把本轮数据读完否则连接一断数据可能丢失ET通知就绪用户必须把缓冲区本轮的数据读取完怎么读取完循环读取recv的时候万一数据读完因为用户并不清楚传输层接收缓冲区的数据已经读完了recv还会被调用此时recv会阻塞如果只有一个进程的情况下该进程就被放到阻塞队列里了还要唤醒效率太低所以ET模式必须将fd设置为非阻塞的工作模式ET非阻塞1. LT模式rev一次需要被设置为非阻塞吗都可以2. 不考虑recv返回值(实际读到字节的大小) 期望值的情况无脑循环直到EAGAIN似乎LT也可以向ET那样循环非阻塞啊ET是OS约束程序员必须循环非阻塞否则数据可能丢失必须循环非阻塞增加IO读写的确定性为什么要保证一次读完这种确定性recv本质是为了应答时给对方在概率上提供一个更大的缓冲区提高TCP传输效率底水位线假设设置的值为100字节那么接收缓冲区数据大小100字节的时候就不通知上层PSHpsh仅仅是让fd就绪而是ET模式逼着用户把数据全部读完否则可能数据丢失而LT没人约束程序员导致可能会每个人实现LT的方式都不一样为了保证统一性LT一般就是阻塞读取LT VS ET谁更高效1. ET通知效率更高有效通知数量更多2. ET尽快读完所有的数据可以给对方有概率的更新一个更大的16位窗口提高对方滑动窗口大小提高网络发送报文的并发度和数据传输效率1. 读事件关心需要进行常设而写事件关心是需要按需设置如果写事件常设了epoll就会一直写事件就绪2. 写事件应该如何按需设置如何发送直接发送如果发送的时候把发送缓冲区写满了(写事件不就绪)再把对写事件的关心设置到epoll内部3. 手动开启对epollout的关心默认就要触发一次写事件就绪不过错误码被设置返回-1因为缓冲区满了

更多文章