一、什么是文件描述符
在Linux下一切皆文件,对于内核而言,所有打开的文件都通过文件描述符引用,文件描述符是一个非负整数,当打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时,使用open或creat返回文件描述符标示该文件,将其作为参数传送给read或write.
在linux中,进程是通过文件描述符(file descriptors 简称fd)来访问文件的,文件描述符实际上是一个整数。在程序刚启动的时候,默认有三个文件描述符,分别是:0(代表标准输入),1(代表标准输出),2(代表标准错误)。再打开一个新的文件的话,它的文件描述符就是3。
文件描述符的变化范围是0~OPEN_MAX-1.
文件描述符:在linux系统中打开文件就会获得文件描述符,它是个很小的正整数。每个进程在PCB(Process Control Block)中保存着一份文件描述符表,文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针。
文件指针:C语言中使用文件指针做为I/O的句柄。文件指针指向进程用户区中的一个被称为FILE结构的数据结构。FILE结构包括一个缓冲区和一个文件描述符。而文件描述符是文件描述符表的一个索引,因此从某种意义上说文件指针就是句柄的句柄(在Windows系统上,文件描述符被称作文件句柄)。
二、FileDescriptor
java中FileDescriptor 可以被用来表示开放文件、开放套接字等。具体到NIO中,则用来操作socket套接字.
在nio中需要首先打开这么一个通道,具体是如何打开的呢,这个是跟文件描述符相关的,下面具体分析.
ServerSocketChannel类中:
public static ServerSocketChannel open() throws IOException {
return SelectorProvider.provider().openServerSocketChannel();
}
SelectorProvider.provider() 这个方法会根据不同的操作系统,返回不同的系统的provider,不同的provider仅仅涉及不同Selector的返回不同,这里对于
openServerSocketChannel 不同系统的实现都是相同的。
public ServerSocketChannel openServerSocketChannel() throws IOException {
return new ServerSocketChannelImpl(this);
}
然后变可以看到如下的实例化:
ServerSocketChannelImpl(SelectorProvider var1) throws IOException {
super(var1);
this.fd = Net.serverSocket(true);
this.fdVal = IOUtil.fdVal(this.fd);
this.state = 0;
}
这里Net.serverSocket方法如下:
static FileDescriptor serverSocket(boolean var0) {
return IOUtil.newFD(socket0(isIPv6Available(), var0, true, fastLoopback));
}
通过这个本地的方法socket0打开一个socket并返回 一个linux中文件描述符的int值。
通过上面部分可以看到OPEN(),操作实际做的是创建一个socket并通过文件描述符来放到ServerSocketChannel中.
三、ServerSocketChannel的初始化
经过上面的open()后,需要配置下非阻塞,以及绑定指定的端口。
这里我们主要看一下
serverChannel.socket().bind(new InetSocketAddress(port));
serverChannel.socket();实际上是通过一个适配器ServerSocketAdaptor 将ServerSocketChannel转换为一个ServerSocket。然后通过bind()中调用
Net.bind(),将文件描述符与本地IP地址和 指定的port绑定,同时监听这个文件描述符.
四、Selector的创建
Selector.open();我们这里在windows系统中,会默认选择创建WindowsSelectorImpl类作为选择器,我们的初始化方法如下:
WindowsSelectorImpl(SelectorProvider var1) throws IOException {
super(var1);
this.wakeupSourceFd = ((SelChImpl)this.wakeupPipe.source()).getFDVal();
SinkChannelImpl var2 = (SinkChannelImpl)this.wakeupPipe.sink();
var2.sc.socket().setTcpNoDelay(true);
this.wakeupSinkFd = var2.getFDVal();
this.pollWrapper.addWakeupSocket(this.wakeupSourceFd, 0);
}
请注意this.wakeupPipe,它的初始化在Pipe.open();最后发现是new()PipeImpl(SelectProvider v).它的作用是创建一对管道,一个输入,一个输出管道。实际上在创建双管道的过程中,有通过socket去验证SourceChannelImpl和SinkChannelImpl是否能正常传输,也即是用来唤醒Selector工作的SourceChannelImpl用来接收SinkChannelImpl发送的数据从而唤醒Selector的工作.
var2.sc.socket().setTcpNoDelay(true);
这里将创建双管道过程中使用的SocketChannel的Tcp延迟设置为无。
最后将source端由前面提到的WindowsSelectorImpl放到了pollWrapper中(pollWrapper.addWakeupSocket(wakeupSourceFd, 0));
五、事件注册
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);这里主要介绍下implRegister();
WindowsSelectorImpl.java
protected void implRegister(SelectionKeyImpl ski) {
growIfNeeded();
channelArray[totalChannels] = ski;
ski.setIndex(totalChannels);
fdMap.put(ski);
keys.add(ski);
pollWrapper.addEntry(totalChannels, ski);
totalChannels++; }
PollArrayWrapper.java
void addEntry(int index, SelectionKeyImpl ski) {
putDescriptor(index, ski.channel.getFDVal());
}
这里的作用是将SelectionKey的socketChannel的文件描述符放到PollArrary中.
六、客户端的链接
同样通过socket创建SocketChannel.
SocketChannelImpl(SelectorProvider var1) throws IOException {
super(var1);
this.fd = Net.socket(true);
this.fdVal = IOUtil.fdVal(this.fd);
this.state = 0;
}
同时创建客户端的Selector管理器
Selector.open();->
WindowsSelectorImpl(SelectorProvider var1) throws IOException {
super(var1);
this.wakeupSourceFd = ((SelChImpl)this.wakeupPipe.source()).getFDVal();
SinkChannelImpl var2 = (SinkChannelImpl)this.wakeupPipe.sink();
var2.sc.socket().setTcpNoDelay(true);
this.wakeupSinkFd = var2.getFDVal();
this.pollWrapper.addWakeupSocket(this.wakeupSourceFd, 0);
}
同样在客户端创建一对管理,管理输入输出。并将相应的文件描述符注册到pollArrary中。
然后通过Net.connect(this.fd, var31, var5.getPort());channel.connect(new InetSocketAddress(ip,port)); 去链接指定的IP和端口,并在本地创建描述符保存在pollArrary中。
七、通信
核心的实现便是轮询的select();实现内容如下:
WindowsSelectorImpl.java
----
protected int doSelect(long timeout) throws IOException {
if (channelArray == null)
throw new ClosedSelectorException();
this.timeout = timeout; // set selector timeout
processDeregisterQueue();
if (interruptTriggered) {
resetWakeupSocket();
return 0;
}
// Calculate number of helper threads needed for poll. If necessary
// threads are created here and start waiting on startLock
adjustThreadsCount();
finishLock.reset(); // reset finishLock
// Wakeup helper threads, waiting on startLock, so they start polling.
// Redundant threads will exit here after wakeup.
startLock.startThreads();
// do polling in the main thread. Main thread is responsible for
// first MAX_SELECTABLE_FDS entries in pollArray.
try {
begin();
try {
subSelector.poll();
} catch (IOException e) {
finishLock.setException(e); // Save this exception
}
// Main thread is out of poll(). Wakeup others and wait for them
if (threads.size() > 0)
finishLock.waitForHelperThreads();
} finally {
end();
}
// Done with poll(). Set wakeupSocket to nonsignaled for the next run.
finishLock.checkForException();
processDeregisterQueue();
int updated = updateSelectedKeys();
// Done with poll(). Set wakeupSocket to nonsignaled for the next run.
resetWakeupSocket();
return updated;
}
private int poll() throws IOException{ // poll for the main thread
return poll0(pollWrapper.pollArrayAddress,
Math.min(totalChannels, MAX_SELECTABLE_FDS),
readFds, writeFds, exceptFds, timeout); }
核心的方法private native int poll0(long pollAddress, int numfds, int[] readFds, int[] writeFds, int[] exceptFds, long timeout);C代码已经忘得差不多了,但实现思路是调用c的select方法,这里的select对应于内核中的sys_select调用,sys_select首先将第二三四个参数指向的fd_set拷贝到内核,然后对每个被SET的描述符调用进行poll,并记录在临时结果中(fdset),如果有事件发生,select会将临时结果写到用户空间并返回;当轮询一遍后没有任何事件发生时,如果指定了超时时间,则select会睡眠到超时,睡眠结束后再进行一次轮询,并将临时结果写到用户空间,然后返回。
这里的select就是轮询pollArray中的FD,看有没有事件发生,如果有事件发生收集所有发生事件的FD,退出阻塞。
关于select系统调用参考了《select、poll、epoll的比较》这篇文章,同时看到nio的select在不同平台上的实现不同,在linux上通过epoll可以不用轮询,在第一次调用后,事件信息就会与对应的epoll描述符关联起来,待的描述符上注册回调函数,当事件发生时,回调函数负责把发生的事件存储在就绪事件链表中,最后写到用户空间。