一.引言
说明几个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
若出错,返回-
【注意】:关闭一个文件,回什邡加在该文件上的所有记录锁;
进程终止,内核自动关闭它所有打开的文件,很多程序因此不显式的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,,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,,SEEK_CUR)==- )
printf("Can't seek.\r\n");
else
printf("Can seek.\r\n") ; exit();
} :/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, ,SEEK_END) < ) // 定位到文件尾
err();
if( write(fd,buf,)!= ) // 写
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);
返回值:成功:读到的字节数;
出错:-
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 < ){
printf("input pere err.\r\n");
exit();
} fd = atoi(argv[]);
if( (flag=fcntl( fd, F_GETFL )) < ){
printf("fcntl F_GETFL err.\r\n");
exit();
} 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();
} 运行结果:
:/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/代替-,都为标准输入,直观一点
example
example.c
example.o
file.hole
Makefile
十七.小结
除了熟悉本章介绍的函数原型和使用,还要掌握如下知识:
1. 文件共享问题,熟悉内核与文件相关的数据结构,便于理解;
2. IO效率:
- 读写文件的buffer区大小不同,对整体效率的影响
- 延迟写与sync的概念