程序員:必須得深入理解Java文件輸入輸出流和文件描述符

本文將深入理解文件描述符,并從 JDK 源碼上分析文件描述符在文件輸入輸出流中的運用。

點個贊收藏下吧??

特別聲明,為避免重復(fù)造輪子,部分內(nèi)容和圖片摘自文末參考資料。本文僅限用于交流學(xué)習(xí),嚴(yán)禁用于商業(yè)用途。

文件描述符是什么?

[1] 在Linux系統(tǒng)中一切皆可以看成是文件,文件又可分為:普通文件、目錄文件、鏈接文件和設(shè)備文件。文件描述符(file descriptor)是內(nèi)核為了高效管理已被打開的文件所創(chuàng)建的索引,其是一個非負(fù)整數(shù)(通常是小整數(shù)),用于指代被打開的文件,所有執(zhí)行I/O操作的系統(tǒng)調(diào)用都通過文件描述符。程序剛剛啟動的時候,0是標(biāo)準(zhǔn)輸入,1是標(biāo)準(zhǔn)輸出,2是標(biāo)準(zhǔn)錯誤。如果此時去打開一個新的文件,它的文件描述符會是3。POSIX標(biāo)準(zhǔn)要求每次打開文件時(含socket)必須使用當(dāng)前進(jìn)程中最小可用的文件描述符號碼,因此,在網(wǎng)絡(luò)通信過程中稍不注意就有可能造成串話。標(biāo)準(zhǔn)文件描述符圖如下:

Linux 進(jìn)程中的文件描述符

[2] 從 Linux 進(jìn)程的數(shù)據(jù)結(jié)構(gòu)也可以看出端倪:

struct task_struct {
    // 進(jìn)程狀態(tài)
    long              state;
    // 虛擬內(nèi)存結(jié)構(gòu)體
    struct mm_struct  *mm;
    // 進(jìn)程號
    pid_t             pid;
    // 指向父進(jìn)程的指針
    struct task_struct __rcu  *parent;
    // 子進(jìn)程列表
    struct list_head        children;
    // 存放文件系統(tǒng)信息的指針
    struct fs_struct        *fs;
    // 一個數(shù)組,包含該進(jìn)程打開的文件指針
    struct files_struct     *files;
};

files 指針指向一個數(shù)組,這個數(shù)組里裝著所有該進(jìn)程打開的文件的指針。每個進(jìn)程被創(chuàng)建時,files 的前三位被填入默認(rèn)值,分別指向標(biāo)準(zhǔn)輸入流、標(biāo)準(zhǔn)輸出流、標(biāo)準(zhǔn)錯誤流。我們常說的「文件描述符」就是指這個文件指針數(shù)組的索引,所以程序的文件描述符默認(rèn)情況下 0 是輸入,1 是輸出,2 是錯誤。如下圖所示:

進(jìn)程文件描述符

所以,在 Linux 中的重定向、管道等操作,只不過是修改了進(jìn)程的 files 數(shù)組前三位的指向?!币磺薪晕募暗脑O(shè)計思想使這些操作都變得非常優(yōu)雅。

FileDescriptor

FileDescriptor 是文件描述符在 JVM 中的抽象。來看看內(nèi)部結(jié)構(gòu):

public final class FileDescriptor {
    // 文件描述符(就是上文所說的 files 數(shù)組下標(biāo))
    private int fd;
    // 該文件描述符所關(guān)聯(lián)的實例(通常是輸入輸出流實例,比如 FileInputStream )
    private Closeable parent;
    private List<Closeable> otherParents;

    private boolean closed;
    // 標(biāo)準(zhǔn)輸入
    public static final FileDescriptor in = new FileDescriptor(0);
    // 標(biāo)準(zhǔn)輸出
    public static final FileDescriptor out = new FileDescriptor(1);
    // 標(biāo)準(zhǔn)錯誤
    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 非常清晰,我們可以直接總結(jié)以下幾點:

  • FileDescriptor 與文件描述符一一對應(yīng),使用 fd 字段保存文件描述符;
  • 單個 FileDescriptor 可以和多個 Closeable 關(guān)聯(lián)(通常是輸入輸出流實例,比如 FileInputStream );
  • FileDescriptor 內(nèi)部有三個公開靜態(tài)常量 in、out 和 err 分別代表標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出和標(biāo)準(zhǔn)錯誤,這仨通常用在 java.lang.System 中;
  • 文件描述符 fd 通常為非負(fù)數(shù);

initIDs

initIDs 方法用于初始化 fd 字段的 ID (我覺得可以理解為 fd 字段的指針)。這是一個 native 方法,可以在 JDK 源碼里找到相應(yīng)的 JNI 實現(xiàn)。以 Window 下的實現(xiàn)為例(因為 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被保存到了全局字段中,后續(xù)其他代碼可以根據(jù)其字段ID來修改 fd 的值。字段ID在這里我理解為一個指針。這里還處理了 handle 字段,可能是版本問題,我沒有在 JDK 里看到這個字段。

文件輸入輸出流

Java IO 里的文件輸入輸出流類有二:FileInputStream 和 FileOutputStream。兩者的類圖繼承結(jié)構(gòu)非常清晰:

文件輸入輸出流類圖

因為兩者實現(xiàn)原理差不多,下邊以 FileInputStream 展開探討。

FileInputStream

先來看看 FileInputStream 的源碼。

內(nèi)部字段很少很簡潔,詳見注釋:

public
class FileInputStream extends InputStream
    /* 文件描述符對象**/
    private final FileDescriptor fd;
    /** 文件路徑 **/
    private final String path;
    /** 可以操作讀寫文件的通道 **/
    private FileChannel channel = null;
    /** 關(guān)閉時用于并發(fā)控制的鎖對象 **/
    private final Object closeLock = new Object();
    private volatile boolean closed = false;
}

還有一個與 FileDescriptor 類似的 initIDs 方法,也是用來設(shè)置內(nèi)部的 fd 字段ID:

private static native void initIDs();

private native void close0() throws IOException;

static {
    initIDs();
}

構(gòu)造方法也挺簡單的,關(guān)鍵是看看其中這個構(gòu)造函數(shù):

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();
    // 將當(dāng)前 FileInputStream 和該文件描述符關(guān)聯(lián)起來
    fd.attach(this);
    path = name;
    // 打開該文件
    open(name);
}

構(gòu)造函數(shù)中新建了一個文件描述符對象 fd,要記得這個對象的內(nèi)部還有一個 long 類型的 fd 字段,默認(rèn)初始化為 0L。而 fd 字段的初始化邏輯是在 open 方法。最終調(diào)用的是 native 方法:

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

題外話,這里提一下怎么通過 這個 native 方法找到對應(yīng)的 C++ 實現(xiàn):

  • 下載相應(yīng)版本的 JDK 源碼;
  • 找到 jdk/src/share/classes/java/io/FileInputStream.java;
  • 執(zhí)行 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 進(jìn)行搜索便很快能找到對應(yīng)的 C++ 方法實現(xiàn);

我們查看 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 方法實現(xiàn),其邏輯是將名為“fd”的 FileDescriptor 類型對象的字段ID保存到全局變量 fid中;

而 open0 方法調(diào)用了 fileOpen。在不同的操作系統(tǒng)上有不同的實現(xiàn),以 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 方法不感興趣,大體意思就是調(diào)用 Window 的系統(tǒng)方法打開了文件,并返回了文件描述符 h。

然后調(diào)用 SET_FD 方法將文件描述符 h 設(shè)置到 fid 中。fid 就是 initIDs 所緩存的字段ID(理解為 fd 字段的指針)。

至此,F(xiàn)ileInputStream 和文件描述符關(guān)聯(lián)了起來。后續(xù)在 FileInputStream 上的讀寫,JVM 都可以通過其內(nèi)部的 fd 字段非常方便地找到需要讀寫的文件!所以,F(xiàn)ileInputStream 還支持指定文件描述符的構(gòu)造形式:

FileOutputStream fdOut = new FileOutputStream(FileDescriptor.out);

這其實就是 System.out 的實現(xiàn)。

總結(jié)

我們學(xué)習(xí)了 Linux 系統(tǒng)的文件操作符概念,理解了”一切皆是文件“的設(shè)計理念。此外,還深入學(xué)習(xí)了文件輸入輸出流類的源碼實現(xiàn),探討它們是怎么利用文件描述符和操作系統(tǒng)進(jìn)行交互。希望大家有所收獲!點個贊收藏下吧??

參考資料

特別聲明,本文部分段落摘自以下資料。

  • [1] 《每天進(jìn)步一點點——Linux中的文件描述符與打開文件之間的關(guān)系》
  • [2] 《Linux 進(jìn)程、線程、文件描述符的底層原理》

其他參考資料

  • Java IO流之文件描述符FileDescriptor
  • 文件描述符(File Descriptor)簡介:這文章講到文件描述符限制的相關(guān)命令,非常實用!
  • 如何查找 jdk 中的 native 實現(xiàn)]:查看 JDK 源碼必備技能
  • 高級Java工程師必備 ----- 深入分析 Java IO (三):介紹Java IO 類庫的好文
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容