1.概述

文件描述符即FileDescriptor类,当应用程序调用linux的open函数后,会返回一个非负整数,这个数字代表了某个文件的标示(linux系统),FileDescriptor类对这个数字进行了一层封装,除了保存这个数字外还提供了其他的方法。

后续可以将此类内部的符号作为参数,再次调用操作系统的IO读写函数,完成设备的IO操作。在Java中我们不能直接调用FD的相关方法,而是通过它创建一个FileInputStream或FileOutputStream对象,通过调用流对象完成IO操作。

2.构造器与常量

2.1 构造器

FileDescriptor类共有俩个构造器,对外仅提供了一个空参数的构造器,另一个构造器仅供类内部使用:

1
2
3
4
5
6
7
8
9
// 空构造器,描述符号写死
public FileDescriptor() {
fd = -1;
}

// 自定义描述符号的构造器,不对外提供
private FileDescriptor(int fd) {
this.fd = fd;
}

由此看出java代码层面只能创建一个描述符号值为-1的实例,但是linux颁发的描述符号都是非负整数,也就是说创建的实例是个无效的描述符,至于为什么这样设计,后面会讲。

2.2 标准输入/输出常量

FileDescriptor类通过私有构造器创建了三个静态常量实例,并且指定了描述符号的值。在linux系统中默认打开一些文件描述符,其中0固定为键盘标准输入、1固定为屏幕标准输出、2固定为屏幕错误输出。对此java也创建了三个固定的描述符对象:

1
2
3
4
5
6
7
8
// 标准输入(键盘)
public static final FileDescriptor in = new FileDescriptor(0);

// 标准输出(屏幕)
public static final FileDescriptor out = new FileDescriptor(1);

// 错误输出(屏幕)
public static final FileDescriptor err = new FileDescriptor(2);


常量in实现了键盘的输入功能,这个很少用就不讲了,接下来我们通过代码的形式,基于out常量实现一个屏幕的输出操作:

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) throws Exception{

// 基于out文件描述符,创建一个屏幕输出流
FileOutputStream fos = new FileOutputStream(FileDescriptor.out);

// 写入输出的内容
fos.write("来一杯冰镇可乐".getBytes());

// 关闭流
fos.close();
}

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
2
3
public boolean valid() {
return fd != -1;
}

4.2 sync

我们都知道Linux的磁盘IO,在内核中默认使用PageCache缓冲,sync方法就是调用操作系统函数强制刷盘。

4.3 attach

当我们基于某个FileDescriptor对象构造一个FileInputStream实例时,会调用FileDescriptor对象的attach方法,将自己注册进去。主要用于后续的流关闭操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 可能会有多个流对象基于此实例进行创建,因此要保证线程安全
synchronized void attach(Closeable c) {

// 如果parent为空,说明是第一个利用此实例创建的FileInputStream对象
if (parent == null) {
parent = c;
// 后续基于此实例创建的FileInputStream对象,放入集合中
} else if (otherParents == null) {
otherParents = new ArrayList<>();
otherParents.add(parent);
otherParents.add(c);
} else {
otherParents.add(c);
}
}

4.4 closeAll

当一个FileInputStream实例(后面简称FIS)调用close方法时,其实是调用内部的FileDescriptor对象的closeAll方法,为了防止多个FileInputStream实例同时调用,也使用关键字保证线程安全:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
synchronized void closeAll(Closeable releaser) throws IOException {

// 只有未关闭状态才会执行关闭操作
if (!closed) {

// 标记为已关闭
closed = true;

// 记录是否出现了异常
IOException ioe = null;

// 将传进来的对象关闭
try (Closeable c = releaser) {

// otherParents集合中如果存在元素,统统关闭
if (otherParents != null) {
for (Closeable referent : otherParents) {
try {
referent.close();
} catch(IOException x) {
// 如果关闭过程中出现异常,记录到ioe中
if (ioe == null) {
ioe = x;
} else {
ioe.addSuppressed(x);
}
}
}
}
} catch(IOException ex) {
// 如果关闭过程中出现异常,记录到ioe中
if (ioe != null)
ex.addSuppressed(ioe);
ioe = ex;
} finally {
// 如果catch到异常则抛出
if (ioe != null)
throw ioe;
}
}
}

这个方法很有意思,当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操作。

评论