• 《UNIX环境高级编程》笔记——3.文件IO


    一.引言

      说明几个I/O函数:open、read、write、lseek和close,这些函数都是不带缓冲(不带缓冲,只调用内核的一个系统调用),这些函数不输入ISO C,是POSIX的一部分;

      多进程共享资源(包括文件)时,会有很多额外的烦恼,需要对共享资源、原子操作等概念深入理解,需要理解涉及的内核有关数据结构,这些数据结构对理解文件、共享有重要作用;

      最后介绍dup、fcntl、sync、fsync和ioctl函数。

    二.文件描述符

      open或creat文件时,内核——文件描述符fd——>进程,用于read、write等函数。内核中维护fd与文件的对应关系,fd是动态的,内核会先分配最小未使用的fd。

      新进程执行时,shell会默认分配三个文件描述符,STDIN_FILENO/STDOUT_FILENO/STDERR_FILENO,一般为0/1/2,定义在<unistd.h>中。现在linux允许1个进程分配的文件描述符很多,一般不用关心最大值。

      【收获】 <unistd.h>的全称为unix standard head,unix的标准调用。

    三.函数open和openat

    #include <fcntl.h>
    int open( const char * path, int oflag, .../*mode_t mode*/);
    int openat( int fd, const char * path, int oflag, .../*mode_t mode*/);

    返回值:成功,返回文件描述符fd
        出错,-1,具体错误保存在errno全局变量中

    只有oflag指定新建文件时,第三个参数才有效,否则没有第三个参数。ISO C用...表示后面参数的数量和类型是可变的

    参数说明:

    path:要打开或创建文件的名字
    oflag:  在<fcntl.h>---<bits/fcntl.h>---<bits/fcntl-linux.h>中定义
      以下五选一,必选
      O_RDONLY:只读打开
      O_WRONLY:只写打开
      O_RDWR:读写打开
      O_EXEC:只执行,在linux里也没找到
      O_SEARCH:只搜索,标准有,linux不支持
      
      以下为可选项
      O_APPEND:每次write时都追加到文件尾端
      O_CLOEXEC:把FD_CLOEXEC常亮设置为文件描述符标志,3.14节说明。与fcntl()函数有关。
      O_CREAT:若文件不存在,则创建它,此时需要第三个参数mode_t
      O_EXCL: O_CREAT|O_EXCL,如果文件存在,返回错误;如果不存在,创建。不存在时,检测是否存在和创建变成原子操作
      O_DIRECTORY:如果不是目录,出错
      O_NOCTTY:如果path是终端,则不将该设备作为此进程的控制终端
      O_NOFOLLOW:如果path时符号链接,则出错
      O_NONBLOCK:如果path时FIFO、块设备、字符特殊文件,则本次open和后续IO操作为非阻塞方式。
      O_TRUNC:若文件存在,且打开方式包含WR,则将文件长度截断为0
      O_SYNQ:每次write等待物理IO完成,包括文件属性的更新,linux在fcntl时不支持此选项
      O_DSYNC:每次write等待物理IO完成,但是如果该写操作不影响读取刚写入的数据,则不需要等待文件属性被更新
      O_RSYNQ:linux处理方式与O_SYNC相同
      O_TTY_INIT:如果打开一个还未打开的终端设备,设置非标准termios参数值。18章讨论。

    mode参数,说明新建文件的权限,头文件<sys/stat.h>
      S_IRUSR 用户读
      S_IWUSR 用户写
      S_IXUSR 用户执行 
      S_IRGRP 组读
      S_IWGRP 组写
      S_IXGRP 组执行
      S_IROTH 其他读
      S_IWOTH 其他写
      S_IXOTH 其他执行

      组合形式:S_IRWXU/S_IRWXG/S_IRWXO



      【注意】以上宏定义都采用八进制,例如"chmod 777”时的777是8进制数据0777

    openat比open多个fd,可以让线程使用相对目录打开文件,而不再是只能打开工作目录。默认1个进程中的多个线程只共享1个工作目录,所有线程都在这个工作目录里使用相对路径可能不方便。

      如果path为绝对路径,fd被忽略;

      如果path为相对路径,fd指定该相对路径的其实位置,fd是打开目录来获取的;

      如果path为相对路径,fd=AT_FDCWD,则路径名在当前工作目录中获取





    四.函数creat

    open支持O_CREAT以后,creat()函数基本就没有太大用了。

    #include <fcntl.h>
    int creat( const char * path,mode_t mode);
    返回值:成功,返回只写打开的文件描述符
          出错,-1

    等效: open(path, O_WRONLY|O_CREAT|O_TRUNC,mode);

    五.函数close

    #include <unistd.h>
    int close( int fd );
    返回值:若成功,返回0
          若出错,返回-1

    注意】:关闭一个文件,回什邡加在该文件上的所有记录锁;

            进程终止,内核自动关闭它所有打开的文件,很多程序因此不显式的close()文件.

    六.函数lseek

      每个打开的文件都有与其关联的“当前文件偏移current file offset”,通常为非负整数,度量从文件开始处计算的字节数。

      读写一般都从当前文件偏移开始;

      open默认将偏移量设置为0,除非用O_APPEN选项。

      可调用lseek显式地设置文件偏移,lseek仅将文件偏移记录在内核中,不引起IO操作。该偏移量用于下一次读写操作。

    #include <unistd.h>
    off_t lseek( int fd, off_t offset, int whence);
    
    返回值:成功,返回新的文件偏移量   出错,
    -1

    参数:
      whence:SEEK_SET----->偏移设置为“0(头)+offset(正数)”;
      whence:SEEK_CUR----->偏移设置为“当前值+offset(正负)”;
    whence:SEEK_END----->偏移设置为“文件长度(尾)+offset(正负)”;

    获取当前偏移,或检测当前文件是否可以设置偏移量的方法(FIFO,管道,网络套接字等不能设置偏移量):

    off_t currpos;
    currpos=lseek(fd,0,SEEK_CUR);

    实例3_1 是否可以lseek测试

    :/work/APUE/3_1$ cat example.c
    /* lseek test */
    #include <stdio.h>    // printf
    #include <stdlib.h>    // exit
    #include <unistd.h>
    
    int main(int args, char *argv[])
    {
        if( lseek(STDIN_FILENO,0,SEEK_CUR)==-1 )
            printf("Can't seek.
    ");
        else
            printf("Can seek.
    ")    ;
            
        exit(0);
    }

    :/work/APUE/3_1$ ./example < example.c    # 普通文件作为example.c的标准输入(重定向了),可以lseek
    Can seek.
    :/work/APUE/3_1$ cat example.c | ./example # 管道过来的输入不能lseek
    Can't seek.

    实例3_2 文件空洞,允许lseek到文件长度之后地方, 下次读或写时,会加大文件长度,中间未操作的地方形成“空洞”,空洞不占用磁盘空间。

    七.函数read

    #include <unistd.h>
    ssize_t read( int fd, void *buf,size_t nbytes);
    返回值:成功,读到的字节数,若到文件尾,返回0;
          出错,-1

    多种情况会导致读到的字节数少于要求读的字节数:
    1. 没读够就到文件尾了。例如想要100bytes,但到文件尾还有30bytes,会返回30(实际读到的字节数);
    2. 已到文件尾,返回0(实际读到的字节数)
    3. 从特殊文件读,有限制:
      终端设备,通常最多1行;
      网络设备,缓冲机制能到导致没有那么多数据可读;
      管道或FIFO,没那么多数据可读;
      某些记录设备,一次最多返回1个记录;
    4. 读时被信号中断


    read对偏移的影响:当前偏移+实际读到的字节数——>新的偏

    八.函数write

    #include <unistd.h>
    ssize_t  write(int fd, const void *buf,size_t nbytes);
    返回值:成功,实际写的字节数
          出错,-1

    返回值,一般等于nbytes,否则出错,出错原因一般是磁盘满或超过文件长度限制;
    write与偏移:
      一般文件,从当前偏移开始写;
      open时用了O_APPEND参数,write时会先定位到文件尾部
      write后,偏移+=实际写入的字节

    九.IO的效率!!!

      上述程序,BUFFSIZE的值对效率影响比较大,太小,循环次数多,频繁read、write系统调用,效率低。以空间换时间

      

    十.文件共享!!!

      unix允许不同进程共享文件,为对共享进行说明,需要先说明内核IO相关数据结构。

      10.1数据结构

      

      以下数据结构的实例均为linux,linux遵循上述结构,但是也不完全一致。

      1.进程结构体中包含文件表,文件表中可以找到多个文件表项

      2.文件表项:内核为所有打开文件维持一张文件表,包括:

        a. 文件状态标志(读、写、添写、同步和非阻塞等);

        b. 文件当前偏移量

        c.指向该文件V节点的指针(linux没有V节点)

      3.v-node和i-node

        每个文件都有,保存在磁盘上,与文件对应,打开文件时获取的,主要包括文件的所有者、文件长度、指向文件实际数据块在磁盘所在位置的指针等

        v-node是与文件系统无关的,所以单独提出来。linux里没有v-node,而是采用“与文件系统无关的i节点”+“与文件系统有关的i节点”的方式。

      【扩展linux的数据结构】

    include/linux/sched.h
    struct task_struct {
        ......
        struct files_struct *files;  // 文件描述符列表
    ...... }

    include/linux/fdtable.h
    /*
     * Open file table structure
     */
    struct files_struct {
      /*
       * read mostly part
       */
        atomic_t count;
        struct fdtable __rcu *fdt;
        struct fdtable fdtab;
      /*
       * written part on a separate cache line in SMP
       */
        spinlock_t file_lock ____cacheline_aligned_in_smp;
        int next_fd;
        unsigned long close_on_exec_init[1];
        unsigned long open_fds_init[1];
        struct file __rcu * fd_array[NR_OPEN_DEFAULT];    //各文件表项
    };

    include/linux/fs.h
    struct file {
        /*
         * fu_list becomes invalid after file_free is called and queued via
         * fu_rcuhead for RCU freeing
         */
        union {
            struct list_head    fu_list;
            struct rcu_head     fu_rcuhead;
        } f_u;
        struct path        f_path;
    #define f_dentry    f_path.dentry
        struct inode        *f_inode;    /* cached value */  // i节点指针
        const struct file_operations    *f_op;

        /*
         * Protects f_ep_links, f_flags, f_pos vs i_size in lseek SEEK_CUR.
         * Must not be taken from IRQ context.
         */
        spinlock_t        f_lock;
    #ifdef CONFIG_SMP
        int            f_sb_list_cpu;
    #endif
        atomic_long_t        f_count;
        unsigned int         f_flags;  // 对应open的flag参数中的一部分
        fmode_t            f_mode;   
        loff_t            f_pos;    // 偏移
        struct fown_struct    f_owner;
        const struct cred    *f_cred;
        struct file_ra_state    f_ra;

        u64            f_version;
    #ifdef CONFIG_SECURITY
        void            *f_security;
    #endif
        /* needed for tty driver, and maybe others */
        void            *private_data;

    #ifdef CONFIG_EPOLL
        /* Used by fs/eventpoll.c to link all the hooks to this file */
        struct list_head    f_ep_links;
        struct list_head    f_tfile_llink;
    #endif /* #ifdef CONFIG_EPOLL */
        struct address_space    *f_mapping;
    #ifdef CONFIG_DEBUG_WRITECOUNT
        unsigned long f_mnt_write_state;
    #endif
    };

      10.2 两个进程打开同一文件

     

      虽然是同一个文件,但是每个进程都有自己对应的文件表项,文件表项中保存着该进程对该文件的当前偏移量;

      在此说明write和lseek中关于偏移的操作:

      1. write nbytes——>该进程对应文件表项的偏移量增加nbytes——>如果偏移大于当前文件长度,则修改i节点中的当前文件长度;

      2. O_APPEND打开的文件,相应标记保存在文件表项中——>每次write,先把文件选项中的当前偏移=i节点中的文件长度

      3. lseek只改变文件表项中当前文件偏移

      可能有多个fd指向同一文件表项的情况,fork子进程时,此时与上图有点差别。文件描述符标志(task_struct)和文件状态标志(文件表项中)的作用范围不同,前者对应进程,后者应用于指向该文件表项的所有进程。

    十一.原子操作

      多个进程打开同一文件,如果有write操作,可能存在已执行问题。以下为几种出问题的情况:  

     11.1. 向文件尾部写入数据

    if( lseek(fd, 0,SEEK_END) < 0)  // 定位到文件尾
        err();    
    if( write(fd,buf,100)!=100 )   // 写
        err();

      lseek和write是分开的,进程1 lseek定位到尾部了,但是还没写,进程2 在尾部write了,此时文件的实际变大了,进程1再写时会覆盖刚才进程2的内容,导致出错。

      解决方法1:是使用O_APPEND打开文件,每次只调用write就可以了,不用再lseek,每次都是原子的。

      解决方法2: 使用pread和pwrite,这两个函数自带偏移,就不存在先lseek在write/read的非原子操作问题了。

    #include <unistd.h>
    ssize_t pread( int fd, void * buf, size_t nbytes,off_t offset);
    返回值:成功:读到的字节数;
          出错:-1
    ssize_t pwrite( int fd, void * buf, size_t nbytes,off_t offset);
    返回值:成功:写入的字节数;
          出错:-1

    pread与“lseek后再read”的区别
    pread无法中断定位和读操作;
    不更新文件偏移

    pwrite区别也类似。

    11.2. 创建一个文件

      先open检测,再创建,也是非原子的。

      解决方法:open使用O_CREAT|O_EXCL创建。

      【注意】其实最好的方法应该还是给文件上锁,比较保险而且直观,后面会介绍。

    十二.函数dup和dup2

      复制1个fd,使新的fd与原来的fd指向同一个文件表项,这种在多线程操作1个文件的场合应该有些用处。

      

      

    #include <unistd.h>
    /* Duplicate fd, returning a new file descriptor on the same file.  */
    int dup( int fd);
    /* Duplicate FD to FD2, closing FD2 and making it open on the same file. */
    int dup2( int fd, int fd2);

    返回值:成功:新的文件描述符
        失败:-1

    dup一定返回最小未使用的fd;
    dup2可以用fd2制定新描述符的值:
      如果fd2已经打开,先关闭;
      如果fd2=fd,返回fd2,不关闭
      否则,fd2的FD_CLOEXEC标记被清除,fd2在进程调用exec时是打开状态

    newfd = dup(1);  // 见上图

      fcntl也可以实现dup的功能

    dup(fd) ~~~~ fcntl(fd,F_DUPFD,0)
    dup(fd,fd2) ~~~~ close(fd2); fcntl(fd,F_DUPFD,fd2)
    dup2与fcntl稍有差别:
     dup2原子,close+fcntl不是;
     errno可能不同

    十三.函数sync、fsync和fdatasync

      大多数磁盘操作——>缓冲区,排入队列——>晚些时候真正写入磁盘,这种方式叫延迟写。内核需要重用缓冲区写入其他内容时,原本在缓冲区的内容会实际写入磁盘。跟cpu的cache机制差不多,为了提高效率。有几个函数可以操作缓冲区与磁盘的一致性:

    #include <unistd.h>
    int fsync( int fd);
    int fdatasync( int fd);
    void sync(void);
    • sync:所有修改的块缓冲区——>写队列,然后返回,不等待写磁盘完成通常,称为update的守护进程,周期性的调用sync函数,定期flush块缓冲区;
    • fsync:只对fd一个文件有作用,且等待写磁盘完成后返回,更新“数据+属性”;
    • fdatasync:与fsync差不多,区别为只更新“数据”;

    十四.函数fcntl

    14.1 fcntl函数

       改变已经打开文件的属性。

    #include <fcntl.h>
    int fcntl( int fd , int cmd, .../*int arg*/);
    返回值:成功,依赖cmd
       失败,-1

    参数说明:
    cmd:
      F_DUPFD:复制fd,返回未使用、>=第三个参数(int arg)、最小的描述符。
          与fd共享文件表项,但有自己的一套文件描述符标志,其中FD_CLOEXEC标志被清除。
      F_DUPFD_CLOEXEC:同上,区别是额外设置FD_CLOEXEC标志。
      F_GETFD:返回fd的文件描述符标志,目前仅有FD_CLOEXEC
      F_SETFD:使用第三个参数(int arg)设置文件描述符标志

      
    F_GETFL:返回fd对应的文件状态标志,是open(fd,flg,...)函数flg参数的一部分,具体标志见后面的表格
      F_SETFL:将文件状态标志设置为第三个参数(int arg)的值,目前支持除了前5个外的其他标志

      F_GETOWN:返回当前接收SIGIO/SIGURG信号的进程ID和进程组ID,后面介绍。
      F_SETOWN:设置接收SIGIO/SIGURG信号的进程ID和进程组ID,第三个参数,正的arg指定进程ID,负的arg指定进程组ID(arg)。

     

     实例1,获取文件属性

    example.c

    /*
    lseek test */ #include <stdio.h> // printf #include <stdlib.h> // exit #include <unistd.h> #include <fcntl.h> #include <errno.h> // errno #include <string.h> // strerror #include <sys/stat.h> // mode int main(int args, char *argv[]) { int fd; int flag; if( args < 2 ){ printf("input pere err. "); exit(1); } fd = atoi(argv[1]); if( (flag=fcntl( fd, F_GETFL )) < 0 ){ printf("fcntl F_GETFL err. "); exit(1); } switch(flag&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("unknow access mode. "); break; } if( flag&O_APPEND ) printf("flag:APPEND. "); // 其他属性就不一一写了 exit(0); }

    运行结果:
    :/work/APUE/3_3$./example 0 < /dev/tty
    read only.
    #说明:先把标准输入重定向为/dev/tty文件(该文件只读),./example 0把标准输入传给测试程序,此时的0相当于/dev/tty,所以显示read only

    :/work/APUE/3_3$ ./example 1 > file
    :/work/APUE/3_3$ cat file
    write only.
    #说明: 先把标准输出重定向到文件file,./example 1把标准输出传给测试程序,相当于file,注意由于已经重定位,所以信息会输出到file里。

    :/work/APUE/3_3$ ./example 1 >> file
    :/work/APUE/3_3$ cat file
    write only.
    write only.
    flag:APPEND.
    #说明:>>追加重定位

    :/work/APUE/3_3$ ./example 5 5<>file  #5<>file意思是在文件描述符5上打开文件, <>是可读可写
    read & write.
    :/work/APUE/3_3$ ./example 5 5>file   #5>file意思是在文件描述符5上打开文件, >是可写
    write only.
    :/work/APUE/3_3$ ./example 5 5<file   #5<file意思是在文件描述符5上打开文件, >是可读
    read only.

     14.2  O_SYNC与write

      write时,只讲数据排入队列,不等到磁盘操作完成;如果在open时,使用O_SYNC,则write会等待磁盘操作完成。

      

      上表的设置O_SYNC是通过fcntl(fd,F_SETFL,arg)设置的,在linux里没有效果

      1和2,1只有read,没有write,2是read和write,所以2的时间比1长;

      2和3,3的O_SYNC没有实际生效,所以时间没有明显增大;

      3和456,4/5/6额外调用sync函数,真正写磁盘,所以时间要长。

      4、5、6只是fdatasync(数据)和fsync(数据+属性)的区别,时间差别不大。

    十五.函数ioctl

      杂货铺

      

    十六./dev/fd

      /dev/fd下面的0/1/2对应STDIN/STDOUT/STDERR, 没有别的啥用处。

    :/work/APUE/3_2$ ls | cat -  # -是标准输入
    example
    example.c
    example.o
    file.hole
    Makefile
    :/work/APUE/3_2$ ls | cat /dev/fd/0  #用/dev/fd/0代替-,都为标准输入,直观一点 example example.c example.o file.hole Makefile

    十七.小结

      除了熟悉本章介绍的函数原型和使用,还要掌握如下知识:

      1. 文件共享问题,熟悉内核与文件相关的数据结构,便于理解;

      2. IO效率:

    • 读写文件的buffer区大小不同,对整体效率的影响
    • 延迟写与sync的概念
  • 相关阅读:
    行高 | line-height (Animations & Transitions)
    色域 | @media.color-gamut (Media Queries)
    自动换行 | word-wrap (Text)
    漫谈死锁
    《金融时间序列分析》第3版-蔡瑞胸
    《广告点击延时反馈建模》
    《Google软件测试之道》
    《持续集成:软件质量改进和风险降低之道》
    《DevOps实践:驭DevOps之力强化技术栈并优化IT运行》
    《架构真经:互联网技术架构的设计原则》
  • 原文地址:https://www.cnblogs.com/liuwanpeng/p/6246050.html
Copyright © 2020-2023  润新知