linux下epoll网络编程模型,05_网络编程之多路复用器以及 epoll 模型精讲

本节课主要讲解了多路复用器在 linux 系统上的实现,包括 select、poll、epoll 各自发展的过程、原理以及区别,并且对应 Java 中网络编程多路复用器的抽象处理。

1. 回顾 NIO

之前学习的 NIO 同步非阻塞模型,它使用一个线程处理客户端 socket 的连接、读写事件。在创建 ServerSocket 和客户端 Socket 时传递非阻塞参数给操作系统,这样在调用它们的 accept 或者 read 系统调用时不会阻塞,而是会立即返回,程序需要自己判断是否可以处理数据。

注意:NIO 在 Java 中代表 new IO,在操作系统上代表 noblocking 非阻塞 IO。

ae86ed01aed1de216b34dfbed35e5b8b.png

优点:

只需要一个线程或几个线程,来解决 N 个 IO 连接的处理问题

缺点:

C10K 问题,即当有很多的 IO 连接后,在 Java 应用程序上需要遍历这些 N 个 IO 连接执行 read 系统调用,时间复杂度是 O(N),因为可能只有几个或者少数的 IO 连接有数据,所以很多的 IO 系统调用是无用的

2. 多路复用器

什么是多路复用器,它是操作系统内核提供的一种 IO 机制。

bbbc0edf6bbda6561a3b20f12794f5a2.png

如何理解多路复用器呢?可以将 IO 看作是在建立在内核上从对文件(socket、磁盘等)进行输入输出的一条路。在 Java 的 NIO 程序中,需要针对多个 IO(ServerSockt、Socket)执行 accept 或者 read 系统调用,即需要执行 N 次 IO (路)的系统调用,那么多路复用就是说多条路(IO)通过执行一次系统调用,获得其中的 IO 状态,然后,由程序自己对着有状态变化的 IO 进行 R/W 操作。

内核提供的多路复用系统调用函数有:select、poll、epoll、kqueue。

其中 select 是 POSIX(可移植操作系统接口)工业标准中规范的一个系统调用。

对于同步、异步、阻塞、非阻塞的理解,是站在只关注 IO,不关注从 IO 读写完之后的事情的角度:

同步:app 自己去操作 IO 的 R/W

异步:内核完成 IO 的 R/W 操作,将数据放入进程的 buffer,不严谨的说好像 app 没有访问 IO 只是访问了 buffer。目前只有 Windows 上的 iocp 是纯异步的操作

阻塞:blocking,

非阻塞:noblocking

在 Linux 以及目前成熟的 Netty 框架中,有下面这些组合:

同步阻塞:程序自己读取 IO,调用了方法一直在等待有效的返回结果

同步非阻塞:程序自己读取 IO,调用方法的一瞬间,得到是否读到结果,程序要解决自己下一次啥时候再去读

异步:一般不讨论,现在只讨论 IO 模型下,Linux 目前没有通用的内核的异步处理方案

异步非阻塞

2.1 select 与 poll

查看 Linux 的 select man 手册:

[root@ecs-02 ~]# man 2 select

SELECT(2) Linux Programmer's Manual SELECT(2)

NAME

select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexing

...省略...

DESCRIPTION

select() and pselect() allow a program to monitor multiple file descriptors, waiting until one or more of the file descriptors become "ready" for some class of I/O opera‐

tion (e.g., input possible). A file descriptor is considered ready if it is possible to perform a corresponding I/O operation (e.g., read(2) without blocking, or a suffi‐

ciently small write(2)).

select() can monitor only file descriptors numbers that are less than FD_SETSIZE; poll(2) does not have this limitation. See BUGS.

...省略...

可以看到 select 系统调用的说明,说它是是一个同步的 IO 多路传输技术。select() 和 pselect() 允许一个程序去监控多个文件描述符,一直等待直到其中一个或者多个文件描述符从一些 IO 操作(可读、可接受等)中已经准备好的时候返回。

image-20210123163349625

select 它会接收一个参数文件描述符,一次只能接收 1024个文件描述符。后来出现的 poll 系统调用没有这个限制了。

在 Java 程序中编写利用多路复用器编写代码时,会在 jvm 中创建一个数组来维护 ServerSocket 和客户端 socket 建立的文件描述符,然后有一个 while 循环然后执行 select 方法调用内核的 select 系统调用,会把这些文件描述符作为参数传入其中,此时只是执行了一次调用系统调用,时间复杂度是 O(1),然后 select 系统调用会返回一个结果,比如有 m 个有状态的 IO,程序再对这个 m 个 IO 执行 read 操作。

NIO、select、poll 的相同点:

它们都是要遍历所有的 IO,询问状态。

NIO、select、poll 的不同点:

NIO 这个 IO 遍历过程的成本在用户态与内核态的切换

多路复用器 select、poll 的对 IO 的遍历过程触发了一次系统调用,即一次用户态和内核态的切换,这个过程中,程序将 fds 文件描述符传递给内核,内核重新根据这次传过来的 fds 进行对应 IO 的遍历和修改状态,这个过程比 NIO 程序自己遍历 IO 速度快的多(重点)

多路复用器 select、poll 的弊端:

每次都要由程序重新传递文件描述符 fds,这就要让内核开辟空间

每次内核被调用之后,都会针对这次调用来触发一次全量遍历 fds,复杂度还是 O(N)

2.2 中断回调

479533932708dd13920e4dc197daf597.png

计算机有内存、cpu、输入输出设备(网卡、键盘、鼠标、磁盘等)组成。

硬件与 CPU 的通信是通过中断信号来完成的,中断指令在内核中会有对应的中断向量表,同时中断也有对应的回调函数 callback。

还有一个时钟振荡器(晶振)时钟中断,它是一个硬中断,它作用是发送一定的频率给 CPU 来发送中断信息,打断 CPU 让它执行切换其他进程调度。

还有一个软中断,它是软件层面调用内核的 int 0x80 中断信号, CPU 会让程序从用户态切换到内核态,同时保护用户态现场。

网卡的中断属于硬中断,内存中有一个 DMA 的区域用来存放网卡数据。

当有客户端连接时,数据通过网线进入网卡,网卡就可以有几种处理方式:第一种是立即发其中断,第二种是将数据放入网卡的 buffer 缓冲区,第三种是数据来的太快,不再执行中断,而是让 CPU 抽出一个时间特意轮询调用读取数据。这完全是内核做的事情。

这三种操作最终都会调用中断,执行回调函数 callback,抽象成处理 event 事件,就是有回调处理事件。

在 epoll 之前的回调处理,只是完成了将网卡发来的数据,走内核网络协议栈(2(物理链路层),3(协议传输层),4(应用处理层) 层),最后会关联到文件描述符的 buffer。所以,某一时刻如果从 app 询问某一个或者某些文件描述符是否可 R/W,就会有状态返回。

2.3 epoll

为了解决 select 和 poll 的弊端问题,Linux 2.6 内核实现了一种 epoll 的多路复用器系统调用。

看下它的介绍,查看 Linux 的 epoll 系统调用手册:

EPOLL(7) Linux Programmer's Manual EPOLL(7)

NAME

epoll - I/O event notification facility

SYNOPSIS

#include

DESCRIPTION

The epoll API performs a similar task to poll(2): monitoring multiple file descriptors to see if I/O is possible on any of them. The epoll API can be used either as an

edge-triggered or a level-triggered interface and scales well to large numbers of watched file descriptors. The following system calls are provided to create and manage an epoll instance:

* epoll_create(2) creates a new epoll instance and returns a file descriptor referring to that instance. (The more recent epoll_create1(2) extends the functionality of epoll_create(2).)

* Interest in particular file descriptors is then registered via epoll_ctl(2). The set of file descriptors currently registered on an epoll instance is sometimes called an epoll set.

* epoll_wait(2) waits for I/O events, blocking the calling thread if no events are currently available.

...省略...

3e8d273a43000388ee4aaf8fd354cb7e.png

epoll 是一个 IO 事件通知功能,它有以下系统调用:

epoll_create(2):创建一个 epoll 实例并且返回一个描述该实例的文件描述符,比如 fd6。

epoll_ctl(2):注册对特定文件描述符的兴趣事件,这个注册在epoll 实例上的文件描述符集合有时也被称之为 epoll 集合。这是相当于把一些文件描述符以及它感兴趣的事件注册到红黑树上,比如就将 ServerSocket 对应的 fd4 注册到 fd6 红黑树上去。

epoll_wait(2):等待 IO 事件,如果没有事件可用就会阻塞调用线程。

原理:

内核创建 epoll 时会创建一个红黑树,用来存放一些文件描述符以及它感兴趣的事件,这些文件描述符可以是处于 listen 状态下的 ServerSocket 或者 Socket;内核对 epoll 的实现中是对于网卡中断的回调做了延伸,当网卡有收到客户端数据时会产生中断,它把网卡的数据放到 fd 的 buffer 上,同时会拿着这个 fd 从红黑树中查找是否存在,如果存在就会把这个 fd 迁移到一个链表上去,所以,程序只需要调用 wait 就能及时取走有状态的 fd 的结果集。

epoll 和 select、poll 相比,优点:

避免了内核每次调用 select 和 poll 是程序传入遍历传入 fds 文件描述符,用户程序只需要调用 wait 一次就可以直接拿到已经有状态的 IO 结果集

Java 的 api 中对应的多路复用器是 selector,它是对操作系统多路复用的实现完成高度抽象和细节封装,它会根据操作系统对多路复用器的具体实现来执行。

epoll 和 select、poll 在 Java 上实现的对比:

针对 select 和 poll,它会在 jvm 上分配一个数组维护 ServerSocket 和 Socket 的文件描述符,在调用 select 或者 poll 系统调用时将这些描述符传给内核,阻塞等待内核的返回

针对 epoll 它不需要维护数组,直接调用 epoll_wait 阻塞等待结果集

3. Java 代码中的 selector 的实现

package com.kaige.system.io;

import java.io.IOException;

import java.net.InetSocketAddress;

import java.nio.ByteBuffer;

import java.nio.channels.SelectionKey;

import java.nio.channels.Selector;

import java.nio.channels.ServerSocketChannel;

import java.nio.channels.SocketChannel;

import java.util.Iterator;

import java.util.Set;

public class SocketMultiplexingSingleThreadv1 {

int port = 8091;

private ServerSocketChannel server = null;

private Selector selector = null; //linux 多路复用器(select poll epoll kqueue) nginx event{}

public static void main(String[] args) {

SocketMultiplexingSingleThreadv1 service = new SocketMultiplexingSingleThreadv1();

service.start();

}

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 不移除会重复循环处理

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();

}

}

}

这个代码中有几个重要的api:

Selector:多路复用器,它的 api:

通过 Selector.open() 静态方法创建,Java 会在 select、poll、epoll 系统调用下优先选择 epoll,可以通过程序启动参数 -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider修改,在 epoll 模型下,约等于调用 epoll_create 系统调用,比如返回了 fd3

select.select() 实例方法会进行阻塞获取事件 IO,对应到 epoll 模型下,就是调用 epoll_wait();对应 select、poll 其实就是调用内核 select(fd4)、poll(fd4);参数是阻塞时间,0 为永久阻塞

select.wakeup() 实例方法会唤醒阻塞,由其他线程调用

ServerSocketChannel:服务端 Socket 用于接收客户端 Socket 连接

ServerSocketChannel.open() 方法会创建 ServerSocketChannel 实例,约等于 listen 状态下的文件描述符 fd4

server.register(selector, SelectionKey.OP_ACCEPT) 方法是将当前实例文件描述符注册多路复用器上,在 epoll 模型上相当于调用了 epoll_ctl(fd3,ADD,fd4,EPOLLIN);在 select、poll 模型中是通过在 jvm 堆内维护一个数组,把 fd4 放进去

SelectionKey:代表 Selector 上所注册的事件 key,可以获取对应的 Channel 和 attachment 以及 IO 文件描述符的状态

SocketChannel:代表客户端 Socket 的信息,它也可以注册到 Selector 上去,也可以执行 R/W 操作

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值