计算机网络之五种IO模型

一、IO读写原理

1. 内核态与用户态

为了避免用户进程直接操作内核,保证内核安全,操作系统将内存(虚拟内存)划分为两部分:
① 内核空间(Kernel-Space):内核模块运行在内核空间,对应的进程处于内核态
② 用户空间(User-Space):用户程序运行在用户空间,对应的进程处于用户态。

1.1 内核态

操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内核空间,也有访问底层硬件设备的权限。内核空间总是驻留在内存中,它是为操作系统的内核保留的。应用程序是不允许直接在内核空间区域进行读写,也是不容许直接调用内核代码定义的函数的。

1.2 用户态

每个应用程序进程都有一个单独的用户空间,对应的进程处于用户态,用户态进程不能访问内核空间中的数据,也不能直接调用内核函数的,因此要进行系统调用的时候,就要将进程切换到内核态才能进行。

1.3 IO底层

  • 内核态进程可以执行任意命令,调用系统的一切资源,而用户态进程只能执行简单的运算,不能直接调用系统资源,所以用户态进程必须通过系统接口(System Call),才能向内核发出指令,完成调用系统资源之类的操作。
  • 用户程序进行IO的读写,依赖于底层的IO读写,基本上会用到底层的两大系统调用:sys_readsys_writesys_readsys_write两大系统调用都会涉及缓冲区。sys_read系统调用会把数据从内核缓冲区复制到应用程序的进程缓冲区。sys_write系统调用会把数据从应用程序的进程缓冲区复制到操作系统内核缓冲区。
  • 应用程序的IO操作实际是内核缓冲区与应用程序进程缓冲区的缓冲复制sys_readsys_write两大系统调用,都不负责数据在内核缓冲区和物理设备(如磁盘、网卡等)之间的交换。这项底层的读写交换操作,是由操作系统内核(Kernel)来完成的。
  • 所以,应用程序中的IO操作,无论是对Socket的IO操作,还是对文件的IO操作,都属于上层应用的开发,它们的在输入(Input)和输出(Output)维度上的执行流程,都是在内核缓冲区和进程缓冲区之间的进行数据交换。

2. 内核缓冲区与进程缓冲区

  • 缓冲区的目的,是为了减少频繁地与设备之间的物理交换。计算机的外部物理设备与内存与CPU相比,有着非常大的差距,外部设备的直接读写,涉及操作系统的中断。发生系统中断时,需要保存之前的进程数据和状态等信息,而结束中断之后,还需要恢复之前的进程数据和状态等信息。为了减少底层系统的频繁中断所导致的时间损耗、性能损耗,于是出现了内核缓冲区。
  • 有了内核缓冲区,操作系统会对内核缓冲区进行监控,等待缓冲区达到一定数量的时候,再进行IO设备的中断处理,集中执行物理设备的实际IO操作,通过这种机制来提升系统的性能。至于具体在什么时候执行系统中断(包括读中断、写中断),则由操作系统的内核来决定,应用程序不需要关心。
  • 上层应用程序使用sys_read系统调用时,仅仅把数据从内核缓冲区复制到上层应用的缓冲区(进程缓冲区);上层应用使用sys_write系统调用时,仅仅把数据从应用的用户缓冲区复制到内核缓冲区中。
  • 内核缓冲区与应用缓冲区在数量上也不同,在Linux系统中,操作系统内核只有一个内核缓冲区。而每个用户程序(进程)则有自己独立的缓冲区,叫做用户缓冲区或者进程缓冲区。Linux系统中的用户程序的IO读写程序,在大多数情况下,并没有进行实际的IO操作,而是在用户缓冲区和内核缓冲区之间直接进行数据的交换。

3. 图示

在这里插入图片描述
举例说明,比如客户端和服务器端之间完成一次socket请求和响应的数据交换 ,其流程如下:

  • 客户端发送请求:客户端程序通过sys_write系统调用,将数据复制到内核缓冲区,Linux将内核缓冲区的请求数据通过客户端器的网卡发送出去。
  • 服务端系统接收数据:在服务端,这份请求数据会被服务端操作系统通过DMA硬件,从接收网卡中读取到服务端机器的内核缓冲区。
  • 服务端程序获取数据:服务端程序通过sys_read系统调用,从Linux内核缓冲区复制数据,复制到用户缓冲区。
  • 服务器端业务处理:服务器在自己的用户空间中,完成客户端的请求所对应的业务处理。
  • 服务器端返回数据:服务器程序完成处理后,构建好的响应数据,将这些数据从用户缓冲区写入内核缓冲区,这里用到的是sys_write系统调用,操作系统会负责将内核缓冲区的数据发送去。
  • 服务端系统发送数据:服务端Linux系统将内核缓冲区中的数据写入网卡,网卡通过底层的通信协议,会将数据发送给目标客户端。

二、IO基本概念

1. 阻塞IO和非阻塞IO

网卡同步数据到内核缓冲区,如果内核缓冲区中的数据未准备好,用户进程发起read操作,阻塞则会一直等待内存缓冲区数据完整后再解除阻塞,而非阻塞则会立即返回不会等待,注意,内核缓冲区与用户缓冲区之间的读写操作肯定是阻塞的

2. 同步和异步

  • 同步:调用者主动发起请求,调用者主动等待这个结果返回,一但调用就必须有返回值。
  • 异步:调用发出后直接返回,所以没有返回结果。被调用者处理完成后通知回调、通知等机制来通知调用者。

三、五种IO模型

引言:TCP传送数据流程

要深入的理解各种IO模型,那么必须先了解下产生各种IO的原因是什么,要知道这其中的本质问题那么我们就必须要知一条消息是如何从过一个人发送到另外一个人的;以两个应用程序通讯为例,我们来了解一下当“A”向"B" 发送一条消息,简单来说会经过如下流程:
在这里插入图片描述
第一步:应用A把消息发送到 TCP发送缓冲区。
第二步: TCP发送缓冲区再把消息发送出去,经过网络传递后,消息会发送到B服务器的TCP接收缓冲区。
第三步:B再从TCP接收缓冲区去读取属于自己的数据。
根据上图我们基本上了解消息发送要经过应用A、应用A对应服务器的TCP发送缓冲区、经过网络传输后消息发送到了应用B对应服务器TCP接收缓冲区、然后最终B应用读取到消息。
在这里插入图片描述
我们把视角切换到上面图中的第三步, 也就是应用B从TCP缓冲区中读取数据。因为应用之间发送消息是间断性的,也就是说在上图中TCP缓冲区还没有接收到属于应用B该读取的消息时,那么此时应用B向TCP缓冲区发起读取申请,TCP接收缓冲区是应该马上告诉应用B现在没有你的数据,还是说让应用B在这里等着,直到有数据再把数据交给应用B。把这个问题应用到第一个步骤也是一样,应用A在向TCP发送缓冲区发送数据时,如果TCP发送缓冲区已经满了,那么是告诉应用A现在没空间了,还是让应用A等待着,等TCP发送缓冲区有空间了再把应用A的数据访拷贝到发送缓冲区。

1. 阻塞IO模型

1.1 概述

所谓阻塞IO就是当应用B发起读取数据申请时,在内核数据没有准备好之前,应用B会一直处于等待数据状态,直到内核把数据准备好了交给应用B才结束。

1.2 术语

在应用调用read()函数读取数据时,其系统调用直到数据包到达且被复制到应用缓冲区中或者发送错误时才返回,在此期间一直会等待,进程从调用到返回这段时间内都是被阻塞的称为阻塞IO。

1.3 图解

在这里插入图片描述

1.4 流程

  1. 用户进程调用read()系统函数,用户进程进入阻塞状态。
  2. 系统内核收到read()系统调用,网卡开始准备接收数据,在一开始内核缓冲区数据为空,内核在等待接收数据,用户进程同步阻塞等待。
  3. 内核缓冲区中有完整的数据后,内核会将内核缓冲区中的数据复制到用户缓冲区。
  4. 直到用户缓冲区中有数据,用户进程才能解除阻塞状态继续执行。

1.5 优缺点

  • 优点:① 开发简单,由于accept()、recv()都是阻塞的,为了服务于多个客户端请求,新的连接创建一个线程去处理即可。② 阻塞的时候,线程挂起,不消耗CPU资源。
  • 缺点:① 每新来一个IO请求,都需要新建一个线程对应,高并发下系统开销大,多线程上下文切换频繁。② 创建线程太多,内存消耗大。

1.6 思考

因为accept()、recv()函数都是阻塞的,如果系统想要支持多个IO请求,就创建更多的线程,如果去解决这个问题呢?如果可以把accept、recv函数变成非阻塞的方式,是不是就可以避免创建多个线程了?这就引入了我们的同步非阻塞IO。

2. 非阻塞IO模型

2.1 概述

按照上面的思路,所谓非阻塞IO就是当应用B发起读取数据申请时,如果内核数据没有准备好会即刻告诉应用B,不会让B在这里等待。

2.2 图解

在这里插入图片描述

2.3 流程

  1. 用户进程发起请求调用read()函数,系统内核收到read()系统调用,网卡开始准备接收数据。
  2. 内核缓冲区数据没有准备好,即刻返回EWOULDBLOCK错误码,用户进程不断的重试查询内核缓冲区数据有没有准备好。
  3. 当内核缓冲区数据准备好了之后,用户进程阻塞,内核开始将内核缓冲区数据复制到用户缓冲区,否则还是返回错误码。
  4. 复制完成后,用户进程解除阻塞,读取数据继续执行。

2.4 优缺点

  • 优点:① 非阻塞, accept()、recv()均不阻塞,用户线程立即返回。 ② 规避了同步阻塞模式的多线程问题。
  • 缺点:假如现在有1万个客户端连接,但只有1个客户端发送数据过来,为了获取这个1个客户端发送的消息,我需要循环向内核发送1万遍recv()系统调用,而这其中有9999次是无效的请求,浪费CPU资源

2.5 思考

针对同步非阻塞IO的缺点,设想如果内核提供一个方法,可以一次性把1万个客户端socket连接传入,在内核中去遍历,如果没有数据,这个方法就一直阻塞,一但有数据,这个方法解除阻塞并把所有有数据的socket返回,把这个遍历的过程交给内核去处理,是不是就可以避免空跑,避免1万次用户态到内核态的切换呢?

3. IO多路复用模型

3.1 概述

由一个线程监控多个网络请求(我们后面将称为fd即文件描述符,Linux系统把网络请求以fd来标识),这样就可以只需要一个或几个线程就可以完成数据状态询问的操作,当有数据准备就绪之后再分配对应的线程去读取数据,这么做就可以节省出大量的线程资源出来,这个就是IO复用模型的思路。
在这里插入图片描述
正如上图,IO复用模型的思路就是系统提供了一种函数可以同时监控多个fd的操作,这个函数就是我们常说到的select、poll、epoll函数,有了这个函数后,应用线程通过调用select函数就可以同时监控多个fd,select函数监控的fd中只要有任何一个数据状态准备就绪了,select函数就会返回可读状态,这时询问线程再去通知处理数据的线程,对应线程此时再发起recvfrom请求去读取数据。

3.2 术语

IO多路复用即一个线程监测多个IO操作,该模型是建立在内核提供的多路分离函数select基础之上的,使用select函数可以避免同步非阻塞IO模型中轮询等待的问题,即一次性将N个客户端socket连接传入内核然后阻塞,交由内核去轮询,当某一个或多个socket连接有事件发生时,解除阻塞并返回事件列表,用户进程再循环遍历处理有事件的socket连接。这样就避免了多次调用recv()系统调用,避免了用户态到内核态的切换。

3.3 图解

在这里插入图片描述

3.4 IO多路复用的三种实现

3.4.1 select函数
3.4.1.1 概述

select函数仅仅知道有几个I/O事件发生了,但并不知道具体是哪几个socket连接有I/O事件,还需要轮询去查找,时间复杂度为O(n),处理的请求数越多,所消耗的时间越长。

3.4.1.2 流程
  • 从用户空间拷贝fd_set(注册的事件集合)到内核空间。
  • 遍历所有fd文件,并将当前进程挂到每个fd的等待队列中,当某个fd文件设备收到消息后,会唤醒设备等待队列上睡眠的进程,那么当前进程就会被唤醒。
  • 如果遍历完所有的fd没有I/O事件,则当前进程进入睡眠,当有某个fd文件有I/O事件或当前进程睡眠超时后,当前进程重新唤醒再次遍历所有fd文件。
3.4.1.3 缺点
  • 单个进程所打开的FD是有限制的,通过 FD_SETSIZE设置,默认1024。
  • 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大。、
  • 每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。
  • select函数在每次调用之前都要对参数进行重新设定,这样做比较麻烦,而且会降低性能。
  • 进程被唤醒后,程序并不知道哪些socket收到数据,还需要遍历一次。
3.4.2 poll函数
3.4.2.1 概述

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的。

3.4.3 epoll函数
3.4.3.1 概述

epoll可以理解为event pool,不同与select、poll的轮询机制,epoll采用的是事件驱动机制,每个fd上有注册有回调函数,当网卡接收到数据时会回调该函数,同时将该fd的引用放入rdlist就绪列表中。当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。

3.4.3.2 流程

在这里插入图片描述

  • 调用epoll_create()创建一个ep对象,即红黑树的根节点,返回一个文件句柄。
  • 调用epoll_ctl()向这个ep对象(红黑树)中添加、删除、修改感兴趣的事件。
  • 调用epoll_wait()等待,当有事件发生时网卡驱动会调用fd上注册的函数并将该fd添加到rdlist中,解除阻塞。
3.4.3.3 优点
  • epoll支持的最大文件描述符上限是整个系统最大可打开的文件数目,1G内存理论上最大创建10万个文件描述符。
  • 每个文件描述符上都有一个callback函数,当socket有事件发生时会回调这个函数将该fd的引用添加到就绪列表中,select和poll并不会明确指出是哪些文件描述符就绪,而epoll会。造成的区别就是,系统调用返回后,调用select和poll的程序需要遍历监听的整个文件描述符找到是谁处于就绪,而epoll则直接处理即可。
  • select、poll采用轮询的方式来检查文件描述符是否处于就绪态,而epoll采用回调机制。造成的结果就是,随着fd的增加,select和poll的效率会线性降低,而epoll不会受到太大影响,除非活跃的socket很多。
3.4.4 总结

在这里插入图片描述

4. 信号驱动IO模型

4.1 概述

复用IO模型解决了一个线程可以监控多个fd的问题,但是select是采用轮询的方式来监控多个fd的,通过不断的轮询fd的可读状态来知道是否就可读的数据,而无脑的轮询就显得有点暴力,因为大部分情况下的轮询都是无效的,所以有人就想,能不能不要我总是去问你是否数据准备就绪,能不能我发出请求后等你数据准备好了就通知我,所以就衍生了信号驱动IO模型。信号驱动IO不是用循环请求询问的方式去监控数据就绪状态,而是在调用sigaction时候建立一个SIGIO的信号联系,当内核数据准备好之后再通过SIGIO信号通知线程数据准备好后的可读状态,当线程收到可读状态的信号后,此时再向内核发起recvfrom读取数据的请求,因为信号驱动IO的模型下应用线程在发出信号监控后即可返回,不会阻塞,所以这样的方式下,一个应用线程也可以同时监控多个fd。类似于下图描述:
在这里插入图片描述

4.2 术语

首先开启套接口信号驱动IO功能,并通过系统调用sigaction执行一个信号处理函数,此时请求即刻返回,当数据准备就绪时,就生成对应进程的SIGIO信号,通过信号回调通知应用线程调用recvfrom来读取数据。

4.3 图解

在这里插入图片描述
总结: IO复用模型里面的select虽然可以监控多个fd了,但select其实现的本质上还是通过不断的轮询fd来监控数据状态, 因为大部分轮询请求其实都是无效的,所以信号驱动IO意在通过这种建立信号关联的方式,实现了发出请求后只需要等待数据就绪的通知即可,这样就可以避免大量无效的数据状态轮询操作。

5. 异步IO模型

5.1 概述

其实经过了上面两个模型的优化,我们的效率有了很大的提升,但是我们当然不会就这样满足了,有没有更好的办法,通过观察我们发现,不管是IO复用还是信号驱动,我们要读取一个数据总是要发起两阶段的请求,第一次发送select请求,询问数据状态是否准备好,第二次发送recevform请求读取数据。
也许你一开始就有一个疑问,为什么我们明明是想读取数据,什么非得要先发起一个select询问数据状态的请求,然后再发起真正的读取数据请求,能不能有一种一劳永逸的方式,我只要发送一个请求我告诉内核我要读取数据,然后我就什么都不管了,然后内核去帮我去完成剩下的所有事情?当然既然你想得出来,那么就会有人做得到,有人设计了一种方案,应用只需要向内核发送一个read请求,告诉内核它要读取数据后即刻返回;内核收到请求后会建立一个信号联系,当数据准备就绪,内核会主动把数据从内核复制到用户空间,等所有操作都完成之后,内核会发起一个通知告诉应用,我们称这种一劳永逸的模式为异步IO模型。
在这里插入图片描述

5.2 术语

应用告知内核启动某个操作,并让内核在整个操作完成之后,通知应用,这种模型与信号驱动模型的主要区别在于,信号驱动IO只是由内核通知我们合适可以开始下一个IO操作,而异步IO模型是由内核通知我们操作什么时候完成。

5.3 图解

在这里插入图片描述
总结:异步IO的优化思路是解决了应用程序需要先后发送询问请求、发送接收数据请求两个阶段的模式,在异步IO的模式下,只需要向内核发送一次请求就可以完成状态询问和数拷贝的所有操作。

总结

  • 我们通常会说到同步阻塞IO、同步非阻塞IO,异步IO几种术语,通过上面的内容,那么我想你现在肯定已经理解了什么是阻塞什么是非阻塞了,所谓阻塞就是发起读取数据请求的时,当数据还没准备就绪的时候,这时请求是即刻返回,还是在这里等待数据的就绪,如果需要等待的话就是阻塞,反之如果即刻返回就是非阻塞。
  • 我们区分了阻塞和非阻塞后再来分别下同步和异步,在IO模型里面如果请求方从发起请求到数据最后完成的这一段过程中都需要自己参与,那么这种我们称为同步请求;反之,如果应用发送完指令后就不再参与过程了,只需要等待最终完成结果的通知,那么这就属于异步。
  • 我们再看同步阻塞、同步非阻塞,他们不同的只是发起读取请求的时候一个请求阻塞,一个请求不阻塞,但是相同的是,他们都需要应用自己监控整个数据完成的过程。而为什么只有异步非阻塞 而没有异步阻塞呢,因为异步模型下请求指定发送完后就即刻返回了,没有任何后续流程了,所以它注定不会阻塞,所以也就只会有异步非阻塞模型了。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值