NIO学习-1-文件描述符

一、什么是文件描述符

在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描述符关联起来,待的描述符上注册回调函数,当事件发生时,回调函数负责把发生的事件存储在就绪事件链表中,最后写到用户空间。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值