moon

V1

2022/05/18阅读:15主题:自定义主题1

这次 moon 要把网络 I/O 一网打尽

微信公众号:moon聊技术
关注选择“ 星标 ”, 重磅干货,第一 时间送达!
[如果你觉得文章对你有帮助,欢迎关注,在看,点赞,转发]

前言

大家好,我是 moon,上一次和大家聊了一下 socket(这次 moon 要把 socket 玩的明明白白),相信大家对 socket 有了一定的认识,对于 socket 还不熟悉的同学,可以先看看 socket 这篇文章,今天这篇文章是基于 socket,再和大家讲一讲网络I/O相关的知识,也刚好为后续 netty 的文章做下铺垫

什么是 I/O ?

I:其实就是 Input,输入
O:其实就是 Output,输出
所以 I/O 很好理解,就是输入和输出

生活中最简单的例子,你用微信和别人聊天,你发送信息给对方,就是输入对方回给你信息,你接受到了,就是输出

一般情况下,在软件中我们常说的 I/O 是指网络 I/O 和磁盘 I/O,今天我们就来聊下网络 I/O

网络 I/O 又是什么?

其实网络 I/O 就是网络中的输入与输出,我们再说详细点,正常的网络通信中,一条消息发送的过程中有一个很重要的媒介,叫做网卡,它的作用有两个

  • 一是将电脑的数据封装为帧,并通过网线(对无线网络来说就是电磁波)将数据发送到网络上去
  • 二是接收网络上其它设备传过来的帧,并将帧重新组合成数据,发送到所在的电脑中。

网卡能接收所有在网络上传输的信号,但正常情况下只接受发送到该电脑的帧和广播帧,将其余的帧丢弃。

所以网络 I/O 其实网络与服务端(电脑内存)之间的输入与输出

为什么会有网络 I/O 模型?

一般情况下,一次网络数据的传输会从客户端发送给服务端,由服务端网卡接受,转交给内存,最后由 cpu 执行相应的业务操作

只要有一点电脑知识的读者大多数都知道,cpu、显卡、内存等电脑中你数得上名字的模块,运行效率最高的就是 cpu 了,所以为了整个网络传输的提效,就诞生出了五种网络 I/O 模型

当然还有一点原因

不管是客户端还是服务端,在发送和接受数据的时候,都是要与 socket 缓冲区去进行交互的,那么什么时候交互?是否需要等待响应?基于这些不同的业务场景,也就有了五种网络 I/O 模型

其实这个结论是通用的,任何模型的调优改造健全一般都是围绕着性能、安全、业务场景这三个方向来前进的

I/O 模型

好了,我们说完了 I/O 模型的诞生,就来具体的研究下具体是哪五种 I/O 模型

阻塞 I/O

阻塞 I/O,顾名思义,就是在各个状态完全阻塞住整个 I/O 过程是个串行化的,在数据传输开始时,都要等到后续的每一个操作完成后才可以继续,这也是 linux 系统默认的 I/O 模型,当然,这种模型的问题就是效率非常的低

第二个问题就是,如果是在生产环境多线程的情况下,会有频繁线程的上下文切换,而这个切换又是非常消耗资源的。

非阻塞 I/O

非阻塞 I/O 针对于阻塞 I/O 有了一些改进

在系统调用内核后,内核如果没有准备好数据,则直接会返回一个错误码,直到数据准备好后,再走后续的流程

中间会有一个一直定时去询问的动作,所以它的缺点也就是在这里,需要一直去处理数据没有准备好的情况,也无法判断该数据多久会准备好。但是在并发的情况下相比阻塞 I/O 来说它的性能会好很多

异步 I/O

其实通过阻塞或者非阻塞 I/O,你能发现一个问题,其实拷贝数据都是由内核完成的,那又何必让应用进程去触发拷贝这个操作呢,由内核直接完成不就好了?

所以就出现了异步 I/O 模型

比如有一个读取数据的请求,应用只需要向内核发送一个 read,告诉内核它要读取数据后即刻返回。

之后内核收到请求并且建立一个信号连接,当数据准备就绪,内核会主动把数据从内核复制到用户空间,等所有操作都完成之后,内核再发起一个通知告诉应用,完成了所有操作,应用程序可以读取数据了,这就是异步 I/O 模型。

异步 IO 的好处是将发送询问请求、发送接收数据请求两个请求合并为一次请求就可以完成状态询问和数拷贝的所有操作,并且无阻塞,内核准备好数据后直接通知。

信号驱动模型

信号驱动就是进程发起一个 IO 操作,会向内核注册一个信号处理程序,然后立即返回

内核将数据报准备好后会发送一个信号给进程,这时候进程便可以在信号处理程序中调用 IO 处理数据报。

I/O 多路复用

I/O 多路复用解决的问题就是单线程怎么管理多个 socket 连接,内核负责轮询所有 socket,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

其主要有 select、poll、epoll 三种多路复用的网络I/O模型

select

select 的设计思路是唤醒模式,通过一个 socket 列表维护所有的 socket,socket 对应文件列表中的 fd,select 会默认限制最大文件句柄数为 1024,间接控制 fd[] 最大为 1024。

  • 如果列表中的 Socket 都没有数据,就挂起进程。
  • 如果有一个 Socket 收到数据,就唤醒进程,将该线程从等待队列中移除,加入到工作队列,然后准备执行 socket 任务。

那么有个问题来了,如果有多个 socket 任务同时唤醒怎么办,也就是说说有多个 socket 任务同时进来,那到底执行哪个 socket 任务?

所以,当进程被唤醒后,至少有一个 socket 是有数据的,这时候只需要遍历一遍 socket 列表就知道了此次需要执行哪些 socket 了。

缺点:

  • 每次 select 都需要将进程加入到监视 socket 的等待队列,每次唤醒都要将进程从 socket 待队列移除。这里涉及两次遍历操作,而且每次都要将 FDS 列表传递给内核,有一定的开销。

  • 进程被唤醒后,只能知道有 socket 接收到了数据,无法知道具体是哪一个 socket 接收到了数据,所以需要用户进程进行遍历,才能知道具体是哪个 socket 接收到了数据。

poll

poll 其实内部实现基本跟 select 一样,区别在于它们底层组织 fd[] 的数据结构不太一样,从而实现了 poll 的最大文件句柄数量限制去除了

epoll

我们前面说到 select 是需要遍历来解决查询就绪 socket 的,效率很低,epoll 就对此做了改进

  • 1.拆分:epoll 将添加等待队列和阻塞进程拆分成两个独立的操作,不用每次都去重新维护等待队列
    • 先用 epoll_ctl 维护等待队列 eventpoll,它通过红黑树存储 socket 对象,实现高效的查找,删除和添加。
    • 再调用 epoll_wait 阻塞进程,底层是一个双向链表。显而易见地,效率就能得到提升。

select 的添加等待队列和阻塞进程是合并在一起的,每次调用select()操作时都得执行一遍这两个操作,从而导致每次都要将fd[]传递到内核空间,并且遍历fd[]的每个fd的等待队列,将进程放入各个fd的等待队列中。

  • 2.直接返回有数据的 fd[]:select 进程被唤醒后,是需要遍历一遍 socket 列表,手动获取有数据的 socket,而 epoll 是在唤醒时直接把有数据的 socket 返回给进程,不需要自己去进行遍历查询。

直接返回有数据的 socket 是怎么实现的?

其实就是 epoll 会先注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似 callback 的回调机制,迅速激活这个文件描述符,当进程调用 epoll_wait() 时便得到通知。

epoll对文件描述符的操作有两种模式:LT(level trigger)和 ET(edge trigger)默认为 LT :

  • LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
  • ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

结语

I/O 模型这块儿可是重中之重,作为网络通信的核心,只要你想去大厂一定会被问到,今天这篇文章应该帮你梳理清楚了各个网络 I/O 模型之间的关系以及优缺点,让你对网络 I/O 模型有了更进一步的了解,下次我们准备开始突击 netty ~

分类:

后端

标签:

后端

作者介绍

moon
V1