[Reprint] understanding of network IO model from Linux operating system level

Understanding Network IO model of Linux from the operating system level

https://segmentfault.com/a/1190000021587865

 

I / O (INPUT OUTPUT), comprising a file I / O, network I / O.

The computer world of speed contempt:

  • Memory read data: nanosecond.
  • Gigabit Ethernet data read: subtle level. 1000 ns = 1 microsecond, a thousand times slower than the memory card.
  • Disk read data: milliseconds. 1 ms = 100,000 ns, 10 times slower than a hard disk memory.
  • A CPU 1 ns clock cycle up and down, is relatively close to the memory of the CPU, the other can not afford.

CPU processing speed is much greater than the data I / O speed of data preparation.

Any programming language will encounter problems with this CPU processing speed and I / O speeds do not match!

How to Network Programming Network I / O optimization: how efficient use of the CPU for network data processing? ? ?

First, the concepts

How to understand the network I / O it from the operating system level? World set their own definition of the concept of a computer. If you do not understand these concepts, we can not really understand the nature of technology and design ideas. So in my opinion, these concepts are the basis to understand the technical and computer world.

1.1 synchronous and asynchronous, blocking and non-blocking

Understanding Network I / O not avoid the topic: synchronous and asynchronous, blocking and non-blocking.

Take Sanji boil water for example, (like user behavior Sanji's program, like water heating system kernel calls), National Cheng Kung University vernacular translation of the concept of these two groups can be understood.

  • Synchronous / asynchronous concerned that after the water to a boil or need me to deal with.
  • Blocking / non-blocking is concerned that during this time the water boil is not done other things.

1.1.1 synchronous blocking

After ignition, Shadeng, do not wait until the water was determined never did anything (blocking), open water off the heat (synchronous).

1.1.2 Non-blocking synchronization

After ignition, watch TV (non-blocking) from time to time to see the water is not open, turn off the heat (synchronous) After the water.

1.1.3 Asynchronous blocking

Switch is pressed, water Shadeng opening (blocking), automatic power-off (asynchronously) After the water.

Network programming model that does not exist.

1.1.4 asynchronous non-blocking

Switch is pressed, the Why Why (non-blocking), the automatic power-off (asynchronously) After the water.

1.2 kernel space and user space

  • Kernel is responsible for network and file read and write data.
  • User invokes the data obtained through the network and file system.

1.2.1 内核态 用户态

  • 程序为读写数据不得不发生系统调用。
  • 通过系统调用接口,线程从用户态切换到内核态,内核读写数据后,再切换回来。
  • 进程或线程的不同空间状态。

1.2.2 线程的切换

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

优化建议:

  • 更少的切换。
  • 共享空间。

1.3 套接字 – socket

  • 有了套接字,才可以进行网络编程。
  • 应用程序通过系统调用socket(),建立连接,接收和发送数据(I / O)。
  • SOCKET 支持了非阻塞,应用程序才能非阻塞调用,支持了异步,应用程序才能异步调用

1.4 文件描述符 –FD 句柄

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

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

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

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

  • 0 标准输入 stdin
  • 1 标准输出 stdout
  • 2 错误输出 stderr

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

  • 连接建立后。
  • 等待数据准备好(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

简介:最原始的网络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

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

缺点:当进程有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

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

2.3.1 I/O 多路复用- select

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

缺点:

  • 句柄上限- 默认打开的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<SelectionKey> keys = selector.selectedKeys(); Iterator<SelectionKey> 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

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

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<AsynchronousSocketChannel, Object>() { @Override public void completed(final AsynchronousSocketChannel client, Object attachment) { serverChannel.accept(null, this); ByteBuffer buffer = ByteBuffer.allocate(1024); client.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() { @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<SocketChannel>() { @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,知其然,更要知其所以然。与君共勉!

文章来源:宜信技术学院 & 宜信支付结算团队技术分享第8期-宜信支付结算部支付研发团队高级工程师周胜帅《从操作系统层面理解Linux的网络IO模型》

分享者:宜信支付结算部支付研发团队高级工程师周胜帅

原文首发于支付结算团队技术公号“野指针”

Guess you like

Origin www.cnblogs.com/jinanxiaolaohu/p/12210923.html