• 【APUE】第3章 文件I/O (3) 文件共享、原子操作、函数dup/dup2、函数sync/fsync/fdatasync、函数fcntl、函数ioct1、目录/dev/fd 使用说明


    1、文件共享

    UNIX系统支持在不同的进程间共享打开文件。为了说明这种共享,以下介绍内核用于所有I/O的数据结构。

    内核使用3种数据结构表示打开文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。

    (1)每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个矢量每个描述符占用一项。与每个文件描述符相关联的是:

    • 文件描述符标志;
    • 指向一个文件表项的指针。

    (2)内核为所有打开文件维护一张文件表。每个文件表项包含:

    • 文件状态标志(读、写、添加、同步和非阻塞等);
    • 当前文件偏移量
    • 指向该文件v节点表项的指针。

    (3)每个打开文件(或设备)都有一个v节点(v-node)结构。v节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点(i-node,索引节点)。这些信息都是在打开文件时从磁盘上读入内存的,所以文件的所有相关信息都是随时可用的。例如i节点包含了文件的所有者、文件长度、指向文件实际数据块在磁盘上的位置等。(注意在linux中是没有v节点结构的但是存在i节点结构)

    如图3-7 显示了一个进程对应的3张表之间的关系。该进程有两个不同的打开文件;一个文件从标准输入打开(文件描述符为0),另一个从标准输出打开(文件描述符为1)。

    如果两个独立的进行各自打开了同一个文件,则有如图3-8所示的关系。

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

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

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

    (2)如果使用O_APPEND标志打开一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有追加标志的文件进行写操作时,文件表项中的当前文件偏移量首先将会被设置为i节点表项中的当前文件长度。

    (3)若一个用lseek定位到文件当前的末尾,则文件表项中的当前偏移量被设置为i节点表项中的当前文件长度。

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

    可能有多个文件描述符指向同一文件表项,例如fork后,此时父子进程各自的每一个打开文件描述符共享一个文件表项。

    上述介绍的一切对于多个进程读取一个文件都能正确工作。每个进程都有自己的文件表项,其中也有它自己的当前文件偏移量。但是当多个进程同一个文件时,则可能产生预想不到的结果。为了说明如何避免这种情况,需要先理解原子操作的概念。

    2、原子操作

    考虑一个进程,它将数据追加到一个文件尾端。

    假定有两个独立的进程A和B都对同一个文件进行追加操作。每个进程都已经打开了该文件,但是未使用O_APPEND标志。此时各数据结构之间的关系如图3-8所示,每个进程都有它自己的文件表项,但是共享一个v节点表项。假定进程A调用lseek,它将进程A的该文件当前偏移量设置为1500字节(当前文件尾端处)。然后内核切换进程,进程B运行。进程B执行lseek,也将其对该文件的当前偏移量设置为1500字节(当前文件尾端处)。然后B调用write,它将B文件当前文件偏移量增加至1600.因为该文件长度已经增加了,所以内核将v节点中的当前文件长度更新为1600。然后内核又进行进程切换,使进程A恢复运行。当A调用write时,就从其当前文件偏移量(1500)处开始将数据写入文件。这样也就覆盖了之前进程B写入该文件的数据

    问题出现在逻辑操作“先定位到文件末尾,然后写”,它使用了两个分开的函数调用。解决的方法是使这两个操作对于其他进程而言称为一个原子操作。任何要求多于一个函数的调用操作都不是原子操作,因为在这两个函数调用之间,内核有可能临时挂起进程

    UNIX系统为这样的操作提供了一种原子操作,即在打开文件时设置O_APPEND标志。这样做使得内核每次在写之前都将进程的当前偏移量设置到该文件的尾端,于是每次写之前就不需要调用lseek。(open函数中的O_APPEND选项实现了lseek函数的部分功能,功能实现了定位和写两个操作成为原子的)

    3、函数dup/dup2

    下面两个函数都可以复制一个现有的文件描述符。

    1 #include<unistd.h>
    2 int dup(int fd);
    3 int dup2(int fd, int fd2);
    4 返回值:
    5 若成功;返回新的文件描述符。
    6 若出错;返回-1

    由于函数dup返回的新文件描述符一定是当前可用文件描述符中的最小值。对于dup2,可以用fd2参数指定新文件描述符的值如果fd2已经打开,则先将其关闭。如果fd等于fd2,则dup2返回fd2,而不关闭它。否则fd2的FD_CLOEXEC文件描述符标志就被清除。这样fd2在进程调用exec时是打开状态。

    这些函数返回的新文件描述符与参数fd共享一个文件表项,如图3-9所示:

    在此图中,我们假定进程启动时执行了:newfd = dup(1);

    当此函数开始执行时,假定下一个可用的文件描述符是3。因为两个文件描述符指向同意以文件表项,所以他们共享同一个文件状态标志(读、写、追加等)以及同一当前文件偏移量。

    每个文件描述符都有它自己的一套文件描述符标志。复制一个文件描述符的另一种方法是使用fcntl函数。实际上调用dup(fd)等效于fcntl(fd, F_DUPFD, 0)。而调用dup2(fd, fd2)等效于close(fd2); fcntl(fd, F_DUPFD, fd2);而后一种情况下,dup2并不完全等同于close加上fctl。它们之间的区别具体如下:

  • 相关阅读:
    【Java】久违了,Hello World!
    【Java】番外——BUG一词的由来
    【Java】数据类型、关键字、标识符
    【Java】一年后,重新开始学习Java!
    【Java】Path环境变量、Java_HOME
    【Java】Java环境安装
    反射
    修改metronome
    python 简单的动漫排名爬虫
    python 打包的 exe 程序自动更新
  • 原文地址:https://www.cnblogs.com/xuelisheng/p/10800686.html
Copyright © 2020-2023  润新知