1. 概念介绍
1.1 设计一个高性能服务器,多个客户端同时链接,并且处理传递过来的所有请求。
①:多线程的方式,涉及到CPU上下文的切换
,操作很多句柄,代价比较大
②:单线程的方式如下图:
上图实现的方式,一直for循环判断各个客户端是否有数据,如果有就做处理,判断数据是在用户态
去做的判断,它不断的询问内核
该网络链接是否有数据。用户态与内核态不断的切换
。
1.2 几点说明
①:我们有ABCDE四个客户端,当CPU处理 A客户端的请求时,B客户端的数据会不会被丢弃?
不会,因为迎接B客户端传递数据的并不是CPU,而是专注IO的DMA控制器
。DMA介绍
② Linux中一切皆文件,每一个网络连接在内核中是以文件描述符 fd
的形式存在
2. select、poll、epoll
2.1 select
将文件描述符收集过来,交给内核,让内核去判断那个有数据。当里面任何一个或者多个有数据的时候,内核会返回,并且有数据的那个文件描述符fd
会被标记置位。返回之后,遍历集合,判断那个fd有数据了,然后读取数据并处理。
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。
-
select传递五个参数:
①:max+1, 表示内核态需要判断的&rest的前几位,提高效率
②:读文件描述符集合
③:写文件描述符集合
④:异常的文件描述符集合
⑤:超时时间 -
提高效率的方式:
将fd的集合放到内核态,让内核来判断那个有数据。 -
缺陷
① 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。可以重用。
- 优劣
很好的解决了select函数的①、②问题。没有解决③、④问题。
2.3 epoll
-
两个重要函数
epoll_ctl
和epoll_wait
-
首先创建一个
eventpoll
对象,创建完成后返回这个对象的id,相当于在内核开辟了一小块空间,并且我们也知道这块空间的位置
,EventPoll的结构:
主要有两块重要的区域,一块存放需要监听的文件描述符列表,一块存放就绪socket信息的列表,epoll_ctl
可以根据 这个对象的id去增删改查内核空间上 eventpoll 对象的信息 -
epoll_wait 中,传入
eventpoll
对象的id,表示此次系统调用需要监听的socket_fd的合集(epoll_ctl添加的需要监听的socket对象)
。 -
eopll_wait也是处于
阻塞状态
(默认的水平触发
),边缘触发
是非阻塞状态,它跟select一样,第一次遍历没有发现就绪状态的socket,就把eventpoll 对象放入socket的等待队列中,当数据就绪后,发现等待队列的不是进程引用而是eventpoll对象,就把当前socket对象的引用追加到eventpoll的就绪链表的末尾
,eventpoll还有一块空间是eventpoll#等待队列,保存的就是调用epoll_wait的进程的引用,会把当前进程放入到工作队列
,又会有机会获取到cpu时间片了。 -
epoll_wait: 返回值,int类型, 0 表示没有就绪,大于0表示有几个就绪,-1表示异常。epoll_wait函数调用的时候,会传入一个epoll_event事件数组指针,该函数返回之前就把就绪的socket事件,信息拷贝到这个数组指针里,返回上传程序,就可以根据这个数组拿到就绪列表了。
-
epoll_wait,传入超时时间为0,就是非阻塞的,需要每次调用去检查就绪的socket信息。
-
event_poll 对象存放的socket集合,采用
红黑树
结构,因为经常的增删查
,时间复杂度 O(Log(n))
8. redis,nginx,java NIO 都使用的epoll,很好的解决了 ③④问题。
2.4 相关问题
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,进行相应的处理。