IO-基础

现代计算机主存和cpu之间会构建多级高速缓存,L1、L2、L3等。CPU尽可能从L1获取数据,只要3秒,L1尽可能从L2获取数据,只要14秒,同样L2->L3,L3->内存,大幅度提升CPU与主存之间交互带来性能损耗,那么什么样的数据会存储到L1、L2、L3中,还有就是数据可见性的问题,有了多级的高速缓存,cpu就不用到主存中获取数据,若此时数据修改,如何得到正确的数据。

IO的操作模型(为了保护操作系统和用户程序之间不受其他程序的影响)

  • 用户模式:用户程序数据以及相关内容存在用户空间里,用户发起一个请求,首先切换到内核模式,将数据读取到内核空间,再拷贝到用户空间,最后切换为用户模式执行程序。
  • 内核模式:独立于普通的应用程序,可以访问到受保护的内存空间。

I/O内存映射

设备通过控制总线,数据总线,状态总线与CPU相连。控制总数传送控制信号。在传统的操作中,都是通过读写设备寄存器的值来实现。但是这样耗费了CPU时钟。而且每取一次值都要读取设备寄存器,造成了效率的低下。在现代操作系统中。引用了I/O内存映射,即把寄存器的值映身到主存,对设备寄存器的操作,转换为对主存的操作,这样极大的提高了效率。

DMA

传统的处理方法为:当设备接收到数据,向CPU报告中断。CPU处理中断,CPU把数据从设备的寄存器数据读到内存。

在现代操作系统中引入的DMA(Direct Memory Access,直接存储器访问)设备,设备接收到数据时,把数据放至DMA内存,不中断CPU,这样节省了大量的CPU时间

零拷贝

零拷贝描述的是CPU不执行拷贝数据从一个存储区域到另一个存储区域的任务,这通常用于通过网络传输时以减少CPU周期和内存带宽。避免操作系统内核缓冲区之间进行数据拷贝操作,避免内核和用户程序之间进行数据拷贝,用户程序可直接避开操作系统直接访问硬件存储。好处:减少CPU的工作,更高效的执行其他任务,减少内存宽带的占用,减少内核和用户空间之间的上下文切换,零拷贝完全依赖于操作系统。

传统的I/O流

 

  • 第1步调用read(),上下文切换到内核,DMA把磁盘数据复制到内核的缓存空间
  • 第2步read()返回,上下文切换到用户进程,CPU把数据复制到用户的缓存空间
  • 第3步write() 上下文切换到内核,CPU把数据复制到内核socket缓存,write返回,上下文切换的进程.
  • 第4步DMA再复制到网卡缓存。

经过4次上下文切换,4次数据拷贝,在数据量比较大时,性能比sendfile方式低下

sendFIle实现I/O零拷贝

把文件数据通过网络发送出去,减少上下文切换,内核的缓存数据直接到网卡数据,不用CPU去复制,有DMA完成。

  • 第1步发出sendfile系统调用,导致用户空间到内核空间的上下文切换(第一次上下文切换)。
  • 第2步通过DMA将磁盘文件中的内容拷贝到内核空间缓冲区中(第一次拷贝: hard driver ——> kernel buffer)。
  • 第3步数DMA发出中断,CPU处理中断,将数据从内核空间缓冲区拷贝到内核中与socket相关的缓冲区(第二次拷贝: kernel buffer ——> socket buffer)。
  • sendfile系统调用返回,导致内核空间到用户空间的上下文切换(第二次上下文切换)。
  • 第4步通过DMA引擎将内核空间socket缓冲区中的数据传递到网卡(第三次拷贝: socket buffer ——> 网卡)。

通过sendfile实现的零拷贝I/O只使用了2次用户空间与内核空间的上下文切换,以及3次数据的拷贝。实现了把数据从文件发送到网卡。

 

mmap内存映射

将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数。

mmap解析

I/O模式

对于一次IO访问,数据先会被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序地址,会经历两个阶段:1.等待数据准备。2.将数据从内核拷贝到进程。

  • 阻塞式IO---用户发起IO请求->内核等待数据加载->内核加载数据完毕->拷贝内核空间数据到用户空间->提醒用户程序完成IO读取操作。两个阶段都被阻塞。

  • 非阻塞式IO---进程一直主动询问IO准备好了没有,准备好了再发起读取操作,这时才把数据从内核空间拷贝到用户空间。

  • 多路复用IO---一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态。当用户进程调用了select,那么整个进程会被block,同时监控所有select监控的socket,当有数据准备好后,select就会返回,在调用read将数据拷贝到用户进程。

  • 信号驱动IO---进程发起读取操作会立即返回,当数据准备好了会以通知的形式告诉进程,进程再发起读取操作,把数据从内核空间拷贝到用户空间。

  • 异步IO---进程发起读取操作会立即返回,等到数据准备好且已经拷贝到用户空间了再通知进程拿数据。两个阶段都不会阻塞。linux下底层依然是epoll是实现的,性能上没有大的提升。windows上了有另一套实现,性能更好,但server端都是linux。

文件描述符----指向操作的文件。

select poll epoll

都是IO多路复用的机制,一个进程可以监视多个描述符,一旦某个描述符就绪,能够通知程序进行相应的读写。本质是同步IO,在读写事件就绪后需要自己负责进行读写,读写这个过程是阻塞的,异步IO就完全不是阻塞的。

  • select (一般是1024个)有文件描述符数量的限制,调用后select函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回,当select函数返回后,可以 通过遍历fdset,来找到就绪的描述符。阻塞直到有描述符就绪,再遍历所有监视的描述符来找到就绪的描述符。
  • poll 与select的区别在于没有文件描述符数量限制。
  • epoll 没有文件描述符数量的限制,epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中。epoll事先通过epoll_ctl()来注册一 个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知此处去掉了遍历文件描述符,而是通过监听回调的的机制。

window采用的是select/poll,linux采用的是epoll。

Linux的IO详细解释

IO和NIO

IO面向字节流、阻塞。NIO面向块、非阻塞。

  • NIO是以块的方式处理数据,但是IO是以最基础的字节流的形式去写入和读出的。所以在效率上的话,肯定是NIO效率比IO效率会高出很多。
  • NIO不在是和IO一样用OutputStream和InputStream 输入输出流的形式来进行处理数据的,但是又是基于这种流的形式,而是采用了通道和缓冲区的形式来进行处理数据的。
  • NIO的通道是可以双向的,但是IO中的流只能是单向的。
  • NIO操作缓冲区(其实也就是一个字节数组),可以建立只读缓冲区、直接缓冲区和间接缓冲区,只读缓冲区很明显就是字面意思,直接缓冲区是为加快 I/O 速度,而以一种特殊的方式分配其内存的缓冲区。
  • NIO比传统的BIO核心区别就是,NIO采用的是多路复用的IO模型,普通的IO用的是阻塞的IO模型,两个之间的效率肯定是多路复用效率更高。
     

什么是通道?什么是缓冲区?

通道:到任何地方的数据都必须经过一个channel对象,一个buffer实质上是一个容器对象,发送给一个通道对象前必须先放到缓冲区中,从通道中读取数据时,也需要先读取到缓冲区中,通过channel读取和写入缓冲区中。所有数据都通过Buffer对象来处理。缓冲区实质上是一个数组。通常它是一个字节数组,但是也可以使用其他种类的数组。但是一个缓冲区不 仅仅 是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程.

都继承于抽象的Buffer.

ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
 

nio的Buffer原理

capacity 缓冲区数组的总长度
position 下一个要操作的数据元素的位置
limit 缓冲区数组中不可操作的下一个元素的位置,limit<=capacity
mark 用于记录当前 position 的前一个位置或者默认是 0

 

position:当写数据到Buffer中时,position表示当前的位置,写入数据后,指向下一个可插入数据的Buffer单远。当切换回读模式时,position会重置为0。

limit:再写模式下,表示最多能往里写多少数据,limi=capacity。在读模式下,表示能读多少数据。

selector

运行单线程处理多个channel,将selector注册到channel中,调用selector()阻塞,直到某个注册的channel就绪时返回。

 

private Selector selector;//初始化selector
    private ServerSocketChannel serverSocketChannel;//初始化channel
    private ExecutorService executorService;//初始化线程池

    private  void init(int port) throws Exception {
        executorService = Executors.newCachedThreadPool();
        selector = Selector.open();//初始化
        serverSocketChannel = ServerSocketChannel.open();//打开channel
        serverSocketChannel.configureBlocking(false);//配置channel信息
        serverSocketChannel.socket().bind(new InetSocketAddress(port));//绑定端口

        //将selector注册到channel     selector监控所有accept事件的请求
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);//注册入 selector OP_ACCEPT事件
        while (true) { //select() 阻塞,直到有一个channel 触发 则返回

            int event = selector.select();// 阻塞,有触发返回1,无则返回0
            if (event != 0) {
                Set<SelectionKey> selectionKeys = selector.selectedKeys();//非线程安全
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    System.out.println("key json: "+JSON.toJSONString(key));
                    iterator.remove();
                    //判断当前key的状态
                    if (key.isValid() && key.isAcceptable()) {
                        //有请求进来,就接收这个请求,设置成非阻塞式,再注册到selector中,监听该请求的读事件
                        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                        SocketChannel client = serverSocketChannel.accept();//接受请求数据
                        client.configureBlocking(false);
                        client.register(selector, SelectionKey.OP_READ);//客户端的channel 注册selector, OP_READ事件,收到请求后变为OP_READ,变为随时可接受数据状态
                    } else if (key.isValid() && key.isReadable()) {
                        key.interestOps(0);//先置为0,不关注任何事件,防止异步线程未处理完该事件又被select
                        executorService.execute(new Task(key));
                    }else if(key.isValid() && key.isConnectable()){
                        System.out.println("isConnectable");
                    }
                }
            }
        }
    }
    private class Task implements Runnable {
        private SelectionKey key;

        public Task(SelectionKey key) {
            this.key = key;
        }
        @Override
        public void run() {
            SocketChannel channel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            int size = -1;
            try {
                while ((size = channel.read(buffer)) > 0) {
                    buffer.flip();
                    out.write(buffer.array(), 0, size);
                    buffer.clear();
                }
                if (out.size() == 0) {
                    key.cancel();
                } else {
                    //解析
                    String string = new String(out.toByteArray(), "utf-8");
                    String response = "收到 nio.client msg :" + string;
                    System.out.println("nio.client msg "+string);

                    ByteBuffer reBuffer = ByteBuffer.wrap(response.getBytes());
                    while (reBuffer.hasRemaining()) {
                        channel.write(reBuffer);
                    }
                }
            } catch (Exception e) {
                key.cancel();
            }finally {
                // 执行完后,设置为OP_READ状态 
                key.interestOps(SelectionKey.OP_READ);
                //如果阻塞了另一个线程,立即返回,没有则返回下一次调用
                key.selector().wakeup();
            }
        }
    }
private Selector selector;
    private SocketChannel socketChannel;
    public void init(String host, int port) throws Exception {
        selector = Selector.open();
        socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        socketChannel.socket().setTcpNoDelay(true);
        socketChannel.connect(new InetSocketAddress(host, port));
        socketChannel.register(selector, SelectionKey.OP_CONNECT);//客户端发出连接事件
        while (true) {
            int event = selector.select();
            if (event != 0) {
                Set<SelectionKey> keys = selector.selectedKeys();//
                Iterator<SelectionKey> key = keys.iterator();
                while (key.hasNext()) {
                    SelectionKey selectionKey = key.next();
                    key.remove();
                    System.out.println("key json : "+JSON.toJSONString(selectionKey));
                    if (selectionKey.isValid() && selectionKey.isConnectable()) {
                        //finishConnect 才算连接成功
                        if (socketChannel.finishConnect()) {
                            selectionKey.interestOps(SelectionKey.OP_READ);//
                        } else {
                            selectionKey.cancel();
                        }
                    } else if (selectionKey.isValid() && selectionKey.isReadable()) {

                        SocketChannel channel = (SocketChannel) selectionKey.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
                        int size = -1;
                        while ((size = channel.read(buffer)) > 0) {
                            buffer.flip();
                            baos.write(buffer.array(), 0, size);
                            buffer.clear();
                            System.out.println("响应消息:"+new String(baos.toByteArray()));
                        }


                    }
                }
            }

        }
    }

    public void send(String msg) {
        byte[] bytes = msg.getBytes();
        ByteBuffer buffer = ByteBuffer.wrap(bytes);
        while (buffer.hasRemaining()) {
            try {
                int write = socketChannel.write(buffer);//往channel里发数据
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void close() {
        try {
            socketChannel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

 

 

 

 

 

 

 

 

 

 

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值