热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

Linux虚拟文件系统的理解VFS是一棵树,树上的节点可以映射到对应的物理位置与之对应的,什么是实际上的文件系统呢,比如说Windows操作系统上的,D盘对应的就是那块磁盘,C盘对




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 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 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 selectiOnKeys= selector.selectedKeys(); //返回的有状态的fd集合
Iterator 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解释为不同的字节码



推荐阅读
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 树莓派Linux基础(一):查看文件系统的命令行操作
    本文介绍了在树莓派上通过SSH服务使用命令行查看文件系统的操作,包括cd命令用于变更目录、pwd命令用于显示当前目录位置、ls命令用于显示文件和目录列表。详细讲解了这些命令的使用方法和注意事项。 ... [详细]
  • Android源码深入理解JNI技术的概述和应用
    本文介绍了Android源码中的JNI技术,包括概述和应用。JNI是Java Native Interface的缩写,是一种技术,可以实现Java程序调用Native语言写的函数,以及Native程序调用Java层的函数。在Android平台上,JNI充当了连接Java世界和Native世界的桥梁。本文通过分析Android源码中的相关文件和位置,深入探讨了JNI技术在Android开发中的重要性和应用场景。 ... [详细]
  • 如何搭建Java开发环境并开发WinCE项目
    本文介绍了如何搭建Java开发环境并开发WinCE项目,包括搭建开发环境的步骤和获取SDK的几种方式。同时还解答了一些关于WinCE开发的常见问题。通过阅读本文,您将了解如何使用Java进行嵌入式开发,并能够顺利开发WinCE应用程序。 ... [详细]
  • 基于dlib的人脸68特征点提取(眨眼张嘴检测)python版本
    文章目录引言开发环境和库流程设计张嘴和闭眼的检测引言(1)利用Dlib官方训练好的模型“shape_predictor_68_face_landmarks.dat”进行68个点标定 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • Python已成为全球最受欢迎的编程语言之一,然而Python程序的安全运行存在一定的风险。本文介绍了Python程序安全运行需要满足的三个条件,即系统路径上的每个条目都处于安全的位置、"主脚本"所在的目录始终位于系统路径中、若python命令使用-c和-m选项,调用程序的目录也必须是安全的。同时,文章还提出了一些预防措施,如避免将下载文件夹作为当前工作目录、使用pip所在路径而不是直接使用python命令等。对于初学Python的读者来说,这些内容将有所帮助。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • 本文介绍了OC学习笔记中的@property和@synthesize,包括属性的定义和合成的使用方法。通过示例代码详细讲解了@property和@synthesize的作用和用法。 ... [详细]
  • 本文介绍了在Win10上安装WinPythonHadoop的详细步骤,包括安装Python环境、安装JDK8、安装pyspark、安装Hadoop和Spark、设置环境变量、下载winutils.exe等。同时提醒注意Hadoop版本与pyspark版本的一致性,并建议重启电脑以确保安装成功。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • Java 11相对于Java 8,OptaPlanner性能提升有多大?
    本文通过基准测试比较了Java 11和Java 8对OptaPlanner的性能提升。测试结果表明,在相同的硬件环境下,Java 11相对于Java 8在垃圾回收方面表现更好,从而提升了OptaPlanner的性能。 ... [详细]
  • 本文整理了Java面试中常见的问题及相关概念的解析,包括HashMap中为什么重写equals还要重写hashcode、map的分类和常见情况、final关键字的用法、Synchronized和lock的区别、volatile的介绍、Syncronized锁的作用、构造函数和构造函数重载的概念、方法覆盖和方法重载的区别、反射获取和设置对象私有字段的值的方法、通过反射创建对象的方式以及内部类的详解。 ... [详细]
  • 一次上线事故,30岁+的程序员踩坑经验之谈
    本文主要介绍了一位30岁+的程序员在一次上线事故中踩坑的经验之谈。文章提到了在双十一活动期间,作为一个在线医疗项目,他们进行了优惠折扣活动的升级改造。然而,在上线前的最后一天,由于大量数据请求,导致部分接口出现问题。作者通过部署两台opentsdb来解决问题,但读数据的opentsdb仍然经常假死。作者只能查询最近24小时的数据。这次事故给他带来了很多教训和经验。 ... [详细]
author-avatar
batman@zhou
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有