参考文章:
简书-浅谈Linux五种IO:http://www.jianshu.com/p/486b0965c296
一、linux基础概念
1.1 内存空间
linux系统中的使用的是虚拟存储器,即操作系统(kernel)可以使用的内存空间不是物理空间,而是线性空间,内存是以页的方式进行管理。kernel是一个特殊的程序,如果从内核的角度去看操作系统,一些比如系统调用这样比较核心的功能只有kernel才有权限去使用,用户程序想要调用比如先通过kernel(内核),比如说用户程序想要和硬件进行交互必须先通过内核。这是为了保护内核的安全,而内存空间则被划分为内核空间和用户空间,其中内核空间位于环0,而用户空间位于环3(环1,环2都只是用来过渡的)。比如一个32位的linux系统,虚拟空间空较高的1G空间会分配给内核空间,而剩下的3G空间会分配给用户空间。
用户空间中你的应用程序如果发起系统调用,必然要经过内核,此时行为由用户模式切换为内核模式,这种切换称为模式切换,大量的模式切换必然给系统带来压力。
1.2 进程概念
什么是进程?什么是线程?目标程序被存放在某种存储介质上,这是一种静态概念,进程则是处于执行期的程序,拥有生命周期,是一种动态概念。但是进程不仅仅只是局限于一条可执行代码,还有打开的文件,挂起的信号,内核内部的数据,处理器状态,一个或者多个具有内存映射的内存地址空间以及一个或者多个执行线程(thread of execution)。一个进程对另一个进程是不可见的,进程通常认为系统中只有内核程序只有自己,进程当然也可以通信,他们通过信号来通信,这种通信被称为IPC。而上文中的执行线程,简称为线程,是一个在进程中活动的对象。每个线程都拥有一个独立的程序计数器,一个栈和进程寄存器。内核的调度对象是线程,而不是进程。但是请注意:linux上的进程和线程实现非常特别:它对线程和进程并不特别的区分,对Linux系统而言,线程只是一种特殊的进程罢了,linux在创建线程的时候和普通进程的时候非常类似,只不过在调用clone()的时候需要传递一些参数来标志并指明需要共享的资源。比如说:httpd服务的mpm支持prefork多进程模型,和worker多线程模型,在linux系统上,性能并没有多大差别,基本可以忽略不计,因为linux系统上进程的设计本身也很轻量。
什么是多任务?多任务操作系统可以划分为两类:一类是:非抢占式多任务(cooperative multitasking)和抢占式多任务(preemptive multitasking)。而在linux中提供的是抢占式多任务,即由调度系统来决定什么时候来停止一个进程的运行,以便其他进程能够得到执行机会。这个强制的挂起举动就是抢占。内核对空间的管理使用虚拟空间,对时间的管理则是使用时间片,timeslice,进程在集昵称在被抢占之前能够运行的时间是预先设置好的。
1.3 进程状态
进程描述符中的state域描述了进程的当前状态。系统中的每个进程都必然处于以下五种的一种:
TASK_RUNNING: 进程是可以执行的,或者正在运行,或者在运行队列中等待运行。
TASK_INTERRUPTIBALE: 进程正在睡眠,也就是说被阻塞,等待某些条件达成。
TASK_UNINTERRUPTIBLE: 不可中断状态,除了就算是接收到信号也不会被唤醒或者准备运行,是不可中断的。
TASK_TARCED: 被追踪
TASK_STOPPED:停止运行。
什么是进程的阻塞?由于某些事件的完成,比如IO等,运行中的进程自动执行阻塞原语,使自己处于阻塞状态。当进程阻塞时,是不占用系统CPU资源的。
1.4 IO
什么是IO?linux上的io一般分为两种,一种是磁盘IO,一种是网络IO,内核中文件系统中读取数据必须等待,比如说机械硬盘的寻址,从网络上读取数据也必须等待通信子网中的传输等等,通常在kernel的system call中,这种IO分为两部分,由于内存的读写速度和网络上比如网卡等数据读写速度不一致,磁盘与内存的读写速度也不一致,因此我们都需要一个缓冲,网络上有发送缓冲,有接收缓冲,不管是网络上还是磁盘上的IO,第一部分都是从磁盘或者scoket中把数据写入到内核空间作为缓冲,第二部分才是从内核空间复制到用户空间*应用程序使用。而往往比较耗时的在第一阶段,内存的中的读写速度比较快。只有真正解放了这两个阶段的才被称为是真正的异步IO。
1.5 同步和异步,阻塞和非阻塞
同步和异步关注点在消息的通知机制上,一个调用,调用方与被调用方,被调用方如何把结果回馈给调用方,同步是指调用方等待被调用方返回结果,然后整个调用才算结束,异步是什么,调用发出之后,调用直接结束,被调用方会使用通知的形式来主动返回结果,比如javascript中的ajax(),比如java中的Future等等
阻塞和非阻塞关闭是程序在等待调用时的状态,阻塞时指调用结果返回之前,线程会被挂起,非阻塞则是指不会挂起,因此同步可以是阻塞,也可以是非阻塞,虽然在等待结果返回,但是并没有被挂起,比如在忙等待,即调用方不断的去轮询是否有结果返回。
二、UNIX中IO模型
Linux上处处都是文件。对于一个文件的读写操作都会调用内核提供的系统命令,返回一个file descriptor id(fd,文件描述符)。而对一个socket的读写也会有相应的描述符,称为socketfd(scoket 描述符),什么是描述符? 描述符就是一个数字,它指向内核中的一个结构体(文件路径、数据区等等一些属性)。
2.1 同步阻塞 (blocking io)
同步阻塞机制,在这个模型中,应用程序会为read操作,调用一个system call,将系统控制权交给kernel,然后进行同步阻塞等待,等到kernel执行完这个系统调用,执行完毕后给应用程序响应,应用程序得到响应后,就不再阻塞,并进行后面的工作。这是最传统的模型,由于同步且阻塞,一个进程无法处理多个请求,必须串行化执行。
最原始的I/O模型,缺省状态下,所有文件操作都是阻塞的。我们以套接字为例子来说明此模型:在进程空间(即上述的用户空间)中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才按返回,在此期间会一直等待,进程在调用recvfrom开始到它返回的整段时间内都是被阻塞的,因此被称为阻塞I/O模型,如图1-1所示.
总结大致步骤如下:
- 用户空间调用recvfrom,这是一个系统调用
- 阻塞的方式等待内核空间准备好数据(无数据->有数据的过程)
- 内核准备好之后
- 阻塞的方式从内核空间复制到用户空间
- 返回成功
上述调用时,用户的应用程序(即你的程序)是阻塞的,回忆什么是阻塞?阻塞就是你的当前线程会挂起等到整个调用结束返回成功,期间什么都不能做。
2.2 同步非阻塞(noblocking io)
在linux下,应用程序可以通过设置文件描述符的属性EWOULDBLOCK,IO操作可以立即返回,比如说一个write操作立刻返回,但是在这个返回的时间点,数据并没有真正的写入到指定的地方。此时依旧是同步的,因为应用程序会主动的不断循环去问kernel,进入一种"忙等待"状态。由此可以看出,虽然是非阻塞的,但是性能也不高,因为应用程序的忙等,下面来仔细描述一下。
同样是系统调用recvfrom,从应用层到内核的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,一般都对非阻塞I/O模型进行轮询检查这个状态,内核是不是有数据到来,如图1-2所示。
总结大致步骤如下:
- 你的应用程序看上去是不阻塞的,它会轮询(可以理解为while死循环)的方式去调用recvfrom这个系统调用,直到返回成功。
- 假设第一次内核无数据报准备好,返回EWOULDBLOCK,继续轮询;
- 假设第二次内核无数据报准备好,返回EWOULDBLOCK,继续轮询;
- 假设第三次内核数据报准备好,返回EWOULDBLOCK,继续下一步;
- 类似上面过程内核空间复制到用户空间,返回成功(这个复制过程依旧阻塞)。
和上一种模型进行比较:
此时性能不一定会比上面的阻塞要好,这要看情况,因为只是从阻塞挂起变为忙等。
- 如果你的数据量比较大或者说处理过程比较久,那么你的应用程序一直在while循环里不断的询问,毫无疑问对CPU的压力更大,选择阻塞的方式去等待更好;
- 但是如果你的数据量很小或者说请求处理的速度非常的迅速,由于可以避免线程的切换调度,线程是有代价的(比如额外的内存资源,以及线程管理的CPU时间切片等等...),此时忙等会更好。
因此,只能上述二者相对适应不同的场景。这里可以再回顾下阻塞的概念就非常明了,因为此时确实是非阻塞的,当前线程并没有挂起,但是是同步的,因为依旧是调用者(用户空间的应用程序)去主动等待被调用者(系统调用)的响应,因此是同步非阻塞模型,而且非阻塞还只是第一阶段,还是忙等...
2.3 多路复用(IO multiplexing)
linux提供了select/poll, 进程通过将一个或者多个fd传递给select或者poll系统调用,阻塞在select操作上,这样select/poll就可以帮我们探测多个fd是否处于就绪状态。select/poll是顺序扫描fd是否就绪,而且支持的fd数量有限,因此它的使用受到一些制约(比如select的限制就是1024)。Linux还提供了一个epoll系统调用,epoll使用基于事件驱动方式代替顺序扫描,因此性能更高。当fd就绪时,就立即回调函数rollback.
比如要执行read操作,一个system call会被传给kernel,此时应用程序之后,并不等待结果而是立即返回,虽然是立即返回,但是返回函数是一个阻塞的方式,比如select(),poll()函数,阻塞住,在这种模型中,IO函数是非阻塞的,但是从实际效果上,异步阻塞IO和第一种同步阻塞IO还是一样的,应用程序都要一直等待IO操作成功,即数据被成功写入或者读取,才能继续。不同点在于用一个select函数可以为多个描述符提供通知,提高了并发性。
举个例子,假如有1W个并发,一万个read并发,但是网上没有数据,这一万个read会通知阻塞,此时会有由select这样的函数来同时监听这个一万个请求的状态,一旦有数据就通知负责通知,这种异步阻塞IO和同步非阻塞IO的区别来看:同步非阻塞是应用程序主动忙等,而异步阻塞IO是通过select和poll这样的多路复用函数同时检测多个事件句柄来告知是否可以有数据操作。
select, poll, epoll简介:
select:由bsd研发, select中的大小限制通过一个位数组fdset决定的,是1024,比如说httpd中prefork模型是使用select()函数,提供的网络服务最大也就是1024的并发,select()每次调用前都需要重新初始化fdset,拥有以下特点:
(1) 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd(文件描述符)很多的时候会很大
(2) 同时每次调用select都需要在内核中遍历传递进来的所有fd,这个开销在fd很多时也很大
(3) select支持的文件描述符数量太小,是1024,当然1024这个数字也是经过考虑的,并不意味着这个数字增加就能提高并发,只能说是一个比较合适的数字,从这点看,下面的poll()函数虽然没有1024的限制,但是性能也不会有多大变化
poll: 并不比select更高明,是linux正统分支参照select的思想研发,但是没有1024的限制
epoll:直到linux2.6版本来出现了直接由内核支持的方式,就是epoll,这被认为是linux2.6下性能最好的多路IO就绪通知方法,epoll可以同时支持水平出发和边缘出发,可以有效避免上面的3个缺点,但是其实现代码非常复杂,这里不详述。
演示一个简易server的demo:
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<errno.h>
#include<netdb.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<netdb.h>
#include<sys/time.h>
#include<string.h>
#include<sys/select.h>
#include<pthread.h>
using namespace std; int max_fd(int a[], int n)
{
int max = ;
for(int i = ; i < n; i++)
{
if(max < a[i])
{
max = a[i];
}
} return max;
} int main(int argc, char*argv[])
{
int port = ;
int N = ;
if (argc != )
{
cout<<"command error"<<endl;
exit(-);
} port = atoi(argv[]);
N = atoi(argv[]);
if(N > FD_SETSIZE)
{
N = FD_SETSIZE;
}
int server_sock = ;
struct sockaddr_in server_addr;
memset(&server_addr, , sizeof(server_addr)); if((server_sock = socket(AF_INET, SOCK_STREAM, )) == -)
{
cout<<"create socket error"<<endl;
exit(-);
} server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr =htonl(INADDR_ANY);
server_addr.sin_port = htons(port); if(bind(server_sock, (struct sockaddr*)&server_addr,sizeof(sockaddr))
== -)
{
cout<<"bind error"<<endl;
exit(-);
} if(listen(server_sock, ) == -)
{
cout<<"listent error"<<endl;
exit(-);
} fd_set fd[];
FD_ZERO(&fd[]);
FD_SET(server_sock, &fd[]);
int *sock = new int[N];
memset(sock, , sizeof(int)*N);
sock[] = server_sock;
int count = ; while()
{
struct timeval tv = {, };
FD_ZERO(&fd[]);
fd[] = fd[];
int ret = select(max_fd(sock, N)+, &fd[], NULL, NULL, &tv);
if(ret < )
{
cout<<"select error"<<endl;
}
else if(ret == )
{
cout<<"time out"<<endl;
}
else
{
if(FD_ISSET(sock[], &fd[]) && count < N-)
{
struct sockaddr_in client_addr;
memset(&client_addr, , sizeof(client_addr));
unsigned int len = sizeof(client_addr);
int new_sock=accept(sock[],(struct sockaddr*)&client_addr, &len);
if(new_sock == -)
{
cout<<"accept error"<<endl;
}
else
{
for(int i = ; i < N; i++)
{
if(sock[i] == )
{
sock[i] = new_sock;
FD_SET(new_sock, &fd[]);
count++;
break;
}
}
} } char recvbuf[] = {};
char sendbuf[] = {};
for(int i = ; i < N; i++)
{
if(FD_ISSET(sock[i], &fd[]))
{
if(recv(sock[i], recvbuf, sizeof(recvbuf), ) <= )
{
cout<<"recv error"<<endl;
FD_CLR(sock[i], &fd[]);
close(sock[i]);
sock[i] = ;
count--;
continue;
} strcpy(sendbuf, recvbuf); if(send(sock[i], sendbuf, sizeof(sendbuf), ) <= )
{
cout<<"send error"<<endl;
FD_CLR(sock[i], &fd[]);
close(sock[i]);
sock[i] = ;
count--;
continue;
}
} }//end for } }//end while
return ; }
其中核心的代码大致上是在一个while循环中执行以下逻辑:
int res = select(maxfd+, &readfds, NULL, NULL, );
if (res > )
{
for (int i = ; i < MAX_CONNECTION; i++)
{
if (FD_ISSET(allConnection[i], &readfds))
{
handleEvent(allConnection[i]);
}
}
}
// if(res == 0) handle timeout, res < 0 handle error
Java1.4中Nio的用法非常的类似.... 下面贴上Java实现的代码...
package nonblock;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.net.*;
import java.util.*; public class EchoServer{
private Selector selector = null;
private ServerSocketChannel serverSocketChannel = null;
private int port = 8000;
private Charset charset=Charset.forName("GBK"); public EchoServer()throws IOException{
selector = Selector.open();
serverSocketChannel= ServerSocketChannel.open();
serverSocketChannel.socket().setReuseAddress(true);
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(port));
System.out.println("服务器启动");
} public void service() throws IOException{
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT );
while (selector.select() > 0 ){
Set readyKeys = selector.selectedKeys();
Iterator it = readyKeys.iterator();
while (it.hasNext()){
SelectionKey key=null;
try{
key = (SelectionKey) it.next();
it.remove(); if (key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = (SocketChannel) ssc.accept();
System.out.println("接收到客户连接,来自:" +
socketChannel.socket().getInetAddress() +
":" + socketChannel.socket().getPort());
socketChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.register(selector,
SelectionKey.OP_READ |
SelectionKey.OP_WRITE, buffer);
}
if (key.isReadable()) {
receive(key);
}
if (key.isWritable()) {
send(key);
}
}catch(IOException e){
e.printStackTrace();
try{
if(key!=null){
key.cancel();
key.channel().close();
}
}catch(Exception ex){e.printStackTrace();}
}
}//#while
}//#while
} public void send(SelectionKey key)throws IOException{
ByteBuffer buffer=(ByteBuffer)key.attachment();
SocketChannel socketChannel=(SocketChannel)key.channel();
buffer.flip(); //把极限设为位置,把位置设为0
String data=decode(buffer);
if(data.indexOf("\r\n")==-1)return;
String outputData=data.substring(0,data.indexOf("\n")+1);
System.out.print(outputData);
ByteBuffer outputBuffer=encode("echo:"+outputData);
while(outputBuffer.hasRemaining())
socketChannel.write(outputBuffer); ByteBuffer temp=encode(outputData);
buffer.position(temp.limit());
buffer.compact(); if(outputData.equals("bye\r\n")){
key.cancel();
socketChannel.close();
System.out.println("关闭与客户的连接");
}
} public void receive(SelectionKey key)throws IOException{
ByteBuffer buffer=(ByteBuffer)key.attachment(); SocketChannel socketChannel=(SocketChannel)key.channel();
ByteBuffer readBuff= ByteBuffer.allocate(32);
socketChannel.read(readBuff);
readBuff.flip(); buffer.limit(buffer.capacity());
buffer.put(readBuff);
} public String decode(ByteBuffer buffer){ //解码
CharBuffer charBuffer= charset.decode(buffer);
return charBuffer.toString();
}
public ByteBuffer encode(String str){ //编码
return charset.encode(str);
} public static void main(String args[])throws Exception{
EchoServer server = new EchoServer();
server.service();
}
} /****************************************************
* 作者:孙卫琴 *
* 来源:<<Java网络编程精解>> *
* 技术支持网址:www.javathinker.org *
***************************************************/
其核心代码也是大致如下的逻辑:
while(selecor.select()>0){
Iterator it = ..itetartor();
while(it.hasNext()){
SelectionKey key = it.next();
handleEvent(key);
}
}
而查看jdk文档中关于selecor.select()的描述:
This method performs a blocking selection operation. It returns only after at least one channel is selected, this selector's wakeup method is invoked, or the current thread is interrupted, whichever comes first.
虽然看上去是非阻塞的,但是实际上会阻塞在selector.select()方法上。
jdk1.4中的nio使用的就是多路复用的方式。
2.4 信号/事件驱动(signal driven IO)
首先开启套接字接口的信号驱动I/O功能,并且通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是阻塞的)。当数据准备就绪时,就为该线程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并且通知主循环函数处理数据,如图所示:
同样,应用程序提交read请求的system call,然后,kernel开始处理相应的IO操作,而同时,应用程序并不等待Kernel返回响应,就会开始执行其他的处理请求(应用程序并没有被IO操作阻塞)。当kernel执行完毕,返回read的响应,就会产生一个信号或执行一个线程的回调函数来完成这次IO处理过程。此时第一阶段已经被完全解放,但是注意到,上面的所有模型中,第二阶段都是阻塞的。而信号/事件驱动的IO实现模型根据操作系统的不同有epoll, kqueue, /dev/poll这几种。
epoll和select一样,属于IO复用模型。 这里还要说的是,虽然epoll本身是IO复用模型,但是它的一些内部实现其实是用到了信号驱动IO、异步IO的特点,因为epoll是依靠中断来判断IO是否可用的,只不过封装到用户这里后就变成复用IO模型了。如果你想实现信号驱动IO或者复用IO,也只需要对epoll再进行简单封装。比如你可以另开一个线程去调用epoll主循环并且处理接收到的数据包,这就相当于信号驱动IO。
Epoll 不仅会告诉应用程序有I/0 事件到来,还会告诉应用程序相关的信息,这些信息是应用程序填充的,因此根据这些信息应用程序就能直接定位到事件,而不必遍历整个FD 集合。
c++大致代码:
int res = epoll_wait(epfd, events, , );
for (int i = ; i < res;i++)
{
handleEvent(events[n]);
}
JDK 6.0 以及JDK 5.0 update 9 的 nio支持epoll (仅限 Linux 系统 ),对并发idle connection会有大幅度的性能提升,这就是很多网络服务器应用程序需要的。
启用的方法如下:
-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider
例如在 Linux 下运行的 Tomcat 使用 NIO Connector ,那么启用 epoll 对性能的提升会有帮助。
而 Tomcat 要启用这个选项的做法是在 catalina.sh 的开头加入下面这一行
CATALINA_OPTS='-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider'
2.5 异步IO (asyncrhonous IO)
异步IO的概念就是异步,真正的异步,上面所有模型中第二阶段都是阻塞的,这个模型中,当一个异步过程调用发出后,调用者不能立即等到结果,实际处理这个调用的函数在完成后,通过状态、通知和回调来通知调用这的输入输出操作。换句话说:信号驱动通知的是第一阶段的就绪事件,而真正的异步IO是通知第一阶段,第二阶段都结束的IO完成事件。为了实现真正的异步IO,专门定义了一套以aio开头的API,例如:aio_read.此时第一阶段,第二阶段中,应用程序完全被解放,即使只有一个进程也可以处理多个用户请求。但是这种技术目前在网络IO并不十分的成熟,多用于磁盘IO上。
信号驱动告诉我们何时IO就绪,而异步IO告诉我们何时IO完成。
总结:这里以httpd服务器的三种mpm为例,prefork,woker,event,
其中prefork是多进程模型,一个master进程,多个worker进程,本质上是多路复用即异步阻塞模型,多个worker进程会阻塞在select()函数中;
worker模型是多进程多线程模型,每线程处理一个用户请求,但是linux上线程和进程没有多大区别,因此在linux系统上表现和prefork模型差不多;
event才是真正的多进程模型,每个进程可以处理多个用户请求,本质上是signal drivenIO,信号通知机制,实现了第一阶段的异步非阻塞化,linux系统中用的是epoll()
三、I/O多路复用技术总结
在I/O编程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。
I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。
与传统的多线程/多进程模型相比,I/O多路复用的最大优势是系统开销小(虽然在linux系统上进程(线程)都是轻量级的设计,但是这依旧是并发瓶颈),节省了系统资源。
I/O多路复用的场景:
服务器需要同时处理多个处理监听状态或者多个连接状态的套接字;
服务器需要同时处理多种网络协议的套接字。
目前支持I/O多路复用的系统调用有select、pselect、poll、epoll。
在Linux网络编程中,很长一段时间都使用select做轮询和网络事件通知,然后select的一些固有缺陷导致了它的应用受到很大的限制,因此Linux选择了epoll作为替代方案。
epoll跟select相比有以下优点:
1.支持一个进程打开的socket描述符(fd)不受限制,也就是说仅仅受限于操作系统的最大文件句柄数。
select的最大缺陷是单个进程所开的FD是有一定限制的,它由FD_SIZE所设定,默认值是1024.对那些需要支持上万个TCP连接的大型服务器来说实在太小了。
我们可以选择修改这个宏然后重新编译,不过这会带来网络效率的下降,因为这种设计即使增大1024这个数字,性能也不会上升。
也可以选择传统Apache的方案(多进程或者多线程),但是创建进程的代价也不可忽视。另外,进程间的数据交换对Java而言非常麻烦,由于没有共享内存,需要通过Socket通信或者其他方法进行数据同步,相对负责。
epoll则没有这一限制,它限制的是操作系统,但是这个数字远大于1024,例如1GB的机制大约是10W个句柄,具体的值可以通过系统映射文件/proc/sys/fs/file-max来查看。
2. IO效率不会随着FD数目的增加而线性下降。
传统的select/poll的另一个致命弱点,就是当你拥有一个很大的socket集合时,由于网络延时或者链路空闲,任一时刻只有少部分的socket是"活跃的",但是select/epoll每次都扫描全部的集合,导致效率呈线性下降,即fd set的size越大,性能越差。epoll中则不存在这个问题,它只会针对"活跃的"socket进行操作-这是因为在内核的实现中,epoll是根据每个fd上面的callback函数实现的。那么只有"活跃"的socket才会主动调用callback函数,其他idle状态的socket则不会,在这点上,epoll实现了一个伪的AIO。针对epoll和select性能对比的benchmark测试表明:如果所有的socket都处于活跃状态-例如一个高速LAN环境。epoll并不会比select/poll效率高太多;相反,如果过多的使用epoll_ctl,效率还会降低。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上。
3. 使用mmap加速内核与用户空间的消息传递
无论是select,poll 还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存复制就显得非常的重要,epoll是通过内核与用户空间mmap同一块内存来实现的。
4. epoll的API更加简单
包括创建一个epoll描述符、添加监听事件、阻塞等待所监听的事件发生、关闭epoll描述符等等。
其它的select替代方案:
用来解决select/poll问题的方法不仅仅只有epoll,epoll只是一种Linux的实现方案。在freeBSD下由kqueue,而dev/poll是最古老的Solaris的方案,使用难度依次递增。
kqueue是FreeBSD下的宠儿,它实际是一个功能相当丰富的kernel事件队列,它不仅仅是select/poll的升级,而且可以处理signal,目录结构变化,进程等多种事件。
kqueue是边缘触发的。/dev/poll是Solaris的产物,是这一系列高性能API中最早出现的。Kernel提供了一个特殊的设备文件/dev/poll,应用程序打开这个文件得到操作fd_set的句柄,通过写入pollfd来修改它,一个特殊的ioctl调用来替换select。不过由于出现的年代太早了,所以/dev/poll的接口比较原始。
四、Java IO 演进历史
jdk1.4推出NIO之前,基于Java的Socket全部采用同步阻塞模式(BIO),很长的时间内,大部分应用服务器都采用C、C++开发,因为它们可以直接使用系统的异步能力。
jdk1.4,NIO以JSR-51的身份正式发布,开始支持异步非阻塞编程,包括:
- 进行异步I/O操作的缓冲区ByteBuffer等;
- 进行异步I/O操作的管道Pipe;
- 进行各种I/O操作的Channel,包括ServerSocketChannel和SocketChannel;
- 多种字符的编码和解码;
- 多路复用器selector;
- 基于Perl实现的正则表达式类库;
- 文件通道FileChannel;
它依旧面临着一些问题,特别是文件系统的处理能力不足:
- 没有统一的文件属性(例如读写权限);
- API能力比较弱,很多需要自己实现,用NIO编程非常麻烦;
- 底层存储系统的一些高级API无法使用;
- 所有的文件操作都是同步阻塞调用,不支持异步文件读写操作;
2011年7月28日,JDK1.7发布。它的一个亮点就是NIO2.0,由JSR-203演进而来,主要有以下3个方面的改进:
- 提供能够批量获取文件属性的API,这些API具有平台无关性,且不与文件系统耦合。
- 提供AIO功能,支持基于文件的异步I/O操作和针对网络套接字的异步操作;
- 完成JSR-51定义的通道功能,包括对配置和多播数据包的支持等等。