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

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