搞懂网络I/O模型

什么叫做I/O

I/O在计算机的全称是Input/Output,可以直接被翻译为输入输出。因为程序运行时的数据是在内存中驻留,并由CPU来进行处理的。所以涉及到数据交换的地方,如和硬盘或者是网络进行数据交换等,就需要I/O接口。
I/O 模型指在描述数据在计算机和外部存储设备中的传输过程,当发生计算机和外部设备的数据交换时,都会触发一次I/O操作。

一个I/O操作可以简单分解为以下四个步骤

1、进程向操作系统请求外部数据
2、操作系统将外部数据加载到内核缓冲区
3、操作系统将数据从内核缓冲区拷贝到进程缓冲区
4、进程读取数据继续后面的工作
在这里插入图片描述

图中涉及了I/O操作中用户进程和内核间的交互

因为用户进程是没有权限操作硬件设备的,所以它是无法直接进行I/O操作的。当需要进行I/O操作时,用户进程需要通知内核(CPU)进行相关操作。

在这个过程中内核(CPU)的状态会发生一写变化

通常当CPU运行用户进程时,CPU是处于用户态的,当CPU处于该状态时,只能进行一些简单的如加、减、乘、除等操作。
如果需要进行I/O操作(控制硬件设备),则需要CPU将状态改为内核态,这样就能获取更高的的操作权限,对硬件设备进行操作。

这里引出了一个CPU中状态的概念

操作系统的核心是内核(kernel),也就是我们常说的CPU,他拥有操作所有指令的权限,在 CPU 的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如清内存、设置时钟等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加。
因此操作系统将运行内存被分为两块,一块叫做用户空间,一块叫做内核空间。他们的大小比例关系为3:1。其中内核空间是受保护的,因为他保存着控制低层硬件设备的指令。当CPU属于用户态时,只能访问用户空间,需要访问内核空间时,需要将CPU状态升级为内核态,这样区分的目的也是为了提供操作系统的稳定性及可用性。
在这里插入图片描述
上图可以看到,应用程序和内核间无法直接通信,必须通过系统调用,而系统调用的成本很高。
当用户进程想要执行 IO 操作时,由于没有执行这些操作的权限,只能发起系统调用请求操作系统帮忙完成。而系统调用会产生中断陷入到内核,也就是进行了一次上下文切换操作。
用户进程通过系统调用访问系统资源的时候,需要切换到内核态,而这对应一些特殊的堆栈和内存环境,必须在系统调用前建立好。而在系统调用结束后,cpu会从核心模式切回到用户模式,而堆栈又必须恢复成用户进程的上下文。而这种切换就会有大量的耗时。

进程切换

到了内核,为了控制进程执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。

这里讲的进程上下文切换和上文的进程上下文切换不同,上文主要指的是同一进程的CPU权限等级修改,这里是指CPU执行不行的进程时,对上下文的保存,因为多个进程共享一个CPU,所以当CPU运行某个进程时,其他进程是挂起的,所以CPU需要有保存上下文的能力,记录进程挂起前的运行情况。

进程是资源分配的基本单位,因此进程切换时,需保存、装载各种状态数据等资源,代价就比较高。

I/O中断

在DMA技术出现以前,应用程序与磁盘之间的I/O操作都是通过CPU中断完成。外部存储设备采用主动中断的方式通过CPU,CPU负责将数据从外部存储设备拷贝至内核缓冲区,再从内核缓冲区拷贝至用户缓冲区,每次都会有上下文切换(CPU状态切换的上下文保存)的消耗及拷贝的消耗。
在这里插入图片描述

DMA

DMA全称叫直接内存存取(Direct Memory Access),是一种允许外围设备直接访问系统主存的机制
CPU通知DMA将外部数据拷贝至内核缓冲区,完成后DMA通知CPU,将内核缓冲区中的数据拷贝到用户缓冲区。通过这种方式大大降低了CPU的工作压力。
在这里插入图片描述

引入DMA后一次 read和write的流程

在这里插入图片描述
首先,期间共发生了 4 次用户态与内核态的上下文切换,因为发生了两次系统调用,一次是 read() ,一次是 write(),每次系统调用都得,每次系统调用都得先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。

  1. 先从用户态切换到内核态,等内核完成任务后,再从内核态切换回用户态。
  2. 第一次切换,用户态切内核态,将数据从内核缓冲区拷贝到用户缓冲区。
  3. 第二次切换,内核态切用户态,操作用户缓冲区中的数据。 第三次切换,用户态切内核态,将数据拷贝到socket缓冲区。
  4. 第四次切换,数据拷贝完后切换回用户态。
    上下文切换到成本并不小,一次切换需要耗时几十纳秒到几微秒,虽然时间看上去很短,但是在高并发的场景下,这类时间容易被累积和放大,从而影响系统的性能。
    其次,还发生了 4 次数据拷贝,其中两次是 DMA 的拷贝,另外两次则是通过 CPU 拷贝的,下面说一下这个过程:
  5. 第一次拷贝,把磁盘上的数据拷贝到操作系统内核的缓冲区里,这个拷贝的过程是通过 DMA 搬运的。
  6. 第二次拷贝,把内核缓冲区的数据拷贝到用户的缓冲区里,于是我们应用程序就可以使用这部分数据了,这个拷贝到过程是由 CPU 完成的。
  7. 第三次拷贝,把刚才拷贝到用户的缓冲区里的数据,再拷贝到内核的 socket 的缓冲区里,这个过程依然还是由 CPU 搬运的。
  8. 第四次拷贝,把内核的 socket 缓冲区里的数据,拷贝到网卡的缓冲区里,这个过程又是由 DMA 搬运的。
    我们回过头看这个文件传输的过程,我们只是搬运一份数据,结果却搬运了 4 次,过多的数据拷贝无疑会消耗 CPU 资源,大大降低了系统性能。
    这种简单又传统的文件传输方式,存在冗余的上文切换和数据拷贝,在高并发系统里是非常糟糕的,多了很多不必要的开销,会严重影响系统性能。
    所以,要想提高文件传输的性能,就需要减少「用户态与内核态的上下文切换」和「内存拷贝」的次数。
如何优化性能

先来看看,如何减少「用户态与内核态的上下文切换」的次数呢?

读取磁盘数据的时候,之所以要发生上下文切换,这是因为用户空间没有权限操作磁盘或网卡,内核的权限最高,这些操作设备的过程都需要交由操作系统内核来完成,所以一般要通过内核去完成某些任务的时候,就需要使用操作系统提供的系统调用函数。
而一次系统调用必然会发生 2 次上下文切换:首先从用户态切换到内核态,当内核执行完任务后,再切换回用户态交由进程代码执行。
所以,要想减少上下文切换到次数,就要减少系统调用的次数。

再来看看,如何减少「数据拷贝」的次数?

在 前面我们知道了,传统的文件传输方式会历经 4 次数据拷贝,而且这里面,「从内核的读缓冲区拷贝到用户的缓冲区里,再从用户的缓冲区里拷贝到 socket 的缓冲区里」,这个过程是没有必要的。
因为文件传输的应用场景中,在用户空间我们并不会对数据「再加工」,所以数据实际上可以不用搬运到用户空间,因此用户的缓冲区是没有必要存在的。

零拷贝

一次I/O读写都要从硬盘->内核-> 用户空间, DMA只是解决了硬盘到内核空间的问题,为了解决内核空间到用户空间的数据拷贝问题,提出了零拷贝的概念。
简单介绍一下零拷贝的三种实现思路。
1、用户态直接 I/O : 应用程序直接访问硬件存储,内核只辅助数据传输。硬件上的数据直接拷贝给用户空间,也就不存在内核空间缓冲区和用户空间缓冲区间的数据拷贝了。
2、减少数据拷贝次数:在数据传输过程中,减少数据在用户空间缓冲区和系统内核空间缓冲区之间的 CPU 拷贝次数,同时也避免数据在内核空间内部的 CPU 拷贝。
3、写时复制:多个进程共享同一块数据时,如果某进程要对这份数据修改,那将其拷贝到自己的进程地址空间中。

mmap + write
在前面我们知道,read() 系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,于是为了减少这一步开销,我们可以用 mmap() 替换 read() 系统调用函数。

buf = mmap(file, len);
write(sockfd, buf, len);
在这里插入图片描述

整个过程发生了4次用户态和内核态的上下文切换和3次拷贝,具体流程如下:

  1. 用户进程通过mmap()方法向操作系统发起调用,上下文从用户态转向内核态
  2. DMA控制器把数据从硬盘中拷贝到读缓冲区
  3. 上下文从内核态转为用户态,mmap调用返回
  4. 用户进程通过write()方法发起调用,上下文从用户态转为内核态
  5. CPU将读缓冲区中数据拷贝到socket缓冲区
  6. DMA控制器把数据从socket缓冲区拷贝到网卡,上下文从内核态切换回用户态,write()返回

mmap的方式节省了一次CPU拷贝,同时由于用户进程中的内存是虚拟的,只是映射到内核的读缓冲区,所以可以节省一半的内存空间,比较适合大文件的传输。

上述的缓存内容从内核缓冲区到用户缓冲区的操作也被称为 缓存 I/O。

缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
缓存 I/O 的缺点:数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是非常大的。

CPU中断信号(I/O中断)

当I/O设备收到数据传输操作的时候,会发一个中断信号给CPU,让CPU放下手头上的活,先处理这件事情。

下面我们来介绍常见的几种I/O模型

再次之前先介绍一下什么叫做进程堵塞

正在执行的进程,由于期待的某些事件未发生,如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等,则由系统自动执行阻塞原语(Block),使自己由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也因此只有处于运行态的进程(获得CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的

堵塞I/O(block I/O)同步堵塞

该模型下的I/O操作,当用户进程发起一次从磁盘读取数据的请求后,在CPU将数据拷贝到用户缓冲区之前,该用户进程一直是处于一个被堵塞的状态。只有当数据已经被复制到用户缓冲区之后,才会解除堵塞状态。
在这里插入图片描述

非堵塞I/O(nonBolock I/O)同步非堵塞

该模型下的I/O操作,当用户进程发起一次从磁盘读取数据的请求后,在CPU将数据拷贝到用户缓冲区之前,该用户进程将不会被堵塞。在数据拷贝完成之前,用户进行会有规律的不停询问数据有没有被拷贝好(用户进程不停主动询问kernel是否将数据准备好了)。当用户被拷贝至内核缓冲区之后,等待下一次询问发现内核数据已经准备好了,用户进程就停止询问,并堵塞进程等待数据从内核缓冲区拷贝至用户缓冲区。
在这里插入图片描述

I/O多路复用(I/O multiplexing)同步堵塞

通过一个线程或进程,去轮询所有的I/O请求,如果有已就绪的I/O请求,则执行请求。否则堵塞该线程。常见的实现由select,poll及epoll三种模型。当发现有就绪的I/O请求,则通知用户进程,然后用户进程等待数据从内核缓冲区拷贝至用户缓冲区。
在这里插入图片描述

信号驱动I/O(single driven I/O)同步非堵塞

用户进程发起一个I/O操作请求,然后就去做自己的事情,当数据被拷贝到内核缓冲区后,内核会发送一个信号,通知用户进程,然后用户进程堵塞等待数据从内核缓冲区拷贝至用户缓冲区,虽然在数据被拷贝到内核缓冲区时不是堵塞的,但是数据从内核缓冲区拷贝至用户缓冲区时时堵塞的,所以是个同步模型。该模型主要依赖于socket的信号驱动式功能
在这里插入图片描述

异步I/O(asynchronous I/O)

用户进程发起一次I/O操作的请求,然后用户进程就去干别的事情了,等到数据被拷贝到用户缓冲区后,内核会通知用户进程,让用户进程来处理数据。
在这里插入图片描述

最后,我们来看看框架中常用的那些网络模型,基本都是基于NIO去做处理的。

在此之前先介绍一下什么叫文件描述符fd

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。
文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

select

  1. 复制用户数据到内核空间(函数调用的参数等等都需要从用户空间复制到内核空间)
  2. 估计超时时间
  3. 遍历每个文件并调用f_op->poll 取得文件当前就绪状态, 如果前面遍历的文件都没有就绪,向文件插入wait_queue节点
  4. 遍历完成后检查状态:
    a). 如果已经有就绪的文件转到5;
    b). 如果有信号产生,重启poll或select(转到 1或3);
    c). 否则挂起进程等待超时或唤醒,超时或被唤醒后再次遍历所有文件取得每个文件的就绪状态
  5. 将所有文件的就绪状态复制到用户空间(遍历一遍所有的fd)
  6. 清理申请的资源
    select就是巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠,有资源可读/写时唤醒。下面我们看看select睡眠的详细过程。select会循环遍历它所监测的fd_set(一组文件描述符(fd)的集合)内的所有文件描述符对应的驱动程序的poll函数。驱动程序提供的poll函数首先会将调用select的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个bitmask告诉select当前资源哪些可用。当select循环遍历完所有fd_set内指定的文件描述符对应的poll函数后,如果没有一个资源可用(即没有一个文件可供操作),则select让该进程睡眠,一直等到有资源可用为止,进程被唤醒(或者timeout)继续往下执行。
    到这里明白了select进程被唤醒的过程。由于该进程是阻塞在所有监测的文件对应的设备等待队列上的,因此在timeout时间内,只要任意个设备变为可操作,都会立即唤醒该进程,从而继续往下执行。这就实现了select的当有一个文件描述符可操作时就立即唤醒执行的基本原理。

poll

poll 和 select 没有本质上的区别,区别在于select方法的入参只能传入size为1024的一个fd数组,有最大长度限制,poll对这个进行了改良

epoll

epoll 是在poll的基础上又进行了改良,poll会遍历所有的待处理的文件标识符,即使他们都是等待中的,遍历开销比较大,而epoll会遍历一个新的队列,队列中只保存可以处理的文件标识符信息。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
} @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment信号驱动式I/O模型是一种异步的I/O模型,它可以同时处理多个I/O操作,提高系统的并发性和响应性。下面是详细介绍: 1. 应用程序向内核_music, container, false); Button btnPlay = view.findViewById(R.id.btn_play); Button btnPause = view.findViewById(R.id.btn发起I/O请求,请求在指定的文件描述符上监听I/O事件。 2. 内核在指定的文件_pause); Button btnStop = view.findViewById(R.id.btn_stop); mMediaPlayer = MediaPlayer.create(getContext(), R.raw.music); btn描述符上等待I/O事件发生。 3. 当I/O事件发生时,内核会向应用程序发送Play.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (!mMediaPlayer.isPlaying()) { 一个信号,通知应用程序有I/O事件需要处理。 4. 应用程序收到信号后,可以在 mMediaPlayer.start(); } } }); btnPause.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View信号处理函数中处理相应的I/O事件。在信号处理函数中,应用程序可以读写数据,或 v) { if (mMediaPlayer.isPlaying()) { mMediaPlayer.pause(); } } }); btnStop.setOnClickListener(new View者关闭文件描述符等操作。 5. 当应用程序完成对I/O事件的处理后,可以再次向内核.OnClickListener() { @Override public void onClick(View v) { if (mMediaPlayer.isPlaying()) { mMediaPlayer.stop(); 发起I/O请求,等待下一个I/O事件的发生。 信号驱动式I/O模型可以有效地 mMediaPlayer = MediaPlayer.create(getContext(), R.raw.music); } } }); return view; } @Override 避免阻塞等待I/O操作完成的情况,提高系统的并发性和响应性。但是, public void onDestroy() { super.onDestroy(); if (mMediaPlayer != null) { mMediaPlayer.release(); mMediaPlayer =信号驱动式I/O模型需要应用程序处理信号,增加了一定的复杂度。同时, null; } } } ``` 7.其他模块的Fragment和Java代码与音乐模块类似,这里由于信号可能会被其他信号打断,因此需要应用程序进行信号处理函数的重入处理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值