行业资讯
📅 2026/9/2 21:15:27
Linux —— 五种IO模型与非阻塞IO
目录1. 五种IO模型2. 高级IO重要概念2.1 同步通信 vs 异步通信(synchronous communication/ asynchronouscommunication)2.2 阻塞 VS 非阻塞3. 非阻塞IO3.1 fcntl3.2 实现函数SetNoBlock3.3 代码的实现1. 五种IO模型五种IO模型就是解决IO通信时如何高效通信的问题。例子钓鱼钓鱼 等 钓 高效的钓鱼 等的比重非常低单位时间内钓鱼的效率很高阻塞IO: 在内核将数据准备好之前, 系统调用会⼀直等待. 所有的套接字, 默认都是阻塞方式。阻塞IO是最常见的IO模型。非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码。非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询。这对CPU来说是较大的浪费, ⼀般只有特定场景下才使用。信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应⽤程序进行IO操作。(用的不多SIGIO信号默认是没有开启的)IO多路转接: 虽然从流程图上看起来和阻塞IO类似。实际上最核心在于IO多路转接能够同时等待多个⽂件描述符的就绪状态。select 、poll、epoll 只解决等的问题拷贝依旧是之前老的IO函数来做但是一次只能等到 1个fd。select 、poll、epoll一次能等待多个 fd。异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据不太常用)。小结任何IO过程中, 都包含两个步骤. 第⼀是等待, 第二是拷贝。而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间。 让IO更高效, 最核心的办法就是让等待的时间尽量少。2. 高级IO重要概念2.1 同步通信 vs 异步通信(synchronous communication/ asynchronouscommunication)同步和异步关注的是消息通信机制。所谓同步就是在发出⼀个调⽤时在没有得到结果之前该调⽤就不返回. 但是⼀旦调⽤返回就得到返回值了; 换句话说就是由调⽤者主动等待这个调⽤的结果;异步则是相反调⽤在发出之后这个调⽤就直接返回了所以没有返回结果; 换句话说当⼀个异步过程调⽤发出后调⽤者不会⽴刻得到结果; ⽽是在调⽤发出后被调⽤者通过状态、通知来通知调⽤者或通过回调函数处理这个调⽤。另外, 在之前的多进程多线程的时候, 也提到同步和互斥. 这⾥的同步通信和进程之间的同步是完全不相⼲的概念。进程/线程同步也是进程/线程之间直接的制约关系是为完成某种任务而简历的两个或多个线程这个线程需要在某些位置上协调他们的⼯作次序而等待、传递信息所产⽣的制约关系.。尤其是在访问临界资源的时候。在以后看到 “同步” 这个词的时候一定要搞清楚是同步通信异步通信的同步还是同步与互斥中的同步。二者是没有任何关系的。同步IO和异步IO的区分只要参加了 等 拷贝的任意一项就是同步IO一项都没参加的话就是异步IO。什么叫做IO模型模型就是一种规律IO的情况只有这五种要么是五种当中的一种要么是任意多个的组合大部分都是阻塞式IO2.2 阻塞 VS 非阻塞阻塞和⾮阻塞关注的是程序在等待调⽤结果消息返回值时的状态。阻塞调⽤是指调⽤结果返回之前当前线程会被挂起. 调⽤线程只有在得到结果之后才会返回。非阻塞调⽤指在不能⽴刻得到结果之前该调⽤不会阻塞当前线程。3. 非阻塞IO3.1 fcntl例子scanf()scanf() 函数键盘不进行输入的话就会被阻塞住阻塞住的原因是读条件不就绪读条件不就绪就会阻塞住。阻塞是OS自动做的。代码将阻塞变为非阻塞open() 函数中的 flag 参数默认是阻塞的方式打开的flag当中可以设置一个选项O_NONBLOCKrecv() 函数中的 flag 参数也是可以进行设置的一旦设置了如果底层文件描述符没有数据的话此时recv就会出错返回。如何设计非阻塞将 fd 直接设置为非阻塞的方式 - fcntl上层即便用阻塞的接口它也是非阻塞的。任何文件在底层都有 struct file 结构体包含了 int flag 、int mode一个文件究竟是阻塞还是非阻塞本质就是 struct file 结构体中的flag标志位是否被设置NONBLOCK此时上层是阻塞的接口来调用的话依旧是非阻塞的。fcntl 函数常见的5种功能复制⼀个现有的描述符cmdF_DUPFD获得/设置⽂件描述符标记 (cmdF_GETFD或F_SETFD)获得/设置⽂件状态标记 (cmdF_GETFL或F_SETFL)获得/设置异步I/O所有权 (cmdF_GETOWN或F_SETOWN)获得/设置记录锁 (cmdF_GETLK,F_SETLK或F_SETLKW)此处只是⽤第三种功能, 获取/设置⽂件状态标记, 就可以将⼀个⽂件描述符设置为非阻塞。3.2 实现函数SetNoBlockvoid SetNoBlock(int fd) { int fl fcntl(fd, F_GETFL); if (fl 0) { perror(fcntl); return; } fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置fd为非阻塞 }之前是阻塞的就是因为没有设置上面的代码中的标志位。返回值失败返回 -13.3 代码的实现以 read() 为例因为 read() 是默认的阻塞式的IO。// testNonBlock.cc #include iostream #include unistd.h #include fcntl.h void SetNonBlock(int sockfd) { int f1 fcntl(sockfd, F_GETFL); if (f1 0) { perror(fcntl); return; } fcntl(sockfd, F_SETFL, f1 | O_NONBLOCK); // O_NONBLOCK设置fd为非阻塞 } int main() { // 默认标准输入是阻塞的 char buffer[64]; while (true) { ssize_t n read(0, buffer, sizeof(buffer) - 1); // sizeof(buffer)期望读取的大小 n实际读取的大小 if (n 0) { buffer[n] 0; printf(##%s, buffer); } else if (n 0) { printf(read end of file!\n); } else { perror(read); } } }运行结果阻塞式IO变为非阻塞式IO运行结果没有卡住以上信息是它打出来的#include unistd.hssize_t read(int fd, void *buf, size_t count);返回值调用系统函数失败最关心的就是为什么会失败全局变量errno当一个库函数或系统调用失败该函数自动设置errno表明该函数所对应的错误码多进程中errno会写时拷贝多线程中errno是线程局部存储errno变量每个线程一旦修改调用库函数修改errno每个线程都有自己的errno互相不冲突。errno会线程安全的在多线程多进程中使用。错误码就会告诉我们是真的出错了还是其他情况printf(read error, reat ret: %ld, errno:%d\n, n, errno);将错误码准换成错误码的字符串描述printf(read error, reat ret: %ld, errno:%d, err msg:%s\n, n, errno, strerror(errno));之前用到的perror函数就是拿着错误码转成错误码描述上个函数出错了将全局变量errno设置了。// errno 11 - EAGAIN or EWOULDBLOCK (两个宏值)#include iostream #include unistd.h #include fcntl.h #include string.h void SetNonBlock(int sockfd) { int f1 fcntl(sockfd, F_GETFL); if (f1 0) { perror(fcntl); return; } fcntl(sockfd, F_SETFL, f1 | O_NONBLOCK); // O_NONBLOCK设置fd为非阻塞 } int main() { SetNonBlock(0); // 设置非阻塞只需要设置一次就行了 // 默认标准输入是阻塞的 char buffer[64]; while (true) { ssize_t n read(0, buffer, sizeof(buffer) - 1); // sizeof(buffer)期望读取的大小 n实际读取的大小 if (n 0) { buffer[n] 0; printf(##%s, buffer); } else if (n 0) { printf(read end of file!\n); break; } else { // IO非阻塞你就是不输入 - 读条件不满足 - 以后讨论IO问题全部站在内存(进程)视角看待IO // read 会以出错形式返回 // 读条件不满足算真的出错了吗---- 严格来说不算出错 // 为什么read()返回值 0 ??? // a.真的IO出错了 // b.其他情况被迫返回小于0 // perror(read); // Resource temporarily unavailable // printf(read error, reat ret: %ld, errno:%d, err msg:%s\n, n, errno, strerror(errno)); // EAGAIN or EWOULDBLOCK // errno 11 - EAGAIN or EWOULDBLOCK (两个宏值) if(errno EAGAIN || errno EWOULDBLOCK ) { printf(你的数据没有准备好下次再来\n); // 做其他事情 continue; } else { perror(raed error\n); } sleep(1); } } }轮询检测在读数据之前IO函数可能会被信号给中断。回来的时候继续read()if (errno EAGAIN || errno EWOULDBLOCK) { printf(你的数据没有准备好下次再来\n); // 做其他事情 sleep(1); continue; } if (errno EINTR) { sleep(1); // 信号中断回来的时候继续read continue; } else { perror(raed error\n); }为什么用标准输入来演示为什么不用标准输出---- 因为标准输出大部分的情况下输出条件都是满足的输出缓冲区默认是空的输出条件默认是满足的所以printf这些接口不会卡住看不到卡住的现象很难看到现象。标准输入没有数据时比较容易看到现象。