高性能网络服务器编程

下面来解释下为什么Linux AIO 和Java AIO都不使用!

基本的IO编程过程(包括网络IO和文件IO):

  1. 打开文件描述符(windows是handler,java是stream或channel)
  2. 多路捕获(Multiplexe,即select和poll和epoll)IO可读写的状态
  3. 可以读写的文件描述符进行IO读写

由于IO设备速度和CPU内存比速度会慢,为了更好的利用CPU和内存,会开多线程,每个线程读写一个文件描述符。

高性能的网络编程(即IO编程)

  1. 需要松绑IO连接和应用程序线程的对应关系,这就是非阻塞(nonblocking)、异步(asynchronous)的要求的由来(构造一个线程池,epoll监控到有数的fd,把fd传入线程池,由这些worker thread来读写io)
  2. 需要高性能的OS对IO设备可读写(数据来了)的通知方式:从level-triggered notification到edge-triggered notification

select poll epoll

select poll epoll三个都是是Linux下 I/O 多路复用的具体实现,select 出现的最早,之后是 poll,再是 epoll。

不理解的朋友可以参考面试中的Linux中的介绍。

  • select和poll的效率都很低,它们的问题在于这两个函数都需要将所有的文件描述符链表内容全部从用户进程内存中复制到操作系统内核中,内核需要将所有文件描述符遍历一遍,这个过程非常低效。
  • epoll很巧妙,它将用户关心的描述符放到内核的一个事件表中,从而只需要在用户空间和内核空间拷贝一次。它事先通过 epoll_ctl() 来注册描述符,一旦基于某个描述符就绪时,内核会采用类似 callback 的回调机制,迅速激活这个描述符,当进程调用 epoll_wait() 时便得到通知。
epoll实现:三个核心点是:1、mmap,2、红黑树,3、rdlist(就绪描述符链表是一个双向链表)
  1. mmap是共享内存,用户进程和内核有一段地址(虚拟存储器地址)映射到了同一块物理地址上,这样当内核要对描述符上的事件进行检查的时候就不用来回的拷贝了。mmap映射内存必须是页面大小的整数倍,面向流的设备不能进行mmap,mmap的实现和硬件有关。
  2. 红黑树是用来存储这些描述符的,因为红黑树的特性,就是良好的插入,查找,删除性能O(lgN)。
  3. rdlist 就绪描述符链表这是一个双链表,epoll_wait()函数返回的也是这个就绪链表。

内部用了一个红黑树记录添加的socket,用了一个双向链表接收内核触发的事件。

epoll极其高效的原因:
  1. 在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。所以,epoll_wait非常高效。
  2. 这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把socket放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。所以,当一个socket上有数据到了,内核在把网卡上的数据copy到内核中后就来把socket插入到准备就绪链表里了。(注:好好理解这句话!)
    从上面这句可以看出,epoll的基础就是回调呀!
  3. 执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。

内核通知模式

  1. level-triggered notification:当 epoll_wait() 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait() 时,会再次响应应用程序并通知此事件。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
  2. edge-triggered notification:当 epoll_wait() 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait() 时,不会再次响应应用程序并通知此事件。很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

select 和 poll 都是只支持level-triggered notification,而epoll支持edge-triggered notification。
注意还要打开epoll的edge-triggered notification。而java的NIO和NIO.2都只是用了epoll,没有打开edge-triggered notification,所以不如JBoss的Netty。

AIO 的问题

select,poll,epoll都需要用一个函数去监控一大堆fd,AIO不需要了,你把fd告诉内核,应用程序无需等待,内核会通过信号等软中断告诉应用程序,数据来了,你直接读了,所以,用了AIO可以废弃select,poll,epoll。

但linux的AIO的实现方式是内核和应用共享一片内存区域,应用通过检测这个内存区域(避免调用nonblocking的read、write函数来测试是否来数据,因为即便调用nonblocking的read和write由于进程要切换用户态和内核态,仍旧效率不高)来得知fd是否有数据,可是检测内存区域毕竟不是实时的,你需要在线程里构造一个监控内存的循环,设置sleep,总的效率不如epoll这样的实时通知。

所以,AIO是渣,适合低并发的IO操作。所以java7引入的NIO.2引入的AIO对高并发的网络IO设计程序来说,也是渣!
只有Netty的epoll+edge-triggered notification最牛,能在linux让应用和OS取得最高效率的沟通。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值