操作系统中的BIO、NIO和多路复用器(SELECT、POLL、EPOLL)的演进和实现

一、前言

1、计算机的硬件通常由CPU、内存、网卡、键盘、鼠标等组成;
2、当计算机开机时,首先会运行一个内核到内存中,同时把内存划分为内核空间和用户空间,内核空间中指令引导CPU的执行,用户空间存放APP(软件);
3、内核空间存在一个保护模式,就是用户空间的APP不能直接访问内核空间,生怕APP篡改内核中的指令,导致出现蓝屏;
在这里插入图片描述

1、用户空间能间接访问内核空间?

用户空间的APP如果想要访问硬盘的文件,必须经过内核空间,但由于保护模式,导致不能直接访问,这时候系统提供了中断和系统调用来解决;
当APP访问内核空间时(不进行函数编程,不违反保护模式),内核空间会生成一个系统调用的编码,并返回给APP,当CPU读取到APP中内核空间传递的编码时,会中断程序,并根据编码去内核空间,找到对应的执行指令,执行此指令并把获取到的数据存在自己的内存中,之后CPU保护和恢复线程,执行原来中断的APP,并把自己内存中的数据存放到APP中;

中断:晶振原理,按一定的规律,给CPU产生一个时钟中断,使得CPU进行上下文切换(中断一般对应着一个系统回调);
系统调用:CPU上下切换时,会根据内核中的系统回调,切换到下一个APP的执行;

2、CPU上下文切换,那么原来执行到一半到数据保存在哪里,存在什么弊端?

2.1、CPU上下切换时,会把数据存放到APP中,同时清空自己的内存,通过内核的系统调度,再执行其它APP,当切换回原来的APP时,会从APP中读取之前存放的数据,之后继续执行,
2.2、CPU的切换分为两种: 不同APP之间并发执行的切换; 用户空间访问内核空间,进行的切换;
2.3、弊端:在CPU切换时(特别是APP多的时候),存在着频繁的保护线程和恢复线程,使得CPU在系统调度中的时间过多,APP的执行时间过少;

二、网络通信IO的演变过程

一、BIO

BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善;

BIO演示:
准备一个类;

在linux上预编译java文件;
在这里插入图片描述
查看文件,发现class文件,编译成功;
在这里插入图片描述
运行class文件;
在这里插入图片描述

之后创建一个新的xshell窗口,连接相同的linux系统,执行ll命令,发现里面有8个线程(有部分是用户线程,如GC回收线程),其中out.8211是主线程;
在这里插入图片描述

打开主线程;
在这里插入图片描述
此文件是线程与内核进行交互的情况,移至最后一行,发现accept之后,就没有再运行下去,其实此时是一个阻塞状态(因为java代码中,还没有其它客户端连接到Socket);
在这里插入图片描述
查看进程,java监听本地socket(0.0.0.0:8090),接收(0.0.0.0:*)任意的ip+端口的数据包,因为此时还没有客户端连接,所以(0.0.0.0:?)没有显示具体的端口号;
在这里插入图片描述

再开启一个xshell窗口,连接相同的linux系统,之后连接ServerSocket;
在这里插入图片描述

此时,在第一个xshell窗口,出现client的连接端口号;
在这里插入图片描述
同时,在第二个xshell窗口,原本accept是阻塞状态,监听到有客户端连接进来后,显示客户端的IP和端口号;
PS:accept在主线程中接收客户端,同时开启另外一个线程;
在这里插入图片描述
通过主线程clone内核的一个系统调用,开启新的线程,同时,主线程继续等待新的客户端的连接;
PS:java线程:是通过调用内核的系统调用,得到在操作系统的一个轻量级进程;在这里插入图片描述
客户端连接后,会另外开启一个线程,显示连接状态,(listen)
在这里插入图片描述
查看文件,我们开启新的线程,会显示到文件中
在这里插入图片描述
客户端连接成功,势必存在读写数据,我们通过vim out.8281打开线程,发现recv存在阻塞状态(没数据传输)
在这里插入图片描述

综上所述,我们知道accept和recv是存在阻塞状态的,如果当java只有一个线程,当accept阻塞时,则无法执行剩下代码或recv阻塞时,无法执行剩下代码,因此我们可以创建新的线程,把recv放到新的线程里(这就是我们java代码要new Thread的原因)
PS:这就是最早的BIO,每一个线程对应每一个连接;
在这里插入图片描述
BIO流程图:
在这里插入图片描述
二、NIO

为了解决BIO的问题,于是NIO诞生;

NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理;

准备一个java类;在这里插入图片描述

在linux系统上运行此class文件,此时没有客户端连接进来的时候,发现并没有阻塞
在这里插入图片描述
查看此主线程,里面是如何运行的(跟BIO是一样的,socket,bind,listen)
在这里插入图片描述
再往下看,发现accept()有接收到数据,并且返回值=-1,此时没有阻塞,而是继续执行write;
在这里插入图片描述
此时另外再开一个xshell窗口,连接相同的linux系统,执行命令,连接服务器
在这里插入图片描述
连接成功,输出打印
在这里插入图片描述

综上所述,每次accept的时候,如果没有连接进来,则默认返回一个-1,然后继续往下执行,recv操作也是如此;

优势:规避了多线程的问题;
弊端:假设1万个连接只有一个发来数据,每循环一次,就必须向内核发送一万次recv的系统调用,那么这里就有9999次是无意义的,消耗资源(用户空间向内核空间的循环遍历,复杂度在系统调用上);

三、AIO

AIO(NIO.2):异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理;

PS:如果程序自己读取IO,那么这个IO模型,无论是BIO,NIO,多路复用器都是同步IO模型;

AIO分为三种:select、poll、epoll;

select和poll的区别;
PS:select和poll的fds都是保存在jvm数组里,并未在内核中开辟新的空间存放;
在这里插入图片描述
epoll的流程图:
在这里插入图片描述
过程:listen之后,epoll_create()内核开辟一块空间,用于存放客户端的事件,epoll_ctl()添加客户端事件到空间中去,内核会辨别是否存在事件(连接和IO),如果有就放到另一块空间中,等待服务端的epoll_wait,没有的话继续存放在原位置,直到有事件发送,服务端一旦收到epoll_wait的返回值,就会跟客户端进行连接或IO;

select和poll与epoll的区别:

在于select和poll没有create和ctl,而epoll有;
wait是共有的,会阻塞;

java代码(单个线程)演示多路复用器:

在这里插入图片描述

PS:此代码的accept和read都是同一个线程在处理,由于epoll.wait会出现阻塞(也就是java代码中select),如果不设置超时时间,会导致后面的代码无法执行,因此可以开新的线程分别执行accept和read;

四、应用场景

在网络通信中,如果有请求到来(不管是几个同时到还是只有一个到),都会调用对应IO处理函数处理,所以:

(1)NIO适合处理连接数目特别多,但是连接比较短(轻操作)的场景,Jetty,Mina,ZooKeeper等都是基于java nio实现;

(2)BIO方式适用于连接数目比较小且固定的场景,这种方式对服务器资源要求比较高,并发局限于应用中;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值