read系统调用,mmap系统调用

【转】read系统调用,mmap系统调用 

2012-07-23 09:54:28|  分类: Linux |  标签:linux  文件系统  虚拟内存  存储系统   |字号 订阅

一般情况下,操作文件既可以使用标准I/O,也可直接使用系统调用。两者有何区别呢?

在输入输出中,直接使用底层的系统调用效率是非常低的,为什么?

(1)  系统调用会影响系统性能。执行系统调用时,Linux必须从用户态代码

     切换到内核态,然后再返回用户代码。

(2)  硬件会对底层系统调用一次所能读写的数据块做出一定的限制。



带缓存的文件操作是标准C 库的实现,第一次调用带缓存的文件操作函数时标准库会自动分配内存并且读出一段固定大小的内容存储在缓存中。所以以后每次的读写操作并不是针对硬盘上的文件直接进行的,而是针对内存中的缓存的。何时从硬盘中读取文件或者向硬盘中写入文件有标准库的机制控制。不带缓存的文件操作通常都是系统提供的系统调用,更加低级,直接从硬盘中读取和写入文件,由于IO瓶颈的原因,速度并不如意,而且原子操作需要程序员自己保证,但使用得当的话效率并不差。另外标准库中的带缓存文件IO 是调用系统提供的不带缓存IO实现的。

“术语不带缓冲指的是每个read和write都调用嗯内核中的一个系统调用。所有的磁盘I/O都要经过内核的块缓冲(也称内核的缓冲区高速缓存),唯一例外的是对原始磁盘设备的I/O。既然read或write的数据都要被内核缓冲,那么术语“不带缓冲的I/O“指的是在用户的进程中对这两个函数不会自动缓冲,每次read或write就要进行一次系统调用。“--------摘自<unix环境编程>

库函数与系统调用的层次关系

open、read、write、close等系统函数称为无缓冲I/O(Unbuffered I/O )函数,因为它
们位于C 标准库的I/O 缓冲区的底层 。用户程序在读写文件时既可以调用C 标准I/O 库函
数,也可以直接调用底层的Unbuffered I/O 函数,那么用哪一组函数好呢?

     用Unbuffered I/O 函数每次读写都要进内核,调一个系统调用比调一个用户空间的函
      数要慢很多,所以在用户空间开辟I/O 缓冲区还是必要的,用C 标准I/O 库函数就比
      较方便,省去了自己管理I/O 缓冲区的麻烦。

     用C 标准I/O 库函数要时刻注意I/O 缓冲区和实际文件有可能不一致,在必要时需调
      用 fflush(3)。

     我 们知道UNIX 的传统是Everything is a file,I/O 函数不仅用于读写常规文件,也用
      于读写设备,比如终端或网络设备。在读写设备时通常是不希望  缓冲的,例如向代
      表网络设备的文件写数据就 是希望数据通过网络设备发送出去,而不希望只写到缓
      冲区里就算完事儿了,当网络设备接收到数据时应用程序也希望第一时间被通知到,
      所以网络编程通常直接调 用Unbuffered I/O 函数。

C 标准库函数是C 标准的一部分,而Unbuffered I/O 函数是UNIX 标准的一部分,在所 支
持C 语言的平台上应该都可以用C 标准库函数(除了  些平台的C 编译器没有完全符合C
标准之外),而只  在 UNIX 平台上才能使用Unbuffered I/O 函数,所以C 标准I/O 库函数
在头文件 stdio.h中声明,而read、write等函数在头文件unistd.h中声明。在支持
C 语言的非UNIX 操作系统上,标准I/O 库的底层可能由另外一组系统函数支持,例如
Windows 系统的底层是Win32 API,其中读写文件的系统函数是ReadFile、WriteFile。



先看两段代码:

//使用fopen库函数,每次读取1个字节

// 标准库函数是带缓冲的
fread.c

[cpp] view plaincopy

    #include <stdio.h> 
    #include <stdlib.h> 
    void main() 
    { 
        FILE *pf = fopen("test.file", "r"); 
        char buf[2] = {0}; 
        int ret = 0; 
        do { 
            ret = fread(buf, 1, 1, pf); 
        }while(ret); 
    }  



read.c

[cpp] view plaincopy

    #include <stdio.h> 
    #include <stdlib.h> 
    void main() 
    { 
        int fd = open("test.file", 0); 
        char buf[2] = {0}; 
        int ret = 0; 
        do { 
            ret = read(fd, buf, 1); 
        }while(ret); 
    } 



将它们编译后得到的可执行程序fread和read分别在同一台PC(linux系统)上执行,得到的如果如下:

[c-sharp] view plaincopy

    [xiangy@compiling-server test_read]$ time ./fread 
    real    0m0.603s 
    user    0m0.597s 
    sys     0m0.006s 
    [xiangy@compiling-server test_read]$ time ./read 
    real    0m15.240s 
    user    0m3.847s 
    sys     0m11.392s 
    [xiangy@compiling-server test_read]$ ll test.file 
    -rw-r--r-- 1 xiangy svx8004 23955531 Sep 24 17:17 test.file  



发现没有?fread与read的效率差有数十倍之多!可见啊~ read一个字节这种写法是相当不可取的!

但是,事情为什么会是这样的呢?让我们用strace来看看:

[c-sharp] view plaincopy

    [xiangy@compiling-server test_read]$ strace ./fread 
    execve("./fread", ["./fread"], [/* 34 vars */]) = 0 
    …… 
    read(3, "BZh91AY&SY/20v/322/25/4/320/240/177/377/377/377/377/376"..., 4096) = 4096 
    …… 
    [xiangy@compiling-server test_read]$ strace ./read 
    execve("./read", ["./read"], [/* 34 vars */]) = 0 
    …… 
    read(3, "B", 1)                         = 1 
    …… 



看到了吧~fread库函数在内部做了缓存,每次读取4096个字节;而read就老老实实一个字节一个字节地读……

那么再想想,我们读的是什么?是磁盘。难道上面提到的差异,就是因为这4096倍的读磁盘次数差而引起的吗?并不是这样。
磁 盘是块设备,每次读取的最小单位是块。而当我们通过系统调用读一个字节时,linux会怎么做呢?它会是读取一个块、然后返回一个字节、再把其余字节都丢 掉吗?当然不会,这样的操作系统也太拙劣了……换个角度想一想,如果真是每read一个字节就操作一次磁盘去读一个块,那么上面的test.file有 24M之大,近两千四百万次的磁盘读操作也不大可能会在15秒钟完成吧~
实际上linux的文件系统层(fs层)不但不是这样拙劣,反而很高明。不仅会将每次读的一整块数据缓存下来,还有预读机制(一次预读多个块,以减少磁盘寻道时间)。

那么,fread与read执行的效率差来自于哪里呢?实际上就是来自于4096倍的系统调用次数差!fread库函数中缓存的作用并不是减少读磁盘的次数,而是减少系统调用的次数。

由此可见,系统调用比起普通函数调用有很大的开销,编写代码时应当注意尽量减少系统调用的使用。



为了进一步减少系统调用的次数,关于读文件的这个问题,我们还可以这样做:

mmap.c

[cpp] view plaincopy

    #include <stdio.h> 
    #include <stdlib.h> 
    #include <sys/types.h> 
    #include <sys/stat.h> 
    #include <unistd.h> 
    #include <sys/mman.h> 
    void main() 
    { 
        int fd = open("test.file", 0); 
        struct stat statbuf; 
        char *start; 
        char buf[2] = {0}; 
        int ret = 0; 
        fstat(fd, &statbuf); 
        start = mmap(NULL, statbuf.st_size, PROT_READ, MAP_PRIVATE, fd, 0); 
        do { 
            *buf = start[ret++]; 
        }while(ret < statbuf.st_size); 
    } 



同样是遍历整个文件,但是读文件的过程中不需要使用系统调用。(原理是:mmap的执行,仅仅是在内核中建立了文件与虚拟内存空间的对应关系。用户访问这些虚拟内存空间时,页面表里面是没有这些空间的表项的,于是产生缺页异常。内核捕捉这些异常,逐渐将文件读入。)



将其编译后得到的可执行程序mmap和之前的fread、read分别在同一台PC上执行,得到的如果如下:

[c-sharp] view plaincopy

    [xiangy@compiling-server test_read]$ time ./fread 
    real    0m0.901s 
    user    0m0.892s 
    sys     0m0.010s 
    [xiangy@compiling-server test_read]$ time ./mmap 
    real    0m0.112s 
    user    0m0.106s 
    sys     0m0.006s 
    [xiangy@compiling-server test_read]$ time ./read 
    real    0m15.549s 
    user    0m3.933s 
    sys     0m11.566s 
    [xiangy@compiling-server test_read]$ ll test.file 
    -rw-r--r-- 1 xiangy svx8004 23955531 Sep 24 17:17 test.file  



mmap方式与fread方式相比,效率还要高出几倍。

你可能感兴趣的:(编程)