文件描述符
在Linux中,進(jìn)程是通過(guò)文件描述符(file descriptors,簡(jiǎn)稱(chēng)fd)而不是文件名來(lái)訪問(wèn)文件的熟尉,文件描述符實(shí)際上是一個(gè)整數(shù)弊仪。
內(nèi)核中熙卡,對(duì)應(yīng)于每個(gè)進(jìn)程都有一個(gè)文件描述符表,表示這個(gè)進(jìn)程打開(kāi)的所有文件励饵。文件描述符就是這個(gè)表的索引驳癌。
文件描述表中每一項(xiàng)都是一個(gè)指針,指向一個(gè)用于描述打開(kāi)的文件的數(shù)據(jù)塊———file對(duì)象役听,file對(duì)象中描述了文件的打開(kāi)模式颓鲜,讀寫(xiě)位置等重要信息表窘。
當(dāng)進(jìn)程打開(kāi)一個(gè)文件時(shí),內(nèi)核就會(huì)創(chuàng)建一個(gè)新的file對(duì)象甜滨。因此乐严,我們?cè)谶M(jìn)程中使用多線(xiàn)程打開(kāi)同一個(gè)文件,每個(gè)線(xiàn)程會(huì)有各自的文件描述符衣摩,每個(gè)線(xiàn)程也會(huì)有保存自己的讀取位置麦备,互不影響。
需要注意的是昭娩,file對(duì)象不是專(zhuān)屬于某個(gè)進(jìn)程的凛篙,不同進(jìn)程的文件描述符表中的指針可以指向相同的file對(duì)象,從而共享這個(gè)打開(kāi)的文件栏渺。比如呛梆,如果在調(diào)用fork之前父進(jìn)程已經(jīng)打開(kāi)文件,則fork后子進(jìn)程有一個(gè)父進(jìn)程描述符表的副本磕诊。父子進(jìn)程共享相同的打開(kāi)文件集合填物,因此共享相同的文件位置。
file對(duì)象有引用計(jì)數(shù)霎终,記錄了引用這個(gè)對(duì)象的文件描述符個(gè)數(shù)滞磺,只有當(dāng)引用計(jì)數(shù)為0時(shí),內(nèi)核才銷(xiāo)毀file對(duì)象莱褒,因此某個(gè)進(jìn)程關(guān)閉文件击困,不影響與之共享同一個(gè)file對(duì)象的進(jìn)程。
每個(gè)file結(jié)構(gòu)體都指向一個(gè)file_operations結(jié)構(gòu)體广凸,這個(gè)結(jié)構(gòu)體的成員都是函數(shù)指針阅茶,指向?qū)崿F(xiàn)各種文件操作的內(nèi)核函數(shù)。比如在用戶(hù)程序中read一個(gè)文件描述符谅海,read通過(guò)系統(tǒng)調(diào)用進(jìn)入內(nèi)核脸哀,然后找到這個(gè)文件描述符所指向的file結(jié)構(gòu)體,找到file結(jié)構(gòu)體所指向的file_operations結(jié)構(gòu)體扭吁,調(diào)用它的read成員所指向的內(nèi)核函數(shù)以完成用戶(hù)請(qǐng)求撞蜂。在用戶(hù)程序中調(diào)用lseek、read侥袜、write蝌诡、ioctl、open等函數(shù)系馆,最終都由內(nèi)核調(diào)用file_operations的各成員所指向的內(nèi)核函數(shù)完成用戶(hù)請(qǐng)求送漠。file_operations結(jié)構(gòu)體中的release成員用于完成用戶(hù)程序的close請(qǐng)求,之所以叫release而不叫close是因?yàn)樗灰欢ㄕ娴年P(guān)閉文件由蘑,而是減少引用計(jì)數(shù)闽寡,只有引用計(jì)數(shù)減到0才關(guān)閉文件。
file對(duì)象中包含一個(gè)指針尼酿,指向dentry對(duì)象爷狈。“dentry”是directory entry(目錄項(xiàng))的縮寫(xiě)裳擎,dentry對(duì)象代表一個(gè)獨(dú)立的文件路徑涎永,如果一個(gè)文件路徑被打開(kāi)多次,那么會(huì)建立多個(gè)file對(duì)象鹿响,但它們都指向同一個(gè)dentry對(duì)象羡微。為了減少讀盤(pán)次數(shù),內(nèi)核緩存了目錄的樹(shù)狀結(jié)構(gòu)惶我,稱(chēng)為dentry cache妈倔,其中每個(gè)節(jié)點(diǎn)是一個(gè)dentry結(jié)構(gòu)體。
每個(gè)dentry結(jié)構(gòu)體都有一個(gè)指針指向inode結(jié)構(gòu)體绸贡。inode結(jié)構(gòu)體保存著從磁盤(pán)inode讀上來(lái)的信息盯蝴。在上圖的例子中,有兩個(gè)dentry听怕,分別表示/home/akaedu/a和/home/akaedu/b捧挺,它們都指向同一個(gè)inode,說(shuō)明這兩個(gè)文件互為硬鏈接尿瞭。inode結(jié)構(gòu)體中保存著從磁盤(pán)分區(qū)的inode讀上來(lái)信息闽烙,例如所有者、文件大小声搁、文件類(lèi)型和權(quán)限位等鸣峭。
每個(gè)進(jìn)程剛剛啟動(dòng)的時(shí)候,文件描述符0是標(biāo)準(zhǔn)輸入酥艳,1是標(biāo)準(zhǔn)輸出摊溶,2是標(biāo)準(zhǔn)錯(cuò)誤。如果此時(shí)去打開(kāi)一個(gè)新的文件充石,它的文件描述符會(huì)是3莫换。
java中的FileDescriptor
在java中,有著與文件描述符對(duì)應(yīng)的一個(gè)類(lèi)對(duì)象:FileDescriptor骤铃。我們看一下FileDescriptor與Channel的關(guān)系:
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)
參數(shù)fd就是FileDescriptor實(shí)例拉岁。
看一下他是怎么產(chǎn)生的:
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()
這個(gè)靜態(tài)方法:
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
類(lèi)加載階段,fis_fd
就被初始化了惰爬,fid_fd
相當(dāng)于是FileInputStream.fd
字段的一個(gè)內(nèi)存偏移量喊暖,便于在必要時(shí)操作內(nèi)存給它賦值。
看一下FileDescriptor的實(shí)例化過(guò)程:
public /**/ FileDescriptor() {
fd = -1;
handle = -1;
useCount = new AtomicInteger();
}
static {
initIDs();
}
FileDescriptor也有一個(gè)initIDs
撕瞧,他和FileInputStream.initIDs
的方法類(lèi)似陵叽,把設(shè)置IO_fd_fdID
為FileDescriptor.fd
字段的內(nèi)存偏移量狞尔。
/* 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");
}
接下來(lái)再看FileInputStream
構(gòu)造函數(shù)中的open(name)
方法,字面上看巩掺,這個(gè)方法打開(kāi)了一個(gè)文件偏序,他也是一個(gè)本地方法,open方法直接調(diào)用了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
// 打開(kāi)一個(gè)文件并獲取到文件描述符
fd = handleOpen(ps, flags, 0666);
if (fd != -1) {
SET_FD(this, fd, fid);
} else {
throwFileNotFoundException(env, path);
}
} END_PLATFORM_STRING(env, ps);
}
其中的handleOpen函數(shù)打開(kāi)了一個(gè)文件描述符研儒,相當(dāng)于和文件建立了聯(lián)系,并且將返回的文件描述符描述符賦值給了局部變量fd,然后調(diào)用了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
字段的內(nèi)存偏移量端朵。這個(gè)方法相當(dāng)于設(shè)置FileDescriptor.fd
的值等于文件描述符fd。
需要注意的是燃箭,F(xiàn)ileDescriptor有兩個(gè)字段:handle和fd冲呢,上面的代碼表示我們只設(shè)置了fd字段為文件描述符,沒(méi)有提到handle字段遍膜,這是因?yàn)椋?/p>
在 win32 的實(shí)現(xiàn)中將 創(chuàng)建好的 文件句柄 設(shè)置到 handle 字段碗硬,在 linux 版本中則使用的是 FileDescriptor 的 fd 字段。
由此瓢颅,可知 handle 和 fd 是共存的但并不同時(shí)在使用恩尾,在 win32 平臺(tái)上使用 handle 字段,在 linux 平臺(tái)上使用 fd 字段挽懦。
所以翰意,F(xiàn)ileInputStream打開(kāi)文件的過(guò)程總結(jié)如下:
- 創(chuàng)建 FileDescriptor 對(duì)象
每一個(gè) FileInputStream 有一個(gè) FileDescriptor,代表這個(gè)流底層的文件的fd
調(diào)用 native 方法 open, 打開(kāi)文件
內(nèi)部調(diào)用 handleOpen 打開(kāi)文件信柿,返回文件描述符 fd
初始化 FileDescriptor 對(duì)象
- 將 文件描述符 fd 設(shè)置到冀偶,F(xiàn)ileDescriptor 對(duì)象的 fd 中
再談java文件讀取
在java-NIO-Buffer這篇文章中我們提到了FileInputStream.read
方法,再來(lái)回顧一下:
JNIEXPORT jint JNICALL
Java_java_io_FileInputStream_readBytes(JNIEnv *env, jobject this,
jbyteArray bytes, jint off, jint len) {//除了前兩個(gè)參數(shù)渔嚷,后三個(gè)就是readBytes方法傳遞進(jìn)來(lái)的进鸠,字節(jié)數(shù)組、起始位置形病、長(zhǎng)度三個(gè)參數(shù)
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分配的直接緩沖區(qū)客年,也就是堆外內(nèi)存
if (nread > 0) {
(*env)->SetByteArrayRegion(env, bytes, off, nread, (jbyte *)buf);// 將直接緩沖區(qū)的內(nèi)容copy到bytes數(shù)組中
} 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
字段的內(nèi)存偏移量漠吻。注意到fd = GET_FD(this, fid);
這個(gè)方法量瓜,獲取到其對(duì)應(yīng)的文件描述符,然后使用該文件描述符讀取文件內(nèi)容途乃,填充緩沖區(qū)绍傲。由此可見(jiàn),java底層讀取文件都是通過(guò)文件描述符來(lái)進(jìn)行的耍共。比如:
文章開(kāi)始提到每個(gè)進(jìn)程剛剛啟動(dòng)的時(shí)候烫饼,文件描述符0是標(biāo)準(zhǔn)輸入猎塞,1是標(biāo)準(zhǔn)輸出,2是標(biāo)準(zhǔn)錯(cuò)誤枫弟。如果此時(shí)去打開(kāi)一個(gè)新的文件邢享,它的文件描述符會(huì)是3鹏往,F(xiàn)ileDescriptor中的fd為0淡诗,1,2時(shí)也表示同樣的意義伊履。
FileOutputStream fileOutputStream = new FileOutputStream(FileDescriptor.out);
fileOutputStream.write('hello world');// 控制臺(tái)打印 hello world韩容,因?yàn)閒ileOutputStream使用了標(biāo)準(zhǔn)輸出的文件描述符