• 【APUE】Chapter3 File I/O


    这章主要讲了几类unbuffered I/O函数的用法和设计思路。

    3.2 File Descriptors

      fd本质上是非负整数,当我们执行open或create的时候,kernel向进程返回一个fd。

      unix系统中有几个特殊的fd:

      0:standard input

      1:standard output

      2:standard error

      这几个带有特殊含义的整数都有对应的可读性强的符号表示:STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO

      具体的定义都包含在unistd.h头文件中:

      

    3.3 open & openat Functions

      int open(const char *path, int oflag, ...)

      返回值为file descriptor

      其中oflag中包含各种选项的组合;并要求“只读、只写、读写、搜索、执行”这五类选项必须有且只有一个。

      open函数还有一个特性:通过open或者openat返回的file descriptor一定保证是可用的最小的descriptor。写一个小栗子如下:

    #include <unistd.h>
    #include <fcntl.h> 
    #include <stdlib.h>
    #include <stdio.h>
    
    int main(void)
    {
        int val1 = open("test_fd1",O_CREAT);
        printf("fd:%d
    ",val1);
        int val2 = open("test_fd2",O_CREAT);
        printf("fd:%d
    ",val2);
        exit(0);
    }

      编译运行后如下:

      

      由于0,1,2都有特殊的含义,所以后开的两个file descriptor为3、4。

      书上还提到了文件名长度的问题:如果是比较老的一些系统,文件名的长度超过了14,则系统可能把超过14个长度的部分截断了。

    3.4 create Function

    3.5 close Function

      关闭file deescriptor,一旦被close了,那么Process中各种加在这个文件上的锁也就被release了。

    3.6 lseek Function

      与文件偏移量有关。

      off_t lseek(int fd, off_t offset, int whence)

      如果成功返回new file offset,如果失败则返回-1

      具体执行什么操作需要根据whence参数来确定:

        SEEK_SET:重置file的偏移量为参数中offset的值

        SEEK_CUR:设置file的偏移量为当前偏移量+offset的值,这里offset可正可负

        SEEK_END:设置file的偏移量为当前文件的size+offset的值,这里offset依然可正可负

      如果要判断文件的当前偏移量,lseek(fd, 0, SEEK_CUR)即可。

      lseek还可以判断当前的file是否能够被seeking(比如pipe FIFO或socket就不能够)代码如下:

    #include <stdio.h>
    #include <unistd.h>
    #include <stdlib.h> 
    
    int main()
    {
        if (lseek(STDIN_FILENO,0,SEEK_CUR)==-1) { 
            printf("cannot seek
    "); 
        }
        else { 
            printf("seek OK
    "); 
        } 
        exit(0);
    }

      执行结果如下:

        

        从文件来的file descriptor就可以seek;从pipie来的file descriptor就不能够seek。

        上述的代码用lseek是否返回负1来判断是否成功,那么是否判断lseek是否为负数就可以知道是否成功了呢?

        书上说这样做的是不安全的,因为有些文件偏移量是允许为负数的。

      这里有一个问题,如果lseek操作后,文件的偏移量超过了文件的size怎么办?这种情况也是允许的,如果偏移量超过了文件的size,那么执行write操作就会将文件的size扩展,并且中间没有执行写操作的部分被灌以空白字符代替。例子如下:

    #include "apue.h" 
    #include <stdio.h>
    #include <stdlib.h>
    #include <fcntl.h>
    
    char buf1[] = "abcdefghij";
    char buf2[] = "ABCDEFGHIJ";
    
    int main()
    {
        int fd;
        fd = creat("file.hole", FILE_MODE);
        write(fd, buf1, 10);
        lseek(fd, 16384, SEEK_SET); /*offset now = 16384*/
        write(fd, buf2, 10); /*offset now = 16394*/
        exit(0);
    }

      运行结果如下:

      

      文件大小是16394,文件的size被扩展了。

      既然文件能够随着lseek而自动扩充size,那么这种扩充是不是无限的?并不是无限的。

      书上说大多数操作系统,提供了两种方式操作file offsets:一种是32 bits offsets,另一种是64 bits offsets

      2^32 = 4GB ,我猜这也就是为什么比较老的文件系统最大单个文件只能支持4GB的原因了

      2^64 = 非常大的GB

      但是,即使系统提供了32 & 64两种文件偏移量接口,但是最终能够支持多大的单体文件,还需要结合底层文件系统。

      

    3.7 read Function

      ssize_t read(int fd, void *buf, size_t nbytes)

      read的操作从文件当前的offset执行,read返回前文件的offset会增加,增加的值就是读入的bytes数;返回的是读入的bytes数。

      一般来说,read读入的bytes数就是nbytes;但有如下几种情况实际读入的bytes是要小于nbytes的:

      (1)如果是regular file,并且文件已经读到头了

      (2)read from terminal device

      (3)read from a network

      (4)read from pipe of FIFO

      (5)interrupted by a signal and a partial amount of data has already been read

      

    3.8 write Function

      

    3.9 I/O Efficiency

      书上列了这么一段程序:

    #include "apue.h"
    
    #define    BUFFSIZE    4096
    
    int
    main(void)
    {
        int        n;
        char    buf[BUFFSIZE];
    
        while ((n = read(STDIN_FILENO, buf, BUFFSIZE)) > 0)
            if (write(STDOUT_FILENO, buf, n) != n)
                err_sys("write error");
    
        if (n < 0)
            err_sys("read error");
    
        exit(0);
    }

      上述的代码主要测试BUFFSIZE的大小与读写的效率,主要利用Linux Shell的测试的效率。

      准备了一个504M的文本文件,如下:

      

      用如下命令测试不同BUFFERSIZE下的读写时间:

    time -p ./a.out < ./loadlog_tsinghua.txt > o

      先经过标准输入读入txt文件,再经过标准输出写入文件o中。

      测试BUFFSIZE为不同的值:

      524288

      

      8192

      

      4096

      

      2048

      

      1024

      

      32

      

      通过上述的比较,选择一个合适的buffer size是可以提高读写效率的。

      最优的读写速度集中在4096这个buffer size左右,这个与测试的linux环境系统的block size有关系。

    3.10 File Sharing

      这个部分主要针对这样一个问题:不同的process可能对同一个文件进程写或者读操作;假设上述操作都是可行的,那么背后的机制大概是什么样的。

      书上举了一个一般性的文件共享结构(可能与实际的不完全相同,但是可以帮助理解原理

      

      (1)每个进程都有一个process table entry,里面的内容都是file descriptos

      (2)每个file descriptor中包含两部分内容:

          a. fd flags:文件操作方式

          b. file pointer:指向一个file table entry

      (3)每个file  table entry中包含如下的内容:

          a. file status flags:指的是read, write, append, sync, nonblocking等

          b. 当前的文件offset

          c. 指向v-node table entry的指针

      通过上面的结构分析:一个文件只能有一个v-node table entry;但是不同的file descritpor指向同一个v-node table entry。这样就实现了文件在不同进程中的共享。

      通过上述的结构,重新分析之前提到的几个读写操作函数:

        (1)执行了write操作后,current file offset增加的数量就是写入的数量

        (2)如果文件以O_APPEND的flag被打开,执行了write的时候,current file offset先从i-node中获取当前文件的size,然后再执行write操作,这样就保证了一定是append的

        (3)lseek的操作只修改了file table entry中的current file offset,并不会产生I/O操作

      除此之外,还有一个问题需要注意:可以有多个file descriptor指向同一个file table entry的(详情见dup函数);因此也带来一个问题,process table entry中的file flags和file table entry中的file status flags的影响面是不同的:

       (1)file flags影响的只是单个进程中的单个file descriptor

       (2)file status flags影响的是所有连到这个file table entry的进程的fd

      如何对file descriptor flags和file status flags都进行有效的操作呢?这就里就有一个管家级的函数fcntl,后面会提到。

    3.11 Atomic Operations

      原子操作这个概念之前就见过了,书上给出了更好的定义:指的是包含好几步的operation,要么一起执行完了,要么都不执行,不存在只执行了部分步骤的过程。

      这部分只给出了简单的例子,多个process对同一个文件执行写操作,那么就容易产生不同步的现象。

      或者两个进程都要对同一个文件执行append的操作:之前说过了append必然要经过lseek的过程,这里就可能存在不同步。

      后续4.15和14.3章节会给出更具体的例子。

      

      

    3.12 dup and dup2 Functions

      int dup(int fd)

      这个函数的作用是:复制已有的file descriptor。

      函数的返回值,还是lowest-numbered available file descriptor。

      那么,如果需要指定返回的file descriptor的值怎么办呢?还有另一个类似功能的函数:int dup2(int fd, int fd2)

      其中fd2是希望得到的的file descriptor值,分如下几种情况:

        (1)如果fd2已经打开了,那么先关上,再返回fd2;此时fd2就与fd指向同一个fie entry table

        (2)如果fd与fd2相等,则返回的就是fd2

        (3)否则fd2的FD_CLOEXEC fle descriptor flag被清空了

      总的来说,最后达到的效果如下图所示:

      

      这种情况属于同一个process中对同一个文件产生了不同的file descriptor。

     

    3.13 sync, fsync, and fdatasync Functions

      这里先介绍UNIX system的文件系统的一种delay-write的机制:在执行写操作的时候,一般先把内容写到buffer中,再queue到队列中,最后在某个时候把内容写入到disk中

      上面描述的内容的核心就是:这里执行完write操作,并不是等着内容真的写到disk上才返回的;如果需要强写同步的环境,则需要考虑delay-write的影响。为了更好的理解这一部分的几个函数,需要对unix系统io操作的概貌有一个简略的了解。

      在网上找了一个有全貌的blog(http://blog.csdn.net/ybxuwei/article/details/22727565

      

      int sync(void)

        这是一个类似全局update催促函数,把kernel's blocks buffer(内核缓冲区)flush,但是不等待write完成了再回来。

      int fsync(void)

        只针对single file,fsync把kernel's block buffer内容flush,并且必须等着内容写入后才返回,严格写同步。

      int fdatasync(void)

        跟fsync功能类似,只不过只等着文件数据部分更新,不等着文件属性信息更新完。

      

    3.14 fcntl Function

      int fcntl(int fd, int cmd, ... );

      这是个比较综合的函数,主要操作与file descriptor相关的内容。

      cmd表示命令的格式,由各种宏定义符号表示,比如F_GETFL表示就是返回file descriptor对应的file status flags的值(这里需要注意,file descriptor有个指针指向file table entry, file status flags是属于file table entry里的值;具体见上面的关系图,就可以搞清楚了

      示例代码如下:

    #include <unistd.h>
    #include <fcntl.h>
    #include <stdio.h>
    #include <stdlib.h> 
    
    int main(int argc, char *argv[])
    {
        int val;
        val = fcntl(atoi(argv[1]), F_GETFL, 0); /*F_GETFL返回file status flags*/
        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: 
                printf("error
    ");
        }
        if (val & O_APPEND) { 
            printf(", append"); 
        }  
        if (val & O_NONBLOCK) { 
            printf(", nonblcking"); 
        } 
        if (val & O_SYNC) { 
            printf(", synchronous writes"); 
        } 
        putchar('
    ');
        exit(0);
    }

      代码执行结果如下:

      

      上面的例子体验了一下fcntl操作file descriptor的效果。

      书上在这一部分还对delay-write的机制进行了讨论,如果用同步机制fsync会造成什么影响。

      修改3.5.c的代码如下(主要功能还是从STDIN读再从STDOUT写,测试读写时间):

    #include "apue.h" 
    #include <unistd.h>
    #include <stdlib.h>
    #include <errno.h> 
    #include <fcntl.h>
    
    #define BUFFSIZE 524288
    
    void set_fl(int fd, int flags)
    {
        int val;
        if ((val=fcntl(fd, F_GETFL,0))<0) { 
            err_sys("fcntl F_GETFL error"); 
        } 
        val |= flags;
        if (fcntl(fd, F_GETFL, val)<0) { 
            err_sys("fcntl F_GETFL error"); 
        } 
    }
    
    int main(int argc, char *argv[])
    {
        /*set_fl(STDOUT_FILENO, O_SYNC);*/
        int n;
        char buf[BUFFSIZE];
        
        while ((n=read(STDIN_FILENO, buf, BUFFSIZE))>0) { 
            write(STDOUT_FILENO, buf, n);
            fsync(STDOUT_FILENO);
        } 
        if (n<0) { 
            err_sys("read error"); 
        } 
    }

      这部分代码主要增加了write同步的功能,有两种实现方法:

      (1)利用fcntl函数设置O_SYNC的flags

      (2)更直接一些,直接在每次执行write紧跟着调用fsync(fd)

      另外还有一个因素是BUFFSIZE取多少的问题,我们下面一个个分类讨论。

      1. 测试O_SYNC是否有用?

        这个不上图了,经过测试,把O_SYNC设置上了,并没有对读写速度造成影响。

      2. 测试fsync是否会对执行速度造成影响?

        首先将BUFFSIZE设为4096,执行的时间太长了,没等到执行完,截图如下:

        

        我们做实验的文件大小是504M而每次写的buffer是0.004M,这样需要等待的次数是巨大的。最起码在我实验的系统上是等不起这样的同步的。

        如果我们只等数据呢?把fdatasync(fd)呢?效果还是然并卵,所以这样的方式真的是不科学的。

        下面再把BUFFSIZE的数值改到比较大的524,288,结果如下:

        

        最起码说明,如果BUFFSIZE比较大的时候,因为同步等待的次数少了(504M的文件大小0.524M的buffer已经可以比4096可以接受的多了

        看书看到这里,大概了解了为什么要有delay-write的机制,还有之前提到的缓存机制。

        这一章开头的说的read() write()是unbuffered I/O并不是完全真的不带缓存,直接往disk上干:而是说在用户层没有缓存;这二者在kernel层还是设有缓存的。比如,kernel的缓存是100byte,每次write写的是10byte,则把kernel的缓存写了10次满了之后,才真的输出到disk上。如果在用户层增加一个缓存层,50bytes为buffer size,则系统层调用两次write就可以执行真正的I/O操作了,减少了调用调用wrtie()次数。

        对于这个问题,还需要后面的章节继续加深理解;

        目前搜到了这篇文章,讲的比较透彻(http://www.360doc.com/content/11/0521/11/5455634_118306098.shtml

      

  • 相关阅读:
    VScode 关闭回车后自动格式化代码
    『转载』专利申请
    『转载』 免费公用DNS服务及三大运营商DNS大全 含IPV4和IPV6
    正则表达式和元字符
    随机背景图
    秒表
    数组相关的函数
    对象的结构语法
    数组的结构语法
    展开合并运算符
  • 原文地址:https://www.cnblogs.com/xbf9xbf/p/4930496.html
Copyright © 2020-2023  润新知