1.概述
文件描述符即FileDescriptor类,当应用程序调用linux的open函数后,会返回一个非负整数,这个数字代表了某个文件的标示(linux系统),FileDescriptor类对这个数字进行了一层封装,除了保存这个数字外还提供了其他的方法。
后续可以将此类内部的符号作为参数,再次调用操作系统的IO读写函数,完成设备的IO操作。在Java中我们不能直接调用FD的相关方法,而是通过它创建一个FileInputStream或FileOutputStream对象,通过调用流对象完成IO操作。
2.构造器与常量
2.1 构造器
FileDescriptor类共有俩个构造器,对外仅提供了一个空参数的构造器,另一个构造器仅供类内部使用:
1 | // 空构造器,描述符号写死 |
由此看出java代码层面只能创建一个描述符号值为-1的实例,但是linux颁发的描述符号都是非负整数,也就是说创建的实例是个无效的描述符,至于为什么这样设计,后面会讲。
2.2 标准输入/输出常量
FileDescriptor类通过私有构造器创建了三个静态常量实例,并且指定了描述符号的值。在linux系统中默认打开一些文件描述符,其中0固定为键盘标准输入、1固定为屏幕标准输出、2固定为屏幕错误输出。对此java也创建了三个固定的描述符对象:
1 | // 标准输入(键盘) |
常量in实现了键盘的输入功能,这个很少用就不讲了,接下来我们通过代码的形式,基于out常量实现一个屏幕的输出操作:
1 | public static void main(String[] args) throws Exception{ |
main方法执行完毕后,可以在控制台看到打印的结果,这个效果和我们常用的System.out.println()方法一致,System对象内部就是通过out常量创建一个FileOutputStream,考虑到性能问题,还会继续封装成BufferedOutputStream对象,最后再封装成PrintStream实现。
常量err和out几乎一样,因为是错误输出,所以在控制台打印的颜色是红色。
3.成员变量
3.1 fd变量
fd即文件描述符号值,除了上述的三种特殊情况可以提前知道符号值,当我们想要对linux的IO设备进行读写时,必须调用open函数获取对应的文件描述符号,因为linux系统的所有IO函数都是基于文件描述符号操作的。
以FileInputStream为例,内部的native open0()方法会调用linux的open函数拿到文件描述符号,并赋值给自己的fd属性,也就是说fd只能通过底层的C语言进行赋值。个人猜测,这么设计是防止开发人员随意设置文件描述符号值进行IO操作,引起不必要的麻烦。
3.2 parent
用于记录哪些FileOutputStream实例是基于自己创建的,第一个赋值在parent属性中,主要用于后续的流关闭操作。
3.3 otherParents
用于记录哪些FileOutputStream实例是基于自己创建的,除了第一个,后续的就添加到otherParents集合中,主要用于后续的流关闭操作。
3.4 closed
默认为true,当调用classAll()方法后变为false,如果文件描述符已关闭,就无法再进行IO操作了。
4.内部方法
4.1 valid
判断一个文件描述符是否有效,就是看符号值是否为-1,在java层面new出来的实例都是无效的,需要借助native方法调用操作系统open函数对其再次赋值:
1 | public boolean valid() { |
4.2 sync
我们都知道Linux的磁盘IO,在内核中默认使用PageCache缓冲,sync方法就是调用操作系统函数强制刷盘。
4.3 attach
当我们基于某个FileDescriptor对象构造一个FileInputStream实例时,会调用FileDescriptor对象的attach方法,将自己注册进去。主要用于后续的流关闭操作:
1 | // 可能会有多个流对象基于此实例进行创建,因此要保证线程安全 |
4.4 closeAll
当一个FileInputStream实例(后面简称FIS)调用close方法时,其实是调用内部的FileDescriptor对象的closeAll方法,为了防止多个FileInputStream实例同时调用,也使用关键字保证线程安全:
1 | synchronized void closeAll(Closeable releaser) throws IOException { |
这个方法很有意思,当FileInputStream实例使用完毕后,调用自己的close进行流关闭,并且会将关闭自己的逻辑封装成一个Closeable,作为参数调用内部FileDescriptor实例的closeAll方法。
也就是说当多个FileInputStream实例引用一个FileDescriptor实例时,其中一个FileInputStream实例执行了close方法,其他的FileInputStream实例也就无法进行读写了,因为操作系统已经把这个文件描述符关闭了。
closeAll方法中并未对parent进行关闭,这个我很疑惑,但这不重要。重点是参数releaser内部包含了Linux系统close方法的调用,在操作系统层面已经将文件描述符关闭了,java层面无论如何都不可能再读写的。
5.总结
FileDescriptor对象是操作系统和java流对象之间的桥梁,以linux系统为例,FileDescriptor的本质就是记录linux系统open函数返回的符号,这个符号的本质就是linux系统给文件发送的临时索引,通过这个临时身份证就可以找到对应的文件,java流对象就可以和linux进行函数调用,完成IO操作。