操作系统使用文件描述符来指代一个打开的文件,对文件的读写操作,都需要文件描述符作为参数。Java虽然在设计上使用了抽象程度更高的流来作为文件操作的模型,但是底层依然要使用文件描述符与操作系统交互,而Java世界里文件描述符的对应类就是FileDescriptor。
操作系统中的文件描述符本质上是一个非负整数,其中0,1,2固定为标准输入,标准输出,标准错误输出,程序接下来打开的文件使用当前进程中最小的可用的文件描述符号码,比如3。
但是文件描述符是无法在Java代码里设置的,因为FileDescriptor只有私有和无参的构造函数:
可以看到JDK的JNI代码中,使用JVM_Open打开文件,得到文件描述符,而JVM_Open已经不是JDK的方法了,而是JVM提供的方法,所以我们需要在hotspot中寻找其实现:
这里的open不是我们以前学C语言时打开文件用的fopen函数,fopen是C标准库里的函数,而open不是,open是POSIX规范中的函数,是不带缓冲的I/O,不带缓冲的I/O相关的函数还有read,write,lseek,close,不带缓冲指的是这些函数都调用内核中的一个系统调用,而C标准库为了减少系统调用,使用了缓存来减少read,write的内存调用。(参考《UNIX高级编程》)
为什么会有这样一个奇怪的初始化过程呢,为什么要专门弄一个initIDs方法来提前保存字段ID呢?这是因为特定类的字段ID在一次Java程序的声明周期中是不会变化的,而获取字段ID本身是一个比较耗时的过程,因为如果字段是从父类继承而来,JVM需要遍历继承树来找到这个字段,所以JNI代码的最佳实践就是对使用到的字段ID做缓存。(参考使用 Java Native Intece 的最佳实践)
标准输入,标准输出,标准错误输出是所有操作系统都支持的,对于一个进程来说,文件描述符0,1,2固定是标准输入,标准输出,标准错误输出。
FileDescriptor的代码不多,除了提到的fd变量,initIDs初始化构造方法,in/out/err三个标准描述符,只剩下attach和closeAll这两个方法,这两个方法和文件描述符的关闭有关。
首先通过锁关闭流程不会被并发调用,设置closed为true,接着关闭关联的Channel,这个以后分析NIO的时候再来说。接着就是关闭FileDescriptor了。
FileDescriptor的关闭流程有点绕,效果是会把关联的Closeable对象(其实只可能是FileInputStream/FileOutputStream/RandomAccessFile,而这三个类的close方法实现是一模一样的)通通都关闭掉(效果是这些对象的closed设置为true,关联的Channel关闭,这样这个对象就无法使用了),最后这些关联的对象中,只会有一个对象的close0本地方法被调用,这个方法中调用系统调用close来真正关闭文件描述符:
也就是说FileInputStream#close是会吧输入/出流对应的系统资源关闭的,也就是输入/出流对应的文件描述符会被关闭,而如果这个文件描述符还关联这其他输入/出流,如果文件描述符都被关闭了,这些流自然也就不能用了,所以closeAll里把这些关联的流通通都关闭掉,使其不再可用。
网友评论 ()条 查看