操作系统使用文件描述符来指代一个打开的文件,对文件的读写操作,都需要文件描述符作为参数。Java虽然在设计上使用了抽象程度更高的流来作为文件操作的模型,但是底层依然要使用文件描述符与操作系统交互,而Java世界里文件描述符的对应类就是FileDescriptor。
Java文件操作的三个类:FileIntputStream
,FileOutputStream
,RandomAccessFile
,打开这些类的源码可以看到都有一个FileDescriptor成员变量。
注:本文使用的JDK版本为8。
FileDescriptor与文件描述符
操作系统中的文件描述符本质上是一个非负整数,其中0,1,2固定为标准输入,标准输出,标准错误输出,程序接下来打开的文件使用当前进程中最小的可用的文件描述符号码,比如3。
文件描述符本身就是一个整数,所以FileDescriptor的核心职责就是保存这个数字:
public final class FileDescriptor { private int fd; }
但是文件描述符是无法在Java代码里设置的,因为FileDescriptor只有私有和无参的构造函数:
public FileDescriptor() { fd = -1; } private FileDescriptor(int fd) { this.fd = fd; }
那Java是在何时会设置FileDescriptor的fd字段呢?这要结合FileIntputStream
,FileOutputStream
,RandomAccessFile
的代码来看了。
我们以FileInputStream
为例,首先,FileInputStream
有一个FileDescriptor
成员变量:
public class FileInputStream extends InputStream { private final FileDescriptor fd;
在FileInputStream
实例化时,会新建FileDescriptor
实例,并使用fd.attach(this)
关联FileInputStream
实例与FileDescriptor
实例,这是为了日后关闭文件描述符做准备。
public FileInputStream(File file) throws FileNotFoundException { String name = (file != null ? file.getPath() : null); fd = new FileDescriptor(); fd.attach(this); path = name; open(name); } private void open(String name) throws FileNotFoundException { open0(name); } private native void open0(String name) throws FileNotFoundException;
但是上面的代码也没有对FileDescriptor#fd
进行赋值,实际上Java层面无法对他赋值,真正的逻辑是在FileInputStream#open0
这个native方法中,这就要下载JDK的源码来看了:
// /jdk/src/share/native/java/io/FileInputStream.c JNIEXPORT void JNICALL Java_java_io_FileInputStream_open(JNIEnv *env, jobject this, jstring path) { fileOpen(env, this, path, fis_fd, O_RDONLY); } // /jdk/src/solaris/native/java/io/io_util_md.c void fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags) { WITH_PLATFORM_STRING(env, path, ps) { FD fd; #if defined(__linux__) || defined(_ALLBSD_SOURCE) /* Remove trailing slashes, since the kernel won't */ char *p = (char *)ps + strlen(ps) - 1; while ((p > ps) && (*p == '/')) *p-- = '\0'; #endif fd = JVM_Open(ps, flags, 0666); // 打开文件拿到文件描述符 if (fd >= 0) { SET_FD(this, fd, fid); // 非负整数认为是正确的文件描述符,设置到fd字段 } else { throwFileNotFoundException(env, path); // 负数认为是不正确文件描述符,抛出FileNotFoundException异常 } } END_PLATFORM_STRING(env, ps); }
可以看到JDK的JNI代码中,使用JVM_Open
打开文件,得到文件描述符,而JVM_Open
已经不是JDK的方法了,而是JVM提供的方法,所以我们需要在hotspot中寻找其实现:
// /hotspot/src/share/vm/prims/jvm.cpp JVM_LEAF(jint, JVM_Open(const char *fname, jint flags, jint mode)) JVMWrapper2("JVM_Open (%s)", fname); //%note jvm_r6 int result = os::open(fname, flags, mode); // 调用os::open打开文件 if (result >= 0) { return result; } else { switch(errno) { case EEXIST: return JVM_EEXIST; default: return -1; } } JVM_END // /hotspot/src/os/linux/vm/os_linux.cpp int os::open(const char *path, int oflag, int mode) { if (strlen(path) > MAX_PATH - 1) { errno = ENAMETOOLONG; return -1; } int fd; int o_delete = (oflag & O_DELETE); oflag = oflag & ~O_DELETE; fd = ::open64(path, oflag, mode); // 调用open64打开文件 if (fd == -1) return -1; // 问打开成功也可能是目录,这里还需要判断是否打开的是普通文件 { struct stat64 buf64; int ret = ::fstat64(fd, &buf64); int st_mode = buf64.st_mode; if (ret != -1) { if ((