漫谈I/O——同步非阻塞I/O

73 阅读3分钟

参考

Linux 网络包发送过程:25 张图,一万字,拆解 Linux 网络包发送过程 (qq.com)

图解Linux网络包接收过程: 图解Linux网络包接收过程 (qq.com)

当内核收到了一个网络包:当内核收到了一个网络包 (qq.com)

epoll 是如何实现 IO 多路复用的:图解 | 深入揭秘 epoll 是如何实现 IO 多路复用的! (qq.com)

你管这破玩意叫 IO 多路复用: 你管这破玩意叫 IO 多路复用? (qq.com)

认认真真的聊聊中断: 认认真真的聊聊中断 (qq.com)

认认真真的聊聊"软"中断:认认真真的聊聊"软"中断 (qq.com)

【视频】netty视频

【书】计算网络自顶向下

【书】UNIX网络编程卷1:套接字联网API(第三版)

【书】图解TCP_IP

本文笔者只是做整合以及阅读总结,建议大家看看原文

同步非阻塞I/O

既然阻塞I/O会阻塞程序,那么一个简单想法我不阻塞不就行了,于是就有了非阻塞I/O。其原理也很简单就是原先会发生阻塞的read处,如果没有数据读入就直接返回-1。过程如下:

image.png

但是这仍然由缺陷那就是非阻塞的 read,指的是在数据到达前,即数据还未到达网卡,或者到达网卡但还没有拷贝到内核缓冲区之前,这个阶段是非阻塞的。当数据已到达内核缓冲区,此时调用 read 函数仍然是阻塞的,需要等待数据从内核缓冲区拷贝到用户缓冲区,才能返回。

例子

public class NIO01 {
​
    private final static Logger log = LoggerFactory.getLogger(MyServer.class);
​
    public static void main(String[] args) throws IOException {
        // 使用 nio 来理解非阻塞模式, 单线程
// 0. ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(16);
// 1. 创建了服务器
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false); // 非阻塞模式
// 2. 绑定监听端口
        ssc.bind(new InetSocketAddress(8080));
// 3. 连接集合
        List<SocketChannel> channels = new ArrayList<>();
        while (true) {
            // 4. accept 建立与客户端连接, SocketChannel 用来与客户端之间通信
            SocketChannel sc = ssc.accept(); // 非阻塞,线程还会继续运行,如果没有连接建立,但sc是null
            if (sc != null) {
                log.debug("connected... {}", sc);
                sc.configureBlocking(false); // 非阻塞模式
                channels.add(sc);
            }
            for (SocketChannel channel : channels) {
                // 5. 接收客户端发送的数据
                int read = channel.read(buffer);// 非阻塞,线程仍然会继续运行,如果没有读到数据,read 返回 0
                if (read > 0) {
                    buffer.flip();
                    ByteBufferUtil.debugRead(buffer);
                    buffer.clear();
                    log.debug("after read...{}", channel);
                }
            }
        }
    }
}

非阻塞I/O的优缺点

优点:

  • 解决了read函数阻塞的问题

缺点:

  • 在内核态和用户之间反复纵横跳,浪费大量CPU时间

优化

程序员的智慧是无穷的,面对这种情况我们当然也有解决方案。我们可以在每一个accept客户端连接之后,将连接保存到一个数组里面。然后遍历数组调用read方方法,如果返回的不是-1,就去执行相关的业务代码。当然我们仍然避免不了内核态到用户态的反复横跳而且加强版的死循环反复纵横跳。所以最终这个问题还是抛到了操作系统本身。

总结

显然非阻塞I/O只是解决了阻塞I/O的部分的问题,但read函数还是存在阻塞的。当在read函数的阻塞期间发生其他连接请求之类的,还是无法及时响应。所以多线程仍然需要。并且非阻塞I/O会在用户态和内核态之间反复切换,造成大量的CPU浪费。所以总的来看非阻塞I/O优势并不明显。