BIO、NIO与AIO

BIO、NIO与AIO

    • 相关概念
      • 一、同步阻塞I/O(BIO)
      • 二、同步非阻塞I/O(NIO)
      • 三、异步非阻塞I/O(AIO)
    • NIO中的多路复用器
      • 详解epoll
      • epoll是如何工作的?
      • epoll的工作模式
    • 补充知识
      • 关于中断
      • 中断过程
      • 中断优先权
      • 向量中断

相关概念

一、同步阻塞I/O(BIO)

同步阻塞I/O,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制来改善。BIO方式适用于连接数目比较小且固定的架构,这种方式对服务端资源要求比较高,并发局限于应用中,在jdk1.4以前是唯一的io现在,但程序直观简单易理解

二、同步非阻塞I/O(NIO)

同步非阻塞I/O,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求时才启动一个线程进行处理。NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,jdk1,4开始支持

三、异步非阻塞I/O(AIO)

异步非阻塞I/O,服务器实现模式为一个有效请求一个线程,客户端的IO请求都是由操作系统先完成了再通知服务器用其启动线程进行处理。AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,jdk1.7开始支持。

不同层次表现

CPU层次: 现代操作系统通常使用异步非阻塞方式进行IO(有少部分IO可能会使用同步非阻塞),即发出IO请求后,并不等待IO操作完成,而是继续执行接下来的指令(非阻塞),IO操作和CPU指令互不干扰(异步),最后通过中断的方式通知IO操作的完成结果。

线程层次: 操作系统为了减轻程序员的思考负担,将底层的异步非阻塞的IO方式进行封装,把相关系统调用(如read和write)以同步的方式展现出来,然而同步阻塞IO会使线程挂起,同步非阻塞IO会消耗CPU资源在轮询上,3个解决方法:

  • 多线程(同步阻塞)
  • IO多路复用(select、poll、epoll)
  • 直接暴露出异步的IO接口,kernel-aio和IOCP(异步非阻塞)

NIO中的多路复用器

不同操作系统的内核都有自己的多路复用设计,windows内核使用的是select,Linux内核中有select、poll和epoll。

select和poll每次向内核询问是否有等待响应的请求时,需要携带大量的文件描述符,然后将文件描述符集合交给内核去遍历检查,对CPU还是存在一定的消耗,会随着监听fd数目的增长而降低效率。

select最多同时监听1024个fd,poll在这一点上没有限制。

而epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。

原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。

epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

详解epoll

epoll的接口一共有三个函数:

  1. int epoll_create(int size);

    创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

    注意:size参数只是告诉内核这个 epoll对象会处理的事件大致数目,而不是能够处理的事件的最大个数。在Linux最新的一些内核版本的实现中,这个 size参数没有任何意义。

  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

    epoll的事件注册函数,epoll_ctl向epoll对象中添加、修改或者删除感兴趣的事件,返回0表示成功,否则返回–1,此时需要根据errno错误码判断错误类型。

    它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

    epoll_wait方法返回的事件必然是通过 epoll_ctl添加到 epoll中的。

    第1个参数是epoll_create()的返回值。

    第2个参数表示动作,用三个宏来表示:

    EPOLL_CTL_ADD:注册新的fd到epfd中;
    EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
    EPOLL_CTL_DEL:从epfd中删除一个fd; 
    

    第3个参数是需要监听的fd。

    第4个参数是告诉内核需要监听什么事,struct epoll_event结构如下:

    1 typedef union epoll_data {  
    2     void *ptr;  
    3     int fd; 
    4     __uint32_t u32;  
    5     __uint64_t u64;  
    6 } epoll_data_t;  
    7  
    8 struct epoll_event {  
    9     __uint32_t events; /* Epoll events */
    10     epoll_data_t data; /* User data variable */ 
    11 };
    

    events可以是以下几个宏的集合:

    EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
    EPOLLOUT:表示对应的文件描述符可以写; 
    EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
    EPOLLERR:表示对应的文件描述符发生错误; 
    EPOLLHUP:表示对应的文件描述符被挂断; 
    EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
    EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
    
  3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

    *Linux-2.6.19又引入了可以屏蔽指定信号的epoll_wait: epoll_pwait。
    

    等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。如果返回–1,则表示出现错误,需要检查errno错误码判断错误类型。

    第1个参数 epfd是 epoll的描述符。

    第2个参数 events则是分配好的 epoll_event结构体数组,epoll将会把发生的事件复制到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存。内核这种做法效率很高)。

    第3个参数 maxevents表示本次可以返回的最大事件数目,通常 maxevents参数与预分配的events数组的大小是相等的。

    第4个参数 timeout表示在没有检测到事件发生时最多等待的时间(单位为毫秒),如果 timeout为0,则表示epoll_wait在 rdllist链表中为空,立刻返回,不会等待。

epoll是如何工作的?

当创建一个网络NIO请求时,

首先,调用epoll_create创建一个epoll文件描述符,并指定监听的数目大小;

然后,调用epoll_ctl向epoll文件描述符中添加/修改/删除需要侦听的文件描述符及其事件,这里包括端口连接请求和数据读请求;

当网卡监听到连接请求时,会向内核发起中断通知,内核收到通知后,会将监听到连接文件描述符异步唤醒并加入到Ready队列;

当调用epoll_wait就可以从Ready队列中拿到等待连接的文件描述符;

然后,再调用epoll_ctl向epoll文件描述符中添加数据读请求监听;

当网卡监听到指定端口有数据进来,再次向内核发起中断通知,内核收到通知后,会数据放置到套接字缓冲区,然后将监听文件描述符异步唤醒并加入到Ready队列;

当调用epoll_wait就可以从Ready队列中拿到等待读取数据的文件描述符;

读取完套接字缓冲区中的数据后,会继续保持监听,直到下次调用epoll_wait时又有数据进来,再次读入数据。

任何被侦听的文件符只要其被关闭,会自动从epoll被侦听的文件描述符集合中删除。

epoll的工作模式

epoll有LT、ET两种工作模式:

LT(水平触发)模式ET(边缘触发)模式

默认情况下,epoll采用 LT模式工作,这时可以处理阻塞和非阻塞套接字,而ET表示可以将一个事件改为 ET模式。ET模式的效率要比 LT模式高,但只支持非阻塞套接字。

ET模式与LT模式的区别在于:

当一个新的事件到来时,ET模式下当然可以从 epoll_wait调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字没有新的事件再次到来时,在 ET模式下是无法再次从 epoll_wait调用中获取这个事件的;

而 LT模式则相反,只要一个事件对应的套接字缓冲区还有数据,就总能从 epoll_wait中获取这个事件。

因此,在 LT模式下开发基于 epoll的应用要简单一些,不太容易出错,而在 ET模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲区中的用户请求得不到响应。

默认情况下,Nginx是通过 ET模式使用 epoll的

补充知识

关于中断

指处理机处理程序运行中出现的紧急事件的整个过程.程序运行过程中,系统外部、系统内部或者现行程序本身若出现紧急事件,处理机立即中止现行程序的运行,自动转入相应的处理程序(中断服务程序),待处理完后,再返回原来的程序运行,这整个过程称为程序中断;当处理机接受中断时,只需暂停一个或几个周期而不执行处理程序的中断,称为简单中断.中断又可分为屏蔽中断和非屏蔽中断两类.可由程序控制其屏蔽的中断称为屏蔽中断或可屏蔽中断.屏蔽时,处理机将不接受中断.反之,不能由程序控制其屏蔽,处理机一定要立即处理的中断称为非屏蔽中断或不可屏蔽中断.非屏蔽中断主要用于断电、电源故障等必须立即处理的情况.处理机响应中断时,不需执行查询程序.由被响应中断源向CPU发向量地址的中断称为向量中断,反之为非向量中断.向量中断可以提高中断响应速度。

中断过程

按照事件发生的顺序,中断过程包括 :

  1. 中断源发出中断请求;
  2. 判断当前处理机是否允许中断和该中断源是否被屏蔽;
  3. 按照中断优先权排队;
  4. 处理机执行完当前指令,如当前指令无法立即执行完,则暂停当前程序,保护断点地址和处理机当前状态,转入相应的中断服务程序;
  5. 执行中断服务程序;
  6. 恢复被保护的状态,执行“中断返回”指令回到被中断的程序或转入其他程序。

上述过程中前四项操作是由硬件完成的,后两项是由软件完成的。

中断优先权

在某一时刻有几个中断源同时发出中断请求时,处理器只响应其中优先权最高的中断源。当处理机正在运行某个中断服务程序期间出现另一个中断源的请求时,如果后者的优先权低于前者,处理机不予理睬,反之,处理机立即响应后者,进入所谓的“嵌套中断”。中断优先权的排序按其性质、重要性以及处理的方便性决定,由硬件的优先权仲裁逻辑或软件的顺序询问程序来实现。

向量中断

对应每个中断源设置一个向量。这些向量顺序存在主存储器的特定存储区。向量的内容是相应中断服务程序的起始地址和处理机状态字。在响应中断时,由中断系统硬件提供向量地址,处理机根据该地址取得向量,并转入相应的中断服务程序。

你可能感兴趣的:(Java)