前言我们去面试的时候,问redis、nginx、netty的底层模型是什么?redis->epollnginx->epollnetty->epoll?在BIO层面,当我们开机的时候,我们的Kernel(内核)首先被加载到内存中。内核用于管理我们的硬件。同时内核也会创建一个GDT表,然后将其划分为两个空间(用户空间和内核空间),空间中的内容处于保护模式,不可修改。同时,还有CPU的概念。CPU有自己的指令集,指令集分为几个级别,从0到3,Kernel属于0级。APP只能使用3级指令集。从上面我们可以知道,我们的应用程序不能直接访问我们的Kernel,也就是程序不能直接访问我们的磁盘、声卡、网卡等设备,只有内核才能访问,那怎么办呢?只有APP才能调用Kernel提供的syscall(系统软中断和硬中断)来获取硬件中的内容。软中断硬中断:硬中断指的就是我们的键盘。当一个按钮被按下时,我们的硬中断就会被触发,也就是内核会有一个中断号,然后得到一个callback回调函数说到这里,其实只是想引出一个概念,就是之间的成本问题IO和内核/***服务器读取文件*@author:Moxi*@create:2020-07-01-20:40*/publicclassTestSocket{publicstaticvoidmain(String[]args)throwsIOException{ServerSocketserver=newServerSocket(8090);System.out.println("step1:newServerSocket(8090)");while(true){Socketclient=server.accept();System.out.println("step2:client"+client.getPort());newThread(()->{try{InputStreamin=client.getInputStream();BufferedReaderreader=newBufferedReader(newInputStreamReader(in));while(true){System.out.println(reader.readLine());}}catch(IOExceptione){e.printStackTrace();}},"t1").start();}}}捕获程序是否有对内核的系统调用,然后输出strace-ff-o./ooxxjavaTestSocket然后我们执行上面的程序,得到我们的结果然后我们用jpsc查看当前TestSocket进程号的命令jps2912Jps2878TestSocket然后我们进入如下目录,start2878是线程id号,这个目录是用来存放我们在2878进程下cd/proc/2878可以看到这个线程的一些信息的。通过查看任务目录,我们可以看到所有线程都有一个目录,就是fd目录。在这个目录下,有我们的一些IO流。上面的0,1,2分别对应输入流、输出流和错误流。在java中,我们的流就是一个对象,但是在linux系统中,一个流就是一个一个的文件。下面4和5对应我们的socket通信,分别对应ipv4和ipv6。通过netstat命令查看。然后我们使用nc连接到8090端口nclocalhost8090。查看文件,还有一个额外的套接字。我们查看系统调用,发现通过系统调用收到了一个端口号为58181的请求。我们还可以看到前面有5个。这个5其实就是上图中对应的socket。走吧是ipv4。从这里我们其实可以知道我们在原来的调用中写的代码Socketclient=server.accept()对应的是系统级别的,也是调用了系统方法。同时,关于系统调用,有以下方法:bindconnectlistenselectsocket首先我们要知道java其实是一种解释型语言。通过JVM虚拟机,我们把我们的.java文件转换成字节码文件,然后在调用我们的os的syscall方法,我们要清楚无论怎么调用,最后都要调用内核方法,然后调用我们的硬件。以上模型就是BIO的通信。里面有很多方块。我们只能通过多线程来避免阻塞主线程。但是从上面我们可以知道,如果连接数比较多,服务器需要创建很多与之对应的线程,线程的创建也需要消耗资源,因为线程使用的栈是exclusive(默认栈大小为1MB),同时CPU资源调度也需要浪费。最根本的原因是上述问题是因为BIO被阻塞造成的。由于BIO存在线程阻塞的问题,后来NIO提出了NIO的概念。在NIO中,存在C10K的问题,C10K=10,000个客户端。但是在你连接的服务器里面,没有多少数据可以发给你,所以我们要做的是,每次有人发消息,我就去连接。也就是每次遍历10000个client是非常耗时的,因为很多client可能不会发送请求。多路复用的时候,我们不需要遍历10K个client,而是把我们的fds文件发送给内核,然后内核判断最后需要连接的client,这样我们就不用遍历所有他们中的。所以这里的Select是一个多路复用器,通过多路复用返回的就是状态,然后我们需要程序来判断这些状态。说白了就是用一个multiplexer来决定哪些路径可以通过,然后就不用轮询所有的了。该模型是通过select将fds文件传递给内核来完成的,即内核需要完成10K文件的主动遍历。这个10K调用比之前的10K系统调用更省时。存在以下问题:每次传输大量数据(重复劳动)然后内核需要主动遍历(复杂度O(N))解决方案,通过在内核中开辟一个空间,当一个client来的时候每次,就把这个文件丢进内核,这样就不用每次都给内核传10K的文件了。然后使用事件驱动模型。如下图,一个异步的事件驱动进程也用epoll,Redis是轮询,Nginx是阻塞?我们使用strace命令查看nginx和redis的运行进程,可以发现同样是使用epoll,但是nginx是阻塞的,redis是轮询的(非阻塞)。首先是因为Redis只有一个线程,这个线程要做很多事情,比如接收客户端,LRU,LFU(淘汰过滤器),RDB/AOF(数据备份的fork线程)。也就是说,对于Redis中的C10K问题,redis也是通过epoll的事件驱动来处理的,即通过epoll,将每个需要读取的client操作放在一个原子序列化队列中,一个client端包括以下操作:读、计算、写等。在redis6.X版本中,也有IO线程的概念。首先,为了保留序列化原子性的特性,即计算仍然是序列化的。面向行的处理,但是在读取数据的时候,使用多线程并发IO读取。为什么需要多线程阅读?首先,因为读取操作需要CPU系统调用,如果通过多线程读取,可以充分发挥CPU的多核作用,而nginx只需要做一件事,就是等待客户过来。需要做其他事情,所以设置为block。在使用Kafka实现零拷贝方面,首先有两个角色,一个是消息生产者,一个是消息消费者。也就是说,我们可以通过开辟一块内存空间直接到达磁盘,这样可以减少内核的系统调用。读的时候,如果是原来的方式,需要先请求内核,然后内核发起读请求读取磁盘上的文件给内核,然后kafka读取内核中的信息。那么什么是零拷贝?零拷贝意味着没有拷贝发生。零拷贝的前提是数据不需要处理。JVM中有一个RandomAccessFile,可以直接开辟一个堆空间或者额外的堆空间。
