程序员:必须得深入理解Java文件输入输出流和文件描述符

本文将深入理解文件描述符,并从 JDK 源码上分析文件描述符在文件输入输出流中的运用。

点个赞收藏下吧👍

特别声明,为避免重复造轮子,部分内容和图片摘自文末参考资料。本文仅限用于交流学习,严禁用于商业用途。

文件描述符是什么?

[1] 在Linux系统中一切皆可以看成是文件,文件又可分为:普通文件、目录文件、链接文件和设备文件。文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,其是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行I/O操作的系统调用都通过文件描述符。程序刚刚启动的时候,0是标准输入,1是标准输出,2是标准错误。如果此时去打开一个新的文件,它的文件描述符会是3。POSIX标准要求每次打开文件时(含socket)必须使用当前进程中最小可用的文件描述符号码,因此,在网络通信过程中稍不注意就有可能造成串话。标准文件描述符图如下:

Linux 进程中的文件描述符

[2] 从 Linux 进程的数据结构也可以看出端倪:

struct task_struct {
    // 进程状态
    long              state;
    // 虚拟内存结构体
    struct mm_struct  *mm;
    // 进程号
    pid_t             pid;
    // 指向父进程的指针
    struct task_struct __rcu  *parent;
    // 子进程列表
    struct list_head        children;
    // 存放文件系统信息的指针
    struct fs_struct        *fs;
    // 一个数组,包含该进程打开的文件指针
    struct files_struct     *files;
};

files 指针指向一个数组,这个数组里装着所有该进程打开的文件的指针。每个进程被创建时,files 的前三位被填入默认值,分别指向标准输入流、标准输出流、标准错误流。我们常说的「文件描述符」就是指这个文件指针数组的索引,所以程序的文件描述符默认情况下 0 是输入,1 是输出,2 是错误。如下图所示:

进程文件描述符

所以,在 Linux 中的重定向、管道等操作,只不过是修改了进程的 files 数组前三位的指向。”一切皆文件“的设计思想使这些操作都变得非常优雅。

FileDescriptor

FileDescriptor 是文件描述符在 JVM 中的抽象。来看看内部结构:

public final class FileDescriptor {
    // 文件描述符(就是上文所说的 files 数组下标)
    private int fd;
    // 该文件描述符所关联的实例(通常是输入输出流实例,比如 FileInputStream )
    private Closeable parent;
    private List<Closeable> otherParents;

    private boolean closed;
    // 标准输入
    public static final FileDescriptor in = new FileDescriptor(0);
    // 标准输出
    public static final FileDescriptor out = new FileDescriptor(1);
    // 标准错误
    public static final FileDescriptor err = new FileDescriptor(2);

    public boolean valid() {
        return fd != -1;
    }

    /* This routine initializes JNI field offsets for the class */
    private static native void initIDs();

    static {
        initIDs();
    }
}

FileDescriptor 非常清晰,我们可以直接总结以下几点:

  • FileDescriptor 与文件描述符一一对应,使用 fd 字段保存文件描述符;
  • 单个 FileDescriptor 可以和多个 Closeable 关联(通常是输入输出流实例,比如 FileInputStream );
  • FileDescriptor 内部有三个公开静态常量 in、out 和 err 分别代表标准输入、标准输出和标准错误,这仨通常用在 java.lang.System 中;
  • 文件描述符 fd 通常为非负数;

initIDs

initIDs 方法用于初始化 fd 字段的 ID (我觉得可以理解为 fd 字段的指针)。这是一个 native 方法,可以在 JDK 源码里找到相应的 JNI 实现。以 Window 下的实现为例(因为 Window 的相对容易找到 = =):

/* field id for jint 'fd' in java.io.FileDescriptor */
jfieldID IO_fd_fdID;

/* field id for jlong 'handle' in java.io.FileDescriptor */
jfieldID IO_handle_fdID;

/**************************************************************
 * static methods to store field IDs in initializers
 */

JNIEXPORT void JNICALL
Java_java_io_FileDescriptor_initIDs(JNIEnv *env, jclass fdClass) {
    CHECK_NULL(IO_fd_fdID = (*env)->GetFieldID(env, fdClass, "fd", "I"));
    CHECK_NULL(IO_handle_fdID = (*env)->GetFieldID(env, fdClass, "handle", "J"));
}

可见 fd 的字段ID被保存到了全局字段中,后续其他代码可以根据其字段ID来修改 fd 的值。字段ID在这里我理解为一个指针。这里还处理了 handle 字段,可能是版本问题,我没有在 JDK 里看到这个字段。

文件输入输出流

Java IO 里的文件输入输出流类有二:FileInputStream 和 FileOutputStream。两者的类图继承结构非常清晰:

文件输入输出流类图

因为两者实现原理差不多,下边以 FileInputStream 展开探讨。

FileInputStream

先来看看 FileInputStream 的源码。

内部字段很少很简洁,详见注释:

public
class FileInputStream extends InputStream
    /* 文件描述符对象**/
    private final FileDescriptor fd;
    /** 文件路径 **/
    private final String path;
    /** 可以操作读写文件的通道 **/
    private FileChannel channel = null;
    /** 关闭时用于并发控制的锁对象 **/
    private final Object closeLock = new Object();
    private volatile boolean closed = false;
}

还有一个与 FileDescriptor 类似的 initIDs 方法,也是用来设置内部的 fd 字段ID:

private static native void initIDs();

private native void close0() throws IOException;

static {
    initIDs();
}

构造方法也挺简单的,关键是看看其中这个构造函数:

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();
    // 将当前 FileInputStream 和该文件描述符关联起来
    fd.attach(this);
    path = name;
    // 打开该文件
    open(name);
}

构造函数中新建了一个文件描述符对象 fd,要记得这个对象的内部还有一个 long 类型的 fd 字段,默认初始化为 0L。而 fd 字段的初始化逻辑是在 open 方法。最终调用的是 native 方法:

/**
    * Opens the specified file for reading.
    * @param name the name of the file
    */
private native void open0(String name) throws FileNotFoundException;

题外话,这里提一下怎么通过 这个 native 方法找到对应的 C++ 实现:

  • 下载相应版本的 JDK 源码;
  • 找到 jdk/src/share/classes/java/io/FileInputStream.java;
  • 执行 javah java.io.FileInputStream 便可以生成 Header文件 java_io_FileInputStream.h :
/*
 * Class:     java_io_FileInputStream
 * Method:    open0
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_java_io_FileInputStream_open0 (JNIEnv *, jobject, jstring);

  • 使用 C++ 方法名 Java_java_io_FileInputStream_open0 进行搜索便很快能找到对应的 C++ 方法实现;

我们查看 JDK 源码(jdk/src/share/native/java/io/FileInputStream.c),有以下代码:

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

/**************************************************************
 * 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;");
}

/**************************************************************
 * Input stream
 */

JNIEXPORT void JNICALL
Java_java_io_FileInputStream_open0(JNIEnv *env, jobject this, jstring path) {
    fileOpen(env, this, path, fis_fd, O_RDONLY);
}

这里可以看到 initIDs 方法实现,其逻辑是将名为“fd”的 FileDescriptor 类型对象的字段ID保存到全局变量 fid中;

而 open0 方法调用了 fileOpen。在不同的操作系统上有不同的实现,以 Window 为例:

void
fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags)
{
    FD h = winFileHandleOpen(env, path, flags);
    if (h >= 0) {
        SET_FD(this, h, fid);
    }
}

这里对展开 winFileHandleOpen 方法不感兴趣,大体意思就是调用 Window 的系统方法打开了文件,并返回了文件描述符 h。

然后调用 SET_FD 方法将文件描述符 h 设置到 fid 中。fid 就是 initIDs 所缓存的字段ID(理解为 fd 字段的指针)。

至此,FileInputStream 和文件描述符关联了起来。后续在 FileInputStream 上的读写,JVM 都可以通过其内部的 fd 字段非常方便地找到需要读写的文件!所以,FileInputStream 还支持指定文件描述符的构造形式:

FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);

这其实就是 System.out 的实现。

总结

我们学习了 Linux 系统的文件操作符概念,理解了”一切皆是文件“的设计理念。此外,还深入学习了文件输入输出流类的源码实现,探讨它们是怎么利用文件描述符和操作系统进行交互。希望大家有所收获!点个赞收藏下吧👍

参考资料

特别声明,本文部分段落摘自以下资料。

  • [1] 《每天进步一点点——Linux中的文件描述符与打开文件之间的关系》
  • [2] 《Linux 进程、线程、文件描述符的底层原理》

其他参考资料

  • Java IO流之文件描述符FileDescriptor
  • 文件描述符(File Descriptor)简介:这文章讲到文件描述符限制的相关命令,非常实用!
  • 如何查找 jdk 中的 native 实现]:查看 JDK 源码必备技能
  • 高级Java工程师必备 ----- 深入分析 Java IO (三):介绍Java IO 类库的好文
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,776评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,527评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,361评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,430评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,511评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,544评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,561评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,315评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,763评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,070评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,235评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,911评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,554评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,173评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,424评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,106评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,103评论 2 352

推荐阅读更多精彩内容