6

分布式技术专题-功能原理分析-彻底明白网络IO模型的原理(上)

 3 years ago
source link: https://my.oschina.net/liboware/blog/5048842
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Linux的网络IO模型

网络IO的本质是socket的读写,socket在Linux中被抽象为流IO可以理解为对流的操作。

IO的分类和范畴

IO本身可以分为内存IO、网络IO和磁盘IO还有缓存IO等,一般讨论IO时更多是指后(网络IO和磁盘IO,因为这两个是最慢的哈哈),此处特别分析和说明网络IO

操作处理的分类

阻塞/非阻塞

针对函数/方法的实现方式而言,即数据就绪之前是立刻返回还是等待,即发起IO请求后是否会阻塞

阻塞IO机制

阻塞IO情况下,当用户调用read后,用户线程会被阻塞,等内核数据准备好并且数据从内核缓冲区拷贝到用户态缓存区后read才会返回。可以看到是阻塞的两个部分

  • CPU把数据从磁盘读到内核缓冲区。

  • CPU把数据从内核缓冲区拷贝到用户缓冲区。

非阻塞IO机制

  • 非阻塞IO发出read请求后发现数据没准备好,会继续往下执行,此时应用程序会不断轮询polling内核询问数据是否准备好,当数据没有准备好时,内核立即返回EWOULDBLOCK错误。

  • 直到数据被拷贝到应用程序缓冲区,read请求才获取到结果。并且你要注意!这里最后一次 read 调用获取数据的过程,是一个同步的过程,是需要等待的过程。这里的同步指的是内核态的数据拷贝到用户程序的缓存区这个过程。

同步/异步

IO读操作指数据流经:网络 -> 内核缓冲区 -> 用户内存

  • 同步和异步的主要区别在于数据从内核缓冲区 -> 用户内存这个过程需不需要用户进程等待

  • 等待内核态准备数据结束之后,会自动回通知用户态的线程进行读取信息数据,此时之前用户态的线程不需要等待,可以去做其他操作。

对于一个网络IO,会涉及到两个系统对象,一个是调用这个IO的process(or thread)【用户态】,另一个就是系统内核(kernel)【内核态】

当一个用户态发生read操作发生时,它会经历两个阶段:

  • 第一阶段:用户态线程等待内核态的数据准备 (Waiting for the data to be ready)

  • 第二阶段:用户态线程,将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。

    • 第一步:通常涉及等待网络上的数据分组到达,然后被复制到内核的某个缓冲区

    • 第二步:把数据从内核缓冲区复制到(用户态)应用进程缓冲区。网络应用处理的是两大类问题:网络IO、数据计算。前者给应用带来的性能瓶颈更大。

网络IO的模型大致有如下几种:

  • 同步模型(synchronous IO)
  • 阻塞IO模型(blocking IO)
  • 非阻塞IO模型(non-blocking IO)
  • 多路复用IO模型(multiplexing IO)
  • 信号驱动IO模型(signal-driven IO)
  • 异步IO(asynchronous IO)

阻塞IO模型(blocking IO)

Linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程如下:

当用户进程调用了recvfrom这个系统调用,如上所述,会有两个阶段

准备数据:

  • 很多时候数据在一开始还没有到达,这个时候kernel就要等待足够的数据到来。而用户进程会一直阻塞。

  • 当kernel等到数据准备好了,它会将数据从kernel中拷贝到用户内存,然后kernel返回,用户进程结束block状态,重新运行。

Blocking IO的特点就是IO执行的两个阶段都是block了的。

非阻塞IO模型(non-blocking IO)[poll]

在Linux中,可以通过设置socket使其变为non-blocking,其流程如下:

  • 当用户进程调用了recvfrom这个系统调用,如果kernel中的数据还没有准备好,那么用户进程不会block而是立刻返回一个error,即从用户的角度而言,不需要等待,马上得到一个结果。

  • 从图中可以看出,用户进程在判断结果是一个error后,了解到数据还没有准备好,于是就不断重复上述操作直至kernel中的数据准备好,然后它马上将数据拷贝到了用户内存,然后返回。

多路复用IO模型(multiplexing IO)

select/epoll/evpoll,也被称作是Event-Driven IO。好处是单个process可以同时处理多个网络连接的IO。

  • 基本原理可见下面的“IO复用技术”。也叫多路IO就绪通知。

  • 这是一种进程预先告知内核的能力,让内核发现进程指定的一个或多个IO条件就绪了,就通知用户进程

  • 使得一个进程能在一连串的事件上等待。

  • 这个流程和Blocking IO的流程其实并没有太多不同,事实上仅从图中看起来,由于需要进行两次系统调用,可能更差一些。但是,Select的优势在于它可以同时处理多个连接。

  • 如果处理的连接数不是很高的话,使用“Select/Epoll 的 Web Server”不一定比使用“多线程 + BIO的Web Server”性能更好,反而延迟会更大。

Select/Epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

在IO多路复用模型中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

异步IO(asynchronous IO)

用户进程发起read操作之后,立刻就可以开始去做其它的事。

  1. 从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block

  2. kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了

非阻塞和异步的区别

经过上面的介绍,会发现non-blocking IO和asynchronous IO的区别还是很明显的。

  • 在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。
  • asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。

IO复用技术

在IO编程过程中,当需要处理多个请求时,可以使用多线程和IO复的方式进行处理。

IO复用是什么?

把多个IO的阻塞复用到一个select之类的阻塞上,从而使得系统在单线程的情况下同时支持处理多个请求

IO复用常见的应用场景:

  • 服务器需要同时处理多个处于监听状态和多个连接状态的套接字;
  • 服务器需要处理多种网络协议的套接字
  • IO复用的实现方式目前主要有select、poll和epoll/evpoll。

select和poll的原理基本相同:

  1. 注册待侦听的fd(这里的fd创建时最好使用非阻塞)

  2. 每次调用都去检查这些fd的状态,当有一个或者多个fd就绪的时候返回

  3. 返回结果中包括已就绪和未就绪的fd

select和poll与epoll机制的比较

Linux网络编程过程中,相比于select/poll,epoll是有着更明显优势的一种选择

  • 支持一个进程打开的socket描述符不受限制(仅受限于操作系统的最大文件句柄数 unlimit)。

    • Select的缺陷:一个进程所打开的FD受限,默认是2048;尽管数值可以更改,但同样可能导致网络效率下降;可以选择多进程的解决方案,但是进程的创建本身代价不小,而且进程间数据同步远比不上线程间同步的高效。

    • epoll所支持的FD上限是最大可以打开文件的数目:/proc/sys/fs/file-max

IO效率可能随着文件描述符数目的增加而线性下降。

  • epoll扫描系统的机制不同

    • select/poll是线性扫描FD的集合

    • epoll是根据FD上面的回调函数实现的,活跃的socket会主动去调用该回调函数,其它socket则不会,相当于市是一个AIO,只不过推动力在OS内核。

  • 使用mmap加速内核与用户空间的消息传递,zero-copy的一种

  • epoll的API更加简单

  • IO复用还有一个 水平触发 和 边缘触发 的概念:

    • 水平触发:当就绪的fd未被用户进程处理后,下一次查询依旧会返回,这是select和poll的触发方式。
    • 边缘触发:无论就绪的fd是否被处理,下一次不再返回。理论上性能更高,但是实现相当复杂,并且任何意外的丢失事件都会造成请求处理错误。epoll默认使用水平触发,通过相应选项可以使用边缘触发。

IO模型的总结

最后,再举几个不是很恰当的例子来说明这四个IO Model,有A,B,C,D四个人在钓鱼:

  • A用的是最老式的鱼竿,所以呢,得一直守着,等到鱼上钩了再拉杆;(同步阻塞)

  • B的鱼竿有个功能,能够显示是否有鱼上钩,所以呢,B就和旁边的MM聊天,隔会再看看有没有鱼上钩,有的话就迅速拉杆;(非阻塞)

  • C用的鱼竿和B差不多,但他想了一个好办法,就是同时放好几根鱼竿,然后守在旁边,一旦有显示说鱼上钩了,它就将对应的鱼竿拉起来;(io多路复用机制)

  • D是个有钱人,干脆雇了一个人帮他钓鱼,一旦那个人把鱼钓上来了,就给D发个短信。(异步机制)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK