epoll

103 阅读6分钟

BIO blocking

服务端监听端口,等待accept事件,accept会堵塞主线程,收到accept事件,程序会拿到一个客户端与服务端链接的一个socket,针对这个socket,我们都可以读写,但是这个socket的读写方法都会堵塞当前线程,一般我们用多线程的方式来进行c/s交互,但是这样很难做到c10k,比如说一万个客户端,就需要一万个服务端线程去支持,这样的话cup就爆炸了,线程上下文切换,机器负载拉飞。

NIO

Java角度看,NIO包给我们提供了一套非堵塞的接口,这样就不需要我们为每一个c/s长连接保留一个单独的线程处理了,

NIO具备非堵塞特性,我们可用一个线程去检查n个socket,NIO API给我们提供了一个Selector,我们要把检查的socket注册到selector中,然后主线程堵塞在seletor#select方法中,当选择器发现某个socket就绪了,就会唤醒主线程,通过selector获取就绪的socket并进行相应的处理

select函数

每次调用kernel#select函数,都会涉及到用户态内核态转换,还需要传递检查socket集合,其实就是要检查的fd(文件描述符),程序运行在linux系统上,这种系统上,一切皆文件,socket也不例外,这里传递的fd就是文件系统中对应socket生成的文件描述符,操作系统,这个select函数被调用后,它首先会按照fd集合,去检查内存中每个socket套接字的状态,这个过程是O(n)的,然后检查一遍后,如果有就绪的socket,那就直接返回,不会堵塞当前调用线程,否则那就说明当前fd集合对应的socket没有就绪状态的,那就需要堵塞当前调用线程,直到某个socket有数据,才会唤醒线程

select函数监听socket,这个socket有没有数量限制

它默认最大可以监听默认1024个实际小于1024

他为啥是1024呢

这是因为fd_set这个数据结构是bitmap位图结构,他就是一个长的二进制数,这个bitmap默认是1024的长度,修改这个长度比较麻烦

为啥大小是1024,出于性能考虑,select函数检查到就绪的socket后它做了两件事

第一件事跑到就绪状态的socket对应的fd文件中设置一个标记,标记一个mask,他表示当前fd对应的socket就绪了

第二件事 返回select函数,唤醒java线程,站到Java层面,它会收到一个int值,表示有几个socket处于就绪状态,具体哪几个就绪,Java层面是不知道的,接下来又是一个O(n)系统调用,检查fd_set集合中每个socket就绪状态,其实就是检查文件系统中指定socket文件描述符的状态

还要涉及参数数据的拷贝,数据太庞大,也不利于系统处理速度

操作系统调度和操作系统中断

操作系统调度 cpu同一时刻只能调度一个进程,操作系统最主要的任务就是系统调度,就是n个进程,在cpu上切换执行,未挂起的cpu都在工作队列,都有机会获得cpu的执行权

挂起的进程,从工作队列中移除,反映到Java层面就是线程堵塞,linux线程其实就是轻量级进程

操作系统中断(非常重要)

比如咱们用键盘打字,如果cpu正执行其他程序,一直不释放,字也打不来了,其实不是这样的,就是因为系统中断的存在,当我们按下一个键会给这个主板发送一个电流信号,主板感知后,就会触发cpu中断,cpu中断其实就是让正在执行任务的进程先保留程序上下文,然后避让出cpu,给中断程序让道,给中断程序拿到cpu权限,进行相应的代码执行。

select函数,它第一遍轮询,没有发现就绪状态的socket,他就会把当前进程保留给需要检查的socket的等待队列中,socket有三块核心区域,读缓存,写缓存,等待队列

poll函数

传参不一样

epoll函数

技术产生背景

为了解决select和poll的缺陷

(1)这两个函数的每次进行系统调用都需要我们提供它所要监听的socket文件描述符集合,程序主线程死循环调用select/poll函数,涉及用户空间数据到内核空间数据拷贝,相对来说比较耗费性能,需要监听socket集合,数据变化非常小

(2)select和poll返回值int,他没有告诉是哪几个socket就绪,系统唤醒后,会进行新的一轮系统调用,去检查哪个socket是就绪的

epoll

解决这两个问题,就需要epoll函数在内核空间内,有对应的的数据结构去存储一些数据,这个数据结构就是eventpoll对象

evenpoll对象可以通过系统函数epoll_create()去创建,创建完成后返回一个eventpoll对象的id,相当于在内核内开辟了一块空间,并且我们这知道这块空间的位置

eventpoll的结构,两块重要的区域,其中一块是存放需要监视的socket_fd描述符列表,另一块就是就绪列表,存放就绪状态的socket信息,另外还提供了两个系统函数,一个是epoll_ctl函数,另一个是epoll_wait函数

epoll_ctl可以根据eventpoll_id去增删改内核空间上的evenpoll对象的的检查列表(即关注的socket信息),去增加或者修改需要检查的文件描述符,epoll_wait主要参数是eventpoll_id,表示此次系统调用需要监听的socket_fd集合,是eventpoll中那些已经指定好的socket信息,epoll_wait他默认情况下会堵塞调用线程,知道eventpoll中关联的某个或者某些个socket就绪后,epoll_wait才会返回

就绪列表怎么维护的?

epoll_wait()函数返回值是int类型,获取就绪的socket是怎么实现的

它调用的时候会传入一个epoll_event事件数组指针,函数正常返回之前,会把就绪的socket事件信息拷贝到这个数组指针里,返回上层程序,可以通过这个数组拿到就绪列表

epoll_wait()可不可以设置成非堵塞

他有个参数可以表示堵塞的时间长度,这个参数设置为0

eventpoll需要存放需要监视的socket集合信息,数据结构?

红黑树,需要经常增删改查,相对稳定的查找效率,复杂度O(log(N))