第2章 高并发IO的底层原理
前言
IO底层原理是隐藏在Java编程知识之下的基础知识,是开发人员必须掌握的基本原理,可以说是基础的基础,更是大公司面试通关的必备知识。
IO读写的基本原理
为了避免用户进程直接操作内核,保证内核安全,操作系统将内存(虚拟内存)划分为两部分:一部分是内核空间(KernelSpace),另一部分是用户空间(User-Space)。在Linux系统中,内核模块运行在内核空间,对应的进程处于内核态;用户程序运行在用户空间,对应的进程处于用户态。
用户态进程如何执行系统调用呢?答案是:用户态进程必须通过系统调用(System Call)向内核发出指令,完成调用系统资源之类的操作。
用户程序进行IO的读写依赖于底层的IO读写,基本上会用到底层的read和write两大系统调用。虽然在不同的操作系统中read和write两大系统调用的名称和形式可能不完全一样,但是它们的基本功能是一样的。
操作系统层面的read系统调用并不是直接从物理设备把数据读取到应用的内存中,write系统调用也不是直接把数据写入物理设备。上层应用无论是调用操作系统的read还是调用操作系统的write,都会涉及缓冲区。
应用程序的IO操作实际上不是物理设备级别的读写,而是缓存的复制
。read和write两大系统调用都不负责数据在内核缓冲区和物理设备(如磁盘、网卡等)之间的交换。这个底层的读写交换操作是由操作系统内核(Kernel)来完成的
。所以,在应用程序中,无论是对socket的IO操作还是对文件的IO操作,都属于上层应用的开发,它们在输入(Input)和输出(Output)维度上的执行流程是类似的,都是在内核缓冲区和进程缓冲区之间进行数据交换。
内核缓冲区与进程缓冲区
缓冲区的目的是减少与设备之间的频繁物理交换。
操作系统会对内核缓冲区进行监控,等待缓冲区达到一定数量的时候,再进行IO设备的中断处理,集中执行物理设备的实际IO操作,通过这种机制来提升系统的性能。至于具体什么时候执行系统中断(包括读中断、写中断)则由操作系统的内核来决定,应用程序不需要关心。
内核缓冲区与应用缓冲区在数量上也不同。在Linux系统中,操作系统内核只有一个内核缓冲区。每个用户程序(进程)都有自己独立的缓冲区,叫作用户缓冲区或者进程缓冲区。
典型的系统调用流程
read调用把数据从内核缓冲区复制到应用的用户缓冲区,write调用把数据从应用的用户缓冲区复制到内核缓冲区。
以read系统调用为例,看一下一个完整输入流程的两个阶段:
- 应用程序等待数据准备好。
- 从内核缓冲区向用户缓冲区复制数据。
四种主要的IO模型
同步阻塞IO
阻塞IO指的是需要内核IO操作彻底完成后才返回到用户空间执行用户程序的操作指令。
“阻塞”指的是用户程序(发起IO请求的进程或者线程)的执行状态。
传统的IO模型都是阻塞IO模型,并且在Java中默认创建的socket都属于阻塞IO模型。
同步IO是指用户空间(进程或者线程)是主
动发起IO请求的一方,系统内核是被动接收方。异步IO则反过来,系统内核是主动发起IO请求的一方,用户空间是被动接收方。
同步阻塞IO(Blocking IO)指的是用户空间(或者线程)主动发起,需要等待内核IO操作彻底完成后才返回到用户空间的IO操作。在IO操作过程中,发起IO请求的用户进程(或者线程)处于阻塞状态。
同步非阻塞IO
非阻塞IO(Non-Blocking IO,NIO)指的是用户空间的程序不需要等待内核IO操作彻底完成,可以立即返回用户空间去执行后续的指
令,即发起IO请求的用户进程(或者线程)处于非阻塞状态,与此同时,内核会立即返回给用户一个IO状态值。
在Java中,非阻塞IO的socket被设置为NONBLOCK模式。
IO多路复用
在Linux系统中,新的系统调用为select/epoll系统调用。通过该系统调用,一个用户进程(或者线程)可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核就能够将文件描述符的就绪状态返回给用户进程(或者线程),用户空间可以根据文件描述符的就绪状态进行相应的IO系统调用。
IO多路复用(IO Multiplexing)属于一种经典的Reactor模式实现,有时也称为异步阻塞IO,Java中的Selector属于这种模型。
IO多路复用模型的缺点是,本质上select/epoll系统调用是阻塞式的,属于同步IO,需要在读写事件就绪后由系统调用本身负责读写,也就是说这个读写过程是阻塞的。要彻底地解除线程的阻塞,就必须使用异步IO模型。
异步IO
异步IO(Asynchronous IO,AIO)指的是用户空间的线程变成被动接收者,而内核空间成为主动调用者。在异步IO模型中,当用户线程收到通知时,数据已经被内核读取完毕并放在了用户缓冲区内,内核在IO完成后通知用户线程直接使用即可。
异步IO类似于Java中典型的回调模式,用户进程(或者线程)向内核空间注册了各种IO事件的回调函数,由内核去主动调用。
异步IO模型的缺点是应用程序仅需要进行事件的注册与接收,其余的工作都留给了操作系统,也就是说需要底层内核提供支持。
异步IO模型的缺点是应用程序仅需要进行事件的注册与接收,其余的工作都留给了操作系统,也就是说需要底层内核提供支持。
通过合理配置来支持百万级并发连接
- 配置Linux操作系统中文件句柄数的限制,默认是1024。
ulimit -n # 显示和修改当前用户进程的基础限制命令,-n选项用于引用或设置当前的文件句柄数量的限制值
文件句柄数不够,会导致什么后果呢?当单个进程打开的文件句柄数量超过了系统配置的上限值时会发出“Socket/File:Can't open so many files
”的错误提示。
对于高并发、高负载的应用,必须调整这个系统参数,以适应并发处理大量连接的应用场景。可以通过ulimit来设置这两个参数,方法如下:
ulimit -n 1000000
ulimit命令只能用于临时修改,如果想永久地把最大文件描述符数量值保存下来,可以编辑/etc/rc.local开机启动文件,在文件中添加如下内容:
ulimit -SHn 1000000
-S表示软性极限值;
-H表示硬性极限值。
硬性极限值是实际的限制,就是最大可以是100
万,不能再多了。软性极限值则是系统发出警告(Warning)的极限值,超过这个极限值,内核会发出警告。
普通用户通过ulimit命令可将软性极限值更改到硬性极限值的最大设置值。如果要更改硬性极限值,必须拥有root用户权限。
要彻底解除Linux系统的最大文件打开数量的限制,可以通过编辑Linux的极限配置文件/etc/security/limits.conf来做到。修改此文
件,加入如下内容:
soft nofile 1000000
hard nofile 1000000
soft nofile表示软性极限,hard nofile表示硬性极限。