写点什么

从操作系统层面理解 Linux 下的网络 IO 模型

  • 2020-02-06
  • 本文字数:5756 字

    阅读完需:约 19 分钟

从操作系统层面理解Linux下的网络IO模型

I/O( INPUT OUTPUT),包括文件 I/O、网络 I/O。


计算机世界里的速度鄙视:


  • 内存读数据:纳秒级别。

  • 千兆网卡读数据:微妙级别。1 微秒=1000 纳秒,网卡比内存慢了千倍。

  • 磁盘读数据:毫秒级别。1 毫秒=10 万纳秒 ,硬盘比内存慢了 10 万倍。

  • CPU 一个时钟周期 1 纳秒上下,内存算是比较接近 CPU 的,其他都等不起。


CPU 处理数据的速度远大于 I/O 准备数据的速度 。


任何编程语言都会遇到这种 CPU 处理速度和 I/O 速度不匹配的问题!


在网络编程中如何进行网络 I/O 优化:怎么高效地利用 CPU 进行网络数据处理???

一、相关概念

从操作系统层面怎么理解网络 I/O 呢?计算机的世界有一套自己定义的概念。如果不明白这些概念,就无法真正明白技术的设计思路和本质。所以在我看来,这些概念是了解技术和计算机世界的基础。

1.1 同步与异步,阻塞与非阻塞

理解网络 I/O 避不开的话题:同步与异步,阻塞与非阻塞。


拿山治烧水举例来说,(山治的行为好比用户程序,烧水好比内核提供的系统调用),这两组概念翻译成大白话可以这么理解。


  • 同步/异步关注的是水烧开之后需不需要我来处理。

  • 阻塞/非阻塞关注的是在水烧开的这段时间是不是干了其他事。

1.1.1 同步阻塞

点火后,傻等,不等到水开坚决不干任何事(阻塞),水开了关火(同步)。


1576642331113004942.png

1.1.2 同步非阻塞

点火后,去看电视(非阻塞),时不时看水开了没有,水开后关火(同步)。


1576642338581018229.png

1.1.3 异步阻塞

按下开关后,傻等水开(阻塞),水开后自动断电(异步)。


1576642347459009266.png


网络编程中不存在的模型。

1.1.4 异步非阻塞

按下开关后,该干嘛干嘛 (非阻塞),水开后自动断电(异步)。


1576642355117019190.png

1.2 内核空间 、用户空间

1576642436600045337.png


  • 内核负责网络和文件数据的读写。

  • 用户程序通过系统调用获得网络和文件的数据。

1.2.1 内核态 用户态

1576642443468051843.png


  • 程序为读写数据不得不发生系统调用。

  • 通过系统调用接口,线程从用户态切换到内核态,内核读写数据后,再切换回来。

  • 进程或线程的不同空间状态。

1.2.2 线程的切换

1576642455359030556.png


用户态和内核态的切换耗时,费资源(内存、CPU)


优化建议:


  • 更少的切换。

  • 共享空间。

1.3 套接字 – socket

1576642467801001182.png


  • 有了套接字,才可以进行网络编程。

  • 应用程序通过系统调用 socket(),建立连接,接收和发送数据(I / O)。

  • SOCKET 支持了非阻塞,应用程序才能非阻塞调用,支持了异步,应用程序才能异步调用

1.4 文件描述符 –FD 句柄

1576642483782050770.png


1576642490076078154.png


1576642496249034409.png


网络编程都需要知道 FD??? FD 是个什么鬼???


Linux:万物都是文件,FD 就是文件的引用。像不像 JAVA 中万物都是对象?程序中操作的是对象的引用。JAVA 中创建对象的个数有内存的限制,同样 FD 的个数也是有限制的。


1576642507958005360.png


Linux 在处理文件和网络连接时,都需要打开和关闭 FD。


每个进程都会有默认的 FD:


  • 0 标准输入 stdin

  • 1 标准输出 stdout

  • 2 错误输出 stderr

1.5 服务端处理网络请求的过程

1576642527144025402.png


  • 连接建立后。

  • 等待数据准备好(CPU 闲置)。

  • 将数据从内核拷贝到进程中(CPU 闲置)。


怎么优化呢?


对于一次 I/O 访问(以 read 举例),数据会先被拷贝到操作系统内核的缓冲区,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。


所以说,当一个 read 操作发生时,它会经历两个阶段:


  • 等待数据准备 (Waiting for the data to be ready)。

  • 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。


正是因为这两个阶段,Linux 系统升级迭代中出现了下面三种网络模式的解决方案。

二、IO 模型介绍

2.1 阻塞 I/O - Blocking I/O

1576642540134016122.png


简介:最原始的网络 I/O 模型。进程会一直阻塞,直到数据拷贝完成。


缺点:高并发时,服务端与客户端对等连接,线程多带来的问题:


  • CPU 资源浪费,上下文切换。

  • 内存成本几何上升,JVM 一个线程的成本约 1MB。


public static void main(String[] args) throws IOException {        ServerSocket ss = new ServerSocket();        ss.bind(new InetSocketAddress(Constant.HOST, Constant.PORT));        int idx =0;        while (true) {            final Socket socket = ss.accept();//阻塞方法            new Thread(() -> {                handle(socket);            },"线程["+idx+"]" ).start();        }    }
static void handle(Socket socket) { byte[] bytes = new byte[1024]; try { String serverMsg = " server sss[ 线程:"+ Thread.currentThread().getName() +"]"; socket.getOutputStream().write(serverMsg.getBytes());//阻塞方法 socket.getOutputStream().flush(); } catch (Exception e) { e.printStackTrace(); } }
复制代码

2.2 非阻塞 I/O - Non Blocking IO

1576642704039025856.png


简介:进程反复系统调用,并马上返回结果。


缺点:当进程有 1000fds,代表用户进程轮询发生系统调用 1000 次 kernel,来回的用户态和内核态的切换,成本几何上升。


public static void main(String[] args) throws IOException {        ServerSocketChannel ss = ServerSocketChannel.open();        ss.bind(new InetSocketAddress(Constant.HOST, Constant.PORT));        System.out.println(" NIO server started ... ");        ss.configureBlocking(false);        int idx =0;        while (true) {            final SocketChannel socket = ss.accept();//阻塞方法            new Thread(() -> {                handle(socket);            },"线程["+idx+"]" ).start();        }    }    static void handle(SocketChannel socket) {        try {            socket.configureBlocking(false);            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);            socket.read(byteBuffer);            byteBuffer.flip();            System.out.println("请求:" + new String(byteBuffer.array()));            String resp = "服务器响应";            byteBuffer.get(resp.getBytes());            socket.write(byteBuffer);        } catch (IOException e) {            e.printStackTrace();        }    }
复制代码

2.3 I/O 多路复用 - IO multiplexing

1576642721053094559.png


简介:单个线程就可以同时处理多个网络连接。内核负责轮询所有 socket,当某个 socket 有数据到达了,就通知用户进程。多路复用在 Linux 内核代码迭代过程中依次支持了三种调用,即 SELECT、POLL、EPOLL 三种多路复用的网络 I/O 模型。下文将画图结合 Java 代码解释。

2.3.1 I/O 多路复用- select

1576642729644083157.png


简介:有连接请求抵达了再检查处理。


缺点:


  • 句柄上限- 默认打开的 FD 有限制,1024 个。

  • 重复初始化-每次调用 select(),需要把 fd 集合从用户态拷贝到内核态,内核进行遍历。

  • 逐个排查所有 FD 状态效率不高。


服务端的 select 就像一块布满插口的插排,client 端的连接连上其中一个插口,建立了一个通道,然后再在通道依次注册读写事件。一个就绪、读或写事件处理时一定记得删除,要不下次还能处理。


public static void main(String[] args) throws IOException {        ServerSocketChannel ssc = ServerSocketChannel.open();//管道型ServerSocket        ssc.socket().bind(new InetSocketAddress(Constant.HOST, Constant.PORT));        ssc.configureBlocking(false);//设置非阻塞        System.out.println(" NIO single server started, listening on :" + ssc.getLocalAddress());        Selector selector = Selector.open();        ssc.register(selector, SelectionKey.OP_ACCEPT);//在建立好的管道上,注册关心的事件 就绪        while(true) {            selector.select();            Set keys = selector.selectedKeys();            Iterator it = keys.iterator();            while(it.hasNext()) {                SelectionKey key = it.next();                it.remove();//处理的事件,必须删除                handle(key);            }        }    }    private static void handle(SelectionKey key) throws IOException {        if(key.isAcceptable()) {                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();                SocketChannel sc = ssc.accept();                sc.configureBlocking(false);//设置非阻塞                sc.register(key.selector(), SelectionKey.OP_READ );//在建立好的管道上,注册关心的事件 可读        } else if (key.isReadable()) { //flip            SocketChannel sc = null;                sc = (SocketChannel)key.channel();                ByteBuffer buffer = ByteBuffer.allocate(512);                buffer.clear();                int len = sc.read(buffer);                if(len != -1) {                    System.out.println("[" +Thread.currentThread().getName()+"] recv :"+ new String(buffer.array(), 0, len));                }                ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes());                sc.write(bufferToWrite);        }    }
复制代码

2.3.2 I/O 多路复用 – poll

1576642741019008193.png


简介:设计新的数据结构(链表)提供使用效率。


poll 和 select 相比在本质上变化不大,只是 poll 没有了 select 方式的最大文件描述符数量的限制。


缺点:逐个排查所有 FD 状态效率不高。

2.3.3 I/O 多路复用- epoll

简介:没有 fd 个数限制,用户态拷贝到内核态只需要一次,使用事件通知机制来触发。通过 epoll_ctl 注册 fd,一旦 fd 就绪就会通过 callback 回调机制来激活对应 fd,进行相关的 I/O 操作。


缺点:


  • 跨平台,Linux 支持最好。

  • 底层实现复杂。

  • 同步。


 public static void main(String[] args) throws Exception {        final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open()                .bind(new InetSocketAddress(Constant.HOST, Constant.PORT));        serverChannel.accept(null, new CompletionHandler() {            @Override            public void completed(final AsynchronousSocketChannel client, Object attachment) {                serverChannel.accept(null, this);                ByteBuffer buffer = ByteBuffer.allocate(1024);                client.read(buffer, buffer, new CompletionHandler() {                    @Override                    public void completed(Integer result, ByteBuffer attachment) {                        attachment.flip();                        client.write(ByteBuffer.wrap("HelloClient".getBytes()));//业务逻辑                    }                    @Override                    public void failed(Throwable exc, ByteBuffer attachment) {                        System.out.println(exc.getMessage());//失败处理                    }                });            }
@Override public void failed(Throwable exc, Object attachment) { exc.printStackTrace();//失败处理 } }); while (true) { //不while true main方法一瞬间结束 } }
复制代码


当然上面的缺点相比较它优点都可以忽略。JDK 提供了异步方式实现,但在实际的 Linux 环境中底层还是 epoll,只不过多了一层循环,不算真正的异步非阻塞。而且就像上图中代码调用,处理网络连接的代码和业务代码解耦得不够好。Netty 提供了简洁、解耦、结构清晰的 API。


 public static void main(String[] args) {        new NettyServer().serverStart();        System.out.println("Netty server started !");    }
public void serverStart() { EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new Handler()); } }); try { ChannelFuture f = b.localAddress(Constant.HOST, Constant.PORT).bind().sync(); f.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { workerGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } }}
class Handler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; ctx.writeAndFlush(msg); ctx.close(); }
@Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); }}
复制代码


bossGroup 处理网络请求的大管家(们),网络连接就绪时,交给 workGroup 干活的工人(们)。

三、总结

回顾

  • 同步/异步,连接建立后,用户程序读写时,如果最终还是需要用户程序来调用系统 read()来读数据,那就是同步的,反之是异步。Windows 实现了真正的异步,内核代码甚为复杂,但对用户程序来说是透明的。

  • 阻塞/非阻塞,连接建立后,用户程序在等待可读可写时,是不是可以干别的事儿。如果可以就是非阻塞,反之阻塞。大多数操作系统都支持的。

Redis,Nginx,Netty,Node.js 为什么这么香?

这些技术都是伴随 Linux 内核迭代中提供了高效处理网络请求的系统调用而出现的。了解计算机底层的知识才能更深刻地理解 I/O,知其然,更要知其所以然。与君共勉!


本文转载自宜信技术学院网站。


原文链接:http://college.creditease.cn/detail/337


2020-02-06 10:331255

评论

发布
暂无评论
发现更多内容

二次元界福音:MakeGirlsMoe创建动漫人物

不脱发的程序猿

GitHub 开源 4月日更 二次元 MakeGirlsMoe

融云推出超值套餐包,音视频20万分钟免费享

融云 RongCloud

盘点几代会声会影图标

奈奈的杂社

公安重点人员管控系统搭建,智慧派出所系统

为什么拥有云原生数据平台对电信公司很重要?

VoltDB

云原生 5G VoltDB 电信

数字货币,已成为理解现代经济不可排斥的一个因素

CECBC

数字经济

用DeBug的方式,带你掌握HBase文件在Snapshot的各种变化

华为云开发者联盟

HBase 元数据 数据迁移 数据备份 Snapshot

单片机异常复位后如何保存变量数据

不脱发的程序猿

嵌入式 单片机 4月日更 硬件研发 单片机异常复位

激光雷达(LiDAR)技术

澳鹏Appen

人工智能 大数据 智能驾驶 激光雷达 点云标注

区块链版「滴滴+Uber」,让出行带来收益

CECBC

移动互联网

融云X-Meetup南京站 探讨实时通信架构的高质量设计

融云 RongCloud

Java开发8年,40W年薪被别人叫垃圾?请你们不要口嗨了,好好去刷题吧!

Java架构追梦

Java 架构 面试 金三银四 年薪40W

Android AAC音频硬编解码你知道多少?

Engineer-Jsp

HBase底层读写过程

五分钟学大数据

HBase 4月日更

Github上堪称最全的面试题库(Java岗)到底有多香

钟奕礼

Java 编程 程序员 架构 面试

如何抓住新社交风口下的音视频通讯大潮?

融云 RongCloud

荷小鱼 x mPaaS | 借助 H5 容器改善 App 白屏、浏览器兼容等问题

蚂蚁集团移动开发平台 mPaaS

html5 mPaaS 离线包 教育科技

嘉楠科技发布勘智K210-MicroPython 无需寄存器手册就能上手开发

将AI部署到现实?或许你该读读这本书!

澳鹏Appen

人工智能 大数据 AI 伦理

入职字节跳动那一天,我哭了(蘑菇街被裁,奋战7个月拿下offer)

Java 编程 程序员 架构 面试

flink流计算可视化web平台

无情

sql 流计算 flin

如何美化 GitHub 个人主页?

彭宏豪95

GitHub 写作 markdown IT 4月日更

阿凡提EGGNETWORK恒价通证+加密社交催生新玩法 EFTalk

币圈那点事

Github连夜下架!阿里新产Java全栈面试突击小册太香了

Java架构之路

Java 程序员 架构 面试 编程语言

软件测试分类体系,系统学习

程序员阿沐

软件测试 测试工程师 黑盒测试 白盒测试 测试类型

Java程序员都要懂得知识点:原始数据类型

华为云开发者联盟

Java 字符串 StringBuffer 原始数据类型 布尔类型

阿里云 RTC QoS 弱网对抗之变分辨率编码

阿里云视频云

阿里云 WebRTC 分辨率 视频编解码 视频云

货运物流移动端解决方案:为货运物流行业打造高性能、高粘性的“双端”触点

蚂蚁集团移动开发平台 mPaaS

移动开发 mPaaS 移动端 智慧物流

构建用户安全评级,UGC智能化审核应用实践

爱奇艺技术产品团队

人工智能

已拿到8个Offer!阿里巴巴Java面试参考指南(泰山版)

钟奕礼

Java 编程 程序员 架构 面试

一体化智能安全防御 京东云星盾安全加速正式发布

京东科技开发者

互联网 网络安全

从操作系统层面理解Linux下的网络IO模型_行业深度_周胜帅_InfoQ精选文章