Linux文件共享原理

来源:互联网 发布:mysql压缩包下载地址 编辑:程序博客网 时间:2024/06/05 17:40
前段时间读到《Computer Systems: A Programmer's Perspective》的第10章—系统级I/O,书中从整体角度对Unix I/O做了介绍,对Unix/Linux初学者来说,"Unix共享文件"一节的解释得尤为有用。故作为笔记,记录于此。
1. Unix I/O
        一个Unix文件就是一个m个字节的序列:
            B0, B1, ..., Bk, ..., Bm-1
        所有的I/O设备,如网络、磁盘和终端,都被模型化为文件,而所有的输入和输出都被当做对相应文件的读/写来执行。这种将设备优雅地映射为文件的方式,允许Unix内核引出一套被称为Unix I/O的API,使得所有的输入和输出都能以一种统一且一致的方式来执行:
        1) 打开文件。应用程序访问I/O设备时,调用系统的API来要求内核打开相应的文件。内核返回一个小的非负整数,叫做文件描述符(file descriptor),它在后续对此文件的所有操作中标识这个文件。内核记录有关这个打开文件的所有信息,应用程序只需记录返回的fd,以便后续访问。
        2) 改变当前的文件位置。对于每个打开的文件,内核保存着一个文件位置k,初始为0。该位置是当前位置距文件头的字节偏移量,应用程序可以通过执行seek操作显式设置文件当前位置为k。
        3) 读写文件。读操作就是从文件当前位置k开始,拷贝n>0个字节到内存,若还未读够n个字节就达到文件尾,则会触发一个称为end-of-file(EOF)的条件,应用程序能检测到这个条件。注:文件结尾并没有明确写有"EOF"符号。
        类似地,写操作就是从内存拷贝n>0个字节到文件,写完后,文件当前位置从k更新为k+n。
        4) 关闭文件。应用程序完成对文件的访问后,通知内核关闭该文件,作为响应,内核释放文件打开时创建的数据结构,并将这个fd恢复到可用fd pool中。无论一个进程因何种原因终止,内核都会关闭该进程所有的打开文件并释放由内核维护的描述该文件的数据结构。

2. 共享文件涉及的概念
        在类Unix系统中,有多种方式可以实现共享文件,但共享文件的底层原理是一致的。要理解共享文件的概念,必须先搞清楚内核如何表示打开的文件。内核用三个相关的数据结构来表示打开的文件:
        1) 描述符表(descriptor table)
        每个进程均有自己独立的描述符表,它的表项是由进程打开的文件描述符来索引的。每个打开的描述符表项指向文件表中的一个表项。
        2) 文件表(file table)
        打开文件的集合是由一张文件表来表示的,所有进程共享这张表。每个文件表的表项包括有当前的文件位置、引用计数(reference count,即当前指向该表项的描述符表项数),以及一个指向v-node表中对应表项的指针。关闭一个描述符会减小相应的文件表表项中的引用计数,当引用计数减小到零时,内核删除这个文件表表项。
        3) v-node表(v-node table)
        同文件表一样,所有进程共享这张v-node表,每个表项描述一个文件的元数据(metadata,应用程序可以调用stat或fstat来获取文件元数据)。

3. 实例解说"共享文件"    
        本文前面的部分主要介绍了"
共享文件"背后需要了解的众多概念,下面通过具体的实例来说明共享文件的背后的原理。
        1) 典型情况(没有共享文件)
                    
        上图所示的内核数据结构,同一个进程打开了两个文件,fd 1和 fd 4通过不同的打开文件表表项来引用两个不同的文件。显然,这里没有共享文件且每个fd对应一个不同的文件。
        2) 同一进程的
共享文件
                    
        上图中,同一个进程的多个描述符通过不同的文件表表项来引用同一个文件。例如,若以同一个filename调用open函数两次,就会发生这种情况。本例体现的关键思想是:每个描述符都有它自己的文件位置,所以对不同描述符的读操作可以从文件的不同位置获取数据,即虽然两个fd最终访问的是同一个文件,但由于内核为每个fd独立维护着各自的文件位置,因此,这两个fd互不干扰,每个fd对文件位置的改变对另一个fd是不可见的。
        实例:
        假设磁盘文件foobar.txt由6个ascii码字符"foobar"组成,下面这段代码的输出是什么? 
    #include <stdio.h>    int main()    {        int fd1, fd2;        char c;        fd1 = open("foobar.txt", O_RDONLY, 0);        fd2 = open("foobar.txt", O_RDONLY, 0);        read(fd1, &c, 1);        read(fd2, &c, 1);        printf("c = %c\n", c);        exit(0);    }
        显然,上面的代码对应着本小节给出的情况,对照上面给出的文件共享示意图可知,最终输出是 c = f(而不是 c = o)。
        3) 父子进程的共享文件
        假设在调用fork之前,父进程的打开文件符合典型情况(见本文给出的第1中情况),则调用fork后,父子进程的打开文件如下图所示。
              
        由fork原理可知,子进程有一个父进程描述符表的副本。父子进程共享相同的打开文件表集合,因此也共享相同的文件位置,即:由于父子进程中,相同的fd指向相同的打开文件表项,因此,任一进程修改文件位置对另一进程都是可见的。这一点与同一进程两次打开同一文件的情况不同,需要区分两种情况背后的原理差异。另外一个重要的注意事项是:在内核删除相应文件表表项之前,父子进程必须都关闭了它们的描述符。
        实例:
        假设磁盘文件foobar.txt由6个ascii码字符"foobar"组成,下面这段代码的输出是什么? 
    #include <stdio.h>    int main()    {        int fd;        char c;        fd = open("foobar.txt", O_RDONLY, 0);        if(0 == fork()) {            read(fd, &c, 1);            exit(0);        }        wait(NULL);        read(fd, &c, 1);        printf("c = %c\n", c);        exit(0);    }

        显然,由于子进程继承父进程的描述符表及所有进程共享的同一个打开文件表,因此,上面代码中的fd在父子进程中指向同一个打开文件表项。子进程读1个字节后,文件位置加1,因此,父进程读取的是文件的第2个字节,即输出应该是:c = o

【参考资料】
<Computer Systems: A Programmer's Perspective>. chapter 10

===================== EOF =====================

0 0