使用mmap()和使用read()write()实现文件拷贝的对比

本文对比了使用mmap和read/write两种方法进行文件拷贝的效率。通过实验发现,当缓冲区大小设置不合理时,read/write方法可能远慢于mmap方法;但在合理设置下,两者的效率相近甚至read/write更快。文章深入探讨了背后的原因,并提供了源代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近上课老师说了这样一句话:mmap()内存映射可以实现文件的拷贝,并且速度明显快于一般的文件拷贝,于是
我试着实现了两种文件拷贝所花费时间的比较,首先看代码:

include

include

include

include

include

include

include

include

include

include

define BUFFER_SIZE 1

void my_copy1()
{
int fin,fout; //文件描述符
void *start;
void *end;
struct stat sb;
if((fin = open(“file.in”,O_RDONLY)) < 0){
perror(“open error”);
exit(EXIT_FAILURE);
}
if((fout = open( “file.out”,O_RDWR | O_CREAT | O_TRUNC,00600)) < 0 ){
perror( “write error” );
exit( EXIT_FAILURE );
}

fstat(fin,&sb);

//这块必须给fout一个需求大小的偏移,因为mmap没有扩展空间的能力
if(lseek(fout,sb.st_size-1,SEEK_SET) < 0 ){
    exit(EXIT_FAILURE);
}
if(write(fout, &sb,1) != 1 ){
    exit(EXIT_FAILURE);
}    

start = mmap(NULL,sb.st_size,PROT_READ,MAP_PRIVATE,fin,0);
if(start == MAP_FAILED)
    return;

end = mmap(0,(size_t)sb.st_size,PROT_WRITE,MAP_SHARED,fout,0);
if(end == MAP_FAILED){
    return ;
}

memcpy(end,start,(size_t)sb.st_size);

munmap(start,sb.st_size); //关闭映射
munmap(end,sb.st_size); 

close(fin);
close(fout);
return;

}

void my_copy2()
{
int fin,fout;
int bytes_read,bytes_write;
char buffer[BUFFER_SIZE];
char *ptr;
if((fin = open(“file.in”,O_RDONLY)) < 0){
perror(“open error”);
exit(EXIT_FAILURE);
}
if((fout = open( “file.out”,O_RDWR | O_CREAT | O_TRUNC,00700)) < 0 ){
perror( “write error” );
exit( EXIT_FAILURE );
}

while(bytes_read=read(fin,buffer,BUFFER_SIZE)){
    if((bytes_read==-1)&&(errno!=EINTR))
        break;
    else if(bytes_read>0){
        ptr=buffer;
        while(bytes_write=write(fout,ptr,bytes_read)){
            if((bytes_write==-1)&&(errno!=EINTR))
                break;
            else if(bytes_write==bytes_read)
                break;
            else if(bytes_write>0){
                ptr+=bytes_write;
                bytes_read-=bytes_write;
            }
        }
        if(bytes_write==-1)
           break;
     }
}

close(fin);
close(fout);
return;

}

main()
{
struct timeval tv;
struct timezone tz;
long time_start,time_end;
gettimeofday(&tv,&tz);
time_start = tv.tv_sec*1000000 + tv.tv_usec;
my_copy1();
printf(“\ndone.\n\n”);
gettimeofday(&tv,&tz);
time_end = tv.tv_sec*1000000 + tv.tv_usec;
printf(“using \”mmap()\” to copy costs %ld microseconds \n”,time_end - time_start);

gettimeofday(&tv,&tz);
time_start = tv.tv_sec*1000000 + tv.tv_usec;
my_copy2();
gettimeofday(&tv,&tz);
time_end = tv.tv_sec*1000000 + tv.tv_usec;

//这块之前有时会出现打印出的负数,后来查看gettimeofday()函数,才知道,我所使用的微秒位在满一秒
//的时候会进位到秒,也即是微秒位清零。所以,正确的方法是在计算time_start和time_end的时候加上
//秒这一位,不过要首先转换其成为微秒。
printf(“using \”read() and write()\” to copy costs %ld microseconds \n”,time_end - time_start);
}

代码不是很难,中间使用了一些LinuxC的一些函数,不懂的可以自己查阅相关资料。我现在主要使想就两种
不同的拷贝的实现在所花费的时间上的一些比较以及的出我自己的一些观点,调试程序时可以将BUFFER_SIZE
随意更改一个数字,表示的是使用read函数从文件中一次读取的字符个数。当然,强调了这个必然有原因。
如果BUFFER_SIZE很小的话,最终的结果差别很大。比如我的
BUFFER_SIZE=1时我的运行结果如下:
zhou@zhou:~/LinuxC/file/mmcopy$ ./mmap

done.

using “mmap()” to copy costs 591 microseconds
using “read() and write()” to copy costs 505337 microseconds
zhou@zhou:~/LinuxC/file/mmcopyBUFFERSIZE=10000zhou@zhou: /LinuxC/file/mmcopy两个完全不是一个数量级的。下面换个数字BUFFERSIZE=10000时我的运行情况如下:zhou@zhou: /LinuxC/file/mmcopy ./mmap

done.

using “mmap()” to copy costs 594 microseconds
using “read() and write()” to copy costs 585 microseconds
zhou@zhou:~/LinuxC/file/mmcopy$
这时两个的消耗时间很接近,可以想象。如果BUFFER_SIZE定义的很大的话,那么read()write()方法将会
非常快,但是。如果你要拷贝的文件很小呢,加入只有100字节,但是你却每次申请10000个字节,这样岂不
是很浪费内存。这也就是mmap()的优势,不仅没有浪费内存,而且速度相当的快。
话题一转,这是为什么呢,我的理解使这样的:mmap首先将要拷贝的文件的内容全部映射到内存,然后写到目
的文件,总共的磁盘操作就两次,而read()write()不同,会根据你的BUFFER_SIZE定义的,然后会执行
(文件内容的总的字节数/BUFFER_SIZE)*2 次的磁盘操作,因此在这上面浪费了大量的时间。
再看另一个程序:

#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/times.h>

#define error(fmt,args...) \
    printf(fmt, ##args) ; \
    printf(":%s\n",strerror(errno))

inline int cp_rw(int srcfd,int dstfd,char *buf,int len);
inline int cp_map(int srcfd,int dstfd,size_t len);

int main(int argc,char **argv)
{
    char buf[8192];
    int srcfd,dstfd;
    clock_t start,end;
    struct tms stm,ntm;
    struct stat filestat;
    int tck;
    char cmdline[30];

    if(argc!=3)
        printf("usage: cmd ");

    tck=sysconf(_SC_CLK_TCK);


    start = times(&stm);
    if((srcfd=open(argv[1],O_RDONLY))==-1)
    {
        error("open %s error",argv[1]);
    exit(0);
    }
    if((dstfd=open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0666))==-1)
    {
        error("creat %s error",argv[2]);
    exit(0);
    }

    fstat(srcfd,&filestat);
    if(lseek(dstfd,filestat.st_size,SEEK_SET)==-1)
    {
        error("lseek error");
    exit(0);
    }
    if(write(dstfd," ",1)!=1)
    {
        error("write error");
    exit(0);
    }
    cp_map(srcfd,dstfd,filestat.st_size);
    close(srcfd);
    close(dstfd);
    end = times(&ntm);
    printf("copying %s to %s using cp_map:filesize=%lu MBytes Using %f seconds\n"
           ,argv[1],argv[2],filestat.st_size>>20,(end-start)/(double)tck); 

    sprintf(cmdline,"rm -f %s",argv[2]);
    system(cmdline);


    start = times(&stm);
    if((srcfd=open(argv[1],O_RDONLY))==-1)
    {
        error("open %s error",argv[1]);
    }
    if((dstfd=open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0666))==-1)
    {
        error("creat %s error",argv[2]);
    }
    cp_rw(srcfd,dstfd,buf,sizeof(buf));
    fstat(srcfd,&filestat);
    close(srcfd);
    close(dstfd);
    end = times(&ntm);
    printf("copying %s to %s using cp_rw:filesize=%lu MBytes Using %f seconds\n"
           ,argv[1],argv[2],filestat.st_size>>20,(end-start)/(double)tck);



    return 0;
}

inline int cp_rw(int srcfd,int dstfd,char *buf,int len)
{
    int nread;
    while((nread=read(srcfd,buf,len))>0)
    {
        if(write(dstfd,buf,nread)!=nread)
    {
     error("write error");
     return -1;
    }
    }
    if(nread ==-1)
    {
       error("read error");
       return -1;
    }
    return 0;
}

inline int cp_map(int srcfd,int dstfd,size_t len)
{
    char *src,*dst;
    if((src=mmap(0,len,PROT_READ,MAP_SHARED,srcfd,0))==MAP_FAILED)
    {
        error("mmap src error");
    return -1;
    }
    if((dst=mmap(0,len,PROT_WRITE,MAP_SHARED,dstfd,0))==MAP_FAILED)
    {
        error("mmap dst error");
    return -1;
    }
    if(memcpy(dst,src,len)==NULL)
    {
        error("memcpy error");
    return -1;
    }
    munmap(src,len);
    munmap(dst,len);
    return 0;
}

运行,拷贝一个1.1G的文件,得到如下结果
[root@garden copy]# ./copy /home/ker.tgz ./ker.tgz
copying /home/ker.tgz to ./ker.tgz using cp_map:filesize=1030 MBytes Using 61.900000 seconds
copying /home/ker.tgz to ./ker.tgz using cp_rw:filesize=1030 MBytes Using 34.330000 seconds

使用read/write的方法居然比mmap的快一倍,这是怎么回事呢?理论上mmap系统调用只进行了一次,而且拷贝文件是直接在内核空间进行的,read/write则需要通过系统调用把内核空间的缓存复制到用户空间,再将用户空间缓存复制到内核空间,拷贝次数明显多了一个呢?速度为什么于理论预测的不一致呢?

<think>好的,我现在需要比较理解在Linux或Unix系统中mmapread系统调用之间的差异及其适用场景。首先,我应该回忆一下这两个系统调用的基本功能用途。 mmap(内存映射)系统调用允许将文件或设备映射到进程的地址空间,这样文件的内容就可以像访问内存一样直接读写,而不需要通过传统的readwrite函数。而read系统调用则是从文件描述符中读取数据到用户提供的缓冲区中。两者的基本操作方式不同,一个是通过内存映射,另一个是通过显式的读写操作。 接下来,我需要考虑它们的性能差异。mmap的优势在于避免了数据在用户空间内核空间之间的复制,因为文件直接映射到内存,进程可以像访问普通内存一样访问文件数据。这对于大文件处理可能更高效,因为减少了数据拷贝的开销。而read则需要将数据从内核缓冲区复制到用户空间的缓冲区,这可能在小数据量时效率不错,但大文件时可能会有更多的开销。 另外,内存管理方面,mmap会占用进程的虚拟内存地址空间,映射大文件时需要确保有足够的地址空间。而read则不会占用虚拟内存地址,但需要用户管理缓冲区的大小位置。 使用场景方面,mmap适合需要随机访问大文件的场景,比如数据库管理系统,这样可以直接通过指针访问文件的不同部分,而不必频繁进行lseekread操作。而read适合顺序读取或处理较小文件的情况,尤其是当应用程序需要完全控制数据的读取过程时,比如网络传输或逐块处理数据。 还要考虑两者的同步问题。使用mmap时,对内存的修改会由操作系统在适当的时候写回文件,但需要注意使用msync来确保数据及时同步。而read/write则需要显式的write调用来保存更改,控制更直接,但可能需要更多的代码来处理同步。 此外,错误处理可移植性也是需要考虑的因素。read作为传统的系统调用,几乎所有系统都支持,而mmap虽然广泛存在于Unix-like系统中,但在某些嵌入式系统或旧系统上可能不可用或功能受限。 最后,参考引用中的内容,特别是引用[3]提到的混合使用mmap直接I/O可能导致性能下降,需要注意避免在同一文件上混用这两种方法,以确保系统的高效运行。</think>### mmapread系统调用的差异及适用场景 #### 1. **基本机制** - **`read`系统调用** 通过文件描述符读取数据到用户空间缓冲区,需显式调用函数并管理缓冲区的内存。每次读取时,数据从内核缓冲区复制到用户空间。例如: ```c char buf[1024]; read(fd, buf, sizeof(buf)); ``` - **`mmap`系统调用** 将文件直接映射到进程的虚拟地址空间,文件内容可通过指针直接访问,无需数据复制。例如: ```c void *addr = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0); char *data = (char *)addr; // 直接通过data指针访问文件内容 ``` #### 2. **性能差异** - **`mmap`的优势** - **零拷贝**:避免用户空间与内核空间的数据复制,适合处理大文件或频繁随机访问的场景[^3]。 - **内存管理自动化**:由操作系统负责分页缓存,减少手动缓冲管理的开销。 - **`read`的适用场景** - **小文件顺序读取**:数据量较小时,复制开销可忽略,且代码更简单。 - **流式处理**:如网络传输或逐块处理,需精确控制读取逻辑时更灵活。 #### 3. **内存与资源管理** - **`mmap`的限制** - 占用虚拟地址空间,映射超大文件时需确保系统支持(如64位系统)。 - 修改映射内存后需调用`msync()`同步到文件(若需持久化)。 - **`read`的灵活性** - 用户完全控制缓冲区大小生命周期,适合内存受限的场景。 #### 4. **适用场景对比** | **场景** | **推荐方法** | **原因** | |------------------------|-------------------|-------------------------------------------------------------------------| | 大文件随机访问(如数据库) | `mmap` | 零拷贝提升性能,指针直接操作简化代码 | | 流式读取(如日志处理) | `read` | 顺序访问无需复杂内存映射,控制更直接 | | 多进程共享文件 | `mmap`(MAP_SHARED) | 共享内存映射可高效实现进程间通信 | | 频繁小块I/O(如配置文件) | `read` | 避免映射开销,代码更轻量 | #### 5. **其他注意事项** - **混合使用的风险**:如引用[3]所述,同时使用`mmap``read`/`write`可能导致缓存一致性问题,降低性能。 - **可移植性**:`read`在所有Unix-like系统中行为一致,而`mmap`的实现细节可能因系统或文件系统而异。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值