从系统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)。

  系统I/O效率受限于read、write系统调用的次数,而系统调用次数则又受限于内核缓冲区的大小,即BUFFSIZE,通过设置不同的BUFFSIZE,系统CPU时间是不同的,其最小值出现在BUFFSIZE=4096处,原因是测试所采用的是Linux ext2文件系统,其块长为4096字节,也即缓冲区所能申请到的最大缓冲区大小,我们把4096字节看做是本次最佳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;
}

  

猜你喜欢

转载自www.cnblogs.com/niuyourou/p/12749226.html
Io
IO: