文章目录

  • 1、IO读写的基础原理
    • 1.1 内核缓冲区与进程缓存区
    • 1.2 详解典型的系统调用流程
  • 2、四种主要的IO模型
    • 2.1 同步阻塞IO(Blocking IO)
    • 2.2 同步非阻塞IO(Non-blocking IO)
    • 2.3 IO多路复用(IO Multiplexing)
    • 2.4 异步IO(Asynchronous IO)
  • 3、通过合理配置来支持百万级并发连接
  • 4、小结

深入剖析高并发IO的底层原理和模型,介绍如何通过设置来让操作系统支持高并发

1、IO读写的基础原理

用户级IO读写依赖于底层的IO读写,基本上会用到底层的read&write两大系统调用。

基础知识: read系统调用,并不是直接从物理设备把数据读取到内存中; write系统调用,也不是直接把数据写入到物理设备。上层应用无论是调用操作系统的read,还是调用操作系统的write,都会涉及缓冲区。具体来说,调用操作系统的read,是把数据从内核缓冲区复制到进程缓冲区;而write系统调用,是把数据从进程缓冲区复制到内核缓冲区。

上层的IO操作,实际上不是物理设备级别的读写,而是缓存的复制。read&write两大系统调用,都不负责数据在内核缓冲区和物理设备(如磁盘)之间的交换,这项底层的读写交换,是由操作系统内核(Kernel)来完成的。

在用户程序中,无论是Socket的、还是文件IO操作,都属于上层应用的开发,它们的输入(Input)和输出(Output)的处理,在编程的流程上,都是一致的。

1.1 内核缓冲区与进程缓存区

设置缓冲区的目的:较少频繁地与设备之间的物理交换。减少底层系统的时间损耗、性能损耗,于是出现内存缓存区

有了内存缓冲区,上层应用使用read系统调用时,仅仅把数据从内核缓冲区复制到上层应用的缓冲区(进程缓冲区) ;上层应用使用write系统调用时,仅仅把数据从进程缓冲区复制到内核缓冲区中。底层操作对用户程序透明

Linux系统内核只有一个内核缓存区,每个用户进程都有自己独立的缓冲区(进程缓冲区)。所以,用户程序的IO读写程序,在大多数情况下,并没有进行实际的IO操作,而是在进程缓存区和内核缓存区之间直接进行数据的交换

1.2 详解典型的系统调用流程


以read系统调用为例

  • 等待数据准备好
  • 从内核向进程复制数据

如果read是一个socket

  • 等待数据从网络到达网卡。当所等待的分组到达时,它被复制到内核中的某个缓冲区。这个工作由操作系统自动完成,用户程序无感知
  • 就是把数据从内核缓冲区复制到应用进程缓冲区

如果是在Java服务器端,完成一次socket请求和响应

  • 客户端请求: Linux通过网卡读取客户端的请求数据,将数据读取到内核缓冲区。
  • 获取请求数据: Java服务器通过read系统调用,从Linux内核缓冲区读取数据,再送入Java进程缓冲区。
  • 服务器端业务处理: Java服务器在自己的用户空间中处理客户端的请求。
  • 服务器端返回数据: Java服务器完成处理后,构建好的响应数据,将这些数据从用户缓冲区写入内核缓冲区。这里用到的是write系统调用。
  • 发送给客户端: Linux内核通过网络IO,将内核缓冲区中的数据写入网卡,网卡通过底层的通信协议,会将数据发送给目标客户端。

2、四种主要的IO模型

2.1 同步阻塞IO(Blocking IO)

阻塞于非阻塞

阻塞IO,指的是需要内核IO操作彻底完成后,才返回到用户空间执行用户的操作。阻塞指的是用户空间程序的执行状态。传统的IO模型都是同步阻塞IO。在Java中,默认创建的socket都是阻塞的。

同步与异步

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

Java应用程序进程中,默认情况下,所有的socket连接的IO操作都是同步阻塞IO(Blocking IO)


阻塞IO的特点是:在内核进行IO执行的两个阶段,用户线程都被阻塞了

阻塞IO的优点是:应用的程序开发非常简单;在阻塞等待数据期间,用户线程挂起。在阻塞期间,用户线程基本不会占用CPU资源。

阻塞IO的缺点是:一般情况下,会为每个连接配备一个独立的线程;反过来说,就,是一个线程维护一个连接的IO操作。在并发量小的情况下,这样做没有什么问题。但是,当在高并发的应用场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大。因此,基本上阻塞IO模型在高并发应用场景下是不可用的。

2.2 同步非阻塞IO(Non-blocking IO)

非阻塞IO,指的是用户空间的程序不需要等待内核IO操作彻底完成,可以立即返回用户空间执行用户的操作,即处于非阻塞的状态,与此同时内核会立即返回给用户一个状态值。

简单来说:阻塞是指用户空间(调用线程)一直在等待,而不能干别的事情;非阻塞是指用户空间(调用线程)拿到内核返回的状态值就返回自己的空间, IO操作可以干就干,不可以干,就去干别的事情。

非阻塞IO要求socket被设置为NONBLOCK.强调一下,这里所说的NIO (同步非阻塞IO)模型,并非Java的NIO (New IO)库。


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

举个例子,发起一个非阻塞socket的read读操作的系统调用

(1)在内核数据没有准备好的阶段,用户线程发起IO请求时,立即返回。所以,为了读取到最终的数据,用户线程需要不断地发起IO系统调用。

(2)内核数据到达后,用户线程发起系统调用,用户线程阻塞。内核开始复制数据,它会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存) ,然后内核返回结果(例如返回复制到的用户缓冲区的字节数) 。

(3)用户线程读到数据后,才会解除阻塞状态,重新运行起来。也就是说,用户进程需要经过多次的尝试,才能保证最终真正读到数据,而后继续执行。

同步非阻塞IO的特点:应用程序的线程需要不断地进行IO系统调用,轮询数据是否已经准备好,如果没有准备好,就继续轮询,直到完成IO系统调用为止。

同步非阻塞IO的优点:每次发起的IO系统调用,在内核等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。

同步非阻塞IO的缺点:不断地轮询内核,这将占用大量的CPU时间,效率低下。

总体来说,在高并发应用场景下,同步非阻塞IO也是不可用的。一般Web服务器不使用这种IO模型。这种IO模型一般很少直接使用,而是在其他IO模型中使用非阻塞IO这一特性。在Java的实际开发中,也不会涉及这种IO模型。

这里说明一下,同步非阻塞IO,可以简称为NIO,但是,它不是Java中的NIO,虽然它们的英文缩写一样,希望大家不要混淆。Java的NIO (New IO) ,对应的不是四种基础IO模型中的NIO (None Blocking IO)模型,而是另外的一种模型,叫作IO多路复用模型(IO Multiplexing)

2.3 IO多路复用(IO Multiplexing)

即经典的Reactor反应器设计模式,有时也称为异步阻塞IO, Java中的Selector选择器和Linux中的epoll都是这种模型。

可以避免同步非阻塞IO模型中轮询等待的问题

在IO多路复用模型中,引入了一种新的系统调用,查询IO的就绪状态。在Linux系统中,对应的系统调用为select/epoll系统调用。通过该系统调用,一个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读/可写) ,内核能够将就绪的状态返回给应用程序。随后,应用程序根据就绪的状态,进行相应的IO系统调用。

目前支持IO多路复用的系统调用,有select, epoll等等。select系统调用,几乎在所有的操作系统上都有支持,具有良好的跨平台特性。epoll是在Linux 2.6内核中提出的,是select系统调用的Linux增强版本。在IO多路复用模型中通过select/epoll系统调用,单个应用程序的线程,可以不断地轮询成百上千的socket连接,当某个或者某些socket网络连接有IO就绪的状态,就返回对应的可以执行的读写操作。举个例子来说明IO多路复用模型的流程。发起一个多路复用IO的read读操作的系统调用,流程如下:

(1)选择器注册。在这种模式中,首先,将需要read操作的目标socket网络连接,提前注册到select/epoll选择器中, Java中对应的选择器类是Selector类。然后,才可以开启整个IO多路复用模型的轮询流程。

2)就绪状态的轮询。通过选择器的查询方法,查询注册过的所有socket连接的就绪状态。通过查询的系统调用,内核会返回一个就绪的socket列表。当任何一个注册过的socket中的数据准备好了,内核缓冲区有数据(就绪)了,内核就将该socket加入到就绪的列表中。当用户进程调用了select查询方法,那么整个线程会被阻塞掉。

(3)用户线程获得了就绪状态的列表后,根据其中的socket连接,发起read系统调用,用户线程阻塞。内核开始复制数据,将数据从内核缓冲区复制到用户缓冲区。

(4)复制完成后,内核返回结果,用户线程才会解除阻塞的状态,用户线程读取到了数据,继续执行。

IO多路复用模型的特点: IO多路复用模型的IO涉及两种系统调用(SystemCall) ,另一种是selectlepoll (就绪查询) ,一种是IO操作。IO多路复用模型建立在操作系统的基础设施之上,即操作系统的内核必须能够提供多路分离的系统调用select/epoll.

和NIO模型相似,多路复用IO也需要轮询。负责select/epoll状态查询调用的线程,需要不断地进行selectlepoll轮询,查找出达到IO操作就绪的socket连接。

IO多路复用模型与同步非阻塞IO模型是有密切关系的。对于注册在选择器上的每一个可以查询的socket连接,一般都设置成为同步非阻塞模型。仅是这一点,对于用户程序而言是无感知的。

IO多路复用模型的优点:与一个线程维护一个连接的阻塞IO模式相比,使用select/epoll的最大优势在于,一个选择器查询线程可以同时处理成千上万个连接(Connection) 。系统不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。

Java语言的NIO (New IO)技术,使用的就是IO多路复用模型。在Linux系统上,使用的是epoll系统调用。

IO多路复用模型的缺点:本质上, select/epoll系统调用是阻塞式的,属于同步IO。都需要在读写事件就绪后,由系统调用本身负责进行读写,也就是说这个读写过程是阻塞的。

如何彻底地解除线程的阻塞,就必须使用异步IO模型。

2.4 异步IO(Asynchronous IO)

异步IO,指的是用户空间与内核空间的调用方式反过来。用户空间的线程变成被动接受者,而内核空间成了主动调用者。这有点类似于Java中比较典型的回调模式,用户空间的线程向内核空间注册了各种IO事件的回调函数,由内核去主动调用。


发起一个异步IO的read读操作的系统调用

(1)当用户线程发起了read系统调用,立刻就可以开始去做其他的事,用户线程不阻塞。

(2)内核就开始了IO的第一个阶段:准备数据。等到数据准备好了,内核就会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存) .

(3)内核会给用户线程发送一个信号(Signal) ,或者回调用户线程注册的回调接口,告诉用户线程read操作完成了。

(4)用户线程读取用户缓冲区的数据,完成后续的业务操作。

异步IO模型的特点:在内核等待数据和复制数据的两个阶段,用户线程都不是阻塞的。用户线程需要接收内核的IO操作完成的事件,或者用户线程需要注册一个IO操作完成的回调函数。正因为如此,异步IO有的时候也被称为信号驱动IO。

异步IO异步模型的缺点:应用程序仅需要进行事件的注册与接收,其余的工作都留给了操作系统,也就是说,需要底层内核提供支持。

理论上来说,异步IO是真正的异步输入输出,它的吞吐量高于IO多路复用模型的吞吐量。

就目前而言, Windows系统下通过IOCP实现了真正的异步IO。而在Linux系统下,异步IO模型在2.6版本才引入,目前并不完善,其底层实现仍使用epoll,与10多路复用相同,因此在性能上没有明显的优势。

大多数的高并发服务器端的程序,一般都是基于Linux系统的。因而,目前这类高并发网络应用程序的开发,大多采用IO多路复用模型。

大名鼎鼎的Netty框架,使用的就是IO多路复用模型,而不是异步IO模型。

3、通过合理配置来支持百万级并发连接

这里所涉及的配置,就是Linux操作系统中文件句柄数的限制

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

文件句柄(文件描述符): 在Linux系统中,文件可分为:普通文件、目录文件、链接文件和设备文件。 文件描述符(File Descriptor)是内核为了高效管理已被打开的文件。所有的IO系统调用,包括socket的读写调用,都是通过文件描述符完成的

Linux下,调用ulimit命令,可以看到单个线程能够打开的最大文件句柄数量:

ulimit -n

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

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

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

ulimit -n 1000000

建议以root用户来执行此命令

然而,使用ulimit命令来修改当前用户进程的一些基础限制,仅在当前用户环境有效。直白地说,就是在当前的终端工具连接当前shel期间,修改是有效的;一旦断开连接,用户退出后,它的数值就又变回系统默认的1024。也就是说, ulimit只能作为临时修改,系统重启后,句柄数量又会恢复为默认值。

如果想永久地把设置值保存下来,可以编辑/etc/rc.local开机启动文件,在文件中添加如下内容:

ulimit -n 1000000

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

普通用户通过ulimit命令,可将软极限更改到硬极限的最大设置值。如果要更改硬极限,必须拥有root用户权限。终极解除Linux系统的最大文件打开数量的限制,可以通过编辑Linux的极限配置文件letc/securityllimits.conf来解决,修改此文件,加入如下内容:

soft nofile 1000000
hard nofile 1000000

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

在使用和安装目前非常火的分布式搜索引擎-ElasticSearch,就必须去修改这个文件,增加最大的文件句柄数的极限值。

在服务器运行Netty时,也需要去解除文件句柄数量的限制,修改/etc/security/limits.conf文件即可。

4、小结

聚焦主题:一是底层IO操作的两个阶段,二是最为基础的四种IO模型,三是操作系统对高并发的底层的支持。

IO处理模型,理论上来讲,从阻塞IO到异步IO,越往后,阻塞越少,效率也越优,前三种属于同步IO,因为真正的IO操作都将阻塞应用线程

只有最后一种异步IO模型,才是真正的异步IO模型,可惜目前Linux操作系统尚欠完善。不过,通过应用层优秀框架如Netty,同样能在IO多路复用模型的基础上,开发出,具备支撑高并发(如百万级以上的连接)的服务器端应用。

《Netty、Redis、Zookeeper高并发实战》2️⃣高并发IO的底层原理相关推荐

  1. 高并发IO的底层原理

    从基础讲起,IO的原理和模型是隐藏在编程知识底下的,是开发人员必须掌握的基础原理,是基础的基础,更是通关大公司面试的必备知识. 本章从操作系统的底层原理入手.通过图文并茂的方式,为大家深入剖析高并发I ...

  2. 建立数组并写入数据_redis高并发利器:神奇的位操作,底层原理、数据结构剖析...

    本文主要和大家分享一下redis的高级特性:bit位操作. 力求让大家彻底学会使用redis的bit位操作并掌握其底层实现原理!主要包含以下内容: redis位操作命令示例 底层数据结构分析 为什么他 ...

  3. java高并发实战Netty+协程(Fiber)|系列1|事件驱动模式和零拷贝

    今天开始写一些高并发实战系列. 本系列主要讲两大主流框架: Netty和Quasar(java纤程库) 先介绍netty吧,netty是业界比较成熟的高性能异步NIO框架. 简单来说,它就是对NIO2 ...

  4. Spring Boot实战解决高并发数据入库: Redis 缓存+MySQL 批量入库

    前言 最近在做阅读类的业务,需要记录用户的PV,UV: 项目状况:前期尝试业务阶段: 特点: 快速实现(不需要做太重,满足初期推广运营即可) 快速投入市场去运营 收集用户的原始数据,三要素: 谁 在什 ...

  5. Redis 的高并发实战:抢购系统 --浅奕

    简介: 主要内容: 一.IO 模型和问题 二.资源竞争与分布式锁 三.Redis 抢购系统实例 主要内容: 一.IO 模型和问题 二.资源竞争与分布式锁 三.Redis 抢购系统实例 一.IO 模型和 ...

  6. Java 高并发_JAVA并发编程与高并发解决方案 JAVA高并发项目实战课程 没有项目经验的朋友不要错过!...

    JAVA并发编程与高并发解决方案 JAVA高并发项目实战课程 没有项目经验的朋友不要错过! 1.JPG (37.82 KB, 下载次数: 0) 2018-12-3 09:40 上传 2.JPG (28 ...

  7. 视频教程- 19年录制Redis实战教程 高可用秒杀分布式锁布隆过滤器实战 SpringBoot教程整合-Java

    19年录制Redis实战教程 高可用秒杀分布式锁布隆过滤器实战 SpringBoot教程整合 7年的开发架构经验,曾就职于国内一线互联网公司,开发工程师,现在是某创业公司技术负责人, 擅长语言有nod ...

  8. 多线程与高并发实战第八节

    Atomic类和线程同步新机制 像原来我们写m++你得加锁,在多线程访问的情况下,那现在我们可以用AtomicInteger了,它内部就已经帮我们实现了原子操作,直接写 count.increment ...

  9. java unsafe获取指针_【实战Java高并发程序设计 1】Java中的指针:Unsafe类

    是<实战Java高并发程序设计>第4章的几点. 如果你对技术有着不折不挠的追求,应该还会特别在意incrementAndGet() 方法中compareAndSet()的实现.现在,就让我 ...

最新文章

  1. Java飞行记录器 JRockit Flight Recorder JFR诊断JVM的历史性能和操作
  2. 街头篮球服务器未响应,鹊桥相会《街头篮球》8.12七夕版本预告
  3. 科学家发现大脑动态评估信息重要性机制
  4. Javascript作用域原理---预编译
  5. ELK 企业级日志分析系统
  6. PTA学生成绩排序-C语言详解
  7. java中的tostring_java 中重写toString()方法
  8. 【渝粤题库】广东开放大学 商务交际听说 形成性考核
  9. Git clone之后你的硬盘上究竟发生了什么?
  10. 罗永浩回应被列入老赖名单:就算“卖艺”也会把债务还完
  11. 动态规划爬楼梯(为什么到i级的方法=i-1级的方法+到i-2级的方法)
  12. Linux下一些简单命令的收集
  13. 更改虚拟机centos7的系统时间
  14. STM32实战总结:HAL之数码管
  15. php eclipse aptana,Eclipse安装Aptana插件(注意对应版本问题)
  16. 带宽下载速度单位换算
  17. 葫芦时刻_Hulu将提供离线观看(最终)
  18. 批量搜索多个excel文件
  19. Data Structures in C++:八大基本数据结构概述
  20. SpringBoot项目启动图标展示个性化

热门文章

  1. 并不对劲的bzoj4560:p3269:[JLOI2016]字符串覆盖
  2. 【例题】Python日志记录操作
  3. Linux + ProxyPool 搭建属于爬虫代理IP池
  4. 【Elasticsearch选主流程】
  5. Android本机号码及Sim卡状态的获取
  6. 国内快速升级更新QT方法
  7. html5和css3实现3D立方体效果
  8. 谁是谁的妈, 谁是谁的爸
  9. 2011年Android手机购买意向调查研究报告
  10. win10系统word自动生成的目录变成乱码了怎么解决