本文將深入理解文件描述符,并從 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 是錯誤。如下圖所示:
所以,在 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 類庫的好文