前言
最近在系统的学习redis,redis的高性能除了基于内存、对于不同数据类型会根据数据量的多少而使用不同的数据结构……,还有redis使用了多路复用器epoll,除了redis外nginx、tomcat也都使用到了多路复用器。
在说多路复用器之前先从CPU中断说起
CPU中断
在Linux信号驱动的情况下,网卡接收到其他主机发来的数据,会将接收到的数据存储在内存中,然后向CPU发送一个信号,表示网络中有数据传输过来了需要处理,在计算机中硬件的信号优先级比用户程序的信号优先级高,硬件的信号都需要立即处理(否则可能会有丢数据的情况发生),CPU感应到了刺激,就会停止当前所执行的用户程序(假设是单核CPU),去执行硬件中断程序,执行完后再执行用户程序。
这也是用户态到内核态的切换过程,用户态:CPU在执行用户程序的代码;内核态:CPU在执行操作系统内核代码,比如:磁盘IO、声卡、显卡等操作……。
一个程序的运行必定是需要计算机硬件参与的。那么用户程序具体是怎么访问计算机硬件的呢?
在用户程序代码中会有像"int x80"这样的代码调用,这里的"int"就是interrupt中断的意思,CPU在执行到这行代码时,就会去中断向量表中查找"80"对应的系统调用函数并执行它,执行完后再继续执行用户程序代码。(系统调用函数:可以对操作系统内核执行一些动作的函数)。
hint:操作系统向下管理着计算机硬件。
BIO(Blocking IO)
代码
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketBIO {
public static void main(String[] args) throws Exception {
ServerSocket server = new ServerSocket(9090);
System.out.println("step1: new ServerSocket(9090)");
while (true){
Socket client = server.accept();//阻塞1
System.out.println("step2: client port: " + client.getPort());
new Thread(new Runnable() {
@Override
public void run() {
InputStream inputStream = null;
try {
inputStream = client.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
while (true){
String s = reader.readLine();//阻塞2
System.out.println("step3: read: " + s);
}
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
}
}
在调用accept()和readLine()时都会发生线程阻塞,如果有一个客户端连接进来后,没有发送任何数据,当前线程就会阻塞在readLine() 不往下执行,所以就需要创建一个线程来执行readLine()。
在执行上面代码的过程中,会调用系统调用函数,可以将这个类放在Linux虚拟机上查看运行过程,来更好的理解。使用strace -ff -o out java SocketBIO启动程序并追踪程序运行时所有线程的系统调用,以out开头的文件在当前目录下输出
第一个136620大小的out.2300就是main线程的out文件,使用
vi out.2300
可以看到在执行new ServerSocket(9090)时,调用了socket、bind、listen系统调用函数。因为没有客户端连接进来,在执行server.accept()就发生了线程阻塞,系统调用也卡在了accept上,程序在等待客户端连接。
使用jps和netstat -natp输出
也显示程序处于listen状态。使用
nc localhost 9090连接程序监听的端口9090后,out.2300
accept接收到了fd(文件描述符为5的客户端),并使用clone函数创建了一个2386的线程,main线程继续调用accept等待客户端的连接。
接着查看新创建的线程的out.2386
阻塞在了
recv上,等待数据发送过来
缺点
这样BIO的缺点也就显而易见了,每来一个客户端连接都要创建一个线程,需要消耗大量资源在线程切换上
每个新技术的产生都是为了解决上一个的缺点,所以NIO就产生了
hint:每连接一个客户端进来就会在内核中开辟一个socket空间
NIO(Non-Blocking IO)
顾名思义就是非阻塞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;
import java.util.concurrent.TimeUnit;
public class SocketNIO {
public static void main(String[] args) {
LinkedList<SocketChannel> clients = new LinkedList<>();
try {
ServerSocketChannel ssc = ServerSocketChannel.open();//服务端绑定端口,开启监听
ssc.bind(new InetSocketAddress(9090));
ssc.configureBlocking(false);//OS的 nonblocking 只有服务端监听非阻塞
while (true){
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
SocketChannel client = ssc.accept();//接收客户端连接,不会阻塞,没有客户端连接 返回值 null 在linux中 -1。
//如果来客户端的连接, accept返回的是这个客户端的fd
// NONBLOCKING就是代码能往下走了,只不过有不同的情况
if (client == null) {
System.out.println("client is null");
}else {
client.configureBlocking(false);//nonblocking 读取客户端发送的数据非阻塞
int port = client.socket().getPort();
System.out.println("client port:" + port);
clients.add(client);
}
ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
//遍历已经连接进来的客户端能不能读写数据
for (SocketChannel cli : clients) {
int read = cli.read(buffer);//不会阻塞,>0(有数据)-1 0
if (read > 0) {
buffer.flip();
byte[] aaa = new byte[buffer.limit()];
buffer.get(aaa);
String b = new String(aaa);
System.out.println(cli.socket().getPort() + ":" + b);
buffer.clear();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
使用java SocketNIO启动程序,就会一直打印"client is null",继续使用strace -ff -o out java SocketNIO追踪程序中所有线程的系统调用,将日志输出到out开头的文件中
关键是
fcntl
优点
规避了多线程的问题
缺点
如果连接了大量的客户端,就需要一直频繁的调用recv,一直频繁的在用户态和内核态间切换,消耗大量资源在切换上,那么为了避免频繁的用户态到内核态切换,能不能内核提供一些函数可以一次监听多个客户端呢,多路复用器就登场了
hint:recv对应一个客户端,即监听一个客户端