• UNIX_文件I/O


    lseek函数

    off_t lseek(int filedes, off_t offset, int whence);

    whence:  SEEK_SET, 表示将文件的偏移量设置为距文件开始offset个字节。

         SEEK_CUR, 当前+offset个字节,offset可正可负。

         SEEK_END, 文件长度+offset个字节,可正可负。

    成功,返回新的文件偏移量;失败,返回-1。

    lseek仅将当前的文件偏移量记录在内核中,并不引起任何I/O操作。然后,该偏移量用于下一个读写操作。

    空洞

    offset可以大于文件的当前长度,这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这是允许的。位于文件中,但是没有写过的字节都被读为0。

    空洞并不占用磁盘空间。具体处理方式与文件的实现有关,当定位到超出文件尾端后写时,新写的数据需要分配磁盘块,但对于原文件尾端和新开始写的位置之间的部分,则不需要分配空间

    有空洞文件与无空洞文件的长度虽然可以相同,but,它们实际占用的磁盘块数是不同的。

    文件共享

    UNIX支持在不同进程间共享打开的文件。内核使用三种数据结构表示打开的文件。

    1. 进程表项
      • 文件描述符标志
      • 指向文件表项目的指针
    2. 文件表项
      • 文件状态标志(such as read, write, append, sync, and nonblocking)
      • 当前文件偏移量
      • 指向该文件V节点的指针
    3. V-node表项
      • v节点信息(包含文件类型,对此文件进程各种操作的指针...)
      • i节点信息(索引节点),(包含文件的所有者,文件长度,文件所在设备...)
      • 当前文件长度

    如图:

    • 每一个进程都对应一个进程表项,一个进程能打开好多文件,所以进程表项里有好多fd;
    • 一个fd就代表一个文件,也可以说一个文件可以由很多个fd来代表;
    • 不同进程会对同一个文件有不同的操作,这个文件表项其实就是把文件和进程联系起来的东东,里面包含一些该进程对这个文件的操作,然后它再用一个指针指向文件本身。

    dup、dup2函数

    #include <unistd.h>
    int dup(int filedes);
    int dup2(int filedes, int filedes2);

    dup和dup2都是用来复制一个现存的文件描述符(已经打开的文件描述符)的函数。成功,返回新的描述符;失败,返回-1.

    dup:返回当前可用fd的最小值;

    dup2:可以指定返回的描述符为fd2。如果fd2已经打开,则先将其关闭。如果fd等于fd2, 则直接返回fd2,即将fd替换为fd2, 而不关闭它。

    这两个函数返回的新文件描述符与原来的fd共享同一个文件表项。如上图中,fd0与fd4。

    /dev/fd

    比较新的系统都提供名为/dev/fd的目录,目录项是名为0,1,2等的文件。打开/dev/fd/n 等效于复制描述符n。

    某些系统提供路径名/dev/stdin, /dev/stdout, /dev/stderr。等效于/dev/fd/0, /dev/fd/1,/dev/fd/2。

    原子操作

    所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)

    如果一个进程要在一个文件的尾端添加数据,由于早期UNIX不支持open的O_APPEND,所以采用先lseek到文件末尾,再write的方法。但若有多个进程,这种方法就会出现问题

    if (lseek(fd, OL, 2) < 0)  //position to EOF
        err_sys("lseek error");
    if (write(fd, buf, 100) != 100)
        err_sys("write error");

      假设有两个独立的进程A和B都对同一个文件进行添加操作。每个进程都已经打开了该文件,但未使用O_APPEND标志。这时A,B都有自己的文件表项,但是共享一个v-node表项。如果A调用了lseek,它将进程A的该文件当前偏移量设置为1500字节(当前文件末尾)。然后内核切换到B,B再执行lseek,将进程B的当前偏移量也设置为1500字节(文件末尾)。然后B调用write,将B的该文件当前偏移量增加到1600。因为文件的长度增加了,所以内核对V节点中的当前文件长度更新为1600。然后,切换回A,A再write时,由于其当前文件表中记录的偏移量是1500,所以将数据写入时就覆盖了B刚写到这个文件中的数据。

    解决方法:  使lseek和write这两个操作对于其他进程而言成为一个原子操作。UNIX提供了一种方法,在打开文件时设置O_APPEND标志,使内核每次对这个文件进行写之前,都将进程的当前偏移量更新到文件末尾,于是在每次写之前就不用调用lseek了。

    sync、fsync、fdatasync

    传统的UNIX系统实现在内核中设有缓冲区高速缓存或者页面高速缓存,大多数磁盘I/O都通过缓冲进行。当我们向一个文件写入数据时,数据通常被内核拷贝到它的其中一个缓冲区,并排队以便在之后的某个时刻写入到磁盘中。这被称为延迟写(delayed write)。这方法虽然减少了磁盘读写次数,BUT降低了文件内容的更新速度,若系统发生故障,将会造成文件更新内容的丢失。

    So,为了保证磁盘上实际文件系统与缓冲区高速缓存中的内容一致,UNIX提供了sync、fsync和fdatasync函数来强制把缓冲里的东东写到磁盘文件里

    #include <unistd.h>
    int fsync(int filedes);
    int fdatasync(int filedes);
    //成功,返回0;出错,返回-1
    void sync(void);

    sync: 全局性的,对整个系统都flush。不管你实际写磁盘结束否,有修改就排入写队列,也就是只要有点儿变动就冲洗内核的块缓冲区。

    fsync: 仅引用一个文件,由文件描述符指定,并且在返回前等待磁盘写的完成。

    fdatasync: 顾名思义,就是只更新data部分,因为fsync还会同步更新文件属性等。

    以上三种方法是系统提供的系统调用,C语言里有一个对C标准扩充的函数fflush,它也有相似的功能。

    #include <stdio.h>
    int fflush(FILE* stream);

     

  • 相关阅读:
    实现一个微型数据库
    InstallShield 12 制作安装包
    .NET MVC学习笔记(一)
    递归和迭代的差别
    nginx 日志和监控
    c语言中的位移位操作
    Android应用程序绑定服务(bindService)的过程源码分析
    关于js中window.location.href,location.href,parent.location.href,top.location.href的使用方法
    iOS Crash 分析(文二)-崩溃日志组成
    js 字符串转换成数字的三种方法
  • 原文地址:https://www.cnblogs.com/beatrice7/p/4041722.html
Copyright © 2020-2023  润新知