Java干货JavaNIO程序员

Java文件描述符

2017-11-24  本文已影响18人  德彪

文件描述符

在Linux中,进程是通过文件描述符(file descriptors,简称fd)而不是文件名来访问文件的,文件描述符实际上是一个整数。

内核中,对应于每个进程都有一个文件描述符表,表示这个进程打开的所有文件。文件描述符就是这个表的索引。

文件描述表中每一项都是一个指针,指向一个用于描述打开的文件的数据块———file对象,file对象中描述了文件的打开模式,读写位置等重要信息。

image.png

当进程打开一个文件时,内核就会创建一个新的file对象。因此,我们在进程中使用多线程打开同一个文件,每个线程会有各自的文件描述符,每个线程也会有保存自己的读取位置,互不影响。

需要注意的是,file对象不是专属于某个进程的,不同进程的文件描述符表中的指针可以指向相同的file对象,从而共享这个打开的文件。比如,如果在调用fork之前父进程已经打开文件,则fork后子进程有一个父进程描述符表的副本。父子进程共享相同的打开文件集合,因此共享相同的文件位置。

file对象有引用计数,记录了引用这个对象的文件描述符个数,只有当引用计数为0时,内核才销毁file对象,因此某个进程关闭文件,不影响与之共享同一个file对象的进程。

每个file结构体都指向一个file_operations结构体,这个结构体的成员都是函数指针,指向实现各种文件操作的内核函数。比如在用户程序中read一个文件描述符,read通过系统调用进入内核,然后找到这个文件描述符所指向的file结构体,找到file结构体所指向的file_operations结构体,调用它的read成员所指向的内核函数以完成用户请求。在用户程序中调用lseek、read、write、ioctl、open等函数,最终都由内核调用file_operations的各成员所指向的内核函数完成用户请求。file_operations结构体中的release成员用于完成用户程序的close请求,之所以叫release而不叫close是因为它不一定真的关闭文件,而是减少引用计数,只有引用计数减到0才关闭文件。

file对象中包含一个指针,指向dentry对象。“dentry”是directory entry(目录项)的缩写,dentry对象代表一个独立的文件路径,如果一个文件路径被打开多次,那么会建立多个file对象,但它们都指向同一个dentry对象。为了减少读盘次数,内核缓存了目录的树状结构,称为dentry cache,其中每个节点是一个dentry结构体。

每个dentry结构体都有一个指针指向inode结构体。inode结构体保存着从磁盘inode读上来的信息。在上图的例子中,有两个dentry,分别表示/home/akaedu/a和/home/akaedu/b,它们都指向同一个inode,说明这两个文件互为硬链接。inode结构体中保存着从磁盘分区的inode读上来信息,例如所有者、文件大小、文件类型和权限位等。

每个进程刚刚启动的时候,文件描述符0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3。

java中的FileDescriptor

在java中,有着与文件描述符对应的一个类对象:FileDescriptor。我们看一下FileDescriptor与Channel的关系:

FileInputStream.getChannel():

public FileChannel getChannel() {
        synchronized (this) {
            if (channel == null) {
                channel = FileChannelImpl.open(fd, path, true, false, this);

                /*
                 * Increment fd's use count. Invoking the channel's close()
                 * method will result in decrementing the use count set for
                 * the channel.
                 */
                fd.incrementAndGetUseCount();
            }
            return channel;
        }
}

其中的FileChannelImpl.open(fd, path, true, false, this)参数fd就是FileDescriptor实例。

看一下他是怎么产生的:

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.incrementAndGetUseCount();
        this.path = name;
        open(name);
}

static {
    initIDs();
}

注意到initIDs()这个静态方法:

jfieldID fis_fd; /* id for jobject 'fd' in java.io.FileInputStream */

JNIEXPORT void JNICALL
Java_java_io_FileInputStream_initIDs(JNIEnv *env, jclass fdClass) {
    fis_fd = (*env)->GetFieldID(env, fdClass, "fd", "Ljava/io/FileDescriptor;");
}

FileInputStream类加载阶段,fis_fd就被初始化了,fid_fd相当于是FileInputStream.fd字段的一个内存偏移量,便于在必要时操作内存给它赋值。

看一下FileDescriptor的实例化过程:

public /**/ FileDescriptor() {
        fd = -1;
        handle = -1;
        useCount = new AtomicInteger();
}

static {
    initIDs();
}

FileDescriptor也有一个initIDs,他和FileInputStream.initIDs的方法类似,把设置IO_fd_fdIDFileDescriptor.fd字段的内存偏移量。

/* field id for jint 'fd' in java.io.FileDescriptor */
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");
}

接下来再看FileInputStream构造函数中的open(name)方法,字面上看,这个方法打开了一个文件,他也是一个本地方法,open方法直接调用了fileOpen方法,fileOpen方法如下:

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 = 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宏:

#define SET_FD(this, fd, fid) \
    if ((*env)->GetObjectField(env, (this), (fid)) != NULL) \
        (*env)->SetIntField(env, (*env)->GetObjectField(env, (this), (fid)),IO_fd_fdID, (fd))

注意到IO_fd_fdID,他是FileDescriptor.fd字段的内存偏移量。这个方法相当于设置FileDescriptor.fd的值等于文件描述符fd。

需要注意的是,FileDescriptor有两个字段:handle和fd,上面的代码表示我们只设置了fd字段为文件描述符,没有提到handle字段,这是因为:

在 win32 的实现中将 创建好的 文件句柄 设置到 handle 字段,在 linux 版本中则使用的是 FileDescriptor 的 fd 字段。

由此,可知 handle 和 fd 是共存的但并不同时在使用,在 win32 平台上使用 handle 字段,在 linux 平台上使用 fd 字段。

所以,FileInputStream打开文件的过程总结如下:

每一个 FileInputStream 有一个 FileDescriptor,代表这个流底层的文件的fd

初始化 FileDescriptor 对象

再谈java文件读取

java-NIO-Buffer这篇文章中我们提到了FileInputStream.read方法,再来回顾一下:

JNIEXPORT jint JNICALL  
Java_java_io_FileInputStream_readBytes(JNIEnv *env, jobject this,  
        jbyteArray bytes, jint off, jint len) {//除了前两个参数,后三个就是readBytes方法传递进来的,字节数组、起始位置、长度三个参数  
return readBytes(env, this, bytes, off, len, fis_fd);  
}

jint
readBytes(JNIEnv *env, jobject this, jbyteArray bytes,
          jint off, jint len, jfieldID fid)
{
    jint nread;
    char stackBuf[BUF_SIZE];
    char *buf = NULL;
    FD fd;
 
    if (IS_NULL(bytes)) {
        JNU_ThrowNullPointerException(env, NULL);
        return -1;
    }
 
    if (outOfBounds(env, off, len, bytes)) {
        JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL);
        return -1;
    }
 
    if (len == 0) {
        return 0;
    } else if (len > BUF_SIZE) {
        buf = malloc(len);// buf的分配
        if (buf == NULL) {
            JNU_ThrowOutOfMemoryError(env, NULL);
            return 0;
        }
    } else {
        buf = stackBuf;
    }
 
    fd = GET_FD(this, fid);
    if (fd == -1) {
        JNU_ThrowIOException(env, "Stream Closed");
        nread = -1;
    } else {
        nread = IO_Read(fd, buf, len);// buf是使用malloc分配的直接缓冲区,也就是堆外内存
        if (nread > 0) {
            (*env)->SetByteArrayRegion(env, bytes, off, nread, (jbyte *)buf);// 将直接缓冲区的内容copy到bytes数组中
        } else if (nread == JVM_IO_ERR) {
            JNU_ThrowIOExceptionWithLastError(env, "Read error");
        } else if (nread == JVM_IO_INTR) {
            JNU_ThrowByName(env, "java/io/InterruptedIOException", NULL);
        } else { /* EOF */
            nread = -1;
        }
    }
 
    if (buf != stackBuf) {
        free(buf);
    }
    return nread;
}

上述代码中的fis_fd是不是很眼熟?他就是FileInputStream.fd字段的内存偏移量。注意到fd = GET_FD(this, fid);这个方法,获取到其对应的文件描述符,然后使用该文件描述符读取文件内容,填充缓冲区。由此可见,java底层读取文件都是通过文件描述符来进行的。比如:

文章开始提到每个进程刚刚启动的时候,文件描述符0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3,FileDescriptor中的fd为0,1,2时也表示同样的意义。

FileOutputStream fileOutputStream = new FileOutputStream(FileDescriptor.out);
fileOutputStream.write('hello world');// 控制台打印 hello world,因为fileOutputStream使用了标准输出的文件描述符

参考

linux 文件描述符表 打开文件表 inode vnode

linux中文件描述符fd和文件指针flip的理解

JNI探秘--FileDescriptor、FileInputStream 解惑

上一篇下一篇

猜你喜欢

热点阅读