第3章 文件I/O

  1. 引言unix系统中的大多数文件I/O 只需要用到5个函数:open、read、write、lseek以及close
  2. 文件描述符
    用于描述文件打开或者创建之后返回的一个非负整数,可以理解为文件的id
    特殊用途的标号: 0:标准输入、1:标准输出  、 2:标准错误
    早期系统一般一个进程最多打开16个文件,现在增加到63个,而有些如mac等可以增加到无限
  3. 函数open和openat
    调用open或openat函数可以打开或创建一个文件
    #include <fcntl.h> 
     
    int open(const char *path, int oflag,... /* mode_t mode */);   
     
    int openat(int fd, const char *path, int oflag, ... /* mode_t mode */ );  

    两函数的返回值:若成功,返回文件描述符;若出错,返回−1 

  4. 函数creat
    也可调用creat函数创建一个新文件。
    #include <fcntl.h> 
    int creat(const char *path, mode_t mode); 
    注意,此函数等效于:
    
    open(path, O_WRONLY|O_CREAT|O_TRUNC, mode); 

    在早期的UNIX系统版本中,open的第二个参数只能是0、1或2。无法打开一个尚未存在的文件,因此需要另一个系统调用creat以创建新文件。现在,open函数提供了选项O_CREAT和O_TRUNC,于是也就不再需要单独的creat函数。

    creat的一个不足之处是它以只写方式打开所创建的文件。在提供open的新版本之前,如果要创建一个临时文件,并要先写该文件,然后又读该文件,则必须先调用creat、close,然后再调用open。现在则可用下列方式调用open实现:
    open(path, O_RDWR|O_CREAT|O_TRUNC, mode); 

  5. 函数close
    可调用close函数关闭一个打开文件。
     
    #include <unistd.h>   
     
    int close (int fd);  
     
    返回值:若成功,返回0;若出错,返回−1 
    关闭一个文件时还会释放该进程加在该文件上的所有记录锁。

    当一个进程终止时,内核自动关闭它所有的打开文件。很多程序都利用了这一功能而不显式地用close关闭打开文件。
  6. 函数lseek
    可以调用lseek显式地为一个打开文件设置偏移量。
    #include <unistd.h>   
     
    off_t lseek(int fd, off_t offset, int whence);  
    返回值:若成功,返回新的文件偏移量;若出错,返回为−1 
    对参数offset的解释与参数whence的值有关。

    若whence是SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节。
    若whence是SEEK_CUR,则将该文件的偏移量设置为其当前值加offset,offset可为正或负。
    若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可正可负。
  7. 函数read
    调用read函数从打开文件中读数据。
    #include <unistd.h> 
    ssize_t read(int fd, void *buf, size_t nbytes);  
    返回值:读到的字节数,若已到文件尾,返回0;若出错,返回-1 
  8. 函数write
    调用write函数向打开文件写数据。
    #include <unistd.h> 
    ssize_t write(int fd, const void *buf, size_t nbytes);  
    返回值:若成功,返回已写的字节数;若出错,返回-1 
  9. I/O的效率

    用图3-5的程序读文件,其标准输出被重新定向到/dev/null上。此测试所用的文件系统是Linux ext4文件系统,其磁盘块长度为4096字节。这也证明了图3-6中系统CPU时间的几个最小值差不多出现在BUFFSIZE为4?096及以后的位置,继续增加缓冲区长度对此时间几乎没有影响。

    大多数文件系统为改善性能都采用某种预读(read ahead)技术。当检测到正进行顺序读取时,系统就试图读入比应用所要求的更多数据,并假想应用很快就会读这些数据。预读的效果可以从图3-6中看出,缓冲区长度小至32字节时的时钟时间与拥有较大缓冲区长度时的时钟时间几乎一样。

  10. 文件共享
    内核使用3种数据结构表示打开文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
    (1)每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
    a.文件描述符标志(close_on_exec);
    b.指向一个文件表项的指针。
    (2)内核为所有打开文件维持一张文件表。每个文件表项包含:
    a.文件状态标志(读、写、添写、同步和非阻塞等,);
    b.当前文件偏移量;
    c.指向该文件v节点表项的指针。
    (3)每个打开文件(或设备)都有一个v节点(v-node)结构。v节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点(i-node,索引节点)。这些信息是在打开文件时从磁盘上读入内存的,所以,文件的所有相关信息都是随时可用的。例如,i节点包含了文件的所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等(4.14节较详细地说明了典型UNIX系统文件系统,并将更多地介绍i节点)。
    图3-7显示了一个进程对应的3张表之间的关系。该进程有两个不同的打开文件:一个文件从标准输入打开(文件描述符0),另一个从标准输出打开(文件描述符为1)。

    从UNIX系统的早期版本[Thompson 1978]以来,这3张表之间的关系一直保持至今。这种关系对于在不同进程之间共享文件的方式非常重要。在以后的章节中涉及其他文件共享方式时还会回到这张图上来。
    如果两个独立进程各自打开了同一文件,则有图3-8中所示的关系。

    我们假定第一个进程在文件描述符3上打开该文件,而另一个进程在文件描述符4上打开该文件。打开该文件的每个进程都获得各自的一个文件表项,但对一个给定的文件只有一个v节点表项。之所以每个进程都获得自己的文件表项,是因为这可以使每个进程都有它自己的对该文件的当前偏移量。


    给出了这些数据结构后,现在对前面所述的操作进一步说明。


    在完成每个write后,在文件表项中的当前文件偏移量即增加所写入的字节数。如果这导致当前文件偏移量超出了当前文件长度,则将i节点表项中的当前文件长度设置为当前文件偏移量(也就是该文件加长了)。


    如果用O_APPEND标志打开一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有追加写标志的文件执行写操作时,文件表项中的当前文件偏移量首先会被设置为i节点表项中的文件长度。这就使得每次写入的数据都追加到文件的当前尾端处。


    若一个文件用lseek定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为i节点表项中的当前文件长度(注意,这与用O_APPEND标志打开文件是不同的,详见3.11节)。


    lseek函数只修改文件表项中的当前文件偏移量,不进行任何I/O操作。

  11. 原子操作
    1)追加文件打开方式是原子操作 (防止多线程破坏,如果是先seek在write会出现冲突)
    2)调用pread相当于调用lseek后调用read,但是pread又与这种顺序调用有下列重要区别。
    调用pread时,无法中断其定位和读操作。
    不更新当前文件偏移量。
    调用pwrite相当于调用lseek后调用write,但也与它们有类似的区别。
    #include <unistd.h> 
     
    ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);   
     
    返回值:读到的字节数,若已到文件尾,返回0;若出错,返回−1  
     
    ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);  
     
    返回值:若成功,返回已写的字节数;若出错,返回−1 
    3)一般而言,原子操作(atomic operation)指的是由多步组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
  12. 函数dup和dup2
    #include <unistd.h> 
    int dup(int fd);  
    int dup2(int fd, int fd2);  
    两函数的返回值:若成功,返回新的文件描述符;若出错,返回-1 
    由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。对于dup2,可以用fd2参数指定新描述符的值。如果fd2已经打开,则先将其关闭。如若fd等于fd2,则dup2返回fd2,而不关闭它。否则,fd2的FD_CLOEXEC文件描述符标志就被清除,这样fd2在进程调用exec时是打开状态。
    dup(fd); 
    等效于 
    
    fcntl (fd, F_DUPFD, 0); 
    而调用 
    
    dup2(fd, fd2); 
    等效于
    
    close(fd2);  
    fcntl(fd, F_DUPFD, fd2); 
  13. 函数sync、fsync和fdatasync
    传统的UNIX系统实现在内核中设有缓冲区高速缓存或页高速缓存,大多数磁盘I/O都通过缓冲区进行。当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式被称为延迟写(delayed write)(Bach[1986]的第3章详细讨论了缓冲区高速缓存)。

    通常,当内核需要重用缓冲区来存放其他磁盘块数据时,它会把所有延迟写数据块写入磁盘。为了保证磁盘上实际文件系统与缓冲区中内容的一致性,UNIX系统提供了sync、fsync和fdatasync三个函数。
    #include<unistd.h> 
    int fsync(int fd);  
    int fdatasync(int fd);  
    返回值:若成功,返回0;若出错,返回-1 
    void sync(void);
    sync只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。
    通常,称为update的系统守护进程周期性地调用(一般每隔30秒)sync函数。这就保证了定期冲洗(flush)内核的块缓冲区。命令sync(1)也调用sync函数。
    fsync函数只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束才返回。fsync可用于数据库这样的应用程序,这种应用程序需要确保修改过的块立即写到磁盘上。
    fdatasync函数类似于fsync,但它只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性。
  14. 函数fcntl
    fcntl函数可以改变已经打开文件的属性。
    #include<fcntl.h> 
    int fcntl(int fd, int cmd, ... /* int arg */);  
    返回值:若成功,则依赖于cmd(见下);若出错,返回-1 
    fcntl函数有以下5种功能。
    (1)复制一个已有的描述符(cmd = F_DUPFD或F_DUPFD_CLOEXEC)。
    (2)获取/设置文件描述符标志(cmd = F_GETFD或F_SETFD)。
    (3)获取/设置文件状态标志(cmd = F_GETFL或F_SETFL)。
    (4)获取/设置异步I/O所有权(cmd = F_GETOWN或F_SETOWN)。
    (5)获取/设置记录锁(cmd = F_GETLK、F_SETLK或F_SETLKW)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值