美文网首页Linux学习之路
APUE读书笔记-03文件输入输出(2)

APUE读书笔记-03文件输入输出(2)

作者: QuietHeart | 来源:发表于2020-04-05 12:04 被阅读0次

    9、文件共享

    内核用于维护进程打开的文件的相关数据结构大致有三个:

    1. 每个进程的进程表中有一个包含所有打开的文件描述符号信息的表,这个表中每一项主要包含:一个文件描述符号的标记以及一个文件符号描述表指针。
    2. 文件符号描述表,由前面的进程表中指针来指向它,主要包含:一个状态标记,一个偏移,以及一个文件虚拟节点地址。如果多个进程打开同样文件,也会导致有多个这样的表,但是虚拟节点只有一个,这样是保证每个进程有自己正确的文件读取偏移量。
    3. 文件虚拟节点:描述文件大小,虚拟节点信息和索引信息等。用于在计算机上支持不同的文件系统。

    对于虚拟节点和索引, v-node 用来在一个计算机系统上面支持多个类型的文件系统。 Sun 将它称作虚拟文件系统,并且将 i-node 中和文件系统相独立的部分叫做 v-node 。随着对 Sun 的网络文件系统( NFS )的支持的加入, v-node 在不同的厂商实现中流行起来。具体的发展情况,请参考原书参考资料, Linux 并不是讲数据分成了 i-nodev-node ,它是用一个和文件系统无关的 i-node 以及和一个文件系统相关的 i-node 。尽管实现有所不同,其概念和v节点类似,在第4章14节中我们会讨论 i-node

    注意,上面的文件描述符号标记和状态标记是不一样的。文件描述符号标记例如 close-on-exec ,意思是使用 exec 的时候是否关闭之前的文件符号;状态标记有例如:读写属性,追加,同步写属性,阻塞等;

    下图描述一个打开了两个文件的进程的这些数据结构的情况。

    打开两个文件的进程的相关数据结构关系

                                                                       v-node table
    process table entry                   file table           -->+------------------+
    +------------------+  --------->+-------------------+     /   |     v-node       |
    |      fd    file  | /          |file status flags  |    /    |  information     |
    |     flags pointer|/           +-------------------|   /     +------------------+
    |     +----+-----+ /            |current file offset|  /      |     i-node       |
    | fd0 |    |     |/|            +-------------------+ /       |  information     |
    |     +----+-----+ |            | v-node pointer    |/        + - - - - - - - - -+
    | fd1 |    |     |----------\   +-------------------+         | current file size|
    |     +----+-----+ |         \                                +------------------+
    | fd2 |    |     | |          ->+-------------------+
    |     +----+-----+ |            |file status flags  |    ---->+------------------+
    |     |          | |            +-------------------|   /     |     v-node       |
    |     |  ......  | |            |current file offset|  /      |  information     |
    |     |          | |            +-------------------+ /       +------------------+
    |     +----------+ |            | v-node pointer    |/        |     i-node       |
    |                  |            +-------------------+         |  information     |
    +------------------+                                          + - - - - - - - - -+
                                                                  | current file size|
                                                                  +------------------+
    

    上图中,一个文件在标准输入上(文件描述符号为0)打开,另外一个在标准输出上(文件描述符号为1)打开。

    下图给出了两个不同进程打开同一个文件的情况。

    两个独立进程打开同一个文件时内核的相关数据结构

    process table entry
    +------------------+
    |      fd    file  |
    |     flags pointer|
    |     +----+-----+ |
    | fd0 |    |     | |
    |     +----+-----+ |
    | fd..|  ......  | |
    |     +----+-----+ |                  file table
    | fd3 |    |     |------------->+-------------------+             v-node table
    |     +----+-----+ |            |file status flags  |    ---->+------------------+
    |     |          | |            +-------------------|   /  +->|     v-node       |
    |     |  ......  | |            |current file offset|  /   |  |  information     |
    |     |          | |            +-------------------+ /    |  +------------------+
    |     +----------+ |            | v-node pointer    |/     |  |     i-node       |
    |                  |            +-------------------+      |  |  information     |
    +------------------+                                       |  + - - - - - - - - -+
                                                               |  | current file size|
                                                               |  +------------------+
    process table entry        ---->+-------------------+     /
    +------------------+      /     |file status flags  |    /
    |      fd    file  |     /      +-------------------|   /
    |     flags pointer|    /       |current file offset|  /
    |     +----+-----+ |   /        +-------------------+ /
    | fd0 |    |     | |  /         | v-node pointer    |/
    |     +----+-----+ | /          +-------------------+
    | fd..|  ......  | |/
    |     +----+-----+ /
    | fd4 |    |     |/|
    |     +----+-----+ |
    |     |          | |
    |     |  ......  | |
    |     |          | |
    |     +----------+ |
    |                  |
    +------------------+
    

    上图中,第一个进程的文件描述符号3和第二个进程的文件描述符号4表示同一个文件。可知,每个进程有它自己的文件表,但是每个文件只有一个 v-node 表。每个进程有一个文件表的原因是这样可以让每个进程有它自己的文件当前偏移。

    如果打开多个文件想要使用一个文件符号描述表,那么用 dup. 使用 fork 好像就是达到这个效果的(见第8章3节)。如果一个进程打开多个文件呢?

    使用 dup 的时候,会使得进程中多个文件描述符号信息的表项指向一个文件符号描述表项。使用 fcntl 一样,不过 fcntl 有可能不是原子的,之前需要别的操作才行,而 dup 只需要一个函数,是原子的,后面会提到原子操作。

    译者注

    原文参考

    参考: APUE2/ch03lev1sec10.html

    10、原子性操作

    原子性的操作就是在操作过程中不会被打断的操作,适用于多线程。例如:

    在文件结尾写入数据需要:

    1. 使用 lseek 定位到文件结尾
    2. 使用 write 写。

    但是这就不是原子操作了,在两步之间可能会被别的线程打断。所以要想原子操作,可以用带有 append 标志的 open 打开文件之后再 write

    还有例如:向一个指定的位置读写数据也类似,使用 preadpwrite 函数指定偏移量的读写,是原子性的。

    另外,前面讲述 open 函数的时候也提到过,对于标记 O_EXCL 如果同时指定了 O_CREAT ,而文件已经存在,那么就出错,利用这点这样可以测试文件是否存在,不存在则创建,并且这个操作是原子的。

    译者注

    原文参考

    参考: APUE2/ch03lev1sec11.html

    11、 dupdup2 函数

    下面两个函数可以用来复制文件描述符号:

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

    两者返回:如果成功返回新的文件描述符号,如果错误返回1(其实一般是数值的-1)。

    dup 返回的文件描述符一定是当前可用文件描述符中的最小数值。用 dup2 则可以用 filedes2 参数指定新描述符的数值。如果 filedes2 已经打开,则先将其关闭。如若 filedes 等于 filedes2 ,则 dup2 返回 filedes2 ,而不关闭它。

    函数返回的新描述符号共享 filedes 参数对应的文件表。如下图:

    调用 dup 之后内核的数据结构关系

    process table entry                                               v-node table    
    +------------------+                                      +-->+------------------+  
    |      fd    file  |                                      |   |     v-node       |  
    |     flags pointer|                                      |   |  information     |  
    |     +----+-----+ |                                      |   +------------------+  
    | fd0 |    |     | |                                      |   |     i-node       |  
    |     +----+-----+ |                                      |   |  information     |  
    | fd1 |    |     |---------\                              |   + - - - - - - - - -+  
    |     +---+------+ |        \                             |   | current file size|  
    | fd2 |  ......  | |         -->+-------------------+     |   +------------------+  
    |     +----+-----+ |        /   |file status flags  |    /   
    | fd3 |    |     |---------/    +-------------------|   /    
    |     +----+-----+ |            |current file offset|  /     
    |     |          | |            +-------------------+ /      
    |     +----------+ |            | v-node pointer    |/       
    |                  |            +-------------------+        
    +------------------+
    

    复制一个描述符的另一种方法是使用 fcntl 函数,下一节将对该函数进行说明。实际上,调用:

    dup(filedes);
    

    等价于:

    fcntl (filedes, F_DUPFD, 0);
    

    而调用:

    dup2(filedes, filedes2) ;
    

    等价于:

    close (filedes2) ;
    fcntl(filedes, F_DUPFD, filedes2);
    

    在最后一种情况下, dup2 并不完全等同于 close 加上 fcntl 。它们之间的区别是:

    • = dup2= 是一个原子操作。
    • dup2fcntl 之间有某些不同的 errno

    译者注

    原文参考

    参考: APUE2/ch03lev1sec12.html

    12、 syncfsync ,和 fdatasync 函数

    一般UNIX实现都在内核中有一个 buffer 缓存,或者页缓存,大多数的磁盘输入输出操作都会通过这个缓存。当我们将数据写入到一个文件中的时候,数据一般都会被内核首先拷贝到这些缓存中去,然后排队,到一定时间的时候会将相应的数据写入磁盘。这个技术也叫做延迟写。

    内核最终会将延迟写的块写入到磁盘上。一般在内核想要使用这块缓存来做其它的磁盘块操作的时候就会这样。为了确保磁盘上面文件系统和缓存中的内容的一致,提供了 syncfsync ,以及 fdatasync 函数。

    #include <unistd.h>
    int fsync(int filedes);
    int fdatasync(int filedes);
    

    返回:如果成功返回0,如果错误返回1(其实一般是-1)。

    void sync(void);
    

    函数 sync 只是简单地将所有被修改的缓存块排队,以用于写;它不会等待磁盘的写入发生。

    函数 sync 一般都会被系统守护进程(一般是 update 进程)周期地调用。这样就保证对内和的块缓存有规律地刷新。命令 sync (参见 Linux/Unix 用户手册 sync )也会调用 sync 函数。

    函数 fsync 只是引用一个单个的文件,这个文件通过文件描述符号参数 filedes 来指定,然后等待磁盘写操作完成,最后返回。使用 fsync 一般用于类似数据库这样的应用程序,以保证所做的修改确实地被写入到了磁盘中。

    fdatasync 函数和 fsync 类似,但是它只是影响文件的数据部分。而通过 fsync 文件的属性也会被同时更新。

    译者注

    原文参考

    参考: APUE2/ch03lev1sec13.html

    13、 fcntl 函数

    fcntl 函数用来改变一个已经打开的文件的性质。其声明如下:

    #include <fcntl.h>
    int fcntl(int filedes, int cmd, ... /* int arg */ );
    

    返回:如果成功,返回值取决于参数 cmd ;如果错误返回1(其实其值一般应该是-1)。

    filedes 是文件描述符号, cmd 是要进行的操作, arg 是操作的参数,不多说了。 fcntl 函数有五种功能:

    1. 复制一个已有文件描述符(cmd=F_DUPFD)。
    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)。

    更多信息参见参考资料。

    下面的例子,对应的程序以一个文件描述符号作为参数,运行之后,打印相应文件描述符号的文件标记。代码如下:

    打印制定文件描述符号的文件标记

    #include "apue.h"
    #include <fcntl.h>
    int main(int argc, char *argv[])
    {
    
        int       val;
    
        if (argc != 2)
            err_quit("usage: a.out <descriptor#>");
    
        if ((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0)
            err_sys("fcntl error for fd %d", atoi(argv[1]));
    
        switch (val & O_ACCMODE) {
        case O_RDONLY:
            printf("read only");
            break;
    
        case O_WRONLY:
            printf("write only");
            break;
    
        case O_RDWR:
            printf("read write");
            break;
    
        default:
            err_dump("unknown access mode");
        }
    
        if (val & O_APPEND)
            printf(", append");
        if (val & O_NONBLOCK)
            printf(", nonblocking");
    #if defined(O_SYNC)
        if (val & O_SYNC)
            printf(", synchronous writes");
    #endif
    #if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC)
        if (val & O_FSYNC)
            printf(", synchronous writes");
    #endif
        putchar('\n');
        exit(0);
    }
    

    运行如下:

    $ ./a.out 0 < /dev/tty
    read only
    $ ./a.out 1 > temp.foo
    $ cat temp.foo
    write only
    $ ./a.out 2 2>>temp.foo
    write only, append
    $ ./a.out 5 5<>temp.foo
    read write
    

    这里,在使用 O_SYNC 标记的时候,会在 write 同时等待写到磁盘上才返回,这样明显占用了系统时间,但是在 ext2 系统上不认这个标记。在正常没有 O_SYNCwrite 会写到磁盘缓存上面,所以占用时间少,正常没有 O_SYNCwrite 后面接着一个 fsync 的话并不是和 O_SYNCwrite 一样了,而是也占用很少的时间,因为在下次新 write 的时候会刷新之前的缓存,所以在 fsync 的时候,实际只把很少的一部分数据写到磁盘中了。

    另外, 5<>temp.foo 表示打开文件 temp.foo 用于读写,并且其文件描述符号为5。这个语法比较特殊,举几个例子如下(以下例子在ubuntu8.04上面实践):

    $touch tempfile
    $5<>tempfile echo 1111 >&5
    

    注意方向不能错,如:

    $echo 1111 >&5 5<>tempfile
    

    是错误的。

    或者:

    $exec 5<>tempfile
    $echo 1111 >&5
    

    exec 相当于单独执行,之后 /dev/fd/ 里面会有5这个描述符号。

    之后我们会发现,文件 tempfile 里面有 1111 这样的内容了。

    关闭描述符号:

    $exec >&5-
    

    译者注

    原文参考

    参考: APUE2/ch03lev1sec14.html

    14、 ioctl 函数

    ioctl 函数可以支持任何的 io 操作而不仅仅是读和写,有许多终端操作就是用这个函数来实现。 ioctl 函数声明如下:

    #include <unistd.h>        /* System V */
    #include <sys/ioctl.h>     /* BSD and Linux */
    #include <stropts.h>       /* XSI STREAMS */
    int ioctl(int filedes, int request, ...);
    

    返回:如果错误返回1(其实一般是-1),如果成功返回0,参数中也会存放其它返回值。

    由于UNIX/LINUX中对所有内容都看作文件,包括系统的设备,程序中我们将一切资源看做文件,然后以文件的方式操作相应的资源(例如 openwritereadioctlclose )。一般在特殊文件(如设备文件)对应的驱动中等地方需要实现这个函数,因为许多驱动的功能,不仅仅只是通过普通系统调用中已有的接口(例如 readwrite 等)就能表达的。例如,我们编写一个摄像机的驱动,想要实现播放,暂停,停止等,显然这些操作都是和设备相关的,所以这个时候就在驱动程序中实现这个函数。

    对于这个函数,参数 filedes 就表示其所操作的文件描述符号; request 表示要操作的命令码(命令码表示某种操作,而其值和具体设备相关,例如上面的摄像机设备,我们可以在驱动中规定一个命令码 PLAY );剩下的 ... 表示 request 命令的参数,参数可有可无。这个函数功能是依据文件描述符号所代表的资源而不同,这里不多说了,写设备驱动的时候,这个函数非常重要。

    译者注

    原文参考

    参考: APUE2/ch03lev1sec15.html

    15、/dev/fd

    比较新的系统都提供名为/dev/fd的目录,其中包含名为 0、1、2等的文件。打开文件/dev/fd/n相当于复制描述符n(假定描述符n是打开的)。所以调用:

    fd = open("/dev/fd/0", mode);
    

    相当于:

    fd = dup(0);
    

    大多数系统忽略所指定的mode,而另外一些则要求mode是所涉及的文件(在这里则是标准输入)原先打开时所使用的mode的子集。例如,若描述符0被只读打开,那么我们也只对fd进行读操作。即使下列调用成功:

    fd = open("/dev/fd/0", O_RDWR);
    

    我们仍然不能对fd进行写操作。某些系统提供路径名/dev/stdin ,/dev/stdout和/dev/stderr。这些相当于/dev/fd/0,/dev/fd/1和/dev/fd/2。

    译者注

    原文参考

    参考: APUE2/ch03lev1sec16.html

    总结

    本章描述了UNIX系统提供的基本输入输出( I/O )函数。这些函数也经常被称作非缓冲 I/O 函数,因为,每次 read 或者 write 都会发起一次内核的系统调用。通过使用 readwrite ,我们看到了不同 I/O 大小的时候对读取一个文件的时间的影响(在一定范围内,请求数据越大,读写系统调用被发起的次数越少,导致占用系统时间也减少)。我们也看到了将被写入的数据刷新到磁盘上面的方法,以及它们对应用程序执行效率的影响。

    当多进程向同一个文件追加内容的时候,以及当多个进程创建同样一个文件的时候,介绍了原子操作。我们也看到了内核内部用来共享打开文件信息的相关数据结构。我们将在后面的章节中,继续谈到这些数据结构。

    我们也讨论了函数 ioctl 和函数 fcntl 。后面第14章的时候,我们会回到这些函数,我们将使用 ioctl 操作 STREAMS I/O 系统,使用 fcntl 来实现记录锁。

    译者注

    原文参考

    参考: APUE2/ch03lev1sec17.html

    相关文章

      网友评论

        本文标题:APUE读书笔记-03文件输入输出(2)

        本文链接:https://www.haomeiwen.com/subject/fgnvuhtx.html