Java文件描述符

文件描述符

在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打开文件的过程总结如下:

  • 创建 FileDescriptor 对象

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

  • 调用 native 方法 open, 打开文件

  • 内部调用 handleOpen 打开文件,返回文件描述符 fd

初始化 FileDescriptor 对象

  • 将 文件描述符 fd 设置到,FileDescriptor 对象的 fd 中

再谈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 解惑

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,581评论 18 399
  • 第一章 Nginx简介 Nginx是什么 没有听过Nginx?那么一定听过它的“同行”Apache吧!Ngi...
    JokerW阅读 32,642评论 24 1,002
  • 一.管道机制(pipe) 1.Linux的fork操作 在计算机领域中,尤其是Unix及类Unix系统操作系统中,...
    Geeks_Liu阅读 3,680评论 1 9
  • 1三个相关数据结构. 关于socket的创建,首先需要分析socket这个结构体,这是整个的核心。 104 str...
    ice_camel阅读 2,802评论 1 8
  • 虚拟文件系统 虚拟文件系统(VFS)作为内核子系统,为用户空间程序提供了文件和文件系统相关的接口。系统中所有的文件...
    大雄good阅读 862评论 0 4