2.1 IO读写的基本原理
操作系统将内存(虚拟内存)划为两部分:
- 内核空间 Kernel-Space
- 用户空间 User-Space
在linux系统中:
- 内核模块运行在内核空间,对应的进程处于内核态;
- 用户程序运行在用户空间,对应的进程处于用户态;
操作系统的核心是内核程序,它独立于普通的应用程序,既有权限访问受保护的内核空间,也有权限访问硬件设备,普通的应用程序没有这样的权限。
内核空间总是驻留在内存中。
应用程序不允许直接在内核空间区域进行读写,也不允许直接调用内核代码定义的函数。
每个应用程序进程都有一个单独的用户空间,对应的进程处于用户态,用户态进程不能访问内核空间中的数据,也不能直接调用内核函数,因此需要将进程切换到内核态才能进行系统调用。
内核态进程可以执行任意命令,调用系统的一切资源,而用户态进程只能执行简单的运算。
用户态进程必须通过系统调用向内核发出指令,完成调用系统资源之类的操作。
用户程序进行IO读写依赖于底层的IO读写,用到底层的read
和write
两大系统调用。
上层应用无论是调用操作系统中的read
还是write
,都会涉及缓冲区。
- 上层应用通过操作系统的
read
系统调用把数据从内核缓冲区复制到应用程序的进程缓冲区 - 通过
write
系统调用把数据从应用的进程缓冲区复制到操作系统的内核缓冲区
应用程序的IO操作实际上不是物理设备级别的读写,而是缓存的复制
2.1.1 内核缓冲区与进程缓冲区
缓冲区的目的:减少与设备之间的频繁物理交换。
- 外部设备的直接读写设计操作系统的中断。
- 发生系统中断时需要保存之前的进程数据和状态等信息,结束中断之后还需要恢复之前的进程数据和状态等信息
- 为了减少底层系统的频繁中断所导致的时间损耗、性能损耗,出现了内核缓冲区
操作系统会对内核缓冲区进行监控,当缓冲区到达一定数量时,再进行IO设备的中断处理,集中执行物理设备的实际IO操作,提升系统的性能。
- 上层应用使用
read
系统调用时,仅仅把数据从内核缓冲区复制到应用的缓冲区(进程缓冲区); - 上层应用使用
write
系统调用时,仅仅把数据从应用的缓冲区复制到内核缓冲区。
Linux中,操作系统内核只有一个内核缓冲区,每个用户程序都有自己独立的缓冲区,叫做用户缓冲区或进程缓冲区。
2.1.2 典型的系统调用流程
以read
系统调用为例:
- 应用程序等待数据准备好。
应用程序等待数据通过网络到达网卡,当所等待的分组到达时,数据被操作系统复制到内核缓冲区中。这个工作由操作系统自动完成,用户程序无感知。 - 从内核缓冲区向用户缓冲区复制数据。
如果是在Java客户端和服务端之间完成一次socket请求和响应(包括read和write)的数据交换,其完整的流程如下:
- 客户端发送请求: Java客户端程序通过write系统调用将数据复制到内核缓冲区,Linux将内核缓冲区的请求数据通过客户端机器的网卡发送出去。在服务端,这份请求数据会从接收网卡中读取到服务端机器的内核缓冲区。
- 服务端获取请求: Java服务端程序通过read系统调用从Linux内核缓冲区读取数据,再送入Java进程缓冲区。
- 服务端业务处理:Java服务器在自己的用户空间中完成客户端的请求所对应的业务处理。
- 服务端返回数据: Java服务器完成处理后,构建好的响应数据将从用户缓冲区写入内核缓冲区,这里用到的是write系统调用,操作系统会负责将内核缓冲区的数据发送出去。
- 发送给客户端: 服务端Linux系统将内核缓冲区中的数据写入网卡,网卡通过底层的通信协议将数据发送给目标客户端。
2.2 四种主要的IO模型
- 同步阻塞IO
- 同步非阻塞IO
- IO多路复用
- 异步IO
- 阻塞与非阻塞:阻塞IO指需要内核IO操作彻底完成后才返回到用户空间执行用户程序的操作指令。“阻塞”指的是用户程序(发起IO请求的进程或者线程)的执行状态。可以说传统的IO模型都是阻塞IO模型,并且在Java中默认创建的socket都属于阻塞IO模型。
- 同步与异步: