网站开发技术路线图,东莞网站建设技术支持,wordpress页面模板选项,网站使用的数据库主要有哪些简介#xff1a; 本文从操作系统实际调用角度#xff08;以CentOS Linux release 7.5操作系统为示例#xff09;#xff0c;力求追根溯源看IO的每一步操作到底发生了什么。 作者 | 道坚 来源 | 阿里技术公众号
前言
本文从操作系统实际调用角度#xff08;以CentOS Linu…简介 本文从操作系统实际调用角度以CentOS Linux release 7.5操作系统为示例力求追根溯源看IO的每一步操作到底发生了什么。 作者 | 道坚 来源 | 阿里技术公众号
前言
本文从操作系统实际调用角度以CentOS Linux release 7.5操作系统为示例力求追根溯源看IO的每一步操作到底发生了什么。
关于如何查看系统调用Linux可以使用 strace 来查看任何软件的系统调动这是个很好的分析学习方法strace -ff -o ./out java TestJava
一 BIO
/*** Alipay.com Inc. Copyright (c) 2004-2020 All Rights Reserved.*/
package io; import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;/*** author xiangyong.ding* version $Id: TestSocket.java, v 0.1 2020年08月02日 20:56 xiangyong.ding Exp $*/
public class BIOSocket {public static void main(String[] args) throws IOException {ServerSocket serverSocket new ServerSocket(8090);System.out.println(step1: new ServerSocket );while (true) {Socket client serverSocket.accept();System.out.println(step2: client\t client.getPort());new Thread(() - {try {InputStream in client.getInputStream();BufferedReader reader new BufferedReader(new InputStreamReader(in));while (true) {System.out.println(reader.readLine());}} catch (IOException e) {e.printStackTrace();}}).start();}}
}
1 发生的系统调用
启动时
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) 5
bind(5, {sa_familyAF_INET, sin_porthtons(8090), sin_addrinet_addr(0.0.0.0)}, 16) 0
listen(5, 50) 0
poll([{fd5, eventsPOLLIN|POLLERR}], 1, -1) 1 ([{fd5, reventsPOLLIN}])
poll函数会阻塞直到其中任何一个fd发生事件。
有客户端连接后
accept(5, {sa_familyAF_INET, sin_porthtons(10253), sin_addrinet_addr(42.120.74.252)}, [16]) 6
clone(child_stack0x7f013e5c4fb0, flagsCLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr0x7f013e5c59d0, tls0x7f013e5c5700, child_tidptr0x7f013e5c59d0) 13168
poll([{fd5, eventsPOLLIN|POLLERR}], 1, -1
抛出线程即我们代码里的 new Thread() 后继续poll阻塞等待连接。
clone出来的线程
recvfrom(6, hello,bio\n, 8192, 0, NULL, NULL)
关于对recvfrom函数的说明其中第四个参数0 表示这是一个阻塞调用。
客户端发送数据后
recvfrom(6, hello,bio\n, 8192, 0, NULL, NULL) 10
2 优缺点
优点
代码简单逻辑清晰。
缺点
由于stream的read操作是阻塞读面对多个连接时 每个连接需要每线程。无法处理大量连接C10K问题。误区可见JDK1.8中对于最初的BIO在Linux OS下仍然使用的pollpoll本身也是相对比较高效的多路复用函数支持非阻塞、多个socket同时检查event只是限于JDK最初的stream API限制无法支持非阻塞读取。
二 NIOnon block
改进使用NIO API将阻塞变为非阻塞 不需要大量线程。
/*** Alipay.com Inc. Copyright (c) 2004-2020 All Rights Reserved.*/
package io;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;/*** author xiangyong.ding* version $Id: NioSocket.java, v 0.1 2020年08月09日 11:25 xiangyong.ding Exp $*/
public class NIOSocket {private static LinkedList SocketChannel clients new LinkedList();private static void startClientChannelHandleThread(){new Thread(() - {while (true){ByteBuffer buffer ByteBuffer.allocateDirect(4096);//处理客户端连接for (SocketChannel c : clients) {// 非阻塞, 0 表示读取到的字节数量, 0或-1表示未读取到或读取异常int num 0;try {num c.read(buffer);} catch (IOException e) {e.printStackTrace();}if (num 0) {buffer.flip();byte[] clientBytes new byte[buffer.limit()];//从缓冲区 读取到内存中buffer.get(clientBytes);System.out.println(c.socket().getPort() : new String(clientBytes));//清空缓冲区buffer.clear();}}}}).start();}public static void main(String[] args) throws IOException {//new socket,开启监听ServerSocketChannel socketChannel ServerSocketChannel.open();socketChannel.bind(new InetSocketAddress(9090));//设置阻塞接受客户端连接socketChannel.configureBlocking(true);//开始client处理线程startClientChannelHandleThread();while (true) {//接受客户端连接; 非阻塞无客户端返回null(操作系统返回-1)SocketChannel client socketChannel.accept();if (client null) {//System.out.println(no client);} else {//设置读非阻塞client.configureBlocking(false);int port client.socket().getPort();System.out.println(client port : port);clients.add(client);}}}
}
1 发生的系统调用
主线程
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) 4
bind(4, {sa_familyAF_INET, sin_porthtons(9090), sin_addrinet_addr(0.0.0.0)}, 16) 0
listen(4, 50) 0
fcntl(4, F_SETFL, O_RDWR|O_NONBLOCK) 0
accept(4, 0x7fe26414e680, 0x7fe26c376710) -1 EAGAIN (Resource temporarily unavailable)
有连接后子线程
read(6, 0x7f3f415b1c50, 4096) -1 EAGAIN (Resource temporarily unavailable)
read(6, 0x7f3f415b1c50, 4096) -1 EAGAIN (Resource temporarily unavailable)
...
资源使用情况 2 优缺点
优点
线程数大大减少。
缺点
需要程序自己扫描 每个连接read需要 O(n)时间复杂度系统调用 此时可能只有一个连接发送了数据高频系统调用导致CPU 用户态内核态切换高。导致CPU消耗很高。
三 多路复用器select、poll、epoll
改进不需要用户扫描所有连接由kernel 给出哪些连接有数据然后应用从有数据的连接读取数据。
1 epoll
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Set;/*** 多路复用socket** author xiangyong.ding* version $Id: MultiplexingSocket.java, v 0.1 2020年08月09日 12:19 xiangyong.ding Exp $*/
public class MultiplexingSocket {static ByteBuffer buffer ByteBuffer.allocateDirect(4096);public static void main(String[] args) throws Exception {LinkedList SocketChannel clients new LinkedList();//1.启动server//new socket,开启监听ServerSocketChannel socketChannel ServerSocketChannel.open();socketChannel.bind(new InetSocketAddress(9090));//设置非阻塞接受客户端socketChannel.configureBlocking(false);//多路复用器JDK包装的代理select /poll/epoll/kqueueSelector selector Selector.open(); //java自动代理默认为epoll//Selector selector PollSelectorProvider.provider().openSelector();//指定为poll//将服务端socket 注册到 多路复用器socketChannel.register(selector, SelectionKey.OP_ACCEPT);//2. 轮训多路复用器// 先询问有没有连接,如果有则返回数量以及对应的对象(fd)while (selector.select() 0) {System.out.println();Set SelectionKey selectionKeys selector.selectedKeys();Iterator SelectionKey iter selectionKeys.iterator();while (iter.hasNext()) {SelectionKey key iter.next();iter.remove();//2.1 处理新的连接if (key.isAcceptable()) {//接受客户端连接; 非阻塞无客户端返回null(操作系统返回-1)SocketChannel client socketChannel.accept();//设置读非阻塞client.configureBlocking(false);//同样把client也注册到selectorclient.register(selector, SelectionKey.OP_READ);System.out.println(new client : client.getRemoteAddress());}//2.2 处理读取数据else if (key.isReadable()) {readDataFromSocket(key);}}}}protected static void readDataFromSocket(SelectionKey key) throws Exception {SocketChannel socketChannel (SocketChannel) key.channel();// 非阻塞, 0 表示读取到的字节数量, 0或-1表示未读取到或读取异常// 请注意这个例子降低复杂度不考虑报文大于buffer size的情况int num socketChannel.read(buffer);if (num 0) {buffer.flip();byte[] clientBytes new byte[buffer.limit()];//从缓冲区 读取到内存中buffer.get(clientBytes);System.out.println(socketChannel.socket().getPort() : new String(clientBytes));//清空缓冲区buffer.clear();}}
}
2 发生的系统调用
启动
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) 4
bind(4, {sa_familyAF_INET, sin_porthtons(9090), sin_addrinet_addr(0.0.0.0)}, 16) 0
listen(4, 50)
fcntl(4, F_SETFL, O_RDWR|O_NONBLOCK) 0
epoll_create(256) 7
epoll_ctl(7, EPOLL_CTL_ADD, 5, {EPOLLIN, {u325, u644324783852322029573}}) 0
epoll_ctl(7, EPOLL_CTL_ADD, 4, {EPOLLIN, {u324, u64158913789956}}) 0
epoll_wait(7
关于对epoll_create对应着Java的 Selector selector Selector.open() 的说明本质上是在内存的操作系统保留区创建一个epoll数据结构。用于后面当有client连接时向该epoll区中添加监听。
有连接
epoll_wait(7,[{EPOLLIN, {u324, u64158913789956}}], 8192, -1) 1
accept(4, {sa_familyAF_INET, sin_porthtons(29597), sin_addrinet_addr(42.120.74.252)}, [16]) 8
fcntl(8, F_SETFL, O_RDWR|O_NONBLOCK) 0
epoll_ctl(7, EPOLL_CTL_ADD, 8, {EPOLLIN, {u328, u643212844375897800712}}) 0
关于epoll_ctl 对应着Java的 client.register(selector, SelectionKey.OP_READ) 。其中 EPOLLIN 恰好对应着Java的 SelectionKey.OP_READ 即监听数据到达读取事件。
客户端发送数据
epoll_wait(7,[{EPOLLIN, {u328, u643212844375897800712}}], 8192, -1) 1
read(8, hello,multiplex\n, 4096) 16
epoll_wait(7,
noteepoll_wait第四个参数-1表示block。poll 和 epoll 对比
根据“1.BIO”中的poll函数调用和epoll函数对比如下 poll和epoll本质上都是同步IO 区别于BIO的是 多路复用充分降低了 system call而epoll更进一步再次降低了system call的时间复杂度。
3 优缺点
优点
线程数同样很少甚至可以把acceptor线程和worker线程使用同一个。时间复杂度低Java实现的Selector在Linux OS下使用的epoll函数支持多个clientChannel事件的一次性获取且时间复杂度维持在O(1)。CPU使用低得益于Selector我们不用向 “2.NIO”中需要自己一个个ClientChannel手动去检查事件因此使得CPU使用率大大降低。
缺点
数据处理麻烦目前socketChannel.read 读取数据完全是基于字节的当我们需要需要作为HTTP服务网关时对于HTTP协议的处理完全需要自己解析这是个庞大、烦杂、容易出错的工作。 性能 现有socket数据的读取socketChannel.read(buffer)全部通过一个buffer 缓冲区来接受一旦连接多起来这无疑是一个单线程读取性能无疑是个问题。那么此时buffer我们每次读取都重新new出来呢如果每次都new出来这样的内存碎片对于GC无疑是一场灾难。如何平衡地协调好buffer的共享既保证性能又保证线程安全这是个难题。
四 Netty
1 研究的目标源码netty提供的入门example
TelnetServer
package telnet;import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.SelfSignedCertificate;/*** Simplistic telnet server.*/
public final class TelnetServer {static final boolean SSL System.getProperty(ssl) ! null;static final int PORT Integer.parseInt(System.getProperty(port, SSL? 8992 : 8023));public static void main(String[] args) throws Exception {// Configure SSL.final SslContext sslCtx;if (SSL) {SelfSignedCertificate ssc new SelfSignedCertificate();sslCtx SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();} else {sslCtx null;}EventLoopGroup bossGroup new NioEventLoopGroup(1);EventLoopGroup workerGroup new NioEventLoopGroup();try {ServerBootstrap b new ServerBootstrap();b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).handler(new LoggingHandler(LogLevel.INFO)).childHandler(new TelnetServerInitializer(sslCtx));b.bind(PORT).sync().channel().closeFuture().sync();} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}
}
TelnetServerHandler
package telnet;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;import java.net.InetAddress;
import java.util.Date;/*** Handles a server-side channel.*/
Sharable
public class TelnetServerHandler extends SimpleChannelInboundHandler String {Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {// Send greeting for a new connection.ctx.write(Welcome to InetAddress.getLocalHost().getHostName() !\r\n);ctx.write(It is new Date() now.\r\n);ctx.flush();}Overridepublic void channelRead0(ChannelHandlerContext ctx, String request) throws Exception {// Generate and write a response.String response;boolean close false;if (request.isEmpty()) {response Please type something.\r\n;} else if (bye.equals(request.toLowerCase())) {response Have a good day!\r\n;close true;} else {response Did you say request ?\r\n;}// We do not need to write a ChannelBuffer here.// We know the encoder inserted at TelnetPipelineFactory will do the conversion.ChannelFuture future ctx.write(response);// Close the connection after sending Have a good day!// if the client has sent bye.if (close) {future.addListener(ChannelFutureListener.CLOSE);}}Overridepublic void channelReadComplete(ChannelHandlerContext ctx) {ctx.flush();}Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {cause.printStackTrace();ctx.close();}
}
TelnetServerInitializer
package telnet;import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.Delimiters;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.ssl.SslContext;/*** Creates a newly configured {link ChannelPipeline} for a new channel.*/
public class TelnetServerInitializer extends ChannelInitializer SocketChannel {private static final StringDecoder DECODER new StringDecoder();private static final StringEncoder ENCODER new StringEncoder();private static final TelnetServerHandler SERVER_HANDLER new TelnetServerHandler();private final SslContext sslCtx;public TelnetServerInitializer(SslContext sslCtx) {this.sslCtx sslCtx;}Overridepublic void initChannel(SocketChannel ch) throws Exception {ChannelPipeline pipeline ch.pipeline();if (sslCtx ! null) {pipeline.addLast(sslCtx.newHandler(ch.alloc()));}// Add the text line codec combination first,pipeline.addLast(new DelimiterBasedFrameDecoder(8192, Delimiters.lineDelimiter()));// the encoder and decoder are static as these are sharablepipeline.addLast(DECODER);pipeline.addLast(ENCODER);// and then business logic.pipeline.addLast(SERVER_HANDLER);}
}
2 启动后的系统调用
主线程(23109)
## 256无实际作用这里只为了兼容旧版kernel api
epoll_create(256) 7epoll_ctl(7, EPOLL_CTL_ADD, 5, {EPOLLIN, {u325, u645477705356928876549}}) 0epoll_create(256) 10epoll_ctl(10, EPOLL_CTL_ADD, 8, {EPOLLIN, {u328, u6417041805914081853448}}) 0epoll_create(256) 13
epoll_ctl(13, EPOLL_CTL_ADD, 11, {EPOLLIN, {u3211, u6417042151607409573899}}) 0epoll_create(256) 16
epoll_ctl(16, EPOLL_CTL_ADD, 14, {EPOLLIN, {u3214, u6417042497300737294350}}) 0epoll_create(256) 19
epoll_ctl(19, EPOLL_CTL_ADD, 17, {EPOLLIN, {u3217, u6417042561450368827409}}) 0epoll_create(256) 10
socket(AF_INET, SOCK_STREAM, IPPROTO_IP) 20
clone(child_stack0x7fc3c509afb0, flagsCLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr0x7fc3c509b9d0, tls0x7fc3c509b700, child_tidptr0x7fc3c509b9d0) 23130
概括为
向OS新建socket并开启clone boss线程23130。为BOSS创建了一个epoll论证参见下面“boss”每个worker创建一个epoll数据结构本质上是在kernel内存区创建了一个数据结构用于后续监听。创建boss线程监听的socket本质上在kernel中创建一个数据结构。
boss23130
bind(20, {sa_familyAF_INET, sin_porthtons(8023), sin_addrinet_addr(0.0.0.0)}, 16) 0
listen(20, 128) 0
getsockname(20, {sa_familyAF_INET, sin_porthtons(8023), sin_addrinet_addr(0.0.0.0)}, [16]) 0
getsockname(20, {sa_familyAF_INET, sin_porthtons(8023), sin_addrinet_addr(0.0.0.0)}, [16]) 0 ##将fd为7号epoll和fd为20号的socket绑定事件epoll_ctl_add和epoll_ctl_mod
epoll_ctl(7, EPOLL_CTL_ADD, 20, {EPOLLIN, {u3220, u6414198059139132817428}}) 0
epoll_ctl(7, EPOLL_CTL_MOD, 20, {EPOLLIN, {u3220, u6420}}) 0
epoll_wait(7, [{EPOLLIN, {u325, u6417295150779149058053}}], 8192, 1000) 1
epoll_wait(7, [], 8192, 1000) 0(不断轮训1S超时一次)
概括为
将上一步中main线程创建的fd20绑定端口8023并开启监听网卡负责监听和接受连接和数据kernel则负责路由到具体进程具体参见关于socket和bind和listenTODO 。将7号socket对应的fd绑定到20号对应的epoll数据结构上去都是操作kernel中的内存。开始1S中一次阻塞等待epoll有任何连接或数据到达。
3 客户端连接
boss (23130)
accept(20, {sa_familyAF_INET, sin_porthtons(11144), sin_addrinet_addr(42.120.74.122)}, [16]) 24
getsockname(24, {sa_familyAF_INET, sin_porthtons(8023), sin_addrinet_addr(192.168.0.120)}, [16]) 0
getsockname(24, {sa_familyAF_INET, sin_porthtons(8023), sin_addrinet_addr(192.168.0.120)}, [16]) 0
setsockopt(24, SOL_TCP, TCP_NODELAY, [1], 4) 0
getsockopt(24, SOL_SOCKET, SO_SNDBUF, [87040], [4]) 0
getsockopt(24, SOL_SOCKET, SO_SNDBUF, [87040], [4]) 0
##抛出 work线程
clone(child_stack0x7fc3c4c98fb0, flagsCLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr0x7fc3c4c999d0, tls0x7fc3c4c99700, child_tidptr0x7fc3c4c999d0) 2301
worker (2301)
writev(24, [{Welcome to iZbp14e1g9ztpshfrla9m..., 37}, {It is Sun Aug 23 15:44:14 CST 20..., 41}], 2) 78
epoll_ctl(13, EPOLL_CTL_ADD, 24, {EPOLLIN, {u3224, u6424}}) 0
epoll_ctl(13, EPOLL_CTL_MOD, 24, {EPOLLIN, {u3224, u6414180008216221450264}}) 0
epoll_wait(13, [{EPOLLIN, {u3211, u6417042151607409573899}}], 8192, 1000) 1
read(11, \1, 128) 1
##开始无限loop
epoll_wait(13, [], 8192, 1000) 0
epoll_wait(13, [{EPOLLIN, {u3224, u6424}}], 8192, 1000) 1
概括
当BOSS轮训epoll_wait等到了连接后首先accept得到该socket对应的fd。连接建立后 BOSS立马抛出一个线程clone函数。worker即新建的线程写入了一段数据这里是业务逻辑。worker将该client对应的fd绑定到了13号epoll上。worker继续轮训监听13号epoll。
4 客户端主动发送数据
worker2301
read(24, i am daojian\r\n, 1024) 14
write(24, Did you say i am daojian?\r\n, 29) 29
##继续无限loop
epoll_wait(13, [], 8192, 1000) 0
概括为
wait到数据后立即read到用户控件内存中读取1024个字节到 用户控件某个buff中。写入数据业务逻辑不必太关注。继续轮训等待13号epoll。
5 客户端发送bye报文服务器断开TCP连接
worker2301
read(24, bye\r\n, 1024) 5
write(24, Have a good day!\r\n, 18) 18
getsockopt(24, SOL_SOCKET, SO_LINGER, {onoff0, linger0}, [8]) 0
dup2(25, 24) 24
##从epoll数据结构中OS中删除fd为24的socket
epoll_ctl(13, EPOLL_CTL_DEL, 24, 0x7f702dd531e0) -1 ENOENT
##关闭24 socket
close(24) 0
##继续等待13 epoll数据
epoll_wait(13, [], 8192, 1000) 0
断开客户端连接概括为
从epoll中删除该客户端对应的fd这里触发源头没找到可能是boss。close关闭客户端24号fd。继续轮训epoll。
6 五个客户端同时连接
boss线程23130
accept(20, {sa_familyAF_INET, sin_porthtons(1846), sin_addrinet_addr(42.120.74.122)}, [16]) 24
clone(child_stack0x7f702cc51fb0, flagsCLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr0x7f702cc529d0, tls0x7f702cc52700, child_tidptr0x7f702cc529d0) 10035accept(20, {sa_familyAF_INET, sin_porthtons(42067), sin_addrinet_addr(42.120.74.122)}, [16]) 26
clone(child_stack0x7f702cb50fb0, flagsCLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr0x7f702cb519d0, tls0x7f702cb51700, child_tidptr0x7f702cb519d0) 10067...
woker线程10035第一个连接
epoll_ctl(13, EPOLL_CTL_ADD, 24, {EPOLLIN, {u3224, u6424}}) 0
epoll_ctl(13, EPOLL_CTL_MOD, 24, {EPOLLIN, {u3224, u643226004877247250456}}) 0
epoll_wait(13, [{EPOLLIN, {u3211, u6417042151607409573899}}], 8192, 1000) 1 1
epoll_wait(13, [], 8192, 1000) 0
worker线程10067第二个连接
epoll_ctl(16, EPOLL_CTL_ADD, 26, {EPOLLIN, {u3226, u6426}}) 0
epoll_ctl(16, EPOLL_CTL_MOD, 26, {EPOLLIN, {u3226, u643221483685433835546}}) 0
epoll_wait(16, [{EPOLLIN, {u3214, u6417042497300737294350}}], 8192, 1000) 1
epoll_wait(16, [], 8192, 1000) 0
epoll_wait(16, [], 8192, 1000) 0
worker线程10067第二个连接
epoll_ctl(19, EPOLL_CTL_ADD, 27, {EPOLLIN, {u3227, u6427}}) 0
epoll_ctl(19, EPOLL_CTL_MOD, 27, {EPOLLIN, {u3227, u643216966479350071323}}) 0
worker线程(8055第四个连接)
epoll_ctl(10, EPOLL_CTL_ADD, 28, {EPOLLIN, {u3228, u6428}}) 0
epoll_ctl(10, EPOLL_CTL_MOD, 28, {EPOLLIN, {u3228, u643302604828697427996}}) 0
worker线程10035第五个连接不在clone线程而是复用了第一个epoll对应的worker
epoll_ctl(13, EPOLL_CTL_ADD, 29, {EPOLLIN, {u3229, u6429}}) 0
epoll_ctl(13, EPOLL_CTL_MOD, 29, {EPOLLIN, {u3229, u6429}}) 0
概括为
epoll和boss、worker之间的关系一共有4个worker对应着4个epoll对象boss和每个worker都有对应自己的epoll。boss根据epoll数量平衡分配连接到每个worker对应的epoll中。
7 总结
下图通过对系统调用的调查得出 netty 和 kernel 交互图 初始化直接创建5个epoll其中7号为boss使用专门用于处理和客户端连接其余4个用来给worker使用用户处理和客户端的数据交互。
work的线程数量取决于初始化时创建了几个epollworker的复用本质上是epoll的复用。
work之间为什么要独立使用epoll为什么不共享
为了避免各个worker之间发生争抢连接处理netty直接做了物理隔离避免竞争。各个worker只负责处理自己管理的连接并且后续该worker中的每个client的读写操作完全由 该线程单独处理天然避免了资源竞争避免了锁。worker单线程性能考虑worker不仅仅要epoll_wait还是处理read、write逻辑加入worker处理了过多的连接势必造成这部分消耗时间片过多来不及处理更多连接性能下降。
8 优缺点
优点
数据处理netty提供了大量成熟的数据处理组件ENCODER、DECODERHTTP、POP3拿来即用。编码复杂度、可维护性netty充分使得业务逻辑与网络处理解耦只需要少量的BootStrap配置即可更多的集中在业务逻辑处理上。性能netty提供了的ByteBuf(底层Java原生的ByteBuffer)提供了池化的ByteBuf兼顾读取性能和ByteBuf内存分配在后续文档中会再做详解。
缺点
入门有一定难度。
五 AIO
1 启动
main线程
epoll_create(256) 5
epoll_ctl(5, EPOLL_CTL_ADD, 6, {EPOLLIN, {u326, u6411590018039084482566}}) 0##创建BOSS 线程(Proactor)
clone(child_stack0x7f340ac06fb0, flagsCLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr0x7f340ac079d0, tls0x7f340ac07700, child_tidptr0x7f340ac079d0) 22704socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) 8
setsockopt(8, SOL_IPV6, IPV6_V6ONLY, [0], 4) 0
setsockopt(8, SOL_SOCKET, SO_REUSEADDR, [1], 4) 0
bind(8, {sa_familyAF_INET6, sin6_porthtons(9090), inet_pton(AF_INET6, ::, sin6_addr), sin6_flowinfo0, sin6_scope_id0}, 28) 0
listen(8, 50)accept(8, 0x7f67d01b3120, 0x7f67d9246690) -1
epoll_ctl(5, EPOLL_CTL_MOD, 8, {EPOLLIN|EPOLLONESHOT, {u328, u6415380749440025362440}}) -1 ENOENT (No such file or directory)
epoll_ctl(5, EPOLL_CTL_ADD, 8, {EPOLLIN|EPOLLONESHOT, {u328, u6415380749440025362440}}) 0
read(0,
22704(BOSS 线程(Proactor))
epoll_wait(5, unfinished ...
2 请求连接
**22704(BOSS 线程(Proactor))处理连接**epoll_wait(5,[{EPOLLIN, {u329, u644294967305}}], 512, -1) 1
accept(8, {sa_familyAF_INET6, sin6_porthtons(55320), inet_pton(AF_INET6, ::ffff:36.24.32.140, sin6_addr), sin6_flowinfo0, sin6_scope_id0}, [28]) 9
clone(child_stack0x7ff35c99ffb0, flagsCLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr0x7ff35c9a09d0, tls0x7ff35c9a0700, child_tidptr0x7ff35c9a09d0) 26241
epoll_wait(5, unfinished ...
26241
#将client 连接的FD加入到BOSS的epoll中以便BOSS线程监听网络事件
epoll_ctl(5, EPOLL_CTL_MOD, 9, {EPOLLIN|EPOLLONESHOT, {u329, u644398046511113}}) -1 ENOENT (No such file or directory)
epoll_ctl(5, EPOLL_CTL_ADD, 9, {EPOLLIN|EPOLLONESHOT, {u329, u644398046511113}}) 0
accept(8, 0x7ff3440008c0, 0x7ff35c99f4d0) -1 EAGAIN (Resource temporarily unavailable)
epoll_ctl(5, EPOLL_CTL_MOD, 8, {EPOLLIN|EPOLLONESHOT, {u328, u648}}) 0
3 客户端发送数据
22704(BOSS 线程(Proactor))处理连接
epoll_wait(5,[{EPOLLIN, {u329, u644294967305}}], 512, -1) 1
##数据读出
read(9, daojian111\r\n, 1024) 12
##数据处理交给其他线程这里由于线程池为空需要先clone线程
clone(child_stack0x7ff35c99ffb0, flagsCLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr0x7ff35c9a09d0, tls0x7ff35c9a0700, child_tidptr0x7ff35c9a09d0) 26532
复制线程处理线程号26532
write(1, pool-1-thread-2-10received : dao..., 41) 41
write(1, \n, 1)
accept(8, 0x7f11c400b5f0, 0x7f11f42fd4d0) -1 EAGAIN (Resource temporarily unavailable)
epoll_ctl(5, EPOLL_CTL_MOD, 8, {EPOLLIN|EPOLLONESHOT, {u328, u648}}) 0
4 总结
从系统调用角度Java的AIO事实上是以多路复用Linux上为epoll等同步IO为基础自行实现了异步事件分发。BOSS Thread负责处理连接并分发事件。WORKER Thread只负责从BOSS接收的事件执行不负责任何网络事件监听。5 优缺点
优点
相比于前面的BIO、NIOAIO已经封装好了任务调度使用时只需关心任务处理。
缺点
事件处理完全由Thread Pool完成对于同一个channel的多个事件可能会出现并发问题。相比nettybuffer API不友好容易出错编解码工作复杂。
原文链接 本文为阿里云原创内容未经允许不得转载。