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 设置模式,没数据直接返回,程序可以继续做其他事,反复轮询即可。

相关推荐
hef288几秒前
Go语言如何刷LeetCode_Go语言LeetCode刷题教程【速学】
jvm·数据库·python
渡我白衣几秒前
【MySQL基础】(4):MySQL 数据类型
数据库·人工智能·深度学习·神经网络·mysql·机器学习·自然语言处理
u0107475463 分钟前
HTML5中SVG描边虚线Stroke-dasharray的配置技巧
jvm·数据库·python
Dontla6 分钟前
JWT认证流程(JSON Web Token)
前端·数据库·json
Mike117.5 小时前
GBase 8a 日期边界写法和时间窗口取数偏差
数据库
SPC的存折7 小时前
1、Redis数据库基础
linux·运维·服务器·数据库·redis·缓存
爱学习的小囧8 小时前
VMware ESXi 6.7U3v 新版特性、驱动集成教程和资源包、部署教程及高频问答详情
运维·服务器·虚拟化·esxi6.7·esxi蟹卡驱动
小疙瘩8 小时前
只是记录自己发布若依分离系统到linux过程中遇到的问题
linux·运维·服务器
dldw7778 小时前
IE无法正常登录windows2000server的FTP服务器
运维·服务器·网络
我是伪码农9 小时前
外卖餐具智能推荐
linux·服务器·前端