美文网首页
四种缓冲I/O(缓冲I/O,直接I/O,内存映射,零拷贝)

四种缓冲I/O(缓冲I/O,直接I/O,内存映射,零拷贝)

作者: 小幸运Q | 来源:发表于2020-09-06 12:18 被阅读0次

    https://byvoid.com/zhs/blog/fast-readfile/
    https://www.cnblogs.com/sumuyi/p/12813787.html
    https://blog.csdn.net/weixin_37782390/article/details/103833306【Java零拷贝】


    缓冲I/O和直接I/O

    • 应用程序内存(用户空间):你malloc、new的内存
    • 用户缓冲区(用户空间):C语言里面的FILE*里面的buffer
    typedef struct
    {
      short bsize;
      ....
      unsigned char* buffer;
    }
    
    • 内核缓冲区(内核空间,内存):Linux的Page Cache,为了加快磁盘IO,将磁盘上的page(一个page一般4K)加载到内存中的内核缓冲区
    缓冲I/O:读写都是三次数据拷贝,方向相反

    磁盘<->内核缓冲区<->用户缓冲区<->应用程序内存

    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    #include<iostream>
    int main(){
        char *charFilePath="1.txt";
        FILE *pfile=fopen(charFilePath,"rb");//打开文件,返回文件操作符
        char *pread;
        size_t result;
        if(pfile)//打开文件一定要判断是否成功
        {
            fseek(pfile,0,SEEK_END);//将文件内部的指针指向文件末尾
            long lsize=ftell(pfile);//获取文件长度,(得到文件位置指针当前位置相对于文件首的偏移字节数)
            rewind(pfile);//将文件内部的指针重新指向一个流的开头
            pread=(char *) malloc(lsize*sizeof(char)+1);//申请内存空间,lsize*sizeof(char)是为了更严谨,16位上char占一个字符,其他机器上可能变化
    
            //用malloc申请的内存是没有初始值的,如果不赋值会导致写入的时候找不到结束标志符而出现内存比实际申请值大,写入数据后面跟随乱码的情况
            memset(pread,0,lsize*sizeof(char)+1);//将内存空间都赋值为‘\0’
    
            result=fread(pread,1,lsize,pfile);//将pfile中内容读入pread指向内存中
        }
        std::cout<<pread<<std::endl;
        fclose(pfile);//关掉文件操作符,和句柄一样,有open就一定有close
        free(pread);//释放内存
        pread=NULL;//指针不再使用,一定要“删除”,防止产生野指针
    }
    
    直接I/O:读写都是两次数据拷贝,方向各自相反。

    磁盘<->内核缓冲区<->应用程序内存

    #include <sys/types.h>    
    #include <sys/stat.h>    
    #include <fcntl.h>
    int main(){
        int flag;
        int in=open("1.txt",O_RDONLY,S_IRUSR);
        char buffer[1024];
        if(in==-1){
            return -1;
        }
        int out=open("2.txt",O_WRONLY|O_CREAT);
        if(out==-1){
            return -1;
        }  
        while ((flag = read(in, buffer, 1024)) > 0)  
        {     
            write(out, buffer, flag);  
        }
        close(in);  
        close(out);    
        return 0;      
    }
    
    缓冲I/O与直接I/O
    • read和fread有什么区别?

    fread会自动分配缓存FILE buffer,fread返回的是一个FILE结构指针。
    而read返回的是一个int的文件号。从测试结果看两者速度差别不是很大,但还是推荐用read。

    • fflush和fsync的区别:

    fflush用于把C标准缓冲的数据写到内核缓冲,而fsync及其其他类似的函数用于将数据从内核缓冲写进磁盘。如果在写数据后不调用fsync,断电的时候最新的部分数据会丢失在内存中。


    内存映射文件与零拷贝

    内存映射

    磁盘<->应用内存(跳过内核缓冲区)
    对变长文件不适合

    mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间

    mmap示意图 image.png
    void *mmap(void *start, size_t length, int prot, int flags, int fd, 
    off_t offset);
    

    mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。(初始化的时候无需拷贝文件到用户空间)实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存(发生缺页中断然后调页过程先在交换缓存空间(swap cache)中寻找需要访问的内存页,如果没有则调用nopage函数把所缺的页从磁盘装入到主存中),而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

    内存中的内容并不会立即更新到文件中,而是有一段时间的延迟,你可以调用msync()来显式同步一下。

    总结:常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。

    而使用mmap操作文件中,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作。而之后访问数据时发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。

    总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。说白了,mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此mmap效率更高。

    • 直接对该段内存写时不会写入超过当前文件大小的内容

    • mmap的缺点: 文件小于4k的页的大小会造成空间浪费。且无法扩展空间。

    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <sys/mman.h>
    #include <iostream>
    using namespace std;
    int main(){
        int fd;
        void *start;
        struct stat sb;
        fd = open("/etc/passwd", O_RDONLY); 
        /*打开/etc/passwd */
        cout<<fd<<endl;
        fstat(fd, &sb); /* 取得文件大小 */
    
        start = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
        if(start == MAP_FAILED) /* 判断是否映射成功 */
            return;
        cout<<start<<endl;
    
        munma(start, sb.st_size); /* 解除映射 */
        closed(fd);
    }
    
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <sys/mman.h>
    #include <stdio.h>
    #include <memory.h>
    #include <iostream>
    using namespace std;
    int main(){
        // mmap读文件
    
        // 打开文件
        int fd = open("1.txt", O_RDONLY);  
        // 读取文件长度
        int len = lseek(fd,0,SEEK_END);  
        // 建立内存映射
        char *addr = (char *) mmap(NULL, len, PROT_READ, MAP_PRIVATE,fd, 0);      
        close(fd);
        // data用于保存读取的数据
        char* data=new char(len+1);
        // 复制过来
        memcpy(data, addr, len);
        // 解除映射
        // cout<<data;
        munmap(addr, len);
    
        // mmap写文件
    
        len=strlen(data);
        // 打开文件
        fd=open("output.txt", O_RDWR|O_CREAT, 00777);
        // lseek将文件指针往后移动file_size-1位
        lseek(fd,len-1,SEEK_END);  
        // 从指针处写入一个空字符;mmap不能扩展文件长度,这里相当于预先给文件长度,准备一个空架子
        write(fd, "", 1);
        // 使用mmap函数建立内存映射
        addr = (char*)mmap(NULL, len, PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
        // 内存映射建立好了,此时可以关闭文件了
        close(fd);
        // 把data复制到addr里
        memcpy(addr, data, len);
        // 解除映射
        munmap(addr, len);
    }
    
    image.png
    零拷贝(一般用于磁盘->socket)

    跳过Socket缓冲区,把内核缓冲区当socket缓冲区用。
    mmap 适合小数据量读写,sendFile 适合大文件传输。

    image.png sendfile linux老版本2.1-2.3:2次用户态和内核态的上下文切换和3次拷贝 sendfile+DMA Scatter/Gather linux新版本2.4:2次用户态和内核态的上下文切换和2次拷贝

    新版本sendfile没有CPU拷贝,但是会传输文件描述符和数据长度给socket缓冲区,一般面试主要还是问新版本。

    1. 用户进程通过sendfile()方法向操作系统发起调用,上下文从用户态转向内核态
    2. DMA控制器利用scatter把数据从硬盘中拷贝到读缓冲区离散存储
    3. CPU把读缓冲区中的文件描述符和数据长度发送到socket缓冲区
    4. DMA控制器根据文件描述符和数据长度,使用scatter/gather把数据从内核缓冲区拷贝到网卡
    5. sendfile()调用返回,上下文从内核态切换回用户态
    
    #include <sys/sendfile.h>  
    size_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); 
    
    • sendfile() 局限于基于文件服务的网络应用程序,比如 web 服务器。据说,在 Linux 内核中实现 sendfile() 只是为了在其他平台上使用 sendfile() 的 Apache 程序
    • 由于网络传输具有异步性,很难在 sendfile () 系统调用的接收端进行配对的实现方式,所以数据传输的接收端一般没有用到这种技术。
    • sendfile方法IO数据对用户空间完全不可见,所以只能适用于完全不需要用户空间处理的情况,比如静态文件服务器。
    • 由于CPU和IO速度的差异问题,产生了DMA技术,通过DMA搬运来减少CPU的等待时间。DMA(Direct Memory Access)直接内存访问技术本质上来说就是一块主板上独立的芯片,通过它来进行内存和IO设备的数据传输,从而减少CPU的等待时间。

    DMA Scatter/Gather 分散/收集功能:

    cpu将读缓冲区中的数据描述信息--内存地址和偏移量记录到socket缓冲区,由 DMA 根据这些将数据从读缓冲区拷贝到网卡,相比之前版本减少了一次CPU拷贝的过程。但是还是需要占用数据总线。


    应用场景:

    对于文章开头说的两个场景:RocketMQ和Kafka都使用到了零拷贝的技术。

    对于MQ而言,无非就是生产者发送数据到MQ然后持久化到磁盘,之后消费者从MQ读取数据。

    对于RocketMQ来说这两个步骤使用的是mmap+write,而Kafka则是使用mmap+write持久化数据,发送数据使用sendfile。

    总结:

    以网卡到磁盘为例:

    开销类型 直接I/O 内存映射 零拷贝
    数据拷贝次数 4 3 2
    内存拷贝次数 2 (内核往返用户) 1 (内核到socket) 0 (内存与IO设备间不算内存拷贝)
    上下文切换次数 4 4 2

    相关文章

      网友评论

          本文标题:四种缓冲I/O(缓冲I/O,直接I/O,内存映射,零拷贝)

          本文链接:https://www.haomeiwen.com/subject/ysttektx.html