《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.\r\n");
    else
        printf("Can seek.\r\n")    ;
        
    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.\r\n"); exit(1); } fd = atoi(argv[1]); if( (flag=fcntl( fd, F_GETFL )) < 0 ){ printf("fcntl F_GETFL err.\r\n"); exit(1); } switch(flag&O_ACCMODE){ case O_RDONLY: printf("read only.\r\n"); break; case O_WRONLY: printf("write only.\r\n"); break; case O_RDWR: printf("read & write.\r\n"); break; default: printf("unknow access mode.\r\n"); break; } if( flag&O_APPEND ) printf("flag:APPEND.\r\n"); // 其他属性就不一一写了 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的概念
posted @ 2017-01-06 14:55  liuwanpeng  阅读(1721)  评论(0编辑  收藏  举报