美文网首页操作系统
详解linux文件IO

详解linux文件IO

作者: tracy_668 | 来源:发表于2018-11-30 07:39 被阅读9次

    open一个文件

    在Linux中,一个进程启动后,会在内核空间创建一个PCB进程控制块,这个PCB中有一个已打开文件描述符表,记录着所有该进程打开的文件描述符和对应的file结构体地址。默认情况下, 一个进程启动后,会打开三个文件,分别是标准输入、标准输出、标准错误,它们分别使用了0、1、2号文件描述符。
    当进程使用open函数打开一个新的文件时,一般会在内核空间申请一个file结构体,并把3号文件描述符对应的file指针执行file结构体,代码如下:

    #include <fcntl.h>
    #include <stdio.h>
    void main() {
        int fd = open("/Users/wusong/CLionProjects/wusongtest/log.txt", O_RDWR);
        printf("new fd = %d\n", fd);
    }
    输出:
    
    new fd = 3
    
    Process finished with exit code 11
    

    原理图如下:


    image.png

    process table entry就是进程的文件描述符表,file table entry用来记录文件的读写打开模式,当前文件偏移量以及v-node指针等, v-node table entry是虚拟文件系统对应的文件节点,inode是磁盘文件系统对应的文件节点,通过这两个节点就能找到最终的磁盘文件。每个进程只有一个process table entry, 默认情况下fd0,fd1,fd2已经使用,新打开的文件log.txt将使用fd3。

    两个进程同时open一个文件

    原理图:


    image.png

    两个进程对应两个文件描述符表,打开同一个文件时,都各自申请一个file table,由于打开的是同一个文件,所以file table都指向了同一个v-node, 两个file table 如何证明呢?

    #include <unistd.h>
    #include <fcntl.h>
    #include <stdio.h>
    int main(int argc, char *argv[])
    {
        int fd = open("/Users/wusong/CLionProjects/wusongtest/log.txt", O_RDWR);
        printf("new fd = %d\n", fd);
        printf("%ld\n", lseek(fd, 0, SEEK_CUR));
        write(fd, "123", 3);
        sleep(10);
        printf("%ld\n", lseek(fd, 0, SEEK_CUR));
        close(fd);
    }
    输出:
    new fd = 3
    0
    3
    #在10秒时间内,启动进程2
    输出:
    new fd = 3
    0
    3
    

    两个进程都分配了fd3给新打开的文件,并且读写位置不受其他进程的影响,读写位置正式file table entry的一个属性, 说明是不同的file table entry。

    一个进程open 两次同一个文件

    一个进程open两次同一个文件,其实跟两个进程open一次的原理相同,都是调用了两次open,反正只要记住,调用一次open函数,就会创建一个file table entry。


    image.png

    由于只有一个进程,所以只有一个process table entry,open了两次,所以是两个file table entry ,fd 3与fd 4的file pointer指向这两个结构体。

    #include <unistd.h>
    #include <fcntl.h>
    #include <stdio.h>
    int main(int argc, char *argv[])
    {
        int fd0 = open("/Users/wusong/CLionProjects/wusongtest/log.txt", O_RDWR);
        int fd1 = open("/Users/wusong/CLionProjects/wusongtest/log.txt", O_RDWR);
        printf("new fd0 = %d\n", fd0);
        printf("new fd1 = %d\n", fd1);
    
        write(fd0, "123", 3);
    
        printf("fd0 lseek %ld\n", lseek(fd0, 0, SEEK_CUR));
        printf("fd1 lseek %ld\n", lseek(fd1, 0, SEEK_CUR));
        close(fd0);
        close(fd1);
    }
    
    输出:
    
    new fd0 = 3
    new fd1 = 4
    fd0 lseek 3
    fd1 lseek 0
    

    上面代码open了两次log.txt,创建了两个file结构体,验证方法还是通过判断读写位置是否是独立的。从输出来看,修改fd0的读写位置不会影响fd1的读写位置。

    使用dup复制文件描述符

    dup函数与open函数不同,open函数会创建一个file table,但是dup只是申请一个fd来指向一个已经存在的file table。原理图如下:


    image.png
    //
    // Created by wusong on 29/11/18.
    //
    #include <unistd.h>
    #include <fcntl.h>
    #include <stdio.h>
    
    int main(int argc, char *argv[])
    {
        int fd, copyfd;
    
        fd = open("/Users/wusong/CLionProjects/wusongtest/log.txt", O_RDWR);
        /*复制fd*/
        copyfd = dup(fd);
        printf("copyfd %d\n", copyfd);
        write(fd, "123", 3);
    
        /*打印出fd和copyfd的偏移量,经过上面的写操作,都变成3了*/
        printf("fd lseek %ld\n", lseek(fd, 0, SEEK_CUR));
        printf("copyfd lseek %ld\n", lseek(copyfd, 0, SEEK_CUR));
    
        close(fd);
        close(copyfd);
        return 0;
    }
    输出:
    copyfd 4
    fd lseek 3
    copyfd lseek 3
    
    

    结果证明只要操作了fd 或copyfd这两个文件描述符中一个的读写位置,就会影响到另一个文件描述符的读写位置。说明这两个文件描述符指向的是同一个file table。

    fork之后open

    如果在调用fork之后调用一次open函数,由于fork之后会返回两次,一次父进程返回,一次子进程返回,那么这个时候其实是相当与两个进程分别调用了一次open函数打开同一个文件


    image.png
    //
    // Created by wusong on 29/11/18.
    //
    
    #include <unistd.h>
    #include <fcntl.h>
    #include <stdio.h>
    int main(int argc, char *argv[])
    {
        int pid = fork();
        int fd = open("/Users/wusong/CLionProjects/wusongtest/log.txt", O_RDWR);
        printf("pid %d %ld\n", pid, lseek(fd, 0, SEEK_CUR));
        write(fd, "123", 3);
        sleep(5);
        printf("pid %d %ld\n", pid, lseek(fd, 0, SEEK_CUR));
        close(fd);
    }
    输出
    pid 9385 0 # 父进程
    pid 0 0  # 子进程
    pid 0 3
    pid 9385 3
    

    可以看到父子进程的读写位置都是3,并不受影响。

    fork之前open

    fork之前调用open函数,也就是只调用了一次,产生了一个fd以及file table, fork之后子进程的文件描述符表示从父进程复制过来,子进程的fd指向和父进程相同的file table。
    原理图如下:


    image.png
    #include <unistd.h>
    #include <fcntl.h>
    #include <stdio.h>
    int main(int argc, char *argv[])
    {
        int fd = open("./log.txt", O_RDWR);
        int pid = fork();
        printf("pid %d %ld\n", pid, lseek(fd, 0, SEEK_CUR));
        write(fd, "123", 3);
        sleep(5);
        printf("pid %d %ld\n", pid, lseek(fd, 0, SEEK_CUR));
        close(fd);
    }
    输出:
    pid 9546 0
    pid 0 3
    pid 0 6
    pid 9546 6
    

    父子进程都各自写入3字节,如果是两个file table,那么最终都应该打印的是3,而不是6。此外,如果想要释放这个file table,也必须父子进程都close一次fd才会释放,如果不close,进程退出的时候会自动close掉所有的文件描述符。

    重定向的本质

    为什么 >/dev/null 2>&1 和 2>&1 >/dev/null效果不同呢?由上面分析我们知道,进程的文件描述符表(File Descriptor Table)将一个文件描述符数字对应到一个文件表(File Table)中的一个文件项,在通过文件表将一个文件项对应到磁盘上具体的node(或者是vnode再到node),重定向本质上就是通过修改文件描述符表中的文件指针,将一个描述符的操作落到其他文件上去,比如3>&1就是将描述符3的文件指针指向和描述符0相同的地方。这个操作在 Linux 系统是通过一个叫 dup2(2) 的系统调用完成的, 2>&1 在程序实现上就是

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(int argc, char *argv[])
    {
        dup2(1, 2);
        fprintf(stderr, "hello, worldn");
        return 0;
    }
    

    我们再来看看 2>&1 >/dev/null 和 >/dev/null 2>&1 的实现,

    int fd = open("/dev/null", "w+");
    dup2(fd, 1);
    dup2(1, 2);
    

    以上就是 >/dev/null 2>&1 的实现。我们先把描述符 1 指向和 /dev/null 的位置,然后再把描述符 2 指向描述符 1 对应文件的位置。由于之前描述符 1 的对应文件已经被 dup2(2) 修改为了 /dev/null 因此描述符 2 现在也指向 /dev/null 了。后续的所有标准输出与标准错误的数据都被定向到了 /dev/null .
    再来看 2>&1 >/dev/null 的实现

    int fd = open("/dev/null", "w+");
    dup2(1, 2);
    dup2(fd, 1);
    

    这里仅仅就是把两条 dup2(2) 语句的顺序调换了一下。但是却产生了完全不同的结果。首先,我们把描述符 2 指向描述符 1 对应的文件,也就是标准输出,然后再把描述符 1 指向 /dev/null 。结果是描述符 1 指向了 /dev/null 而描述符 2 仍然指向标准输出。

    相关文章

      网友评论

        本文标题:详解linux文件IO

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