当前位置: 首页 > news >正文

【网络】高级IO——阻塞IO和非阻塞IO的实现

目录

一.文件描述符的默认行为——阻塞IO

二.非阻塞IO

2.1.在打开文件或创建套接字时设置非阻塞模式:

2.2.在使用网络I/O接口时请求非阻塞行为:

2.3.fcntl函数


一.文件描述符的默认行为——阻塞IO

        在Linux系统中,无论是通过open系统调用打开的文件(包括系统文件、设备文件等),还是通过socket创建的网络套接字(sock),它们对应的文件描述符(fd)默认都是阻塞的。这种阻塞行为是操作系统为了简化同步I/O操作而设计的。

        当进程尝试对一个文件描述符执行读或写操作时,如果所需的数据当前不可用(例如,读操作而缓冲区为空)或无法立即写入数据(例如,写操作而缓冲区已满或磁盘I/O繁忙),则进程将被挂起(阻塞),直到以下条件之一发生:

  1. 对于读操作:有数据可供读取,或者到达文件末尾(EOF)。
  2. 对于写操作:数据已经被成功写入到内核的缓冲区中,即使这些数据还没有被实际写入到磁盘上。

在阻塞模式下,进程需要等待这些条件成立才能继续执行,这可能会导致进程在I/O操作上花费大量时间,从而降低程序的响应性和吞吐量。

下面是一个简单的例子。

#include <iostream>  
#include <unistd.h>  
#include <string.h>  int main() {  std::cout << "This program echoes input in blocking mode. Try typing something and pressing enter.\n";  char buffer[100];  while (true) {  ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);  if (bytes_read > 0) {  buffer[bytes_read] = '\0'; // 确保字符串以null字符结束  std::cout << "Echo: " << buffer << std::endl;  } else if (bytes_read == 0) {  // 对于文件描述符如stdin,在阻塞模式下,read返回0通常意味着EOF(文件结束)  // 但对于stdin,这通常不会发生,除非输入被重定向自一个文件或管道,并且该文件或管道已经到达末尾  std::cout << "Unexpected end of input (this should not normally happen with stdin)." << std::endl;  break;  } else {  // read返回-1时表示发生错误  std::cerr << "Error reading input: " << strerror(errno) << std::endl;  break;  }  }  return 0;  
}

        上面这就是典型的阻塞式IO,当程序运行起来时,执行流会在read处阻塞,因为read今天读取的是0号文件描述符,也就是键盘文件上的数据,只要我不从键盘上输入数据的话,read就会一直阻塞,此时进程会被操作系统挂起,直到硬件设备键盘上有数据时,进程才会重新投入CPU的运行队列,当我们输入数据后,可以立马看到进程显示出了echo回应的结果,同时进程又立马陷入阻塞,等待我进行下一次的输入数据,这样的IO方式就是典型的阻塞式,同时也是最常用,最简单的IO方式。

        为了解决这个问题,Linux提供了非阻塞I/O(Non-blocking I/O)和异步I/O(Asynchronous I/O)等机制。通过设置文件描述符为非阻塞模式,进程可以在I/O操作无法立即完成时立即返回一个错误(通常是EAGAINEWOULDBLOCK),而不是被挂起。这样,进程就可以继续执行其他任务,而不会因等待I/O操作而阻塞。

        对于网络套接字,除了设置非阻塞模式外,还可以使用I/O多路复用技术(如select、poll、epoll)来同时监视多个文件描述符的状态,从而在不增加线程或进程数量的情况下处理多个并发连接。这些技术允许进程在单个线程中高效地管理多个I/O操作,提高了程序的性能和可扩展性。

二.非阻塞IO

在Linux操作系统中,关于设置文件描述符(fd)为非阻塞模式的方式主要有两种。下面是对这两种主要方式的准确描述:

2.1.在打开文件或创建套接字时设置非阻塞模式:

  1. 对于文件(包括设备文件),通常不建议也不常见将其设置为非阻塞模式,因为文件I/O操作(如读、写)通常是同步完成的,且没有像网络I/O那样的等待状态。但是,如果您确实需要对文件描述符设置非阻塞模式(例如,对于管道、FIFO或某些特殊类型的文件),可以在使用open系统调用时,在flags参数中包含O_NONBLOCK标志。
  2. 对于套接字,由于套接字是通过socket系统调用创建的,而不是open,因此您不能在socket调用时直接设置O_NONBLOCK。但是,您可以在socket调用之后,使用fcntl函数和F_SETFL命令来修改套接字文件描述符的标志,以包含O_NONBLOCK。
 // 创建套接字  int sockfd = socket(AF_INET, SOCK_STREAM, 0);  // 设置套接字为非阻塞模式  fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFL, 0) | O_NONBLOCK); 

注意:像fcntl这样的方式,无论对于系统文件还是网络套接字都是完全适用的。

2.2.在使用网络I/O接口时请求非阻塞行为:

        对于网络套接字,虽然您已经在套接字级别设置了非阻塞模式,但在某些情况下,您可能还想在特定的send、recv等I/O调用中请求非阻塞行为,以确保该调用不会因等待数据而阻塞。这可以通过在调用这些函数时包含MSG_DONTWAIT标志来实现。但是,请注意,如果套接字已经设置为非阻塞模式,那么即使没有指定MSG_DONTWAIT,这些调用也不会阻塞。

MSG_DONTWAIT主要用于在套接字处于阻塞模式时,临时请求非阻塞行为。

 // 套接字已经设置为非阻塞模式,但显式使用MSG_DONTWAIT也可以  ssize_t n = recv(sockfd, buf, len, MSG_DONTWAIT);  if (n == -1 && errno == EAGAIN) {  // 处理非阻塞情况下没有数据可读的情况  } 

        然而,在实际应用中,当您希望套接字以非阻塞方式工作时,通常会在套接字创建后立即使用fcntl设置其非阻塞模式,并在后续的网络I/O调用中不再需要显式地指定MSG_DONTWAIT(除非您有特定的理由需要在某些调用中临时恢复阻塞行为)。

        因此,总结来说,设置文件描述符为非阻塞模式的主要方式是通过fcntl和O_NONBLOCK标志(对于套接字和某些特殊类型的文件),而不是通过open的O_NONBLOCK选项(因为套接字不是通过open创建的),并且MSG_DONTWAIT是在网络I/O调用中请求非阻塞行为的额外选项,但它不是设置文件描述符非阻塞模式的独立方法。

2.3.fcntl函数

        当涉及到在Linux中对文件进行控制和管理时,fcntl(file control)函数是一个强大的工具。它提供了一种灵活的方式来执行各种文件操作,从修改文件属性到锁定文件,甚至是改变文件的行为。本文将深入探讨fcntl函数的用法、参数和示例,帮助读者更好地了解如何利用这个功能强大的API来操作文件。

        fcntl函数是Linux系统中用于执行各种文件控制操作的系统调用之一。它可以用于修改文件描述符的属性,如文件状态标志(file status flags)、文件描述符标志(file descriptor flags)、文件锁(file locks)以及其他的一些操作。fcntl函数提供了对文件或文件描述符进行底层控制的接口,使得开发者可以更精细地管理文件的行为。

fcntl函数的原型和参数

在C语言中,fcntl函数的原型如下:

#include <fcntl.h>int fcntl(int fd, int cmd, ... /* arg */);
  1. fd 是要操作的文件描述符。
  2. cmd 是控制操作的命令。
  3. arg 是与命令相关联的可选参数。

fcntl函数的cmd参数决定了具体执行的操作类型,常见的一些操作包括:

  1. F_GETFL:获取文件描述符的状态标志。
  2. F_SETFL:设置文件描述符的状态标志。
  3. F_GETLK:获取文件锁。
  4. F_SETLK:设置或释放文件锁。
  5. F_SETLKW:阻塞地设置或释放文件锁。

文件状态标志(File status flags)

  1. O_RDONLY:只读打开。
  2. O_WRONLY:只写打开。
  3. O_RDWR:读写打开。
  4. O_APPEND:追加写入。
  5. O_CREAT:如果文件不存在则创建文件。
  6. O_EXCL:与O_CREAT一起使用,如果文件存在则报错。
  7. O_TRUNC:如果文件存在且为只写或读写,则将其长度截断为0。

文件描述符标志(File descriptor flags):

  • FD_CLOEXEC:在exec执行期间关闭文件描述符。

其他标志:

  • O_NONBLOCK:非阻塞模式,用于文件描述符,使得对文件的读写操作不会阻塞进程。
  • O_SYNC:使得每次write都等到物理 I/O 操作完成后才返回。
  • O_DIRECTORY:如果文件名是目录,则打开失败。
  • O_DSYNC:等待物理 I/O 数据完成,不等待文件属性更新。
  • O_NOATIME:不更新访问时间戳。
  • O_NOCTTY:如果设备是终端,不将其分配为控制终端。

下面是一个简单的将文件描述符设置为非阻塞的例子

#include <iostream>  
#include <unistd.h>  
#include <fcntl.h>  
#include <errno.h>  
#include <string.h>  int main() {  std::cout << "This program echoes input in non-blocking mode. Try typing something and pressing enter.\n";  char buffer[100];  while (true) {  ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);  if (bytes_read > 0) {  buffer[bytes_read] = '\0'; // 确保字符串以null字符结束  std::cout << "Echo: " << buffer << std::endl;  } else if (bytes_read == 0) {  // 对于文件描述符如stdin,read返回0通常不表示结束,但在某些特殊情况下(如管道关闭)可能会发生  std::cout << "Unexpected end of input (this should not happen with stdin)." << std::endl;  break;  } else {  // read返回-1且errno被设置时表示发生错误  if (errno == EAGAIN || errno == EWOULDBLOCK) {  // 这是非阻塞模式下期望的行为,表示没有数据可读  std::cout << "No data available. Waiting...\n";  sleep(1); // 等待一秒后再次尝试  } else {  // 处理其他类型的错误  std::cerr << "Error reading input: " << strerror(errno) << std::endl;  break;  }  }  }  return 0;  
}

我们看到它阻塞了。

 接下来我将使用fcntl函数来将fd的属性改为非阻塞 

#include <iostream>  
#include <unistd.h>  
#include <fcntl.h>  
#include <errno.h>  
#include <string.h>  /**  * 将指定的文件描述符设置为非阻塞模式。  *  * @param fd 需要设置为非阻塞模式的文件描述符。  */  
void set_no_block(int fd) {  // 尝试获取文件描述符的当前标志(flags)  int flags = fcntl(fd, F_GETFL, 0);  // 检查fcntl调用是否失败  if (flags == -1) {  // 如果失败,则打印错误消息并返回  perror("fcntl");  // 使用perror打印错误信息,它会自动添加"fcntl: "前缀  return;  }  // 使用OR操作将O_NONBLOCK标志添加到现有的flags中  // 这样做的目的是保留文件描述符的其他标志不变,只添加非阻塞特性  fcntl(fd, F_SETFL, flags | O_NONBLOCK);  // 注意:这里没有检查fcntl的返回值,因为即使设置失败,也可能不会立即影响程序的行为  // 在实际使用中,可能需要根据需要添加错误处理逻辑  
}  int main() {  std::cout << "This program echoes input in non-blocking mode. Try typing something and pressing enter.\n";  // 将标准输入设置为非阻塞模式  set_no_block(STDIN_FILENO); // 使用STDIN_FILENO代替0,更具可读性  char buffer[100];  while (true) {  ssize_t bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer) - 1);  if (bytes_read > 0) {  buffer[bytes_read] = '\0'; // 确保字符串以null字符结束  std::cout << "Echo: " << buffer << std::endl;  } else if (bytes_read == 0) {  // 对于文件描述符如stdin,read返回0通常不表示结束,但在某些特殊情况下(如管道关闭)可能会发生  std::cout << "Unexpected end of input (this should not happen with stdin)." << std::endl;  break;  } else {  // read返回-1且errno被设置时表示发生错误  if (errno == EAGAIN || errno == EWOULDBLOCK) {  // 这是非阻塞模式下期望的行为,表示没有数据可读  std::cout << "No data available. Waiting...\n";  sleep(1); // 等待一秒后再次尝试  } else {  // 处理其他类型的错误  std::cerr << "Error reading input: " << strerror(errno) << std::endl;  break;  }  }  }  return 0;  
}

注意:

  1. F_GETFL操作用于获取文件描述符的当前标志,这些标志包括文件的访问模式(如只读、只写、读写)和文件状态标志(如同步、异步、非阻塞等)。F_SETFL操作用于设置文件描述符的标志。
  2. O_NONBLOCK是一个标志,用于指示文件描述符应处于非阻塞模式。在非阻塞模式下,如果操作(如读取或写入)不能立即完成,则调用将返回一个错误,而不是阻塞等待操作完成。这对于需要处理多个输入源或避免阻塞的应用程序非常有用。
  3.  先通过F_GETFL选项获取原有文件描述符的标志位,然后再通过F_SETFL选项将原有的标志位与O_NONBLOCK按位或之后,再重新设置回文件中。这样就可以将文件描述符设置为非阻塞了。
  •  非阻塞IO时,read的返回结果是-1,这样合理吗?

        当在非阻塞模式下调用read函数,并且底层没有数据时,read会返回-1,并设置errnoEAGAINEWOULDBLOCK(这两个错误码在大多数系统上可互换,表示资源暂时不可用)。这是非阻塞IO的一种标准行为,用于告诉调用者当前没有数据可读,而不是表示read函数本身出现了错误。

        底层没有数据,这算错误吗?其实这并不算错误,只不过当底层没有数据时,read以错误的方式返回了,但我们该如何区分read接口是真的调用失败了(比如read读取了一个不存在的fd),还是仅仅底层没有数据罢了,当然通过read的返回值我们是无法区分的,因为read在这两种情况下都返回-1,但可以通过错误码来区分,当非阻塞IO返回时,如果是底层没有数据,错误码会是EWOULDBLOCK或EAGAIN,如果read是真的出错调用了,会有相对应的错误码。

        因此,在处理非阻塞IO时,一种常见的做法是在read返回-1时检查errno,并根据errno的值来决定下一步的操作。如果errno表示资源暂时不可用(EAGAINEWOULDBLOCK),则程序可能会选择等待一段时间后再次尝试读取,或者执行其他任务。如果errno表示真正的错误(如EBADF),则程序应该采取适当的错误处理措施。

在Linux系统中,EAGAINEWOULDBLOCK是两个常见的错误码,它们通常用于指示资源暂时不可用的情况。具体来说:

  1. EAGAIN:这个错误码代表“Try again”(再试一次),意味着请求的操作暂时无法完成,但之后可能会成功。在网络编程中,EAGAIN经常出现在非阻塞套接字的读写操作中,表示暂时没有数据可读或数据无法立即写入。在文件操作中,如果文件描述符被设置为非阻塞模式,并且请求的操作不能立即完成(例如,读取时文件指针已经到达文件末尾,或者写入时磁盘空间不足但系统决定不等待),那么也会返回EAGAIN

  2. EWOULDBLOCK:这个错误码在某些情况下与EAGAIN可互换,同样表示资源暂时不可用。然而,在某些系统或特定的上下文中,EWOULDBLOCK可能更具体地用于指示某个操作被阻塞了,因为它会等待某个条件(如数据到达)变为真,但在非阻塞模式下,这个等待被阻止了。在大多数情况下,EAGAINEWOULDBLOCK可以视为同义词,尤其是在处理非阻塞I/O时。

怎么样?是不是很神奇?

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【项目一】基于pytest的自动化测试框架———解读requests模块
  • 【App】React Native
  • STM32的寄存器深度解析
  • 关系数据库,集合运算符,关系运算符
  • 1-4微信小程序基础
  • 苹果系统(MacOS)中的Finder如何方便展现根目录
  • 多线程篇(其它容器- CopyOnWriteArrayList)(持续更新迭代)
  • 嵌入式鸿蒙系统开发语言与开发方法分析
  • 什么是机器学习力场
  • 【H2O2|全栈】关于CSS(2)CSS基础(二)
  • 关于新版本 tidb dashboard API 调用说明
  • 推荐这款神器:Perplexity
  • mysql笔记9(子查询)
  • 使用LangChain集成ChatGPT插件:以Klarna购物API为例
  • 数据结构:堆排序
  • Android Studio:GIT提交项目到远程仓库
  • AWS实战 - 利用IAM对S3做访问控制
  • canvas实际项目操作,包含:线条,圆形,扇形,图片绘制,图片圆角遮罩,矩形,弧形文字...
  • css选择器
  • iOS动画编程-View动画[ 1 ] 基础View动画
  • JAVA SE 6 GC调优笔记
  • Markdown 语法简单说明
  • Python爬虫--- 1.3 BS4库的解析器
  • Spring Cloud Feign的两种使用姿势
  • SQLServer之创建显式事务
  • vue-cli3搭建项目
  • Vue--数据传输
  • -- 查询加强-- 使用如何where子句进行筛选,% _ like的使用
  • 关于for循环的简单归纳
  • 简单实现一个textarea自适应高度
  • 码农张的Bug人生 - 初来乍到
  • 什么软件可以提取视频中的音频制作成手机铃声
  • 时间复杂度与空间复杂度分析
  • 听说你叫Java(二)–Servlet请求
  • 一些关于Rust在2019年的思考
  • 1.Ext JS 建立web开发工程
  • 哈罗单车融资几十亿元,蚂蚁金服与春华资本加持 ...
  • ​​快速排序(四)——挖坑法,前后指针法与非递归
  • ​经​纬​恒​润​二​面​​三​七​互​娱​一​面​​元​象​二​面​
  • ​软考-高级-系统架构设计师教程(清华第2版)【第20章 系统架构设计师论文写作要点(P717~728)-思维导图】​
  • # linux 中使用 visudo 命令,怎么保存退出?
  • # Pytorch 中可以直接调用的Loss Functions总结:
  • $分析了六十多年间100万字的政府工作报告,我看到了这样的变迁
  • (2)STM32单片机上位机
  • (分享)一个图片添加水印的小demo的页面,可自定义样式
  • (附源码)spring boot北京冬奥会志愿者报名系统 毕业设计 150947
  • (附源码)springboot高校宿舍交电费系统 毕业设计031552
  • (每日一问)操作系统:常见的 Linux 指令详解
  • (四)Tiki-taka算法(TTA)求解无人机三维路径规划研究(MATLAB)
  • (一)模式识别——基于SVM的道路分割实验(附资源)
  • (转)清华学霸演讲稿:永远不要说你已经尽力了
  • ****Linux下Mysql的安装和配置
  • *算法训练(leetcode)第四十七天 | 并查集理论基础、107. 寻找存在的路径
  • .NET Core工程编译事件$(TargetDir)变量为空引发的思考
  • .net 打包工具_pyinstaller打包的exe太大?你需要站在巨人的肩膀上-VC++才是王道