找回密码
 立即注册
首页 业界区 安全 5、FileDescriptor的源码和使用注意事项(windows操作系 ...

5、FileDescriptor的源码和使用注意事项(windows操作系统,JDK8)

乃阕饯 2026-2-6 19:40:02
  操作系统使用文件描述符来指代一个打开的文件,对文件的读写操作,都需要文件描述符指向存储设备的不透明标识符。Java虽然在设计上使用了抽象程度更高的流来作为文件操作的模型,但是底层依然要使用文件描述符与操作系统交互,而Java世界里文件描述符的对应类就是FileDescriptor。同时,Java规定了FileDescriptor只能由JDK的其它类来创建(比如FileInputStream、FileOutputStream、RandomAccessFile等),不能由应用程序自己创建。
  操作系统中的文件描述符本质上是一个非负整数,其中0,1,2固定为标准输入,标准输出,标准错误输出,如下所示(POSIX标准):
1.png

  Java程序接打开的文件使用当前进程可用的文件描述符就被保存在了FileDescriptor中的int fd变量,因此FileDescriptor的核心功能都是围绕着int fd变量来运行的
  1. package java.io;
  2. import java.util.ArrayList;
  3. import java.util.List;
  4. public final class FileDescriptor {
  5.     private int fd;
  6.     private long handle;
  7.     private Closeable parent;
  8.     private List<Closeable> otherParents;
  9.     private boolean closed;
  10.    
  11.     //FileDescriptor只有无参的构造函数,保证了fd不能被应用程序设置
  12.     public /**/ FileDescriptor() {
  13.         fd = -1;
  14.         handle = -1;
  15.     }
  16.     static {
  17.         initIDs();
  18.     }
  19.     static {
  20.         sun.misc.SharedSecrets.setJavaIOFileDescriptorAccess(
  21.             new sun.misc.JavaIOFileDescriptorAccess() {
  22.                 public void set(FileDescriptor obj, int fd) {
  23.                     obj.fd = fd;
  24.                 }
  25.                 public int get(FileDescriptor obj) {
  26.                     return obj.fd;
  27.                 }
  28.                 public void setHandle(FileDescriptor obj, long handle) {
  29.                     obj.handle = handle;
  30.                 }
  31.                 public long getHandle(FileDescriptor obj) {
  32.                     return obj.handle;
  33.                 }
  34.             }
  35.         );
  36.     }
  37.     //POSIX标准中的标准输入,和System.class有关
  38.     public static final FileDescriptor in = standardStream(0);
  39.     //POSIX标准中的标准输出,和System.class有关
  40.     public static final FileDescriptor out = standardStream(1);
  41.     //POSIX标准中的标准错误输出,和System.class有关
  42.     public static final FileDescriptor err = standardStream(2);
  43.     public boolean valid() {
  44.         return ((handle != -1) || (fd != -1));
  45.     }
  46.     public native void sync() throws SyncFailedException;
  47.     private static native void initIDs();
  48.     private static native long set(int d);
  49.     private static FileDescriptor standardStream(int fd) {
  50.         FileDescriptor desc = new FileDescriptor();
  51.         desc.handle = set(fd);
  52.         return desc;
  53.     }
  54.     synchronized void attach(Closeable c) {
  55.         if (parent == null) {
  56.             // first caller gets to do this
  57.             parent = c;
  58.         } else if (otherParents == null) {
  59.             otherParents = new ArrayList<>();
  60.             otherParents.add(parent);
  61.             otherParents.add(c);
  62.         } else {
  63.             otherParents.add(c);
  64.         }
  65.     }
  66.     @SuppressWarnings("try")
  67.     synchronized void closeAll(Closeable releaser) throws IOException {
  68.         if (!closed) {
  69.             closed = true;
  70.             IOException ioe = null;
  71.             try (Closeable c = releaser) {
  72.                 if (otherParents != null) {
  73.                     for (Closeable referent : otherParents) {
  74.                         try {
  75.                             referent.close();
  76.                         } catch(IOException x) {
  77.                             if (ioe == null) {
  78.                                 ioe = x;
  79.                             } else {
  80.                                 ioe.addSuppressed(x);
  81.                             }
  82.                         }
  83.                     }
  84.                 }
  85.             } catch(IOException ex) {
  86.                 /*
  87.                  * If releaser close() throws IOException
  88.                  * add other exceptions as suppressed.
  89.                  */
  90.                 if (ioe != null)
  91.                     ex.addSuppressed(ioe);
  92.                 ioe = ex;
  93.             } finally {
  94.                 if (ioe != null)
  95.                     throw ioe;
  96.             }
  97.         }
  98.     }
  99. }
复制代码
一、设置int fd变量的值

  FileDescriptor.class 的构造函数将int fd的值设置为了-1,但是操作系统中的文件描述符本质上是一个非负整数,因此FileDescriptor.class中表示文件描述符的int fd变量是在FileInputStream.class、FileOutputStream.class、RandomAccessFile.class等这些使用FileDescriptor.class的类中来设置的,比如FileInputStream.class
  1. public
  2. class FileInputStream extends InputStream
  3. {
  4.     /* File Descriptor - handle to the open file */
  5.     private final FileDescriptor fd;
  6.    
  7.     //在FileInputStream实例化时,会新建FileDescriptor实例,并使用fd.attach(this)关联FileInputStream实例与FileDescriptor实例,这是为了之后在程序中关闭文件描述符做准备。
  8.     public FileInputStream(File file) throws FileNotFoundException {
  9.         String name = (file != null ? file.getPath() : null);
  10.         ...省略代码...
  11.         fd = new FileDescriptor();
  12.         fd.attach(this);
  13.         path = name;
  14.         open(name);
  15.     }
  16.    
  17.     private void open(String name) throws FileNotFoundException {
  18.         open0(name);
  19.     }
  20.     //真正对FileDescriptor.class中int fd赋值的逻辑是JNI调用的FileInputStream#open0这个native函数中
  21.     private native void open0(String name) throws FileNotFoundException;
  22. }
复制代码
  1. // /jdk/src/share/native/java/io/FileInputStream.c
  2. JNIEXPORT void JNICALL
  3. Java_java_io_FileInputStream_open(JNIEnv *env, jobject this, jstring path) {
  4.     fileOpen(env, this, path, fis_fd, O_RDONLY);
  5. }
  6. // /jdk/src/solaris/native/java/io/io_util_md.c
  7. void
  8. fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags)
  9. {
  10.     WITH_PLATFORM_STRING(env, path, ps) {
  11.         FD fd;
  12. #if defined(__linux__) || defined(_ALLBSD_SOURCE)
  13.         /* Remove trailing slashes, since the kernel won't */
  14.         char *p = (char *)ps + strlen(ps) - 1;
  15.         while ((p > ps) && (*p == '/'))
  16.             *p-- = '\0';
  17. #endif
  18.         fd = JVM_Open(ps, flags, 0666); // 打开文件拿到文件描述符
  19.         if (fd >= 0) {
  20.             SET_FD(this, fd, fid); // 非负整数认为是正确的文件描述符,设置到fd变量
  21.         } else {
  22.             throwFileNotFoundException(env, path);  // 负数认为是不正确文件描述符,抛出FileNotFoundException异常
  23.         }
  24.     } END_PLATFORM_STRING(env, ps);
  25. }
复制代码
  到了JDK的JNI代码中,使用JVM_Open打开文件,得到文件描述符,而JVM_Open已经不是JDK的方法了,而是JVM提供的方法,所以需要继续查看hotspot中的实现:
  1. // /hotspot/src/share/vm/prims/jvm.cpp
  2. JVM_LEAF(jint, JVM_Open(const char *fname, jint flags, jint mode))
  3.   JVMWrapper2("JVM_Open (%s)", fname);
  4.   //%note jvm_r6
  5.   int result = os::open(fname, flags, mode);  // 调用os::open打开文件
  6.   if (result >= 0) {
  7.     return result;
  8.   } else {
  9.     switch(errno) {
  10.       case EEXIST:
  11.         return JVM_EEXIST;
  12.       default:
  13.         return -1;
  14.     }
  15.   }
  16. JVM_END
  17. // /hotspot/src/os/linux/vm/os_linux.cpp
  18. int os::open(const char *path, int oflag, int mode) {
  19.   if (strlen(path) > MAX_PATH - 1) {
  20.     errno = ENAMETOOLONG;
  21.     return -1;
  22.   }
  23.   int fd;
  24.   int o_delete = (oflag & O_DELETE);
  25.   oflag = oflag & ~O_DELETE;
  26.   fd = ::open64(path, oflag, mode);  // 调用open64打开文件
  27.   if (fd == -1) return -1;
  28.   // 问打开成功也可能是目录,这里还需要判断是否打开的是普通文件
  29.   {
  30.     struct stat64 buf64;
  31.     int ret = ::fstat64(fd, &buf64);
  32.     int st_mode = buf64.st_mode;
  33.     if (ret != -1) {
  34.       if ((st_mode & S_IFMT) == S_IFDIR) {
  35.         errno = EISDIR;
  36.         ::close(fd);
  37.         return -1;
  38.       }
  39.     } else {
  40.       ::close(fd);
  41.       return -1;
  42.     }
  43.   }
  44. #ifdef FD_CLOEXEC
  45.     {
  46.         int flags = ::fcntl(fd, F_GETFD);
  47.         if (flags != -1)
  48.             ::fcntl(fd, F_SETFD, flags | FD_CLOEXEC);
  49.     }
  50. #endif
  51.   if (o_delete != 0) {
  52.     ::unlink(path);
  53.   }
  54.   return fd;
  55. }
复制代码
可以看到JVM最后使用open64这个函数打开文件,网上对于open64这个资料还是很少的,我找到的是man page for open64 (all section 2) - Unix & Linux Commands,从中可以看出,open64是为了在32位环境打开大文件的系统调用,但是不是标准的一部分。(这一部分不是很确定,因为没有明确的资料)
  这里的open()函数不是我们以前学C语言时打开文件用的fopen()函数,fopen是C标准库里的函数,而open()不是,open()是POSIX规范中的函数,是不带缓冲的I/O,不带缓冲的I/O相关的函数还有read(),write(),lseek(),close(),不带缓冲指的是这些函数都调用内核中的一个系统调用,而C标准库为了减少系统调用,使用了缓存来减少read,write的内存调用。(参考《UNIX环境高级编程》)
  因此,我们知道了FileInputStream#open是使用open()系统调用来打开文件,得到文件句柄,现在我们的问题要回到这个文件句柄是如何最终设置到FileDescriptor#fd,我们来看/jdk/src/solaris/native/java/io/io_util_md.c:fileOpen的关键代码:
  1. fd = handleOpen(ps, flags, 0666);
  2. if (fd != -1) {
  3.     SET_FD(this, fd, fid);
  4. } else {
  5.     throwFileNotFoundException(env, path);
  6. }
复制代码
如果文件描述符fd正确,通过SET_FD这个红设置到fid对应的成员变量上,如下宏所示:
  1. #define SET_FD(this, fd, fid) \
  2.     if ((*env)->GetObjectField(env, (this), (fid)) != NULL) \
  3.         (*env)->SetIntField(env, (*env)->GetObjectField(env, (this), (fid)),IO_fd_fdID, (fd))
复制代码
SET_FD宏比较简单,获取FileInputStream上的fid这个变量ID对应的变量,然后设置这个变量的IO_fd_fdID对应的变量(FileDescriptor#fd)为文件描述符。
  这个fid和IO_fd_fdID的来历可以参照/jdk/src/share/native/java/io/FileInputStream.c文件的开头,可以看到这样的代码:
  1. // jdk/src/share/native/java/io/FileInputStream.c
  2. jfieldID fis_fd; /* id for jobject 'fd' in java.io.FileInputStream */
  3. /**************************************************************
  4. * static methods to store field ID's in initializers
  5. */
  6. JNIEXPORT void JNICALL
  7. Java_java_io_FileInputStream_initIDs(JNIEnv *env, jclass fdClass) {
  8.     fis_fd = (*env)->GetFieldID(env, fdClass, "fd", "Ljava/io/FileDescriptor;");
  9. }
复制代码
Java_java_io_FileInputStream_initIDs对应JAVA中FileInputStream.class源码中的static块调用的initIDs函数:
  1. public
  2. class FileInputStream extends InputStream
  3. {
  4.     /* File Descriptor - handle to the open file */
  5.     private final FileDescriptor fd;
  6.     static {
  7.         initIDs();
  8.     }
  9.     private static native void initIDs();
  10. }
复制代码
还有jdk/src/solaris/native/java/io/FileDescriptor_md.c开头:
  1. // jdk/src/solaris/native/java/io/FileDescriptor_md.c
  2. /* field id for jint 'fd' in java.io.FileDescriptor */
  3. jfieldID IO_fd_fdID;
  4. /**************************************************************
  5. * static methods to store field ID's in initializers
  6. */
  7. JNIEXPORT void JNICALL
  8. Java_java_io_FileDescriptor_initIDs(JNIEnv *env, jclass fdClass) {
  9.     IO_fd_fdID = (*env)->GetFieldID(env, fdClass, "fd", "I");
  10. }
复制代码
Java_java_io_FileDescriptor_initIDs对应JAVA中FileDescriptor.class源码中static块调用的initIDs函数:
  1. public final class FileDescriptor {
  2.     private int fd;
  3.     static {
  4.         initIDs();
  5.     }
  6.     private static native void initIDs();   
  7. }
复制代码
以上代码的整个流程为:
①、JVM加载FileDescriptor类,执行static块中的代码
②、执行static块中的代码时,执行initIDs本地函数
③、initIDs本地函数只做了一件事情,就是获取fd字段ID,并保存在IO_fd_fdID变量中
④、JVM加载FileInputStream类,执行static块中的代码
⑤、执行static块中的代码时,执行initIDs本地函数
⑥、initIDs本地函数只做了一件事情,就是获取fd字段ID,并保存在fis_fd变量中
⑦、后续逻辑直接使用IO_fd_fdID和fis_fd
  这样做的理由是因为特定类的字段ID在一次Java程序的声明周期中是不会变化的,而获取字段ID本身是一个比较耗时的过程,因为如果字段是从父类继承而来,JVM需要遍历继承树来找到这个字段,所以JNI代码的最佳实践就是对使用到的字段ID做缓存。
二、设置FileDescriptor in、FileDescriptor out、FileDescriptor err变量

  标准输入,标准输出,标准错误输出是所有操作系统都支持的,对于一个进程来说,文件描述符0,1,2固定是标准输入,标准输出,标准错误输出。Java对标准输入,标准输出,标准错误输出的支持也是通过FileDescriptor实现的,FileDescriptor中定义了FileDescriptor in、FileDescriptor out、FileDescriptor err这三个静态变量:
  1. public final class FileDescriptor {
  2.     //POSIX标准中的标准输入,和System.class有关
  3.     public static final FileDescriptor in = standardStream(0);
  4.     //POSIX标准中的标准输出,和System.class有关
  5.     public static final FileDescriptor out = standardStream(1);
  6.     //POSIX标准中的标准错误输出,和System.class有关
  7.     public static final FileDescriptor err = standardStream(2);
  8. }
复制代码
我们常用的System.out、System.err等,就是基于这三个封装的:
  1. public final class System {
  2.     public final static PrintStream err = null;
  3.     public final static InputStream in = null;
  4.     public final static PrintStream out = null;
  5.     private static void initializeSystemClass() {
  6.         ...省略部分代码...
  7.         FileInputStream fdIn = new FileInputStream(FileDescriptor.in);
  8.         FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);
  9.         FileOutputStream fdErr = new FileOutputStream(FileDescriptor.err);
  10.         setIn0(new BufferedInputStream(fdIn));
  11.         setOut0(newPrintStream(fdOut, props.getProperty("sun.stdout.encoding")));
  12.         setErr0(newPrintStream(fdErr, props.getProperty("sun.stderr.encoding")));
  13.         ...省略部分代码...
  14.     }
  15.    
  16.     private static native void setIn0(InputStream in);
  17.     private static native void setOut0(PrintStream out);
  18.     private static native void setErr0(PrintStream err);
  19. }
复制代码
System.class作为一个特殊的类,该类构造时无法实例化PrintStream err变量、InputStream in变量、PrintStream out变量,构造发生在initializeSystemClass()函数被调用时,但是PrintStream err变量、InputStream in变量、PrintStream out变量是被声明为final的,如果声明时和类构造时没有赋值,是会报错的,所以System在实现时,先设置为null,然后通过native方法来在运行时修改(可以用在我的networkScan项目上),通过setIn0/setOut0/setErr0的注释也可以说明这一点:
  1. /*
  2. * The following three functions implement setter methods for
  3. * java.lang.System.{in, out, err}. They are natively implemented
  4. * because they violate the semantics of the language (i.e. set final
  5. * variable).
  6. */
  7. JNIEXPORT void JNICALL
  8. Java_java_lang_System_setIn0(JNIEnv *env, jclass cla, jobject stream)
  9. {
  10.     jfieldID fid =
  11.         (*env)->GetStaticFieldID(env,cla,"in","Ljava/io/InputStream;");
  12.     if (fid == 0)
  13.         return;
  14.     (*env)->SetStaticObjectField(env,cla,fid,stream);
  15. }
  16. JNIEXPORT void JNICALL
  17. Java_java_lang_System_setOut0(JNIEnv *env, jclass cla, jobject stream)
  18. {
  19.     jfieldID fid =
  20.         (*env)->GetStaticFieldID(env,cla,"out","Ljava/io/PrintStream;");
  21.     if (fid == 0)
  22.         return;
  23.     (*env)->SetStaticObjectField(env,cla,fid,stream);
  24. }
  25. JNIEXPORT void JNICALL
  26. Java_java_lang_System_setErr0(JNIEnv *env, jclass cla, jobject stream)
  27. {
  28.     jfieldID fid =
  29.         (*env)->GetStaticFieldID(env,cla,"err","Ljava/io/PrintStream;");
  30.     if (fid == 0)
  31.         return;
  32.     (*env)->SetStaticObjectField(env,cla,fid,stream);
  33. }
复制代码
三、attach()函数和closeAll()函数

  attach()函数和closeAll()函数都和文件描述符的关闭有关。上文提到过,FileInputStream在构造函数中,会新建FileDescriptor并调用FileDescriptor#attach方法绑定文件流与文件描述符。
3.1、attach()函数
  1. public
  2. class FileInputStream extends InputStream
  3. {
  4.     /* File Descriptor - handle to the open file */
  5.     private final FileDescriptor fd;
  6.     private Closeable parent;
  7.     private List<Closeable> otherParents;
  8.     //在FileInputStream实例化时,会新建FileDescriptor实例,并使用fd.attach(this)关联FileInputStream实例与FileDescriptor实例,这是为了之后在程序中关闭文件描述符做准备。
  9.     public FileInputStream(File file) throws FileNotFoundException {
  10.         String name = (file != null ? file.getPath() : null);
  11.         ...省略代码...
  12.         fd = new FileDescriptor();
  13.         fd.attach(this);
  14.         path = name;
  15.         open(name);
  16.     }
  17.    
  18.     //如果FileDescriptor只和一个FileInputStream实例、FileOutputStream实例、RandomAccessFile等实例等有关联,
  19.     //则只是简单的保存到parent成员中,如果有多个FileInputStream实例、FileOutputStream实例、RandomAccessFile等实例有关联,
  20.     //则所有关联的Closeable都保存到List<Closeable> otherParents变量(实际是ArrayList<Closeable>实例)中。
  21.     synchronized void attach(Closeable c) {
  22.         if (parent == null) {
  23.             // first caller gets to do this
  24.             parent = c;
  25.         } else if (otherParents == null) {
  26.             otherParents = new ArrayList<>();
  27.             otherParents.add(parent);
  28.             otherParents.add(c);
  29.         } else {
  30.             otherParents.add(c);
  31.         }
  32.     }
  33. }
复制代码
  这里其实有个细节,就是Closeable parent变量其实只在这个函数有用到,所以上面的逻辑完全可以写成无论FileDescriptor和几个Closeable对象有关联,都直接保存到List otherParents变量即可,但是极大的概率,一个FileDescriptor只会和一个FileInputStream实例、FileOutputStream实例、RandomAccessFile等实例有关联,只有用户调用FileInputStream(FileDescriptor fdObj)这样样的构造函数才会出现多个Closeable对象对应一个FileDescriptor的情况,这里其实是做了优化,在大概率的情况下不新建ArrayList,减少一个对象的创建开销。
3.2、closeAll()函数
  1. public
  2. class FileInputStream extends InputStream
  3. {
  4.     /* File Descriptor - handle to the open file */
  5.     private final FileDescriptor fd;
  6.     private final Object closeLock = new Object();
  7.     private volatile boolean closed = false;
  8.     private FileChannel channel = null;
  9.     public void close() throws IOException {
  10.         synchronized (closeLock) {
  11.             if (closed) {
  12.                 return;
  13.             }
  14.             closed = true;
  15.         }
  16.         if (channel != null) {
  17.            channel.close();
  18.         }
  19.    
  20.         fd.closeAll(new Closeable() {
  21.             public void close() throws IOException {
  22.                close0();
  23.            }
  24.         });
  25.     }
  26.     private native void close0() throws IOException;
  27. }
复制代码
  首先通过锁保证关闭流程不会被并发调用,设置成员变量boolean closed为true,接着关闭关联的Channel(NIO中的关键组件之一,另外2个NIO关键组件是Buffer、Selector)。接着就是关闭FileDescriptor了。
  FileDescriptor没有提供close()函数,而是提供了一个closeAll()函数:
  1. synchronized void closeAll(Closeable releaser) throws IOException {
  2.     if (!closed) {
  3.         closed = true;
  4.         IOException ioe = null;
  5.         try (Closeable c = releaser) {
  6.             if (otherParents != null) {
  7.                 for (Closeable referent : otherParents) {
  8.                     try {
  9.                         referent.close();
  10.                     } catch(IOException x) {
  11.                         if (ioe == null) {
  12.                             ioe = x;
  13.                         } else {
  14.                             ioe.addSuppressed(x);
  15.                         }
  16.                     }
  17.                 }
  18.             }
  19.         } catch(IOException ex) {
  20.             /*
  21.              * If releaser close() throws IOException
  22.              * add other exceptions as suppressed.
  23.              */
  24.             if (ioe != null)
  25.                 ex.addSuppressed(ioe);
  26.             ioe = ex;
  27.         } finally {
  28.             if (ioe != null)
  29.                 throw ioe;
  30.         }
  31.     }
  32. }
复制代码
  FileDescriptor的关闭流程有点绕,效果是会把关联的Closeable对象(其实就是FileInputStream实例、FileOutputStream实例、RandomAccessFile等实例,而这些实例的close()函数实现是一模一样的)通通都关闭掉(效果是这些对象的成员变量boolean closed设置为true,关联的Channel关闭,这样这个对象就无法使用了),最后这些关联的对象中,只会有一个对象的close0本地函数被调用,这个函数中调用操作系统的close()函数来真正关闭文件描述符。
[code]// /jdk/src/solaris/native/java/io/FileInputStream_md.cJNIEXPORT void JNICALLJava_java_io_FileInputStream_close0(JNIEnv *env, jobject this) {    fileClose(env, this, fis_fd);}// /jdk/src/solaris/native/java/io/io_util_md.cvoid fileClose(JNIEnv *env, jobject this, jfieldID fid){    FD fd = GET_FD(this, fid);    if (fd == -1) {        return;    }    /* Set the fd to -1 before closing it so that the timing window     * of other threads using the wrong fd (closed but recycled fd,     * that gets re-opened with some other filename) is reduced.     * Practically the chance of its occurance is low, however, we are     * taking extra precaution over here.     */    SET_FD(this, -1, fid);    // 尝试关闭0,1,2文件描述符,需要特殊的操作。首先这三个是不能关闭的,    // 如果关闭的,后续打开的文件就会占用这三个描述符,    // 所以合理的做法是把要关闭的描述符指向/dev/null,实现关闭的效果    // 不过Java代码中,正常是没办法关闭0,1,2文件描述符的    if (fd >= STDIN_FILENO && fd

相关推荐

2026-2-8 09:29:49

举报

2026-2-9 07:07:46

举报

懂技术并乐意极积无私分享的人越来越少。珍惜
2026-2-11 10:56:42

举报

您需要登录后才可以回帖 登录 | 立即注册