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的返回,先去忙别的事情了。
所以说, 阻塞和非阻塞最大的区别就是在被调用方返回结果之前的这段时间内,调用方是否一直等待。阻塞指的是调用方一直等待别的事情什么都不做。非阻塞指的是调用方先去忙别的事情。

Netty源码分析一<序一Unix网络I/O模型简介>的更多相关文章

  1. 【Netty源码分析】发送数据过程

    前面两篇博客[Netty源码分析]Netty服务端bind端口过程和[Netty源码分析]客户端connect服务端过程中我们分别介绍了服务端绑定端口和客户端连接到服务端的过程,接下来我们分析一下数据 ...

  2. 【Netty源码分析】客户端connect服务端过程

    上一篇博客[Netty源码分析]Netty服务端bind端口过程 我们介绍了服务端绑定端口的过程,这一篇博客我们介绍一下客户端连接服务端的过程. ChannelFuture future = boos ...

  3. netty源码分析之揭开reactor线程的面纱(二)

    如果你对netty的reactor线程不了解,建议先看下上一篇文章netty源码分析之揭开reactor线程的面纱(一),这里再把reactor中的三个步骤的图贴一下 reactor线程 我们已经了解 ...

  4. netty源码分析之二:accept请求

    我在前面说过了server的启动,差不多可以看到netty nio主要的东西包括了:nioEventLoop,nioMessageUnsafe,channelPipeline,channelHandl ...

  5. Netty源码分析(前言, 概述及目录)

    Netty源码分析(完整版) 前言 前段时间公司准备改造redis的客户端, 原生的客户端是阻塞式链接, 并且链接池初始化的链接数并不高, 高并发场景会有获取不到连接的尴尬, 所以考虑了用netty长 ...

  6. 【转】netty源码分析之LengthFieldBasedFrameDecoder

    原文:https://www.jianshu.com/p/a0a51fd79f62 拆包的原理 关于拆包原理的上一篇博文 netty源码分析之拆包器的奥秘 中已详细阐述,这里简单总结下:netty的拆 ...

  7. Netty源码分析第1章(Netty启动流程)---->第1节: 服务端初始化

    Netty源码分析第一章:  Server启动流程 概述: 本章主要讲解server启动的关键步骤, 读者只需要了解server启动的大概逻辑, 知道关键的步骤在哪个类执行即可, 并不需要了解每一步的 ...

  8. Netty源码分析第1章(Netty启动流程)---->第2节: NioServerSocketChannel的创建

    Netty源码分析第一章:  Server启动流程 第二节:NioServerSocketChannel的创建 我们如果熟悉Nio, 则对channel的概念则不会陌生, channel在相当于一个通 ...

  9. Netty源码分析第1章(Netty启动流程)---->第3节: 服务端channel初始化

    Netty源码分析第一章:Netty启动流程   第三节:服务端channel初始化 回顾上一小节的initAndRegister()方法: final ChannelFuture initAndRe ...

随机推荐

  1. 实现api开发实例页面

    主要实现功能: 1.通过点击不同的option选项,自动生成不同的代码. 功能分析: 1.点击不同的option选项,这里其实就是使用了一个事件即onchange,把这个事件放在<select& ...

  2. Nginx笔记总结十七:nginx生成缩略图配置(http_image_filter_module)

    编译: ./configure --prefix=/usr/local/nginx --with-http_stub_status_module --with-http_realip_module-- ...

  3. Docker的自动构建镜像

    Dockerfile自动构建docker镜像类似ansible剧本,大小几kb手动做镜像:大小几百M+ dockerfile 支持自定义容器的初始命令 dockerfile主要组成部分: 基础镜像信息 ...

  4. 途牛与十八好汉撕X又言和 到底想干啥?

    到底想干啥?" title="途牛与十八好汉撕X又言和 到底想干啥?"> 天下大势,合久必分,分久必合.很多看起来热闹哄哄的"劳燕分飞"事件,最 ...

  5. Lambda表达式和函数试接口的最佳实践 · LiangYongrui's Studio

    1.概述 本文主要深入研究java 8中的函数式接口和Lambda表达式,并介绍最佳实践. 2.使用标准的函数式接口 包java.util.function中的函数是接口已经可以满足大部分的java开 ...

  6. 【深入理解Java虚拟机】类的初始化过程

    类的初始化过程 类的加载过程.png 加载 将 Class 文件以二进制的形式加载到内存中 验证 校验 Class 文件是否安全,是否被正确的修改等 准备 为类变量申请内存,设置默认值,(初始化变量的 ...

  7. 配置github——每次提交后使contributions有记录(有小绿格子)

    # 配置github--每次提交后使contributions有记录(有小绿格子) 这几天都有将自己的代码提交到github上,但是在profile里的contributions的表格中没有我提交的记 ...

  8. 50-Python2和3字符编码的区别

    目录 Python2和3字符编码的区别 python2 python3 Python2和3字符编码的区别 区别点 python2 python3 print 是一个语法结构 是一个函数,print(' ...

  9. 状压dp 持续更新

    前置知识点:二进制状态压缩,动态规划. 1. AcWing 91 最短Hamilton路径 (https://www.acwing.com/problem/content/93/) 给定一张 n 个点 ...

  10. js获取当前日期是一年中的第几天

    js获取当前日期为一年中的第几天 const currentYear = new Date().getFullYear().toString(); // 今天减今年的第一天(xxxx年01月01日) ...