IO多路复用select/poll/epoll介绍

1. 概念介绍

B站学习链接

1.1 设计一个高性能服务器,多个客户端同时链接,并且处理传递过来的所有请求。

①:多线程的方式,涉及到CPU上下文的切换,操作很多句柄,代价比较大
②:单线程的方式如下图:
IO多路复用select/poll/epoll介绍_第1张图片
上图实现的方式,一直for循环判断各个客户端是否有数据,如果有就做处理,判断数据是在用户态去做的判断,它不断的询问内核该网络链接是否有数据。用户态与内核态不断的切换

1.2 几点说明

①:我们有ABCDE四个客户端,当CPU处理 A客户端的请求时,B客户端的数据会不会被丢弃?
不会,因为迎接B客户端传递数据的并不是CPU,而是专注IO的DMA控制器。DMA介绍

② Linux中一切皆文件,每一个网络连接在内核中是以文件描述符 fd的形式存在

2. select、poll、epoll

2.1 select

将文件描述符收集过来,交给内核,让内核去判断那个有数据。当里面任何一个或者多个有数据的时候,内核会返回,并且有数据的那个文件描述符fd会被标记置位。返回之后,遍历集合,判断那个fd有数据了,然后读取数据并处理。
IO多路复用select/poll/epoll介绍_第2张图片

select函数不直接接受fd文件描述符的集合,而是接受fd的一个集合 rset(结构为 bitmap),用来表示哪一个文件描述符是被启用的,被监听的,如被监听的描述符为 1、2、5、7、9, rset为:0110010101 共1024个坑位,在需要被监听的地方置1。
select函数会被阻塞:

当调用select函数时,如果有就绪状态的socket就会直接返回,如果没有,就会阻塞一直等待。

系统调度:cpu同一时刻,它只能运行一个进程,操作最主要的任务就是系统调度,就是有n个线程,然后让这n个线程在cpu上切换执行。未挂起的线程都在工作队列内,都有机会获取到cpu执行权。挂起的线程会从工作队列中移除,反应到java层面就是线程阻塞了。Linux线程就是轻量级的线程。

cpu中断:比如我们打字,如果cpu不中断当前程序,我们就无法输入,键盘给主板发送电流信号,主板感知后,就会触发cpu中断。中断:就是让cpu正在执行的程序便保留程序上下文,然后避让出cpu,给中断程序让道。中断程序拿到cpu执行权,执行代码。

select函数会一直占用内核去轮询那个socket就绪了吗?

第一个阶段: select函数第一遍轮询没有就绪状态的socket,它就会把当前进程的引用追加到当前进程关注的每一个socket对象的等待队列中。socket有三块核心区域,分别是读缓存,写缓存还有等待队列。select函数把当前进程保留到每个需要检查的socket#等待队列之后,就把当前进程从工作队列中移除了。挂起了当前线程,select函数也就不会在运行了。
第二个阶段:假设我们的客户端向当前服务器发送了数据,通过网线到网卡,再通过DMA硬件的这种方式直接将数据写到内存中。整个过程cpu是不参与的。当数据传输完毕以后,就会触发网络传输完毕的中断程序,中断程序会把cpu正在执行的进程给顶掉,执行这个中断程序的逻辑。中断程序的逻辑:根据内存中的数据包,判断tcp/ip 数据包是有端口号的,根据端口号可以找到对应的socket实例,然后把数据导入到socket的读缓冲区里头,完成后,开始去检查socket的等待队列,是否有等待者,如果有的话,就把等待着所有进程移动到工作队列,中断程序这一步就执行完了。进程又回归到工作队列,又有机会获取到cpu时间片。当前进程再执行select函数,再次检查发现又就绪的socket了,就会给就绪的文件描述符fd打上标记,返回到java层面,涉及到用户态和内核态的切换。后续就是轮询检查被打了标记的socket。

当有数据来的时候,FD会被置位(rest会改变),然后返回,遍历那个有数据,读取数据。然后需要将 rset归零(FD_ZERO(&rest)),再重置&rest。

  1. select传递五个参数:
    ①:max+1, 表示内核态需要判断的&rest的前几位,提高效率
    ②:读文件描述符集合
    ③:写文件描述符集合
    ④:异常的文件描述符集合
    ⑤:超时时间

  2. 提高效率的方式:
    将fd的集合放到内核态,让内核来判断那个有数据。

  3. 缺陷
    ① rset 是一个bitmap,最大为1024,
    ② rset会被置位,所以每次循环都需重新设置
    ③ 用户态到内核态数据copy的开销
    ⑤ for循环遍历 时间复杂度O(n)

2.2 poll

poll传入的参数为 pollfd,自己重新封装的一个结构,

 struct  pollfd{
	int fd;   	 // 文件描述符
	short events;	// 事件类型 读 POLLIN、 写 POLLOUT、读和写
	short revents;	// 默认值为0,当有数据时,该参数会被置位。
}

poll方法也是阻塞的。当有数据时,会置位revents字段,后续读取数据时,会重新置为0。可以重用。

IO多路复用select/poll/epoll介绍_第3张图片

  1. 优劣
    很好的解决了select函数的①、②问题。没有解决③、④问题。
2.3 epoll
  1. 两个重要函数 epoll_ctlepoll_wait

  2. 首先创建一个 eventpoll对象,创建完成后返回这个对象的id,相当于在内核开辟了一小块空间,并且我们也知道这块空间的位置EventPoll的结构:主要有两块重要的区域,一块存放需要监听的文件描述符列表,一块存放就绪socket信息的列表,epoll_ctl 可以根据 这个对象的id去增删改查内核空间上 eventpoll 对象的信息

  3. epoll_wait 中,传入 eventpoll对象的id,表示此次系统调用需要监听的socket_fd的合集(epoll_ctl添加的需要监听的socket对象)

  4. eopll_wait也是处于阻塞状态(默认的水平触发),边缘触发是非阻塞状态,它跟select一样,第一次遍历没有发现就绪状态的socket,就把eventpoll 对象放入socket的等待队列中,当数据就绪后,发现等待队列的不是进程引用而是eventpoll对象,就把当前socket对象的引用追加到eventpoll的就绪链表的末尾eventpoll还有一块空间是eventpoll#等待队列,保存的就是调用epoll_wait的进程的引用,会把当前进程放入到工作队列,又会有机会获取到cpu时间片了。

  5. epoll_wait: 返回值,int类型, 0 表示没有就绪,大于0表示有几个就绪,-1表示异常。epoll_wait函数调用的时候,会传入一个epoll_event事件数组指针,该函数返回之前就把就绪的socket事件,信息拷贝到这个数组指针里,返回上传程序,就可以根据这个数组拿到就绪列表了。

  6. epoll_wait,传入超时时间为0,就是非阻塞的,需要每次调用去检查就绪的socket信息。

  7. event_poll 对象存放的socket集合,采用红黑树结构,因为经常的增删查,时间复杂度 O(Log(n))

IO多路复用select/poll/epoll介绍_第4张图片
8. redis,nginx,java NIO 都使用的epoll,很好的解决了 ③④问题。

2.4 相关问题

基于SSD固态硬盘的数据库性能优化

3. 相关面试题

3.1 java BIO

新建一个serverSocket来监听端口,等待appept方法,但该方法会阻塞当前主线程,当接收到一个accpet事件后,程序会拿到一客户端与当前服务端连接的socket。针对这个socket我们可以进行读写,但是socket的读写方法都会阻塞当前线程一般使用多线程的方式来进行c/s交互,但这样很难做到C10k

3.2 NIO 解决多客户端的问题

java NIO的包提供了一套非阻塞的接口,这样我们就不需要为每个c/s长链接保留一个单独的处理线程了。可以用一个线程去检查n个socket,在java层面上,就是 NIO包中提供了一个选择器selector,把需要检测的socket注册到selector中,主线程阻塞在 selectot#select 方法里面。当我们的选择器发现我们的某个socket就绪了,就会唤醒主线程,通过selector获取到就绪的socket,进行相应的处理。

你可能感兴趣的:(Java基础)