Linux五种I/O模型与fcntl非阻塞控制

目录

  • [1 ~> 五种 I/O 模型](#1 ~> 五种 I/O 模型)
    • [1.1 I/O](#1.1 I/O)
    • [1.1 I/O 模型](#1.1 I/O 模型)
    • [1.2 阻塞 vs 非阻塞](#1.2 阻塞 vs 非阻塞)
    • [1.3 同步I/O vs 异步I/O](#1.3 同步I/O vs 异步I/O)
  • [2 ~> 非阻塞I/O](#2 ~> 非阻塞I/O)
    • [2.1 fcntl](#2.1 fcntl)
    • [2.2 非阻塞代码](#2.2 非阻塞代码)

1 ~> 五种 I/O 模型

1.1 I/O

I/O(input / output,输入/输出) 是指计算机核心处理系统(CPU/内存)与外部设备,外设,存储驱动器或网络接口之间的数据传输与通信过程。

IO 的效率其实并不高,当我们调用recv(),只有当接收缓冲区中有数据时recv才能够返回,不然就会阻塞等待数据就绪。

所以,I/O = 等 + 拷贝。为什么效率不高,就是因为要等,而拷贝的时间消耗相比于等待所占的比重就少很多。

所以,高效IO 就是围绕如何在单位时间减少等待的比重

1.1 I/O 模型

(1)阻塞 I/O

在系统调用完成读取或写入之前,应用程序(进程/线程)会一直阻塞,暂停执行。

(2)非阻塞 I/O

如果底层数据没有准备好,系统调用就会立即返回,需要线程定期轮询检查状态。

(3)信号驱动 I/O

当底层数据准备就绪后,内核会主动向应用程序发送信号(如SIGIO)进行通知。

(4)I/O 多路转接

通过select、poll、epoll接口让单个进程/线程同时监控多个文件描述符,当任意文件描述符就绪时就触发响应,然后应用程序进行I/O。

(5)异步 I/O

内核自动完成等待数据就绪以及将数据拷贝到用户空间内存的全过程,仅在全部操作完成后通知应用程序。


下面我们通过一个钓鱼的例子来加深对5种I/O模型的理解:

  • 阻塞I/O:张三在钓鱼时什么也不干,一直盯着鱼鳔看,一旦有鱼上钩了就开始往上钓。
  • 非阻塞I/O:李四在钓鱼时,抛竿后就开始刷抖音,然后时不时抬头看一个鱼竿的动静,没鱼上钩就继续刷抖音,有鱼上钩就往上钓。
  • 信号驱动I/O:王五在钓鱼时,在鱼竿上面系一个铃铛,然后就也去刷抖音了,但是王五只有在铃铛响之后才会开始往上钓鱼。
  • I/O多路转接:赵六钓鱼时直接拉来了一车鱼竿,全部抛竿后,然后过来过去查看鱼竿状态,发现哪个有鱼上钩就开始往上钓。
  • 异步I/O:田七只喜欢吃鱼,于是对秘书说:"你在这钓鱼,我现在去开会,钓到之后你打电话叫我,我过来接你。"

1.2 阻塞 vs 非阻塞

(1)非阻塞I/O的效率是不是比阻塞I/O的效率高?

如果单纯就I/O 的效率来讲,阻塞与非阻塞效率是相同的,因为 I/O = 等 + 拷贝,两者只是等待的方式不同。但非阻塞I/O 在等待的时间里做了更多事,所以,非阻塞对于时间的利用率更高。

(2)那么哪一种方式的效率更高呢?

谁在 I/O 过程中等待的时间最短,那么谁的效率就最高。对于赵六而言,他一次性监控了大量的鱼竿,钓完一杆(I/O),很快就发现又有鱼上钩了(数据就绪)。

这个过程中,赵六没怎么等,几乎一直在上钩。所以,多路转接 I/O 的效率是最高的

1.3 同步I/O vs 异步I/O

阻塞、非阻塞、信号驱动和多路转接 I/O 都属于同步 I/O,对于同步 I/O 而言,可以简单理解为应用进程或线程一定参与了等待或拷贝中的一个环节,或者都参与了。

异步I/O 一定没有参与等待和拷贝任意一个环节。

2 ~> 非阻塞I/O

2.1 fcntl

c 复制代码
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, .../* arg */);
  • fd:即要控制的文件描述符。
  • cmd:代表你要进行什么操作。
  • 第三个参数:根据cmd决定传指针或整数。
  • 返回值:成功由cmd决定;失败返回-1,错误码被设置。

cmd参数:

  • F_GETFL 获取文件状态标志
c 复制代码
int flags = fcntl(fd, F_GETFL);
if(flags == -1) { perror("fcntl"); }
  • F_SETFL 设置文件状态标志
c 复制代码
// 设置文件描述符为非阻塞:
// 1. 获取原有标志
int flags = fcntl(fd, F_GETFL);
// 2. 添加 O_NONBLOCK 非阻塞,不能直接赋值,要保留原有flag
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
  • F_DUPFD 复制文件描述符
c 复制代码
int newfd = fcntl(oldfd, F_DUPFD, low);

类似 dup()

复制 fd,返回一个大于等于 minfd 的新文件描述符。

  • F_SETOWN 设置拥有者(信号驱动 IO 必备)
c 复制代码
// socket就绪时,向这个进程PID发送SIGIO信号
fcntl(sockfd, F_SETOWN, getpid());

今天我们只用到获取和设置文件描述符状态的功能!

2.2 非阻塞代码

当我们正常从标准输入获取数据,默认std::cin是阻塞的(即 0 号文件描述符阻塞)。

c++ 复制代码
// 将指定文件描述符设置为非阻塞
void SetNoblock(int fd)
{
    int fl = fcntl(fd, F_GETFL);
    if(fl < 0)
    {
        std::cout << "出错" << std::endl;
        return;
    }
    // 保持原状态不变,并将将标准输入设置为非阻塞
    fcntl(fd, F_SETFL, fl | O_NONBLOCK); 
}
cpp 复制代码
// 非阻塞IO
void test2()
{
    SetNoblock(0); // 将标准输入设置为非阻塞
    char buff[1024];
    while(true)
    {
        int n = read(0, buff, sizeof(buff));
        if(n > 0)
        {
            buff[n-1] = 0;
            std::cout << buff << std::endl;
        }
        else if(n < 0)
        {
            if(errno == EINTR) 
            {
                std::cout << "信号中断" << std::endl;
                continue;
            }
            else if(errno == EAGAIN | EWOULDBLOCK)
            {
                std::cout << "底层数据没有准备好(未就绪)" << std::endl;
                sleep(2);
                continue;
            }
            else
            {
                std::cout << "出错" << std::endl;
                break;
            }
        }
        else break; // 读到文件末尾
    }
}

文件描述符在非阻塞状态下,如果再调用读或者写之类的接口,就需要对返回值为 -1 这种情况进行更加细致的判断:

  • errno == EAGAIN || errno == EWOULDBLOCK :表示底层数据没有准备好,下次就绪后再来读。
  • errno = EINTR:表示信号中断,重试即可。
  • 其他:表示真正出错了。

    此时,如果底层没有数据就绪,就会直接返回。
相关推荐
半旧5181 小时前
【Java Web02】WEB服务器内部实现技术揭秘
java·服务器·前端
gis开发之家1 小时前
日志怎么打才专业?Spring Boot 4 日志体系与 Logback 配置(生产级实战)
java·spring boot·后端·logback
SaaS发言人1 小时前
渠道数字化解决方案横评:谁更懂快消
java·大数据·人工智能
JL151 小时前
Java并发编程面试全攻略-从synchronized到AQS底层原理
java·开发语言·面试·并发编程
张宇Joaquin1 小时前
高性能计算之MPI——集合通信漫谈
服务器·microsoft·asp.net
ZC跨境爬虫1 小时前
LeetCode 119. 杨辉三角 II(原地更新优化详解 + Java Python 实现)
java·python·leetcode
二炮手亮子2 小时前
Linux命令行合集(涵盖定时任务、防火墙、SHH、线上问题实战等)
linux·运维
程序员黑豆7 小时前
Java包装类:基本类型与对象的桥梁
java·前端·ai编程