ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

从阻塞式IO到epoll——IO精讲

2022-02-03 16:34:47  阅读:173  来源: 互联网

标签:文件 epoll int 精讲 client IO poll select


Linux虚拟文件系统的理解

VFS 是一棵树, 树上的节点可以映射到对应的物理位置
与之对应的,什么是实际上的文件系统呢,比如说Windows操作系统上的,D盘对应的就是那块磁盘,C盘对应的就是这块磁盘.
VFS中, 每一个文件都一个唯一的inode号来代表它
读文件时内存会在内存中开辟一个pagecache页缓存
随后应用程序对文件的操作就是对页缓存的操作
页缓存会变脏,此时可以手动把它持久化入磁盘,也可以等待操作系统统一持久化,但后者可能会引起数据的丢失(没刷之前断电)

挂载

在Linux中运行df命令可以看到不同设备的挂载情况
比如,在根目录中运行df,可以看到sda1分区挂载在/boot目录下
因此,系统开机时磁盘分区会挂载在虚拟文件系统的目录节点上,系统关机后会从上面进行卸载操作.
在 Linux 看来,任何硬件设备也都是文件,它们各有自己的一套文件系统(文件目录结构)。
因此产生的问题是,当在 Linux 系统中使用这些硬件设备时,只有将Linux本身的文件目录与硬件设备的文件目录合二为一,硬件设备才能为我们所用。合二为一的过程称为“挂载”。
由于Linux 系统中“一切皆文件”,因此,挂载,指的就是将设备文件中的顶级目录连接到 Linux 根目录下的某一目录(最好是空目录),访问此目录就等同于访问设备文件。
挂载u盘的操作如下
在这里插入图片描述
在这里插入图片描述

Linux文件类型

-:普通文件
d:目录文件
b:块设备(不受约束的读数据,比如硬盘)
c:字符设备(,读不到过去的,受约束,比如键盘,网卡)
s:socke
p:pipeline
[eventpoll]:
l:链接(软链接,硬链接)

补充:软链接硬链接的区别:
软: 软链接文件和其指向的文件的inode不同,类似于windows的快捷方式
硬:硬链接文件与其指向的文件的inode相同.
无论是软硬链接,修改文件内容都会同时修改

文件描述符

内核为每个进程都维护了一套文件描述符,文件描述符指向了文件的inode号以及文件偏移信息(seek)
一切皆文件:exec 8 <> /dev/tcp/www.baidu.com/80
8这个文件描述符会去描述一个socket文件
任何程序都有0,1,2三个文件描述符,0为标准输入,1为标准输出,2为标准错误. 打开别的文件就会出现别的文件描述符

IO重定向

head -1 text.txt 输出文件的第一行
tail -2 text.txt 输出文件的后两行
head -8 text.txt | tail -1 输出文件的第八行

进程之间有父子关系
除非对变量进行导出处理(使其成为环境变量),否则子进程无法访问到父进程中的变量

管道两边的指令实际上被解释为两个进程分别执行,再将这两个进程的标准输入输出对接

PageCache

system call 系统调用
会执行int 0x80中断
int 0x80是一条 cpu指令,在寄存器中存在一个中断描述符表,表中记录着从0-255的不同中断,而0x80 = 128,对应着call back方法, cpu执行这个方法会保护现场并将用户态切换至内核态

在这里插入图片描述

使用pcstat +文件 查看文件的大小与其在内存中的缓存大小

pagecache虽然会提高io速率,但也存在丢失数据的风险

java中的io

直接文件IO与Buffered文件IO的区别:
直接文件IO(FileInputStream)是直接进行系统调用,把数据写入到pagecache 中
而BufferedInputStream是把数据写入到jvm的一个8kb字节数组中,字节数组满了会进行系统调用
ByteBuffer可以通过getChannel获取FileChannel,然后通过fileChannel的map方法获取到对内存的映射.往这个映射中直接put数据,会直接到达内核的pagecache而不必经过系统调用

目前的jdk没有办法逃离pagecache的限制

在这里插入图片描述
在这里插入图片描述
Socket

socket是一个四元组(clientIP,clientPort,serverIP,serverPort),也就是说,服务端的一个端口号,接受到很多client连接时,不必重新为其分配一个新的端口号,只要四元组不重复就可以建立连接。类似的,客户端的一个端口号也可以与很多个服务端的端口号建立连接。但是,服务端的listen状态的端口号只能有65535个,不能冲突,否则会报错

即使服务端没有accept,客户端依旧可以与服务端建立三次握手并来往消息,这是因为服务端有一个欢迎套接字,它的四元组为(* + * + 服务端的IP + 服务端的端口号),一旦服务端的对应端口调用了accept,就会建立起真正的连接套接字,他的四元组为(clientIP,clientPort,serverIP,serverPort)。但是,这种类似于内核为你建立起来的暂存的连接并不能无限增长,可以设置一个参数BACK_LOG规定最多暂存连接的数量,并且每一个连接,可以缓存的数据量也是被参数所限制的

每个socket都会被一个文件描述符所指向。当用户读取该文件描述符的时候,就是读取对应内核缓冲区的内容
在这里插入图片描述

设置keepalive参数为true,会发送心跳包来证明双方依旧“活着”

网络IO模型

在这里插入图片描述

打开句柄的限制数,对root用户会有宽松

BIO总结: 阻塞式IO,服务端阻塞式等待IO请求。当一个IO请求到来时,主线程会抛出一个线程去处理这个IO,在这个线程中阻塞的读取和写入数据。当这次请求处理完毕后线程会结束。

public class SocketBIO {
    public static void main(String[] args) throws Exception {
        ServerSocket server = new ServerSocket(9090,20);
        System.out.println("step1: new ServerSocket(9090) ");
        while (true) {
            Socket client = server.accept();  //阻塞1
            System.out.println("step2:client\t" + client.getPort());
            new Thread(new Runnable(){

                public void run() {
                    InputStream in = null;
                    try {
                        in = client.getInputStream();
                        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
                        while(true){
                            String dataline = reader.readLine(); //阻塞2

                            if(null != dataline){
                                System.out.println(dataline);
                            }else{
                                client.close();
                                break;
                            }
                        }
                        System.out.println("客户端断开");

                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }



            }).start();
        }
    }
}

带来的问题:C10k问题:当有10k个IO请求时,抛出10k个线程会导致性能急剧下降
解决方案:NIO
在一个单一的线程里面,不停的循环accept,如果有客户端接入,返回fd并且将其放入List中,如果没有,直接返回-1。accept之后,遍历List,取出每一个连接读取数据。

public class SocketNIO {
    //  what   why  how
    public static void main(String[] args) throws Exception {
        LinkedList<SocketChannel> clients = new LinkedList<>();
        ServerSocketChannel ss = ServerSocketChannel.open();  //服务端开启监听:接受客户端
        ss.bind(new InetSocketAddress(9090));
        ss.configureBlocking(false); //重点  OS  NONBLOCKING!!!  //只让接受客户端  不阻塞
        while (true) {
            //接受客户端的连接
            Thread.sleep(1000);
            SocketChannel client = ss.accept(); //不会阻塞?  -1 NULL
            //accept  调用内核了:1,没有客户端连接进来,返回值?在BIO 的时候一直卡着,但是在NIO ,不卡着,返回-1,NULL
            //如果来客户端的连接,accept 返回的是这个客户端的fd  5,client  object
            //NONBLOCKING 就是代码能往下走了,只不过有不同的情况
            if (client == null) {
             //   System.out.println("null.....");
            } else {
                client.configureBlocking(false); //重点  socket(服务端的listen socket<连接请求三次握手后,往我这里扔,我去通过accept 得到  连接的socket>,连接socket<连接后的数据读写使用的> )
                int port = client.socket().getPort();
                System.out.println("client..port: " + port);
                clients.add(client);
            }
            ByteBuffer buffer = ByteBuffer.allocateDirect(4096);  //可以在堆里   堆外
            //遍历已经链接进来的客户端能不能读写数据
            for (SocketChannel c : clients) {   //串行化!!!!  多线程!!
                int num = c.read(buffer);  // >0  -1  0   //不会阻塞
                if (num > 0) {
                    buffer.flip();
                    byte[] aaa = new byte[buffer.limit()];
                    buffer.get(aaa);
                    String b = new String(aaa);
                    System.out.println(c.socket().getPort() + " : " + b);
                    buffer.clear();
                }
            }
        }
    }

}

但是这个IO模型(NIO)依旧不是最快的,它有一个显著的缺点,在C10k问题中,它虽然避免了开辟大量线程造成的性能衰退,但是由于它在每个线程中都会挨个遍历,看是否有数据,而这会造成很多无效的系统调用。

我们可以加入一个叫做“多路复用器”的组件,它是在同步模型下的非阻塞IO组件(Linux没有通用的内核异步处理方案)

在这里插入图片描述

那么多路复用器有哪些呢
select,poll,epoll

select和poll都是传入需要监听的句柄数组,然后给你返回有数据待读取的句柄;不同的是,select限制只能不超过1024个句柄,而poll没有这种限制。而epoll的机制则较为复杂

select

int select (int n,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);

监测的文件描述符可以分为3类,分别等待不同的事件。监测readfds集合中的文件描述符,确认其中是否有可读的数据。监测writefds集合中的文件描述符,确认其中是否有一个写操作可以不阻塞的完成。监测 exceptfds集合中的文件描述符,确认其中是否有出现异常

举例来说,readfds集合中有两个文件描述符,7和9。当调用返回时,如果7还在集合中,该文件描述符就准备好进行无阻塞I/O了。如果9已不在集合中,它可能在被读取时发生阻塞

poll

int poll (struct pollfd *fds, unsigned int nfds,
int timeout);

select使用的是三个不同的文件描述符集合,而poll使用的是一个由nfds个pollfd结构体构成的数组fds,这个pollfd结构体的定义如下

struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

其实,无论是NIO,还是select,poll,他们都需要遍历所有的IO去询问状态。只不过NIO遍历的成本在用户态和内核态的切换。在select和poll模型下,遍历的过程只触发了一次用户态和内核态的切换。在这个过程中,把fds传给内核,内核再遍历并修改状态

select,poll这种机制有一个普遍的弊端:
1.每次都要重新传入需要监测的fds
2.每次在内核中都会触发一个全量的遍历

epoll完美的解决了这两个问题
epoll

epoll其实是由一系列操作组成
1.epoll create:

int epoll_create (int size)

epoll_create会创建一个epoll实例,当调用epoll_create时,会给你返回一个fd,这个fd描述了一块刚刚开辟出来的内核空间,这个空间里面放置了红黑树。这个操作可以解决重复传入fds这一弊端。整个生命周期中只会调用一次。

2.epoll_ctl:

int epoll_ctl (int epfd, int op, int fd, struct
epoll_event *event);

第一个参数是epoll实例的文件描述符,即上文epoll_create创建出来的,第二个参数是需要对实例中的红黑树进行的操作种类,比如删除、添加、修改。
比如,在这里插入图片描述

第三个参数是一个文件描述符,即第二个参数操作的对象。比如第三个参数传入5,第二个参数传入删除,则会在实例中删除5这个文件描述符
第四个参数是一系列事件的结构体,表明你需要关注的事件
3.epoll_wait

int epoll_wait (int epfd, struct epoll_event
*events, int maxevents, int timeout);

epoll wait() 的调用等待epoll实例epfd中的文件fd上的事件,时限为timeout毫秒。返回句柄后,还需用户自己对其进行accept,recv等操作,因此,epoll本质上还是一个同步模型

下面对epoll的工作原理进行梳理。当网卡读取到数据后,会进行中断操作。此时Linux内核会将socket的fd(包括此时文件的状态信息)放入buffer中,上述是一般内核都会进行的操作。而实现了epoll机制的内核,会将fd以及文件所对应的状态,在红黑树中进行检索,如果发现了对应节点,会将该节点拷贝到一个链表中。使用epoll_wait时,会直接将这个链表返回,这个链表就是具有相关状态信息(可读、可写)的文件集合,而无需进行遍历操作,也无需重复拷贝fd集合

epoll与select、poll的对比
在这里插入图片描述
java中多路复用器的使用

public class SocketMultiplexingSingleThreadv1 {

    //马老师的坦克 一 二期
    private ServerSocketChannel server = null;
    private Selector selector = null;   //linux 多路复用器(select poll    epoll kqueue) nginx  event{}
    int port = 9090;

    public void initServer() {
        try {
            server = ServerSocketChannel.open();
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(port));


            //如果在epoll模型下,open--》  epoll_create -> fd3
            selector = Selector.open();  //  select  poll  *epoll  优先选择:epoll  但是可以 -D修正

            //server 约等于 listen状态的 fd4
            /*
            register
            如果:
            select,poll:jvm里开辟一个数组 fd4 放进去
            epoll:  epoll_ctl(fd3,ADD,fd4,EPOLLIN
             */
            server.register(selector, SelectionKey.OP_ACCEPT);


        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        initServer();
        System.out.println("服务器启动了。。。。。");
        try {
            while (true) {  //死循环

                Set<SelectionKey> keys = selector.keys();
                System.out.println(keys.size()+"   size");


                //1,调用多路复用器(select,poll  or  epoll  (epoll_wait))
                /*
                select()是啥意思:
                1,select,poll  其实  内核的select(fd4)  poll(fd4)
                2,epoll:  其实 内核的 epoll_wait()
                *, 参数可以带时间:没有时间,0  :  阻塞,有时间设置一个超时
                selector.wakeup()  结果返回0

                懒加载:
                其实再触碰到selector.select()调用的时候触发了epoll_ctl的调用

                 */
                while (selector.select() > 0) {
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();  //返回的有状态的fd集合
                    Iterator<SelectionKey> iter = selectionKeys.iterator();
                    //so,管你啥多路复用器,你呀只能给我状态,我还得一个一个的去处理他们的R/W。同步好辛苦!!!!!!!!
                    //  NIO  自己对着每一个fd调用系统调用,浪费资源,那么你看,这里是不是调用了一次select方法,知道具体的那些可以R/W了?
                    //幕兰,是不是很省力?
                    //我前边可以强调过,socket:  listen   通信 R/W
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        iter.remove(); //set  不移除会重复循环处理
                        //这里的key有两个状态,一个是新连接,此时应注册.一个是之前注册过的可读,此时应读
                        if (key.isAcceptable()) {
                            //看代码的时候,这里是重点,如果要去接受一个新的连接
                            //语义上,accept接受连接且返回新连接的FD对吧?
                            //那新的FD怎么办?
                            //select,poll,因为他们内核没有空间,那么在jvm中保存和前边的fd4那个listen的一起
                            //epoll: 我们希望通过epoll_ctl把新的客户端fd注册到内核空间
                            acceptHandler(key);
                        } else if (key.isReadable()) {
                            readHandler(key);  //连read 还有 write都处理了
                            //在当前线程,这个方法可能会阻塞  ,如果阻塞了十年,其他的IO早就没电了。。。
                            //所以,为什么提出了 IO THREADS
                            //redis  是不是用了epoll,redis是不是有个io threads的概念 ,redis是不是单线程的
                            //tomcat 8,9  异步的处理方式  IO  和   处理上  解耦
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void acceptHandler(SelectionKey key) {
        try {
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            SocketChannel client = ssc.accept(); //来啦,目的是调用accept接受客户端  fd7
            client.configureBlocking(false);

            ByteBuffer buffer = ByteBuffer.allocate(8192);  //前边讲过了

            // 0.0  我类个去
            //你看,调用了register
            /*
            select,poll:jvm里开辟一个数组 fd7 放进去
            epoll:  epoll_ctl(fd3,ADD,fd7,EPOLLIN
             */
            client.register(selector, SelectionKey.OP_READ, buffer);
            System.out.println("-------------------------------------------");
            System.out.println("新客户端:" + client.getRemoteAddress());
            System.out.println("-------------------------------------------");

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void readHandler(SelectionKey key) {
        SocketChannel client = (SocketChannel) key.channel();
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        buffer.clear();
        int read = 0;
        try {
            while (true) {
                read = client.read(buffer);
                if (read > 0) {
                    buffer.flip();
                    while (buffer.hasRemaining()) {
                        client.write(buffer);
                    }
                    buffer.clear();
                } else if (read == 0) {
                    break;
                } else {
                    client.close();
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();

        }
    }

    public static void main(String[] args) {
        SocketMultiplexingSingleThreadv1 service = new SocketMultiplexingSingleThreadv1();
        service.start();
    }
}

可以为Java指定不同的多路复用实现,Java会依据此将selector解释为不同的字节码

标签:文件,epoll,int,精讲,client,IO,poll,select
来源: https://blog.csdn.net/weixin_52016782/article/details/122706569

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有