zoukankan      html  css  js  c++  java
  • 从系统IO与标准IO看JAVA IO

      首先了解一下系统 I/O 与标准 I/O 的区别:

      系统I/O,又称文件I/O,或是内核态I/O,引用文件的方式是通过文件描述符,一个文件对应一个文件描述符。一个文件描述符用一个非负整数表示,0、1、2系统默认表示标准输入、标准输出、标准错误,某些UNIX系统规定了描述符的上限值OPEN_MAX,这些常量都定义在头文件<unistd.h>中。当读或写一个文件时,使用open或create系统调用返回的文件描述符标识该文件,并将其作为参数传递给read或write系统调用。

      标准I/O,又叫用户态I/O,引用文件的方式则是通过文件流(stream),一般用fopen和freopen函数打开一个流,返回一个指向FILE对象的指针,其他函数如果要引用这个流,则将FILE指针作为参数传递。一个进程预定义了三个流,并且这三个流自动被进程使用,它们是标准输入流、标准输出流和标准出错流,这三个流和系统I/O所规定的三个文件描述符所引用的文件相同。当读或写一个文件时,不像系统I/O,仅定义了read和write两个系统调用函数,标准I/O定义了多个函数,程序员可以根据自己的需求灵活使用。这些函数可以分为每次一个字符的I/O,每次一行的I/O和直接I/O(或者二进制I/O、一次一个对象I/O、面向记录的I/O、面向结构的I/O)。

      一个 fopen 的使用样例:

    #include <stdio.h>
    #include <stdlib.h>
    #define N 100
    int main() {
        FILE *fp;
        char str[N + 1];
        //判断文件是否打开失败
        if ( (fp = fopen("d:\demo.txt", "rt")) == NULL ) {
            puts("Fail to open file!");
            exit(0);
        }
        //循环读取文件的每一行数据
        while( fgets(str, N, fp) != NULL ) {
            printf("%s", str);
        }
       
        //操作结束后关闭文件
        fclose(fp);
        return 0;
    }

      fp 为文件偏移位置,每次 fgets 调用都会更新该位置,以便下次调用使用。这也是 ”流“ 只能顺序操作的原因。直到遇到一个 EOF(文件结束标志) ,操作结束。

      系统I/O效率受限于read、write系统调用的次数,而系统调用次数则又受限于内核缓冲区的大小,即BUFFSIZE,通过设置不同的BUFFSIZE,系统CPU时间是不同的,其最小值出现在BUFFSIZE=4096处,原因是测试所采用的是Linux ext2文件系统,其块长为4096字节,也即缓冲区所能申请到的最大缓冲区大小,我们把4096字节看做是本次最佳I/O长度。如果继续扩大缓冲区大小,对此时间几乎没有影响。

      为什么BUFFSIZE设置为4096个字节的时候,System CPU time最小呢? 因为,read函数读入4096个字节,正好是一个块,如果大于或者小于4096,那么就越块了,这样read函数会阻塞在那里,等着文件系统去翻下一个block,这样就耽误了时间,这个时间就是由于read阻塞而浪费的系统CPU时间。这个也是很多地方讲的所谓的“4K对齐”吧。

      说到这里再提一下操作系统中数据块的概念:数据块是操作系统中文件系统的概念,因为正是操作系统内的文件系统这块负责处理文件的I/O等各种操作。文件系统不可能针对扇区进行寻址,那样的话需要维护的地址表过于庞大。所以,在文件系统级别,文件系统对自己可寻址的硬盘块进行了重新的定义,这个定义是在格式化的时候确定下来的,它必须是扇区大小的整数倍。这个在文件系统级别定义的硬盘块就是OS space allocation block size。在windows中这个叫做簇,在其他操作系统中叫做block(块)。一个块内只能存储一个文件,比如定义块为2k,那么5k的要占用3个块。操作系统的上层是应用,当应用发来I/O请求要读取一个文件时,操作系统收到请求后,首先在文件系统的地址表中找到这个文件对应硬盘上的块地址。然后,对每一个块产生一个I/O请求,发送到驱动模块处理。

      所以,对于系统I/O操作,一个最大的问题就是:需要人为控制缓存的大小及最佳I/O长度的选择,另外就是系统调用与普通函数调用相比通常需要花费更多的时间,因为系统调用具体内核要执行这样的操作:1)内核捕获调用,2)检查系统调用参数的有效性,3)在用户空间和内核空间之间传输数据。

      因此,引入标准I/O的目的就是为了通过标准I/O缓存来避免BUFFSIZE选择不当而带来的频繁的系统调用。根据用户不同的需求,选择不同的I/O函数,然后根据不同的缓存类型,自动调用malloc等缓存分配函数分配合适的缓存,等分配的缓存满之后,再调用系统I/O从标准I/O缓存向内核缓存拷贝数据,这样就进一步减少了系统调用的次数。

      但是不同的标准I/O函数,不同的缓存类型也会带来不同的效率。如上图,当选择系统最佳I/O长度,即BUFFSIZE的大小和文件系统的块长一致,可以得到最佳的时间。当选用标准I/O函数时,每次一个字符函数fgetc、fputc和每次一行函数fgets、fputs函数相比要花费较多的CPU时间,而每次单个字节调用系统I/O则花费更多的时间,如果是一个100M的文件,则要执行大概2亿次函数调用,也就引起2亿次系统调用(从用户缓冲区到内核缓冲区,再到磁盘),而fgetc版本也执行了2亿次函数调用,但只引起了大约25222次系统调用,所以,时间就大大减少了。

      综合以上,标准I/O函数虽然基于系统I/O实现,但很大程度上减少了系统调用的次数,而且不用人为关心缓冲区大小的选择,整体上提高了I/O的效率。另外,标准I/O提供了多种缓存类型,方便程序员根据不同的应用需求选择不同的缓存要求,提高了编程的灵活性,当选择无缓存时,就相当于直接调用系统I/O。

      在 JAVA 中,缓冲流的默认缓冲区大小为 8192 字节。上线提到 Linux ext2文件系统块长为4096字节,此时缓冲区设置为4096字节可以使系统I/O效率最大化。JAVA 属于应用层,其考虑很可能是适配系统 I/O 的最佳缓冲区大小。

      我们看一下 linux 系统 IO 的 read 函数描述,从一个打开的文件读取字节:

    #include <unistd.h>
    
    ssize_t read(int fd, void *buf, size_t count);

      返回值:成功返回读取的字节数,出错返回-1并设置errno,如果在调read之前已到达文件末尾,则这次read返回 0 ,参数 count 是请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读写位置向后移。注意这个读写位置和使用C标准I/O库时的读写位置有可能不同,这个读写位置是记在内核中的,而使用C标准I/O库时的读写位置是用户空间I/O缓冲区中的位置。比如用fgetc读一个字节,fgetc有可能从内核中预读1024个字节到I/O缓冲区中,再返回第一个字节,这时该文件在内核中记录的读写位置是1024,而在FILE结构体中记录的读写位置是1。注意返回值类型是ssize_t,表示有符号的size_t,这样既可以返回正的字节数、0(表示到达文件末尾)也可以返回负值-1(表示出错)。read函数返回时,返回值说明了buf中前多少个字节是刚读上来的。有些情况下,实际读到的字节数(返回值)会小于请求读的字节数count,例如:读常规文件时,在读到count个字节之前已到达文件末尾。例如,距文件末尾还有30个字节而请求读100个字节,则read返回30,下次read将返回0。

      从上面的 read 函数可以看出,进行 read 系统调用时可以指定读取字节的长度,当然这个长度是由内核指定的。在从指定大小缓冲区读取数据时,可以使用 while 循环直到 read 函数返回 -1 或 0。每次读取时,我们尽最大可能将缓冲区填满,处理缓冲区数据,处理完成后再次进行系统调用。

      系统函数每次尽量将内核缓冲区填满,JAVA IO每次尽量将用户缓冲区填满,我们在读取一个文件时,并不是一次性将文件读入内存的,而是按缓冲区大小来进行分组的系统调用,这也是为什么 JAVA 中的 read 要放在 while 循环中。其本质是基于系统调用打开一个流,只能依次向后读取,读取位置由系统函数记录,而应用层只需要在发起系统调用时,按缓冲区大小(或者没有缓冲区按字节读)传入要读取的字节数,至于将文件完整的读取出来,还需要我们在应用层编程时自行处理。

      我们来看 JAVA 中 read 与 read0 的 native 实现,摘自 https://blog.csdn.net/zuoxiaolong8810/article/details/9974525

    JNIEXPORT jint JNICALL
    Java_java_io_FileInputStream_read(JNIEnv *env, jobject this) {
        return readSingle(env, this, fis_fd);//每一个本地的实例方法默认的两个参数,JNI环境与对象的实例
    }
     
    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);
    }
    /*
        env和this参数就不再解释了
        fid就是FileInputStream类中fd属性的内存地址偏移量
        通过fid和this实例可以获取FileInputStream类中fd属性的内存地址
    */
    jint
    readSingle(JNIEnv *env, jobject this, jfieldID fid) {
        jint nread;//存储读取后返回的结果值
        char ret;//存储读取出来的字符
        FD fd = GET_FD(this, fid);//这个获取到的FD其实就是之前handle属性的值,也就是文件的句柄
        if (fd == -1) {
            JNU_ThrowIOException(env, "Stream Closed");
            return -1;//如果文件句柄等于-1,说明文件流已关闭
        }
        nread = (jint)IO_Read(fd, &ret, 1);//读取一个字符,并且赋给ret变量
        //以下根据返回的int值判断读取的结果
        if (nread == 0) { /* EOF */
            return -1;//代表流已到末尾,返回-1
        } else if (nread == JVM_IO_ERR) { /* error */
            JNU_ThrowIOExceptionWithLastError(env, "Read error");//IO错误
        } else if (nread == JVM_IO_INTR) {
            JNU_ThrowByName(env, "java/io/InterruptedIOException", NULL);//被打断
        }
        return ret & 0xFF;//与0xFF做按位的与运算,去除高于8位bit的位
    }
    /*
        fd就是handle属性的值
        buf是收取读取内容的数组
        len是读取的长度,可以看到,这个参数传进来的是1
        函数返回的值代表的是实际读取的字符长度
    */
    JNIEXPORT
    size_t
    handleRead(jlong fd, void *buf, jint len)
    {
        DWORD read = 0;
        BOOL result = 0;
        HANDLE h = (HANDLE)fd;
        if (h == INVALID_HANDLE_VALUE) {//如果句柄是无效的,则返回-1
            return -1;
        }
    //都是WIN API的函数,可以百度搜索它的作用与参数详解,理解它并不难
        result = ReadFile(h,          /* File handle to read */  //文件句柄
                          buf,        /* address to put data */  //存放数据的地址
                          len,        /* number of bytes to read */  //要读取的长度
                          &read,      /* number of bytes read */  //实际读取的长度
                          NULL);      /* no overlapped struct */  //只有对文件进行重叠操作时才需要传值
        if (result == 0) {//如果没读取出来东西,则判断是到了文件末尾返回0,还是报错了返回-1
            int error = GetLastError();
            if (error == ERROR_BROKEN_PIPE) {
                return 0; /* EOF */
            }
            return -1;
        }
        return read;
    }

      

  • 相关阅读:
    题解——[[SHOI2010]最小生成树]
    7.12周总结
    还有5个月就NOIP2019了,我干了什么
    【CQOI2018】破解D-H协议
    【SHOI2006】仙人掌
    【HNOI/AHOI2018】道路
    2019.11纪中集训 宋新波老师和曹天佑学长的勉励
    纪中集训2019.11.05
    【2019.10.25 OI-Killer的模拟赛】3.鸡数
    【华东师附国庆模拟赛】Day2 1.矩阵
  • 原文地址:https://www.cnblogs.com/niuyourou/p/12749226.html
Copyright © 2011-2022 走看看