一.引言
说明几个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的标准输入(重定向了),可以lseekCan seek.:/work/APUE/3_1$ cat example.c | ./example # 管道过来的输入不能lseekCan‘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.hstruct 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/ttyread only.#说明:先把标准输入重定向为/dev/tty文件(该文件只读),./example 0把标准输入传给测试程序,此时的0相当于/dev/tty,所以显示read only :/work/APUE/3_3$ ./example 1 > file:/work/APUE/3_3$ cat filewrite 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的概念