亲宝软件园·资讯

展开

Netty源码分析<序一Unix网络I/O模型简介>

zengjian 人气:3

Unix网络 I/O 模型

    我们都知道,为了操作系统的安全性考虑,进程是无法直接操作I/O设备的,其必须通过系统调用请求内核来协助完成I/O动作,而内核会为每个I/O设备维护一个buffer。以下是其示意图:

注:一个输入操作通常包括两个不同的阶段:

  (1)等待数据准备好;

  (2)从内核进程向用户进程复制数据

    整个请求过程为: 用户空间进程发起请求,内核进程接受到请求后,从I/O设备中获取数据到buffer中,再将buffer中的数据copy到用户进程空间中,该用户进程获取到数据后再响应客户端。

    请求过程中,I/O设备将数据输入到内核buffer中需要时间,内核buffer中数据复制到用户进程也需要时间。因此根据在这两段时间内等待的方式不同,I/O操作可以分为以下几种模式:

    1.阻塞I/O (Blocking I/O)

    2.非阻塞I/O (Non-Blocking I/O)

    3.I/O 复用 (select和poll)

    4.信号驱动I/O(SIGIO)

    5.异步I/O(POSIX的aio_系列函数)

  1.阻塞I/O (Blocking I/O):

  在Unix中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样的:

  当用户进程调用了recvfrom这个系统调用,内核就开始了IO的第一个阶段:等待数据准备。对于网络IO来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候内核就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当内核一直等到数据准备好了,它就会将数据从内核中拷贝到用户内存,然后内核返回结果,用户进程才解除block的状态,重新运行起来。 所以,阻塞式IO的特点就是在IO执行的两个阶段都被block了。

  2.非阻塞I/O:

  Unix下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:

   当用户进程调用recvfrom时,系统不会阻塞用户进程,而是立刻返回一个ewouldblock错误,从用户进程角度讲 ,并不需要等待,而是马上就得到了一个结果。用户进程判断标志是ewouldblock时,就知道数据还没准备好,于是它就可以去做其他的事了,于是它可以再次发送recvfrom,一旦内核中的数据准备好了。并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。 当一个应用程序在一个循环里对一个非阻塞调用recvfrom,我们称为轮询。应用程序不断轮询内核,看看是否已经准备好了某些操作。这通常是浪费CPU时间,但这种模式偶尔会遇到。

   3.I/O 复用模型 (select和epoll)

  I/O复用基本原理就是select/epoll这个函数会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:

  当用户进程调用了select,那么整个进程会被block,而同时,内核会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从内核拷贝到用户进程。 这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个系统调用(select 和 recvfrom),而blocking IO只调用了一个系统调用 (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。对于单个连接来说,select/epoll的优势并没有优势,但对于多个连接,可以真正体现其优势。

顺便说下select/poll/epoll

  select:

  select函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。

  缺点:1、select最大的缺陷就是单个进程所打开的FD是有一定限制的,,它由FDSETSIZE设置,32位机默认是1024个,64位机默认是2048。一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max命令查看。

     2、对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低。

     3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大。

  poll:

  poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

  优点:它没有最大连接数的限制,原因是它是基于链表来存储的。

  缺点:1、大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

     2 、poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

  epoll:

  相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epollctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epollwait便可以收到通知。

  优点:1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。

     2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

     3、内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。

注:水平触发与边缘触发:

  水平触发和边缘触发借鉴的是电子触发的概念。在各种数字电子元器件中,输出是随着输入变化而逻辑变化的,最常见的有“与电门”。在与电门中,两个输入的电极同为正电压(真),则输出一个正电压(真);任意一个输入的电位变成负电压(假),则输出一个负电压(假)。问题在于,什么时候触发电位的变化呢?有两种方案:
1. 在一个输入的电位变化时(边缘触发)
2.在输入的电位状态变成目标状态时(水平触发)
      _______
____| |__
    (1) (2)
  你可能会问,这不是一个概念么?在电子学的概念里,不是。而在这里,网络通信借鉴了这个概念,将消息到达后,读取(触发)的时机形象地分为“水平触发”和“边缘触发”。边缘触发是指消息到来的时刻进行消费,如果一次到达的消息超过了一次消费的最大值,剩余的消息不会被继续消费,要消费这一部分消息要么等到下一次消息的到来,要么在这次消费之后主动触发消费剩余消息。至于水平触发,则是以是否有剩余消息为标准,有剩余,就一直主动消费直到无消息。

举个点餐的例子:

  水平触发:点单后,菜(数据)做好了,服务员端上来问吃不吃(读),你不吃或者吃不完,她过会还会端过来问你吃不吃,提醒你,还没吃完,可以继续吃,反反复复。    

  边缘触发:服务员端上菜后,你一次没有吃完,好了,等你想吃剩下的时候,也别吃了,除非再点菜,才能吃到刚没吃完的。

4.信号驱动式I/O(SIGIO)

   一般示意图如下:

可以看出用户进程在等待数据阶段不是阻塞的。当在调用recvfrom函数时进程阻塞。大致流程是这样的:首先用户进程建立SIGIO信号处理程序,并通过系统调用sigaction执行一个信号处理函数,这时用户进程便可以做其他的事了,一旦数据准备好,系统便为该进程生成一个SIGIO信号,去通知它数据已经准备好了,于是用户进程便调用recvfrom把数据从内核拷贝出来,并返回结果。

 5.异步I/O(POSIX的aio_系列函数)

  一般来说,这些函数通过告诉内核启动操作并在整个操作(包括内核的数据到缓冲区的副本)完成时通知我们。这个模型和前面的信号驱动I/O模型的主要区别是,在信号驱动的I/O中,内核告诉我们何时可以启动I/O操作,但是异步I/O时,内核告诉我们何时I/O操作完成。以下是其示意图:

  当用户进程向内核发起某个操作后,会立刻得到返回,并把所有的任务都交给内核去完成(包括将数据从内核拷贝到用户自己的缓冲区),内核完成之后,只需返回一个信号告诉用户进程已经完成就可以了。

6总结

从上可以看出,阻塞I/O模型、非阻塞I/O模型、I/O复用模型、信号驱动的I/O模型都是同步的,因为应用进程在请求的过程都会阻塞。而异步I/O模型是异步的。

区分下阻塞、非阻塞和同步、异步,其实针对的对象是不一样的。
阻塞、非阻塞说的是调用者;
同步、异步说的是被调用者。

同步请求,A调用B,B的处理是同步的,在处理完之前他不会通知A,只有处理完之后才会明确的通知A。
异步请求,A调用B,B的处理是异步的,B在接到请求后先告诉A我已经接到请求了,然后异步去处理,处理完之后通过回调等方式再通知A。
所以说,同步和异步最大的区别就是被调用方的执行方式和返回时机。同步指的是被调用方做完事情之后再返回,异步指的是被调用方先返回,然后再做事情,做完之后再想办法通知调用方。

阻塞请求,A调用B,A一直等着B的返回,别的事情什么也不干。
非阻塞请求,A调用B,A不用一直等着B的返回,先去忙别的事情了。
所以说, 阻塞和非阻塞最大的区别就是在被调用方返回结果之前的这段时间内,调用方是否一直等待。阻塞指的是调用方一直等待别的事情什么都不做。非阻塞指的是调用方先去忙别的事情。

 

加载全部内容

相关教程
猜你喜欢
用户评论