IO模型笔记(好记性不如烂笔头)

227 阅读8分钟

用户空间和内核空间

虚拟内存被操作系统划分成两块:内核空间(Kernel space)和用户空间(User space),内核空间是内核代码运行的地方,用户空间是用户程序代码运行的地方。

当进程运行在内核空间时就处于内核态,当进程运行在用户空间时就处于用户态。

Kernel space 可以执行任意命令,调用系统的一切资源,User space 只能执行简单的运算,不能直接调用系统资源,必须通过系统接口

(又称 system call),才能向内核发送指令。通过系统接口,进程可以从用户空间切换到内核空间。

简单说,操作系统运行时使用的内存空间就是 kernel space 我们自己安装或者开发的应用程序运行时使用的空间是 User space

str = "my string" // 用户空间 
x = x + 2
file.write(str) // 切换到内核空间
y = x + 4 // 切换回用户空间

上面的代码中,第一行和第二行都是简单的赋值运算,在User space执行。第三行需要写入文件,就要切换到Kernel space ,因为用户不能直接写文件

必须通过内核的安排,第四行又是赋值运算,就要切回到 User space 。

通过 top 命令查看CPU时间在User space 与 Kernel space 之间的分配情况。

第三行中 1.4 us 是CPU消耗在用户空间的百分比, 2.1 sy 是CPU消耗在系统空间的百分比,96.5 id 表示消耗在闲置进程的百分比,这个值越高代表CPU越悠闲

PIO与DMA

上面说到用户应用需要进行磁盘操作,需要先切换到 Kernel space ,那么 Kernel space 又是通过什么方式与磁盘交互的呢?

PIO 很早以前磁盘和内存之间的数据传输是需要CPU控制的,也就是说我们读取磁盘文件到内存中

,数据要经过CPU存储转发,这种方式称之为PIO。显然这种方式非常不合理,需要占用大量

的CPU时间来读取文件,造成文件访问时系统几乎停止响应。

DMA 后来,DMA(直接内存访问,Direct Memory Access)取代了PIO,它可以不经过CPU而直接

进行磁盘访和内存(内核空间)的数据交换。在DMA模式下,CPU只需要向DMA控制器下达指令,

让DMA控制器来处理数据的传输即可,DMA控制器通过系统总线来传输数据,传输完毕后在通知

CPU,这样就在很大程度上降低了CPU占有率,大大节省了系统资源,而DMA的传输速度与PIO差异并不明显,因为这主要取决于慢速设备的速度。

可以肯定的是,PIO模式的计算机现在已经很少见了。

缓存IO与直接IO

缓存IO:数据从磁盘通过DMA拷贝到内核空间,再从内核空间通过CPU拷贝到用户空间

缓存IO又被称作为标准IO,大多数的文件系统默认的IO操作都是缓存IO。在Linux的缓存

IO机制中,数据先从磁盘复制到内核空间的缓冲区,在从内核空间缓冲区复制到应用程序

的地址空间。

读操作:操作系统检查内核缓存区有没用户需要的数据,如果有则从缓存中返回;否则

从磁盘中读取,然后缓存到内核空间缓存区中。

写操作:将数据从用户空间复制到内核空间缓冲区中,这时对于用户程序来说写操作已经

完成,至于什么时间数据落盘由操作系统决定,除非显式的调用了 sync 同步命令。

缓存IO的优点:在一定程度上分离了内核空间和用户空间,保护操作系统本身的运行安全。

可以减少读盘的次数,从而提高性能。

缓存IO的缺点:在缓存IO机制中,DMA总是将数据先存放到内核空间缓冲区,而不是直接

在用户空间和磁盘之间传输,这样数据在传输过程中需要多次进行拷贝。(内核空间到用户空间)

这些数据拷贝所带来的CPU以及内存的开销也是很大的。

直接IO:引入内核缓存区的目的在于提升磁盘的访问性能,因为当进程需要读取磁盘文件时,如果

文件内容已经在内核缓冲区中,那么就不需要再次进行磁盘访问;而当进程需要向磁盘写入

数据时,实际上也只是写入到了内核缓冲区,真正的罗盘操作通过一定的策略延迟执行。

然而对于一些复杂的应用,比如数据库服务(mysql)它们为了充分提高性能,希望绕过

内核缓冲区,由自己在用户空间实现IO管理,包括缓存机制,延迟重写机制等。以支持独特

的查询机制,比如数据库可以根据更加合理的方式提高查询缓存命中率(自己实现的缓存)

另一方面绕过内核缓冲区也可以减少内核空间的开销,因为内核缓冲区本身就是使用的内核空间内存。

直接IO的优点:应用程序直接访问磁盘数据,而不经过内核缓冲区,这样的目的是

减少一次从内核缓冲区到用户程序的数据复制。应用程序自己决定数据落盘时机,最大程度避免数据丢失。

直接IO的缺点:是如果访问的数据不在应用程序缓存中,需要每次从磁盘加载,所以直接IO一般需要

应用程序自己实现缓存。

网络IO

1 操作系统通过DMA将数据从磁盘复制到内核缓冲区

2 CPU将内核缓冲区的数据复制到应用缓冲区(用户空间)

3 CUP将数据从应用缓冲区写入到内核的Socket缓存中

4 操作系统通过DMA将Socket缓存复制到网卡缓存。

从上边的过程可以看出,数据白白从内核模式到用户模式走了一圈,浪费了两次copy,而这两次copy都是 CPU copy,占用CPU资源

网络七层模型

 

这里我们关注第三层,第四层和第七层。第三层网络层确定发送方和接受方的IP,第四层建立连接分发数据端口号是在这一层决定的。而建立连接的方式就包括JAVA的Socket。最终第七层应用层接受到数据包后,按照一定的包规则也就是协议,读取包,并且响应数据并且确定是否关闭连接。

这里一个代码示例可以验证以上说法。

package com.datang.pet.control.test;

import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class Main {
    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(8080);
        System.out.println("启动成功");
        while (true) {
            Socket socket = serverSocket.accept();
            InputStream inputStream = socket.getInputStream();
            String httpRequest = "";
            byte[] httpRequestBytes = new byte[1024];
            int length = 0;
            if ((length = inputStream.read(httpRequestBytes)) > 0) {
                httpRequest = new String(httpRequestBytes, 0, length);
            }
            System.out.println("后边是消息体(" + httpRequest + ")消息体结束");
            OutputStream outputStream = socket.getOutputStream();
            StringBuffer httpResponse = new StringBuffer();
            httpResponse.append("HTTP/1.1 200 OK\n")
                    .append("Content-Type: text/html\n")
                    .append("\r\n")
                    .append("<html><body>")
                    .append("ddddddddddddddddd")
                    .append("</body></html>");
            outputStream.write(httpResponse.toString().getBytes());
            socket.close();
        }
    }
}

View Code

用java代码创建一个ServerSocket监听8080端口。输出接受到的数据内容,并且返回一个HTTP响应格式的内容,最后关闭客户端Socket连接。在浏览器发送请求测试,可以看到浏览器发送了两个请求第二个是浏览器发送的Icon,第一个是我们发送的 sss=111,而服务端也可以接收到HTTP格式的请求体。最后socket.close()关闭客户端连接,此处如果不关闭,浏览器将会一直转圈,代表会话没有结束。

 

 

阻塞的IO

 

 

    public static void telnet() {
        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(8080);
            System.out.println("启动成功");
        } catch (Exception e) {
            System.out.println("启动失败");
            return;
        }

        while (true) {
            try {
                Socket socket = serverSocket.accept();
                System.out.println("获取输入流之前");
                InputStream inputStream = socket.getInputStream();
                System.out.println("获取输入流之后");
                String httpRequest = "";
                byte[] httpRequestBytes = new byte[1024];
                int length = 0;
                if ((length = inputStream.read(httpRequestBytes)) > 0) {
                    httpRequest = new String(httpRequestBytes, 0, length);
                }
                System.out.println("后边是消息体(" + httpRequest + ")消息体结束");
                OutputStream outputStream = socket.getOutputStream();
                outputStream.write("ok".getBytes());
                socket.close();
            } catch (Exception e) {
                System.out.println("客户端异常");
            }
        }
    }

View Code

同步IO和异步IO

两种指的是用户空间和内核空间数据的交互方式

同步:用户空间需要数据,必须等到内核空间给结果才做其他事情。

异步:用户空间需要数据,不需要内核空间给结果就能做其他操作,数据准备结束后内核空间异步通知用户空间,并把数据给用户空间。

阻塞IO和非阻塞IO

两种指的是用户空间和内核空间IO的操作方式

阻塞:用户空间调用 systemcall()向内核空间发送IO操作时,该调用是阻塞的。

非阻塞:用户空间调用 systemcall()向内核空间发送IO操作时,该调用是不阻塞的,直接返回,只是返回时可能没有数据。

以上两种概念同步和阻塞很像,异步和非阻塞很像,但是还有一种叫同步IO非阻塞IO,即用户空间需要数据,发送 systemcall()后直接以非阻塞的方式

返回,但用户空间会轮询的查询内核空间的数据是否准备好。