持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第6天,点击查看活动详情
前言
依稀记得当年大学上课的时候,老师说IO这块考试不考,感兴趣的同学可以自行了解。。。一直被蒙在鼓里,直到校招的时候被面试官一套连招问麻了。。。
现如今鄙人已成为一名练习时长两年半的Java练习生,怎么能不了解网络IO背后的细节呢。本篇揭开一波网络IO的神秘面纱
什么是IO
字面意思,input/output输入输出,linux世界中一切皆文件,而文件就是一串二进制流,不管Socket、FIFO、管道还是终端,对我们来说,一切都是二进制流。
- 在信息的交换过程中只是对这些流进行数据做接收转发,简称为I/O操作。
- 往流中读取数据,系统调用Read,写入数据,系统调用Write。
- 通常一个完整的IO包含两个步骤,磁盘IO和网络IO
磁盘IO
网络IO
- 完整的IO流程为:
-
-
磁盘IO负责把磁盘的数据从磁盘经过内核空间加载到用户空间
-
网络IO负责数据经过用户空间调用内核空间的api发送到网卡
-
用户空间与内核空间
-
操作系统为了支持多个应用同时运行,需要保证不同进程之间相对独立(一个进程的崩溃不会影响其他的进程,恶意进程不能直接读取和修改其他进程运行时的代码和数据)。用户程序不能影响系统调度,因此操作系统内核需要拥有高于普通进程的权限, 以此来调度和管理用户的应用程序。
-
于是内存空间被划分为两部分,内核空间存储的代码和数据具有更高级别的权限。内存访问的相关硬件在程序执行期间会进行访问控制,使得用户空间的程序不能直接读写内核空间的内存。
IO的阶段
-
硬件接口加载数据到内核空间
-
内核空间拷贝数据到用户空间
-
用户空间无法直接操作底层api,对数据进行加载、拷贝至网卡的操作需要切换成内核空间调用对应api完成
零拷贝
一切流行的组件为了追求速度都做了一些零拷贝的优化,先说一下传统拷贝。
传统拷贝
- 传统的文件传输方式会经历4次数据拷贝
-
硬件DMA拷贝到内核空间的内核缓冲区中
-
内核空间的内缓冲区CPU拷贝到用户空间的用户缓冲区
-
数据经过处理后从用户空间的用户缓冲区CPU拷贝到内核空间的内核缓冲区
-
从内核缓冲区DMA拷贝到网卡设备
-
mmp + write 零拷贝
- mmp使用虚拟内存,把内核空间和用户空间的虚拟地址映射在同一个物理地址从而减少数据拷贝次数
- mmap是将读缓冲区的地址和用户缓冲区的地址进行映射,内核缓冲区和应用缓冲区共享,所以节省了一次CPU拷贝,并且用户进程内存是虚拟的,只是映射到内核的读缓冲区,可以节省一半的内存空
sendfile 零拷贝
- sendfile表示在两个文件描述符之间传输数据,它是在操作系统内核中操作的,避免了数据从内核缓冲区和用户缓冲区之间的拷贝操作,因此可以使用它来实现零拷贝。
IO读写原理
-
用户进程层面的IO对应到底层可以简单区分成两块。
-
read系统调用指的是将数据从内核缓冲区复制到进程缓冲区
-
write系统调用指的是把数据从进程缓冲区复制到内核缓冲区
阻塞与非阻塞、异步与同步
-
阻塞:用户线程一直等待内核IO准备好数据,这期间什么都不干。
-
非阻塞:用户线程拿到内核返回的状态就直接返回自己的空间,内核IO准备好了就干,没准备好可以被调度去干别的事情。
-
同步IO:用户线程和内核线程的交互,用户空间线程是主动发起IO请求的一方,内核空间是被动接收的一方
-
异步IO:与上面刚好相反,内核空间是主动发起IO请求的一方,用户空间的线程是被动接受IO状态方。
阻塞IO模型
阻塞IO,按字面意思顾名思义,当用户发生系统调用读取数据,线程从用户态转换为内核态,如果当前线程发现内核数据并没有准备好,会一直阻塞到数据准备好,直到内核空间将数据拷贝到用户进程空间。
整个内核操作分成两个阶段。第一阶段从磁盘硬件中读取数据,第二阶段从内核把数据拷贝到用户空间里。 阻塞IO会在两个阶段里都阻塞住。
-
特点
-
使用阻塞 IO 时,因为线程会阻塞在准备内核准备数据阶段,需要配置多线程来使用,最常见的模型是阻塞 IO+多线程(线程池) ,每个连接一个单独的线程进行处理。疯狂创建线程会导致一个应用程序可以处理的客户端请求受限。面对连接数多的情况,是无法处理 。
-
非阻塞IO模型
- 按照上述对阻塞、非阻塞的描述,非阻塞IO模型核心即用户线程拿到内核返回的状态就直接返回自己的空间。
- 也就是用户空间调用系统调用到内核态时,不需要阻塞到准备好数据而是直接返回用户空间,通过不断轮询判断数据是否准备好,是否需要进入复制数据到用户空间的阶段。
-
特点
-
非阻塞 IO 解决了阻塞 IO每个连接一个线程处理的问题,所以其最大的优点就是 一个线程可以处理多个连接,这也是其非阻塞决定的。
-
但这种模式,也有一个问题,就是需要用户多次发起系统调用。频繁的系统调用是比较消耗系统资源的。
-
多路复用IO模型
-
IO多路复用,用户线程放弃直接进行read或write系统调用,而是通过select、epoll等内核函数,不断轮询内核返回的文件描述符。 整个过程只在调用select、poll、epoll这些调用的时候才会阻塞, accept/recv是不会阻塞
-
简单来说,我目前有 10 个连接,我可以通过一次系统调用将这 10 个连接都丢给内核,让内核告诉我,哪些连接上面数据准备好了,然后我再去读取每个就绪的连接上的数据。
-
因此,IO 多路复用,复用的是系统调用。通过有限次系统调用判断海量连接是否数据准备好了
-
特点
-
IO多路复用模型涉及两种系统调用,一种是就绪查询(select/epoll),一种是IO操作。
-
多路复用IO也需要轮询。负责就绪状态查询系统调用的线程,需要不断的进行select/epoll轮询,查找出达到IO操作就绪的socket连接。
-
异步IO
异步IO模型,其基本流程为:用户线程通过系统调用,向内核注册某个IO操作。内核在整个IO操作(包括数据准备、数据复制)完成后,通知用户程序,执行后续的业务操作。
在异步IO模型中,整个内核的数据处理过程中,包括内核将数据从网络物理设备(网卡)读取到内核缓存区、将内核缓冲区的数据复制到用户缓冲区,用户程序都不需要阻塞。
特点:
- 在内核等待数据和复制数据的两个阶段,用户线程都不是阻塞的。当内核的IO操作(等待数据和复制数据)全部完成后,内核以通知的形式告诉应用程序读数据。