常见的IO模型

常见的IO模型虽然有五种:

同步阻塞IO(Blocking IO)

        阻塞IO,指的是需要内核IO操作彻底完成后,才返回到用户空间执行用户程序的操作指令,阻塞所指的是用户程序(发起IO请求的进程或者线程)的执行状态是阻塞的。比如传统的IO模型都是阻塞IO模型,并且在Java中,默认创建的socket都属于阻塞IO模型。

        同步IO是指用户空间(进程或者线程)是主动发起IO请求的一方,系统内核是被动接受方。

        异步IO则反过来,系统内核主动发起IO请求的一方,用户空间是被动接受方。

        同步阻塞IO,指的是用户空间(或者线程)主动发起,需要等待内核IO操作彻底完成后,才返回到用户空间的IO操作,IO操作过程中,发起IO请求的用户进程(或者线程)处于阻塞状态。直到返回成功后,应用进程才能开始处理用户空间的缓存区数据。其流程图如下:

在此模型下,其表现情况如下:

  • 优点:程序简单,在阻塞等待数据期间进程/线程挂起,基本不会占用 CPU 资源。
  • 一般情况下,会为每个连接配备一个独立的线程,一个线程维护一个连接的IO操作。在并发量小的情况下,这样做没有什么问题。但是,当在高并发的应用场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大。在高并发应用场景中,阻塞IO模型是性能很低的,基本上是不可用的。

同步非阻塞NIO(Non-Blocking IO)

        非阻塞IO,指的是用户空间的程序不需要等待内核IO操作彻底完成,可以立即返回用户空间去执行后续的指令,即发起IO请求的用户进程(或者线程)处于非阻塞的状态,与此同时,内核会立即返回给用户一个IO的状态值。

        阻塞VS非阻塞:阻塞是指用户进程(或者线程)一直在等待,而不能干别的事情;非阻塞是指用户进程(或者线程)拿到内核返回的状态值就返回自己的空间,可以去干别的事情。在Java 中,非阻塞IO的socket套接字,要求被设置为NONBLOCK模式。

        同步非阻塞NIO,指的是用户进程主动发起,不需要等待内核IO操作彻底完成之后,就能立即返回到用户空间的IO操作,IO操作过程中,发起IO请求的用户进程(或者线程)处于非阻塞状态。

在NIO模型中,应用程序一旦开始IO系统调用,会出现以下两种情况:

  1. 在内核缓冲区中没有数据的情况下,系统调用会立即返回,返回一个调用失败的信息。
  2. 在内核缓冲区中有数据的情况下,在数据的复制过程中系统调用是阻塞的,直到完成数据从内核缓冲复制到用户缓冲。复制完成后,系统调用返回成功,用户进程(或者线程)可以开始处理用户空间的缓存数据。

其流程图如下:

 在此模型下,其表现情况如下:

  • 优点:不会阻塞在内核的等待数据过程,每次发起的 I/O 请求可以立即返回,不用阻塞等待,实时性较好。
  • 缺点:轮询将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率较低,所以一般 Web 服务器不使用这种 I/O 模型。

IO多路复用(IO Multiplexing)

        为了提高性能,操作系统引入了一类新的系统调用,专门用于查询IO文件描述符的(含socket连接)的就绪状态。在Linux系统中,新的系统调用为select/epoll系统调用。通过该系统调用,一个用户进程(或者线程)可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核能够将文件描述符的就绪状态返回给用户进程(或者线程),用户空间可以根据文件描述符的就绪状态,进行相应的IO系统调用。

        目前支持IO多路复用的系统调用,有select、epoll等等。select系统调用,几乎在所有的操作系统上都有支持,具有良好的跨平台特性。epoll是在Linux 2.6内核中提出的,是select系统调用的Linux增强版本。

        在IO多路复用模型中通过select/epoll系统调用,单个应用程序的线程,可以不断地轮询成百上千的socket连接的就绪状态,当某个或者某些socket网络连接有IO就绪状态,就返回这些就绪的状态(或者说就绪事件)。

IO多路复用(IO Multiplexing)是高性能Reactor线程模型的基础IO模型,当然,此模型是建立在同步非阻塞的模型基础之上的升级版。其流程图如下:

 在此模型下,其表现情况如下:

  • 优点1:可以基于一个阻塞对象,同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程),这样可以大大节省系统资源。
  • 优点2:一个选择器查询线程,可以同时处理成千上万的网络连接,所以,用户程序不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。这是一个线程维护一个连接的阻塞IO模式相比,使用多路IO复用模型的最大优势。
  • 缺点:当连接数较少时效率相比多线程+阻塞 I/O 模型效率较低,可能延迟更大,因为单个连接处理需要 2 次系统调用,占用时间会有增加。

信号驱动IO模型

        在信号驱动IO模型中,用户线程通过向核心注册IO事件的回调函数,来避免IO时间查询的阻塞。

        在该模型下,用户进程预先在内核中设置一个回调函数,当某个事件发生时,内核使用信号(SIGIO)通知进程运行回调函数。然后进入IO操作的第二个阶段——执行阶段:用户线程会继续执行,在信号回调函数中调用IO读写操作来进行实际的IO请求操作。

        信号驱动IO可以看成是一种异步IO,可以简单理解为系统进行用户函数的回调。因为信号驱动IO仅仅在IO事件的通知阶段是异步的,而在第二阶段,也就是在将数据从内核缓冲区复制到用户缓冲区这个过程,用户进程是阻塞的、同步的。其流程图如下:

 在此模型下,其表现情况如下:

  • 优点:线程并没有在等待数据时被阻塞,可以提高资源的利用率。
  • 缺点:信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知。

异步IO(Asynchronous IO)

        异步IO,指的是用户空间与内核空间的调用方式大反转。用户空间的线程变成被动接受者,而内核空间成了主动调用者。在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕,并放在了用户缓冲区内,内核在IO完成后通知用户线程直接使用即可。

        异步IO类似于Java中典型的回调模式,用户进程(或者线程)向内核空间注册了各种IO事件的回调函数,由内核去主动调用。

        异步IO包含两种:不完全异步的信号驱动IO模型和完全的异步IO模型。

异步IO模型的流程图如下:

在此模型下,其表现情况如下:

  • 优点:异步 I/O 能够充分利用 DMA 特性,让 I/O 操作与计算重叠。
  • 缺点:要实现真正的异步 I/O,操作系统需要做大量的工作。目前 Windows 下通过 IOCP 实现了真正的异步 I/O。

总结:

        同步和异步,是针对应用程序(如Java)与内核的交互过程的方向而言的。

        同步类型的IO操作,发起方是应用程序,接收方是内核。

        同步IO由应用进程发起IO操作,并阻塞等待,或者轮询的IO操作是否完成。

        异步IO操作,应用程序在提前注册完成回调函数之后去做自己的事情,IO交给内核来处理,在内核完成IO操作以后,启动进程的回调函数

        阻塞与非阻塞,关注的是用户进程在IO过程中的等待状态。前者用户进程需要为IO操作去阻塞等待,而后者用户进程可以不用为IO操作去阻塞等待。同步阻塞型IO、同步非阻塞IO、多路IO复用,都是同步IO,也是阻塞性IO。

        异步IO必定是非阻塞的,所以不存在异步阻塞和异步非阻塞的说法。真正的异步IO需要内核的深度参与。异步IO中的用户进程时候根本不去考虑IO的执行,IO操作主要交给内核去完成,而自己只等待一个完成信号。

所以五种 I/O 模型中,前四种属于同步 I/O,因为其中真正的 I/O 操作(recvfrom)将阻塞进程/线程,只有异步 I/O 模型才与 POSIX 定义的异步 I/O 相匹配。

百万并发配置注意点

        以上我们了解了高并发IO的底层原理,但是即使采用了最先进的模型,如果不进行合理的操作系统配置,也没有办法支撑百万级的网络连接并发。

        在 Linux 环境中,任何事物都是用文件来表示,设备是文件,目录是文件,socket 也是文件。用来表示所处理对象的接口和唯一接口就是文件。应用程序在读/写一个文件时,首先需要打开这个文件,打开的过程其实质就是在进程与文件之间建立起连接,句柄的作用就是唯一标识此连接。此后对文件的读/写时,由这个句柄作为代表。最后关闭文件其实就是释放这个句柄的过程,也就是进程与文件之间的连接断开。。

        Linux操作系统中文件句柄数的限制。在生产环境Linux系统中,基本上都需要解除文件句柄数的限制。原因是,Linux的系统默认值为1024,也就是说,一个进程最多可以接受1024个socket连接。这是远远不够的。

        文件句柄,也叫文件描述符。在Linux系统中,文件可分为:普通文件、目录文件、链接文件和设备文件。文件描述符(File Descriptor)是内核为了高效管理已被打开的文件所创建的索引,它是一个非负整数(通常是小整数),用于指代被打开的文件。所有的IO系统调用,包括socket的读写调用,都是通过文件描述符完成的。 

        在Linux下,通过调用ulimit命令,可以看到一个进程能够打开的最大文件句柄数量, 
这个命令的具体使用方法是:        

ulimit -n

ulimit  命令是用来显示和修改当前用户进程一些基础限制的命令,-n选项用于引用或设置当前的文件句柄数量的限制值,Linux的系统默认值为1024。

        理论上1024个文件描述符,对绝大多数应用(例如Apache、桌面应用程序)来说已经足够了。但是,是对于一些用户基数很大的高并发应用,则是远远不够的。一个高并发的应用,面临的并发连接数往往是十万级、百万级、甚至像腾讯QQ一样的上亿级。

        文件句柄数不够,会导致什么后果呢?当单个进程打开的文件句柄数量超过了系统配置的上限值时,就会发出“Socket/File:Can't open so many files”的错误提示。

        所以,对于高并发、高负载的应用,就必须要调整这个系统参数,以适应处理并发处理大量连接的应用场景。可以通过ulimit来设置这两个参数。方法如下:        

ulimit -n  1000000

在上面的命令中,n的设置值越大,可以打开的文件句柄数量就越大。建议以root用户来执行此命令。

        使用ulimit命令有一个缺陷,该命令仅仅只能修改当前用户环境的一些基础限制,仅在当前用户环境有效。也即是说,在当前的终端工具连接当前shell期间,修改是有效的;一旦断开用户会话,或者说用户退出Linux后,它的数值就又变回系统默认的1024了。并且,系统重启后,句柄数量又会恢复为默认值。

        ulimit命令只能用于临时修改,如果想永久地把最大文件描述符数量值保存下来,可以编辑/etc/rc.local开机启动文件,在文件中添加如下内容:

ulimit -SHn 1000000

以上示例增加-S和-H两个命令选项。选项-S表示软性极限值,-H表示硬性极限值。硬性极限是实际的限制,就是最大可以是100万,不能再多了。软性极限值则是系统发出警告(Warning)的极限值,超过这个极限值,内核会发出警告。

        普通用户通过ulimit命令,可将软极限更改到硬极限的最大设置值。如果要更改硬极限,必须拥有root用户权限。

        终极解除Linux系统的最大文件打开数量的限制,可以通过编辑Linux的极限配置文件 /etc/security/limits.conf来解决,修改此文件,加入如下内容:

* soft nofile 1000000 
* hard nofile 1000000

soft nofile表示软性极限,hard nofile表示硬性极限。

        比如,在使用和安装目前非常流行的分布式搜索引擎——ElasticSearch时,基本上就必须去修改这个文件,用于增加最大的文件描述符的极限值。当然,在生产环境运行Netty时,最好是修改/etc/security/limits.conf文件,增加文件描述符数量的限制。

        除了修改应用进程的文件句柄上限之外,还需要修改内核基本的全局文件句柄上限,通过修改    /etc/sysctl.conf  配置文件来更改,参考的配置如下:

fs.file-max = 2048000 
fs.nr_open = 1024000

fs.file-max表示系统级别的能够打开的文件句柄的上限,可以理解为全局的句柄数上限。是对整个系统的限制,并不是针对用户的。

fs.nr_open指定了单个进程可打开的文件句柄的数量限制,nofile受到这个参数的限制,nofile值不可用超过fs.nr_open值。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

逆天至尊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值