第三章 文件I/O
3.1引言
I/O函数(open,read,write,lseek,close)=>原子操作=>多个进程间共享文件=>dup,fcntl,sync,fsync,ioctl函数。
3.2文件描述符
1 在POSIX.1应用程序中,幻数0,1,2虽然已经被标准化,但是应当把他们替换成符号STDIN_FILENO,STDOUT_FILENO,STDERR_FILENO,这些常量在unistd.h头文件中被定义。
2 文件描述符的变化范围在0-OPEN_MAX-1之间。
3.3函数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*/);
其中oflag是文件打开标志,这些标志定义在fcntl.h头文件中。
O_RDONLY,只读打开 O_WRONLY,只写打开 O_RDWR,读写打开 O_EXEC,执行打开,经过验证linux在fcntl.h头文件中并没有这个选项。 O_SEARCH,只搜索打开,对应于目录,经过验证linux在fcntl.h头文件中并没有这个选项。 O_APPEND,每次写的时候都追加到文件的尾端,只有在写的时候设置该文件状态码才有意义。 O_CREAT,如果文件不存在则创建他,使用该标志时,需要指明第三个mode_t权限状态标志。 O_DIRECTORY,如果path引用的不是目录,则出错 O_EXCL,如果同时指定了O_CREAT,而文件已经存在,则出错,使用此标志可以测试文件是否存在。 O_NOCTTY,如果path引用的是终端设备,则不应该将该终端设备作为此进程的控制终端(即不能在自己引用的终端上调用自己) O_NOFOLLOW,如果path引用的是一个符号链接,则出错 O_NONBLOCK,如果path引用的是一个FIFO,一个特殊文件,或者一个字符特殊文件,则此选项为文件的本次打开操作和后续的I/O操作设置非阻塞方式 O_SYNC,使得每次write等待物理I/O操作完成,包括由该write操作引起的文件属性更新所需要的I/O O_TRUNC,如果文件存在且打开方式为写/读写,那么将其长度截断为0 O_DSYNC,使得每次write要等待物理I/O操作完成,但是如果该读写操作并不影响读取刚写入的数据,则不需要等待文件属性被更新 O_RSYNC,使得每一个以文件描述符作为参数进行的read操作进行等待,直至所有对文件同一部分挂起的写操作都完成 **注意**,如果path指定的是绝对路径名称,那么openat函数将会忽略fd参数。**注意**,如果path指定的相对路劲名称,fd是一个已经打开的文件目录,那么path是相对于path来说的。 **注意**,如果fd具有特殊的AT_FDCWD,并且path是相对路径名称,那么openat打开的文件是相对于当前工作目录来说的。 **注意**,openat是POSIX.1中新增的函数,希望解决两个问题,一是让线程可以使用相对路径名打开文件,二是避免TOCTTOU错误。 验证代码:
#include<fcntl.h>
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
/*该程序测试openat函数的使用*/
int main(int argc,char*argv[]){
//使用open打开一个目录进行读取
int d1=open("/home/xcl/桌面/d1",O_RDONLY|O_DIRECTORY);
if(d1<0){
printf("打开目录d1失败");
exit(0);
}
//使用openat打开d2下面的文件
int d2=openat(d1,"/home/xcl/桌面/d1/d2/up2",O_RDONLY);
if(d2<0){
printf("d2打开失败");
}else{
printf("d2打开成功,openat中的fd=d1被忽略");
}
//使用openat打开d1下面的文件
int up1=openat(d1,"up1",O_RDONLY);
if(up1<0){
printf("up1打开失败,d1下面没有文件up1");
}else{
printf("up1打开成功");
}
//使用openat打开当前工作目录下面的文件
int cur=openat(AT_FDCWD,"xcl",O_RDONLY);
if(cur<0){
printf("打开当前目录下面的xcl失败");
}else{
printf("打开当前目录下面的xcl成功");
}
return 0;
}
结果如下:d2打开成功,openat中的fd=d1被忽略up1打开成功打开当前目录下面的xcl成功
3.4 creat函数
#include<fcntl.h>
int creat(const char*path,mode_t mode);
注意,此函数相当于open(path,O_WRONLY|O_CREAT|O_TRUNC,mode),并且open的这种用法具有好的原子性操作。
3.5 close函数
#include<fcntl.h>
int close(int fd);
3.6 函数lseek
#include<unistd.h>
off_t lseek(int fd,off_t offset,int whence);
whence若是SEEK_SET,表示距离文件开始处offset个字节。不可以为负数。
whence如果SEEK_CUR,表示距离当前位置offset个字节。可正数可负数。
whence如果是SEEK_END,表示距离文件尾offset个字节。可正数可负数。
注意:如果lseek执行成功,返回新的文件偏移量;
注意:lseek的返回值可能可以是负数,所以在测试lseek是成功还是失败时不要使用测试返回值v<0而应该是v!=-1表示成功。
推论:可以使用lseek(fd,0,SEEK_CUR)返回当前文件偏移量。
测试标准文件流是否可以lseek
测试标准输入是否可以lseek
测试管道是否可以lseek的代码如下:
#include<fcntl.h>
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
/*该程序是为了测试其他标准输入是否可以设置lseek偏移量*/
int main(int argc,char*argv[]){
if(lseek(STDIN_FILENO,0,SEEK_CUR)!=-1){
printf("Seek OK");
}else{
printf("Seek ERR");
}
return 0;
}
测试结果如下:
xcl@xcl:~/桌面$ ./main
Seek ERRxcl@xcl:~/桌面$ ./main Seek OKxcl@xcl:~/桌面$ cat /etc/passwd|./main
Seek ERRxcl@xcl:~/桌面$
测试空洞文件和非空洞文件的程序如下:
#include<fcntl.h>
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<stdlib.h>
/*该程序的目的在于创建一个空洞文件,并且与非空洞文件进行比较*/
int main(int argc,char*argv[]){
char buf[]="hello,world";
int hole=open("hole",O_WRONLY|O_CREAT,0777);
int nohole=open("nohole",O_WRONLY|O_CREAT,0777);
if(hole<0||nohole<0){
printf("文件创建失败\n");
exit(0);
}
if(write(hole,buf,sizeof(buf))!=sizeof(buf)){
printf("写入到hole失败\n");
exit(0);
}
if(write(nohole,buf,sizeof(buf))!=sizeof(buf)){
printf("写入到nohole失败\n");
exit(0);
}
char buff[]="kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk";
lseek(hole,1000000,SEEK_END);
if(write(hole,buff,sizeof(buff))!=sizeof(buff)){
printf("定位之后写入到hole失败\n");
exit(0);
}
if(write(nohole,buff,sizeof(buff))!=sizeof(buff)){
printf("定位之后写入到nohole失败\n");
exit(0);
}
printf("写入OK");
return 0;
}
测试结果如下:
写入OKxcl@xcl:~/ls -ls hole nohole
988 -rwxrwxr-x 1 xcl xcl 1001204 6月 4 14:34 hole
12 -rwxrwxr-x 1 xcl xcl 108 6月 4 14:34 nohole
3.7 read函数
#include<unistd.h>
ssize_t read(int fd,void*buf,size_t nbytes);
3.8 write函数
#include<unistd.h>
ssize_t write(int fd,const void *buf,size_t nbytes);
写一个函数,测试I/O的效率:
#include<fcntl.h>
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<stdlib.h>//函数atoi
#include<malloc.h>//函数malloc
#include<time.h>//函数clock
/*编写一个程序,根据用户输入的buffsize打下输出buffsize,time,loop等信息,比较I/O效率*/
int main(int argc,char*argv[]){
if(argc<2){
printf("usage: ./a.out nbytes\n");
exit(0);
}
int bufsize=atoi(argv[1]);
int loop=0;
char *buf=(char*)malloc(sizeof(char)*bufsize);
if(buf<0){
printf("创建buf失败\n");
exit(0);
}
time_t start=clock();
int count=0;
while((count=read(STDIN_FILENO,buf,bufsize))>0){
if(write(STDOUT_FILENO,buf,count)<0){
printf("复制时发生错误\n");
}
loop++;
}
time_t end=clock();
int fdd=open("statistic",O_WRONLY|O_CREAT|O_APPEND,0777);
char buff[100];
int len=sprintf(buff,"%ld\t%d\t%.4f\t%d\n",sizeof(buf),bufsize,1.0*(end-start)/CLOCKS_PER_SEC,loop);
write(fdd,buff,len);
return 0;
}
运行一段shell脚本:
#!/bin/bash
i=1
while(( $i<=600000 ))
do
./main $i <fff.tar>/dev/null
((i=2*$i))
done
运行结果如下:
指针字节数 buf字节数 复制时间(s) 循环次数
8 1 212.8002 577239040 8 2 108.7806 288619520 8 4 53.5341 144309760 8 8 26.7041 72154880 8 16 13.5766 36077440 8 32 6.8274 18038720 8 64 3.4192 9019360 8 128 1.7536 4509680 8 256 0.8694 2254840 8 512 0.5284 1127420 8 1024 0.2505 563710 8 2048 0.1671 281855 8 4096 0.1181 140928 8 8192 0.0951 70464 8 16384 0.0886 35232 8 32768 0.0796 17616 8 65536 0.0788 8808 8 131072 0.0774 4404 8 262144 0.0776 2202 8 524288 0.0750 1101
可以发现,在1024以后的到了明显的好转。
3.10 文件共享
内核使用了三种数据结构表示打开文件,打开文件描述符表=>文件表项=>V节点表项。
(1)每个进程在进程表项都有一个记录项,该记录项包含了一张打开文件描述符表,其具有的属性是
进程的纪录项.打开文件描述符表(文件描述符fd,指向文件表项的指针ptr)
(2)文件表项
文件表项(文件状态标志,当前文件偏移量,V节点指针)
(3)V节点
V节点(文件类型,操作文件的函数指针,V节点数据,I节点信息)
注意:之所以每个进程都或者自己的文件表项,是因为这可以使得每个进程都有他自己的对该文件的当前偏移量。
注意:如果有两个文件表项指向了同一个V节点,如果用O_APPEND打开一个文件,那么在每次进行写之前都会将当前文件偏移量设置成为V节点中的当前文件长度信息。
3.11 原子操作
SUS包括了对XSI的扩展,该扩展允许原子性的定位并执行I/O,pread,pwrite就是这种扩展。
#include<unistd.h>
ssize_t pread(int fd,void*buf,size_t nbytes,off_t offset);
ssize_t pwrite(int fd,const void *buf,size_t nbytes,off_t offset);
3.12 函数dup和dup2
#include<unistd.h>
int dup(int fd);
int dup2(int fd,int fd2);
注意:由dup返回的新文件描述符一定是当前可用文件描述符的最小值。
注意:对于dup2可以用fd2指定新文件描述符的值,如果fd2已经打开就先关闭fd2;如果fd2=fd,则dup2返回fd2而不关闭他;否则fd2的FD_CLOEXEC文件描述符标志就被清除,这样,fd2在进程调用exec时是打开的。
这两个函数返回的新文件描述符与参数fd共享一个文件表项。
注意:每个文件描述符都有它自己的一套文件描述符标志,dup或dup2函数总是会清除文件的FD_CLOEXEC标志。
编写一个程序测试以下dup以及dup2函数的使用:
#include<fcntl.h>
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<stdlib.h>//函数atoi
#include<malloc.h>//函数malloc
#include<time.h>//函数clock
/*编写一个程序,根据用户输入的buffsize打下输出buffsize,time,loop等信息,比较I/O效率*/
int main(int argc,char*argv[]){
//打开一个文件,使用dup复制该文件描述符,在该过程中FD_CLOEXEC标志将被清除,并且者两个文件描述符共享同一文件表项,因而享有相同的文件状态信息以及当前文件偏移指针,dup总是返回当前可用的最小文件描述符。
int fd=open("d1/up1",O_WRONLY);
if(fd<0){
printf("打开up1错误\n");
exit(0);
}
char buf1[]="hello";
char buf2[]="world";
int fd2=dup(fd);
if(fd2<0){
printf("复制文件描述符%d发生错误\n",fd);
exit(0);
}else{
printf("文件描述符fd=%d,dup(fd)=%d\n",fd,fd2);
}
//fd2首先写入buf1,
//fd定位到SEEK_CUR-1,再写入buf2必然变成hellworld
write(fd2,buf1,5);
lseek(fd,-1,SEEK_CUR);
write(fd,buf2,5);
//在这里使用dup2(fd2,fd)必然导致fd被关闭,然后fd被重新打开并且共享了fd2的文件表项
return 0;
}
3.13 函数sync,fsync,fdatasync
#include<unistd.h>
int fsync(int fd);
int fdatasync(int fd);
void sync(void);
注意:当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候在写入磁盘,这种方式被称为延迟写。所以内核是有缓冲区块的,所谓使用open,write等是不带缓冲的是指open和write调用一次就执行一次系统调用,并不是说执行一次磁盘I/O。
注意:sync只是将所有修改过的磁盘缓冲区排入写队列,然后就返回,他并不等待实际写磁盘操作结束。
注意:fsync只对由文件描述符fd指定的一个文件起作用,并且等待磁盘I/O完成才返回。
注意:fdatasync函数类似于fsync,但是它只影响文件的数据部分,而除了数据部分,fsync还会同步更新文件的属性。
3.14 函数fcntl
#include<fcntl.h>
int fcntl(int fd,int cmd,.../*int arg*/);
注意:该函数的作用是用来改变已经打开文件的属性。
fcntl函数有以下五种功能:
(1)复制一个已有的文件描述符(cmd=F_DUPFD或者F_DUPFD_CLOEXEC)。
(2)获取/设置文件描述符标志(cmd=F_GETFD或F_SETFD);
(3)获取/设置文件状态标志(cmd=F_GETFL或F_SETFL);
(4)获取/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN);
(5)获取/设置记录锁(cmd=F_GETLK,F_SETLK,F_SETLKW);
说明如下: F_DUPFD,复制文件描述符,新文件描述符作为函数值返回。他是尚未打开的各文件描述符中大于或等于第三个参数中各值的最小值;新文件描述符有它自己的一套文件描述符标志,其FD_CLOEXEC文件描述符标志被清除。 F_DUPFD_CLOEXEC,复制文件描述符,设置与新描述符关联的FD_CLOEXEC文件描述符标志的值,返回新文件描述符。 F_GETFD,获取文件描述符标志,当前只设置了一个文件描述符标志FD_CLOEXEC。 F_SETTD,设置文件描述符标志,新标志值按照第三个参数设置 F_GETFL,获取文件状态标志,有以下值: O_RDONLY,只读打开 O_WRONLY,只写打开 O_RDWR,读写打开 O_EXEC,只执行打开 O_SEARCH,只搜索打开目录 --------前三个的值分别是0,1,2,前五个文件状态标志互斥 O_APPEND,追加写 O_NONEBLOCK,非阻塞模式 O_SYNC,等待写完成(数据和属性) O_DSYNC,等待写完成(仅数据) O_RSYNC,同步读和写 首先必须用屏蔽字O_ACCMODE取得访问方式位,然后将结果与这5个值中的每一个想比较。也就是说O_ACCMODE可以判断是前五种中的哪一种 F_SETFL,将文件状态标志设置为第三个值,可以更改的几个标志是O_APPEND,O_NONBLOCK,O_SYNC,O_DSYNC,O_RSYNC F_GETOWN,获取当前接收SIGIO和SIGURG信号的进程ID或进程组ID F_SETOWN,设置接收SIGIO,SIGURG信号的进程的ID或进程组ID,正的arg指定一个进程ID,负的arg指定一个指定一个|arg|的进程组ID
//写一个程序,首先设置O_APPEND的值,然后删除O_APPEND的值,不知道为什么下面的程序的结果不在想象之中?
#include<fcntl.h>
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<stdlib.h>//函数atoi
#include<malloc.h>//函数malloc
#include<time.h>//函数clock
int main(int argc,char*argv[]){
int fd=open("d1/up1",O_WRONLY|O_APPEND);
if(fd<0){
printf("文件打开失败!\n");
exit(0);
}
//追加一点数据
char buf[]="abcdef";
if(write(fd,buf,6)!=6){
printf("文件写错误!\n");
exit(0);
}
printf("你现在可以查看文件信息,之后你可以输入六个新的字符:");
int len=scanf("%s",buf);
printf("您现在输入的数据是:%s\n",buf);
int flags=fcntl(fd,F_GETFL);
//输出文件状态信息
switch(flags&O_ACCMODE){
case O_RDONLY:
printf("O_RDONLY\n");
break;
case O_WRONLY:
printf("O_WRONLY\n");
break;
case O_RDWR:
printf("O_RDWR\n");
break;
default:
printf("unknown\n");
}
if(flags&O_APPEND){
printf("O_APPEND\n");
}
if(flags&O_SYNC){
printf("O_SYNC\n");
}
if(flags<0){
printf("获取文件给状态标志出错!\n");
exit(0);
}
flags&=~O_APPEND;//清除添加状态
if(fcntl(fd,F_SETFL,flags)<0){
printf("设置文件状态标志出错!\n");
exit(0);
}
//写入新的数据
if(write(fd,buf,6)<0){
printf("清除O_APPEND标志之后写入新数据出错!\n");
exit(0);
}
printf("文件状态标志成功更改,数据写入成功,你现在可以查看数据\n");
return 0;
}
//写一个程序测试buffsize=4096的时候的写到/dev/null的所花费的时间
//正常写到磁盘的时间
//设置O_SYNC后写到磁盘的时间
//写到磁盘后调用fdatasync
//写到磁盘后调用fsync
//设置O_SYNC后写到磁盘,接着调用fsync
代码如下:
#include<fcntl.h>
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<stdlib.h>//函数atoi
#include<malloc.h>//函数malloc
#include<time.h>//函数clock
int main(int argc,char*argv[]){
if(argc<4){
printf("usage ./a.out source dest type\n");
exit(0);
}
int type=atoi(argv[3]);
int fd=open(argv[1],O_RDONLY);
if(fd<0){
printf("源文件不存在!\n");
exit(0);
}
//接受一个文件名,只写方式打开,如果是/dev/null,则是我们要测试的第一种
int fd2=open(argv[2],O_WRONLY|O_CREAT);
if(fd2<0){
printf("打开文件写失败!\n");
exit(0);
}
//如果是设置O_SYNC后写到磁盘
if(type==3||type==6){
int flags=fcntl(fd2,F_GETFL);
flags&=O_SYNC;
fcntl(fd2,F_SETFL,flags);
}
//开始记录时间
char buf[4096];
int count=0;
time_t start=clock();
while((count=read(fd,buf,4096))>0){
if(write(fd2,buf,count)<0){
printf("写入文件出错\n");
}
}
//如果是4调用fdatasync,如果是5,6需要调用fsync
if(type==4){
fdatasync(fd2);
}
if(type==5||type==6){
fsync(fd2);
}
time_t end=clock();
printf("第%d种情况,花费时间%f秒\n",type,1.0*(end-start)/CLOCKS_PER_SEC);
return 0;
}
结果如下:
buffsize=4096的时候的写到/dev/null的所花费的时间
//正常写到磁盘的时间
//设置O_SYNC后写到磁盘的时间
//写到磁盘后调用fdatasync
//写到磁盘后调用fsync
//设置O_SYNC后写到磁盘,接着调用fsync
第1种情况,花费时间0.158586秒
第2种情况,花费时间4.038063秒
第3种情况,花费时间4.116780秒
第4种情况,花费时间4.098190秒
第5种情况,花费时间4.062254秒
第6种情况,花费时间4.150806秒
3.15 函数ioctl
#include<unistd.h>
#include<sys/ioctl.h>
int ioctl(int fd,int request,...);
通常对设备进行read,write,lseek等函数无法完成的操作可以使用ioctl函数完成,例如对磁带的倒带,倒退一个记录,声音大小的调节等待。
3.16 /dev/fd
较新的系统都提供/dev/fd的目录,打开文件/dev/fd/n等效于复制文件描述符n。
即fd=open(“/dev/fd/n”,mode);
等效于dup(n);
在linux中调用creat的打开/dev/fd/n的时候一定要小心,因为linux实现使用指向实际文件的符号链接,在/dev/fd文件上使用creat会导致底层文件截断。