Linux 五种IO模型

1.IO模型引入

我们之前说的IO,说白了就是输入和输出,可以理解成要么读数据,要么写数据。以前的认知中IO就是文件读写、网络收发消息,其实本质都一样:把数据从一个地方搬到另一个地方。比如读文件,是从硬盘读到程序里;写网络,是把程序里的数据发到网卡上。对应的系统调用就是 read 和 write,读用read,写用write。

以前我们简单理解成,read 就是把数据从操作系统拷贝到用户层,write 是把用户层的数据拷贝到操作系统。但这只说对了一半,真正的IO没这么简单。

就拿网络来说,你在程序里调用read去读数据,其实是想把操作系统里TCP接收缓冲区的数据,拷贝到你自己程序的缓冲区里。可万一接收缓冲区里啥数据都没有呢?那read就不会立刻返回,程序就卡在那儿不动,这就叫阻塞。它会一直等,等到缓冲区里有数据来了,才会开始拷贝,然后返回。所以read不只是拷贝,它还要先等数据就绪。

再看write,你想发数据,就是把程序里的数据拷贝到操作系统的TCP发送缓冲区。可如果发送缓冲区已经满了,没地方放了,那write也会阻塞,一直等缓冲区腾出空间,能放下数据了,才会完成拷贝。至于操作系统什么时候真正把数据发出去、发多少、出错怎么处理,那都是操作系统的事,程序管不着。

所以你看,不管是read还是write,都分成两步:第一步是等条件满足,要么等有数据可读,要么等有空间可写;第二步才是真正拷贝数据。所以IO的本质,就是等待加拷贝。

那什么时候高效?很简单,拷贝本身非常快,并且拷贝是操作系统的事情,我们也管不着,真正慢的是等待。所以高效IO,就是尽量少等待,把等待的时间压到最短。

以前单线程一个一个等IO,等完这个等那个,时间全浪费在等待上了。后来用多线程,或者IO多路复用,比如select、poll、epoll这些,能同时盯着很多个文件描述符,一个在等的时候,别的可能已经就绪可以拷贝了。这样等待的时间就重叠起来了,单位时间里能拷贝的数据就变多,IO效率自然就高了。

2.五种IO模型

IO一共就五种模型,所有读写数据的方式,都跑不出这五种。为了好懂,咱们就用钓鱼来比喻,IO = 等待 + 拷贝,对应到钓鱼就是:钓鱼 = 等鱼咬钩 + 拉杆把鱼钓上来。

第一种,阻塞式IO。就像新手张三去钓鱼,抛竿之后,眼睛死死盯着鱼漂,什么都不干,电话来了也不接,就干等着。只要鱼没咬钩,他就一直卡在那儿不动,直到鱼漂动了,他才拉杆。这就是阻塞IO:调用read或write之后,条件不满足就一直阻塞,程序啥也干不了,死等到底,等条件满足了再拷贝数据。

阻塞IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式.

第二种,非阻塞式IO。好比钓友李四,他也钓鱼,但不一直死盯。抛竿之后,他就去看书,过一会儿抬头看一眼鱼漂,没动静就继续看书,再过一会儿再看一眼,反复来回问。这就是非阻塞轮询:调用read/write,没数据或没空间就立刻返回,不卡住你,你可以去干别的,过一会儿再来问,直到数据准备好了再拷贝。

非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.

第三种,信号驱动式IO。像老钓友王五,他给鱼漂装了个铃铛,鱼一咬钩铃铛就响。他就安心看书,铃不响就一直看,铃一响他就知道可以拉杆了。这就是信号驱动:程序先告诉操作系统,数据好了给我发个信号,我先干别的,收到信号我再去读、去拷贝。

信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作.

第四种,IO多路复用。也就是多路转接,好比赵六,他特别猛,直接摆100根鱼竿,然后挨个巡视。一根竿等一条鱼慢,100根一起等,只要任意一根有鱼,他就去拉。这样单位时间里,等待的时间被大大压缩,效率高很多。对应到系统里,就是select、poll、epoll,一个线程同时盯着一大堆文件描述符,哪个就绪就处理哪个,不用一个个死等。

IO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件.

第五种,异步IO。最舒服的,像大老板田七,他只想吃鱼,不想钓鱼。直接让司机小王去岸边钓,自己回去开公司会议。小王钓完了,再打电话通知他:鱼好了,老板记得来拿。田七自始至终,没等、没看、没拉杆,只发起了钓鱼这件事,最后拿结果就行。这就是异步IO:用户进程只发个请求,剩下的等待、拷贝全由操作系统做完,完成后通知你,你全程不参与IO过程。

异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).

这里面有个很重要的区分:同步IO和异步IO。

同步IO,就是前面四种:阻塞、非阻塞、信号驱动、多路复用。它们的共同点是,你自己都参与了IO过程,都在等,只是等的方式不一样。张三死等,李四轮着等,王五等信号,赵六一起等,但都在等,等完之后还要自己去拷贝数据。

异步IO,就是第五种,你完全不参与IO,只发命令,等操作系统把等待和拷贝全都做完,你直接拿结果。

再说说阻塞和非阻塞的区别,很简单:就是等待的方式不一样。阻塞是条件不满足就卡死不动;非阻塞是条件不满足就立刻返回,你去干别的,过会儿再来问,反复轮询。

在这五种里,意义上最常用、最重要、效率最高的,是IO多路复用。异步IO听起来很爽,但实际用起来复杂,代码难写难维护;而多路复用能同时等一大堆IO,把等待时间重叠,大大减少等待占比。

3.非阻塞IO的接口和实现

我们平时要做非阻塞IO,最常用、最标准的办法,就是用 fcntl 这个函数,把文件描述符设置成非阻塞模式。一旦设置完,你再用 read 去读数据,就变成非阻塞IO了。文件描述符默认都是阻塞的,你直接写 while 循环 + read,它就会一直卡住等数据。

先看阻塞版本的 read 是怎么从标准输入读数据的。

我们先建一个用户层的缓冲区 buffer,然后写一个死循环。

然后调用 read,从 0 号文件描述符也就是标准输入读数据。默认是阻塞IO,参数就是文件描述符0、缓冲区、缓冲区大小减1,留一个字节放字符串结束符 \0,防止溢出。

read 会有三种返回情况:

第一种,返回值大于0,说明读到数据了,我们在数据末尾加个 \0,把它当正常字符串打印出来就行。

第二种,返回值等于0,表示读到文件结尾,在网络里就是对端关闭了连接,这时候直接退出循环。

第三种,返回值小于0,就是读取出错了,比如文件描述符非法、资源出错之类,用 cerr 打印错误信息,然后退出循环。

这就是最基础的阻塞式 read。

cpp 复制代码
int main()
{
    char buffer[1024];
    while(true)
    {
        // printf("Please Enter# ");
        // fflush(stdout);
        ssize_t n=read(0,buffer,sizeof(buffer)-1);
        if(n>0)
        {
            buffer[n-1]=0;
            cout<<"echo: "<<buffer<<endl;
        }
        else if(n==0)
        {
            cout<<"read done"<<endl;
            break;
        }
        else
        {
            cerr << "read error" << endl;
            break;
        }
    }
}

运行结果:

每次输入一条指令后,会阻塞住,等待用户输入,用户输入0号文件就有数据,读取用户输入的数据。

第一个参数传文件描述符,比如标准输入就是 0,socket 就是你创建的 fd。

第二个参数是命令,常用的就是两个

一个是 F_GETFL,获取文件状态标记;

一个是 F_SETFL,设置文件状态标记。

先讲 F_GETFL,表示把这个文件描述符当前的所有属性拿出来。一个文件描述符打开时,会带一堆属性,比如只读、只写、同步方式、阻塞方式等等。这些属性都是用位图存的,每一个比特位代表一种状态。

调用 fcntl(fd, F_GETFL) ,系统就会把这些状态打包,以返回值的形式给你,我们用一个变量 fl 存起来。

拿到 fl 之后,我们才能改。因为不能直接覆盖原来的属性,只能在原来的基础上添加一个非阻塞的属性。

所以第二步就是 F_SETFL,用来设置状态。我们要加的非阻塞状态,宏名叫 O_NONBLOCK。

因为是位图,所以只要用 按位或 |,把 O_NONBLOCK 加到原非阻塞IO,代码逻辑几乎一样,只是在最前面多一步:用 fcntl 把文件描述符设置成 O_NONBLOCK。

设置完之后,read 就不会卡住了:没数据的时候直接返回 -1,错误码是 EWOULDBLOCK,表示现在没数据,你可以先去干别的,过一会儿再来读,我们让进程睡眠一秒来代替做别的事情。

这就是非阻塞轮询的核心:不阻塞、不卡死,没数据立刻返回,有数据再处理。

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<fcntl.h>
#include<cerrno>
#include<cstring>
using namespace std;
void SetNoblock(int fd)
{
    int fl=fcntl(fd,F_GETFL);
    if(fl<0)
    {
        perror("fcntl");
        return;
    }
    fcntl(fd,F_SETFL,fl | O_NONBLOCK);
    cout<<" set "<<fd<<" nonblack done"<<endl;
}
int main()
{
    char buffer[1024];
    SetNoblock(0);
    while(true)
    {
        ssize_t n=read(0,buffer,sizeof(buffer)-1);
        if(n>0)
        {
            buffer[n-1]=0;
            cout<<"echo: "<<buffer<<endl;
        }
        else if(n==0)
        {
            cout<<"read done"<<endl;
            break;
        }
        else
        {
            if(errno==EWOULDBLOCK)
            {
                cout<<" 0 fd data not ready,try again"<<endl;
                //do other things
                sleep(1);
            }
            else
            {
                cerr<<"ready errno,n= "<<n<<"errno code: "<<errno<<" ,errno str"<<strerror(errno)<<endl;
            }
        }
    }
}

运行结果:

0号(显示器文件)文件没有数据的时候,就sleep一秒,然后再过来检测有没有数据,有数据就打印出来。

阻塞IO:没数据就死等,程序卡住不动。

非阻塞IO:先用 fcntl 设置模式,没数据直接返回,程序可以继续做其他事,反复轮询即可。

相关推荐
CDN3602 小时前
中小团队安全方案:360CDN 高防服务器基础配置
运维·服务器·安全
Coolmuster_cn2 小时前
如何删除三星手机和平板电脑上的应用程序
服务器·智能手机
程序边界2 小时前
深度Oracle替换工程实践的技术解读(上篇)
数据库·oracle
筱顾大牛2 小时前
黑马点评---用户签到、UV统计
android·服务器·uv
2401_831824962 小时前
RESTful API设计最佳实践(Python版)
jvm·数据库·python
zjeweler2 小时前
redis_tools_gui_v1.2 —Redis图形化漏洞利用工具
数据库·redis·web安全·缓存·安全性测试
暮冬-  Gentle°2 小时前
更优雅的测试:Pytest框架入门
jvm·数据库·python
专利观察员2 小时前
专利检索万字报告分享:《专利数据库3.0时代:2021-2025专利数据库的AI浪潮与选型逻辑重构》
数据库·人工智能·科技·专利检索·专利数据库
人道领域2 小时前
Day | 10【苍穹外卖:SpringTask 和WebSocket 案例】
java·数据库·后端