JNI 探秘 -- FileDescriptor、FileInputStream 解惑

3,782 阅读5分钟
原文链接: vinoit.me

介绍

使用JAVA读取文件时需要用到FileInputStream这个类,最简单的使用方式如下:

public static void main(String[] args){
        try {
            FileInputStream fileInputStream = new FileInputStream("test.txt");
            System.out.println(fileInputStream.read());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
FileInputStream源码中的构造方法一共有3个:

public FileInputStream(String name) throws FileNotFoundException {
        this(name != null ? new File(name) : null);
        public FileInputStream(File file) throws FileNotFoundException {
        String name = (file != null ? file.getPath() : null);
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkRead(name);
        if (name == null) {
            throw new NullPointerException();
        if (file.isInvalid()) {
            throw new FileNotFoundException("Invalid file path");
        fd = new FileDescriptor();
        fd.attach(this);
        path = name;
        open(name);
        public FileInputStream(FileDescriptor fdObj) {
        SecurityManager security = System.getSecurityManager();
        if (fdObj == null) {
            throw new NullPointerException();
        if (security != null) {
            security.checkRead(fdObj);
        fd = fdObj;
        path = null;
         * FileDescriptor is being shared by streams.
         * Register this stream with FileDescriptor tracker.
        fd.attach(this);
fdpath定义如下:

private final FileDescriptor fd;
   * The path of the referenced file
   * (null if the stream is created with a file descriptor)
  private final String path;
参数为String name或者File file的构造方法都新建了一个fileDescriptor,并赋值给fd,而参数为FileDescriptor fdObj的构造方法直接将fdObj参数赋值给fd。其实从这里可以感觉出FileDescriptor(文件描述符)是JAVA中的文件操作核心。

疑惑一

public static void main(String[] args) throws IOException, NoSuchFieldException {
        FileDescriptor fileDescriptor = null;
        FileDescriptor fileDescriptor1 = null;
        FileInputStream fileInputStream = new FileInputStream("test.txt");
        FileInputStream fileInputStream1 = new FileInputStream("test.txt");
        System.out.println(fileInputStream.getFD().valid());
        System.out.println(fileInputStream1.getFD().valid());
        fileDescriptor = fileInputStream.getFD();
        fileDescriptor1 = fileInputStream1.getFD();
输出:

其中fileDescriptorfileDescriptor1的值分别为:

查看FileDescriptor的源码,2个构造方法如下:

public  FileDescriptor() {
    fd = -1;
    private  FileDescriptor(int fd) {
    this.fd = fd;
FileDescriptor中的fd是一个int类型的值。FileDescriptor源码中只有一个public的构造方法,而且fd的初始值为-1,但是FileInputStream中的fd(FileDescriptor类型)的fd值通过调试看到不为-1(2个fd是包含的关系)。输出true的条件就是fd != -1

疑惑一解密

在参数为File file的构造方法的最后调用了一个open方法,初步怀疑在这个方法内改变了fd的内容。通过打断点调试,的确在open方法调用之后fd的值改变了。open方法的最终调用为:

private native void open0(String name) throws FileNotFoundException;
可以看到是一个native方法,对应的JNI方法如下:

JNIEXPORT void JNICALL
    Java_java_io_FileInputStream_open(JNIEnv *env, jobject this, jstring path) {
    fileOpen(env, this, path, fis_fd, O_RDONLY);
env是JNI的一个对象,this表示调用open方法的FileInputStream对象,path为传进来的参数(文件名),O_RDONLY表示只读,fis_fd是在JNI中定义的一个变量:

jfieldID fis_fd; 
 * static methods to store field ID's in initializers
 JNIEXPORT void JNICALL
Java_java_io_FileInputStream_initIDs(JNIEnv *env, jclass fdClass) {
    fis_fd = (*env)->GetFieldID(env, fdClass, "fd", "Ljava/io/FileDescriptor;");
fis_fd通过Java_java_io_FileInputStream_initIDs方法初始化,该方法对应了FileInputStream如下代码:

所以,在FileInputStream类加载阶段,fis_fd就被初始化了,fid_fd相当于是fd字段的一个内存偏移量。open方法直接调用了fileOpen方法,fileOpen方法如下:

void
    fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags)
    WITH_PLATFORM_STRING(env, path, ps) {
        FD fd;
        char *p = (char *)ps + strlen(ps) - 1;
        while ((p > ps) && (*p == '/'))
            *p-- = '\0';
        fd = handleOpen(ps, flags, 0666);
        if (fd != -1) {
            SET_FD(this, fd, fid);
        } else {
            throwFileNotFoundException(env, path);
    } END_PLATFORM_STRING(env, ps);
其中的handleOpen函数打开了一个文件句柄(一个数字),相当于和文件建立了联系,并且将返回的句柄赋值给了局部变量fd,然后调用了SET_FD宏:

     ((*env)->GetObjectField(env, (this), (fid)) != NULL) \
        (*env)->SetIntField(env, (*env)->GetObjectField(env, (this), (fid)),IO_fd_fdID, (fd))
该函数首先判断FileInputStream这个对象的fd属性是不是空,如果不为空,则进行赋值。fd是刚得到的文件句柄,(*env)->GetObjectField(env, (this), (fid))FileInputStream对象的fd字段。但是句柄fdint类型的,而FileInputStream对象的fd字段是FileDescriptor类型的,如何赋值?理所当然,我们需要一个偏移量,一个FileDescriptor中的fd字段的偏移量,也就是IO_fd_fdID的值。IO_fd_fdID是在FileDescriptor对应JNI代码的一个变量,在类加载时期初始化,通过静态代码块:

对应的native方法如下:

jfieldID IO_fd_fdID;
 * static methods to store field ID's in initializers
 JNIEXPORT void JNICALL
Java_java_io_FileDescriptor_initIDs(JNIEnv *env, jclass fdClass) {
    IO_fd_fdID = (*env)->GetFieldID(env, fdClass, "fd", "I");
由此可得,调用open方法之后,FileInputSream对象的fd的值被改变了。

疑惑二

既然FileDescriptor是文件操作的核心,那么read方法调用又是怎么和它联系起来的?

疑惑二解密

FileInputStream中的read方法:

public int read() throws IOException {
        return read0();
    private native int read0() throws IOException;
对应的native方法:

JNIEXPORT jint JNICALL
    Java_java_io_FileInputStream_read(JNIEnv *env, jobject this) {
    return readSingle(env, this, fis_fd);
readSingle()方法:

jint
    readSingle(JNIEnv *env, jobject this, jfieldID fid) {
    jint nread;
    char ret;
    FD fd = GET_FD(this, fid);
    if (fd == -1) {
        JNU_ThrowIOException(env, "Stream Closed");
        return -1;
    nread = IO_Read(fd, &ret, 1);
    if (nread == 0) { 
        return -1;
    } else if (nread == -1) { 
        JNU_ThrowIOExceptionWithLastError(env, "Read error");
    return ret & 0xFF;
虽然java代码中没有表现出对fd的使用,但是在native代码中的确使用了fd

总结

JAVA中的文件操作最终都是要通过FileDescriptor,在Unix/Linux中的文件描述符就是一个数字,对应了进程打开文件数组的下标,该数组的0,1,2号文件分别表示标准输入、标准输出,标准错误输出。这和JAVA中是一致的,FileDescriptor中的fd为0,1,2时也表示同样的意义。所以以下代码也可以用于输出'A':

FileOutputStream fileOutputStream = new FileOutputStream(FileDescriptor.out);
        fileOutputStream.write('A');
当我们通过文件名或者文件对象new一个FileInputStream的时候,做了以下步骤:

  1. 如果FileInputStream类尚未加载,则执行initIDs方法,否则这一步直接跳过。

  2. 如果FileDescriptor类尚未加载,则执行initIDs方法,否则这一步也直接跳过。

  3. new一个FileDescriptor对象赋给FileInputStream的fd属性。

  4. 打开一个文件句柄。

  5. 将文件句柄赋给FileDescriptor对象的fd字段。

注:本文JDK版本为1.8

如若觉得本文尚可,欢迎转载交流,转载请在正文明显处注明原文地址,谢谢!