文件I/O
一、文件描述符
所有执行I/O操作的系统调用都以文件描述符,一个非负整数指代打开的文件,文件描述符用以表示所有类型的已打开文件。针对每个进程,文件描述符都自成一套。
标准文件描述符在<unistd.h>
下定义如下:
二、常用函数
1、函数open和openat
#include<fcntl.h>
int open(const char *path,int oflag,.../mode_t mode*/);
//path:文件名称
//oflag:指代文件访问模式,多选项进行"或"运算,c参数如图所示。
int openat(int fd,char *path,int oflag,.../*mode_t mode*/);
//fd参数把open和openat函数区分开,共有3种可能性:1.path参数指定的是绝对路径名,在这种情况下,fd参数被忽略,openat函数就相当于open函数。
//2.path参数指定的是相对路径名,fd参数指出了相对路径名在文件系统 中的开始地址。fd参数是通过打开相对路径名所在的目录来获取。
//3.)path参数指定了相对路径名,fd参数具有特殊值AT_FDCWD。在这种 情况下,路径名在当前工作目录中获取,openat函数在操作上与open函数类似。
2.文件名和路径名截断
- 如果
NAME_MAX是14
,若创建15个人字符的新文件,新文件名截断14个字符,不返回任何信息,BSD返回出错状态,errno设置为ENAMETOOLONG
。 - 若
NNAME_MAX是14
,而存在一个文件名恰好是14个字符的文件,那么以路径名作为参数的任一函数都无法确定该文件的原始名是什么。因为这些函数无法判断该文件是否被截断过。 - 在
POSIX.1
中,常量_POSIX_NO_TRUNC
(变化值)决定是要截断过长的文件名或路 径名,还是返回一个出错。 - 可以用
fpathconf或pathconf
来查询目录具体支持何种 行为,到底是截断过长的文件名还是返回出错。 - 若
_POSIX_NO_TRUNC
有效,则在整个路径名超过PATH_MAX
,或路径名 中的任一文件名超过NAME_MAX
时,出错返回,并将errno设置为ENAMETOOLONG
。
3.creat函数
创建新的文件夹:
#include<fcntl.h>
int creat(const char *path,mode_t mode);
//creat以只写方式打开所创建的文件
//提供open新版本之前,如果要创建一个临时文件,并要写该文件,然后又读该文件,则必须调用creat、close然后调用open。
//等价于:
int open(path,O_WRONLY|O_CREAT|O_TRUNC,mode);
4.close函数
//关闭已经打开的文件,释放资源
#include<unistd.h>
int close(int fd);
//进项错误检查
if(close(fd)==-1)
errExit("close");
//捕获错误:企图关闭未打开的文件描述符,或两次关闭同一文件描述符
5.lseek函数
//显示的为打开文件设置文件偏移量
//打开的文件,系统记录其偏移量,文件偏移量称为读写偏移量或指针。
//文件偏移量是指执行下一行read()或write()操作的文件起始位置,头部位置表示。
//打开文件除非指定O_APPEND选项,否则偏移量为0。
#include<unistd.h>
off_t lseek(int fd,off_t offset,int whence);
//fd:语已经打开的文件描述符
//offset:指定了一个以字节为单位的数值。
//whence表明应按照哪个基点来解释offset参数:
//若whence是SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个 字节。
//若whence是SEEK_CUR,则将该文件的偏移量设置为其当前值加offset, offset可为正或负。
//若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset, offset可正可负。
//若lseek成功执行返回新的文件偏移量,使用下面获取当前位置:
off_t currpos;
currpos = lseek(fd,0,SEEK_CUR);
//这种方法也可用来确定所涉及的文件是否可以设置偏移量。
//如果文件描述 符指向的是一个管道、FIFO或网络套接字,则lseek返回−1,并将errno设置为 ESPIPE。
int main(void)
{
if(lseek(STDIN_FILEND,0,SEEK_CUR)==-1)
printf("cannot seek\n");
else
pritnf("seek OK\n");
exit(0);
}
6.文件空洞
-
如果文件偏移量已然跨越了文件结尾,然后再执行I/O操作,read()调用将返回0,表示文件结尾。但write()函数可以在文件结尾后任意位置写入数据。从文件结尾后新写入数据间的这段空间被称为
文件空洞
。 -
文件空洞中时存在字节的,读取空洞将返回0填充的缓冲区。
-
文件空洞并不要求在磁盘上占用存储区,具体处理方式与文件系统的实现有关,当定位到超出文件尾端之后写时,对于新写的数据需要分配磁盘块,但是对于原文件尾端和新开始写位置之间的部分则不需要分配磁盘块。
创建具有空洞的文件:
#include<stdio.h>
#include<fcntl.h>
char buf1[] = "abcdefghij";
char buf2[]="ABCDEFGHIJ";
int mian(void)
{
int fd;
if((fd = creat("file.hole",FILE_MODE))<0)
err_sys("creat error");
if(write(fd,buf1,10)!=10)
err_sys("buf1 write error");
if(lseek(fd,16384,SEEK_SET)==-1)
err_sys("lseek error");
if(write(fd,buf2,10)!=10)
err_sys("buf2 write error");
exit(0);
}
lseek偏移量类型为off_t
,实现由各自特定平台决定大小合适数据类型。现在大部分提供两组接口处理偏移量(32、64位)。Single UNIX Specification
向应用程序提供了一种方法,使其通过sysconf
函数确定支持何种环境。下图总结了定义的sysconf
常量:
c99 编译器要求使用 getconf(1)
命令将所期望的数据大小模型映射为编译和 链接程序所需的标志。
7.read函数
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
//返回值:读到的字节数,若已到文件尾,返回0;若出错,返回−1
//ssize_t有符合整数类型,存放字节数或-1。
//文件描述符
//buf:提供用来存放输入数据的内存缓冲区地址
//count:指定最多能读取的字节数。
//read读取从中断读取一连串字符:
//read读取任意字节
//缓冲区结尾需要必须追加字符串结束的空字符
#define MAX_READ 20
char buffer[MAX_READ+1];
ssize_t numread;
numread = read(STDIN_FILEND,budder,MAX_READ);
if(numread == -1)
errexit("read");
buffer[numread] = '\0';
printf("The input data was:%s\n",buffer);
有多种情况可使实际读到的字节数少于要求读的字节数:
-
读普通文件时,在读到要求字节数之前已到达了文件尾端。
-
当从终端设备读时,通常一次最多读一行。
-
•当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节 数。
-
当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将 只返回实际可用的字节数
-
当从某些面向记录的设备(如磁带)读时,一次最多返回一个记录
-
当一信号造成中断,而已经读了部分数据量时。
//读操作从文件的当前偏移量处开始,在成功返回之前,该偏移量 将增加实际读到的字节数。
//函数原型:
int read(int fd, char *buf, unsigned nbytes);
//修改为
ssize_t read(int fd, void *buf, size_t nbytes);
8.write函数
//打开文件中写入数据
#include<unistd.h>
sszie_t write(int fd,const void *buf,size_t nbytes);
//返回值通常与参数nbytes的值相同,否则表示出错。//write出错的常见原因是磁盘已写满,或者超过了给定进程的文件长度限制。
//普通文件,写操作从文件的当前偏移量处开始。
//在打开该文件时,指定了O_APPEND选项,则在每次写操作之前,将文件偏移量设置在文件的当前结尾处。
//在一次成功写之后,该文件偏移量增加实际写的字节数。
9.通用I/O模型以外的操纵:ioctl函数
#include<sys/ioctl.h>
int ioctl(int fd,int request,.../*argp*/);
//fd:文件描述符
//request:指定将在fd执行的操作
//request参数确定argp期望的类型。
//通常情况下argp指向整数或结构的指针。
10.创建临时文件
有些程序需要创建一些临时文件,仅供其在运行期间使用,程序终止后即行删除。
//生成一个唯一文件名并打开该文件,返回一个可以用于I/O调用的文件描述符。
#include<stdlib.h>
int mkstemp(char *template);
//创建一个名称唯一的临时文件,并以读写方式将其打开。
#include<stdio.h>
FILE *tmpfile(void);
三、I/O效率
#define BUFFSIZE 4096
int main(void)
{
int n;
char buf[BUFFSIZE];
while((n=read(STDIN_FILENO,buf,BUFFSIZE))>0)
if(write(STDOUT_FILENO,buf,n)!=n)
err_sys("write error");
if(n<0)
err_sys("read error");
exit(0);
}
//进程终止时,内核会关闭进程的所有打开的文件描述符,此进程不关闭输入和输出文件。
//Unix内核而言,文本文件和二进制文件无区别
如果选取BUFFSIZE
的值是关键,不同值由不同的显示效果,如下所示20种不同的缓冲区长度,读516581760
字节的效果:
- 图中CPU时间的结构最小值差不多出现在
BUFFSIZE为4906
及以后的位置,继续增加缓冲区长度对此时间几乎没有影响。 - 大多数文件系统为改善性能都采用某种预读技术。
- 当检测到正进行顺序读取时,系统就试图读入比应用所要求的更多数据,并假想应用很 快就会读这些数据。
- 预读的效果可以从上图中看出,缓冲区长度小至32字节时 的
时钟时间与拥有较大缓冲区长度
时的时钟时间几乎一样。 - 应当了解,在什么时间对实施文件读、写操作的程序进行性能度量。
- 操作系统试图用高速缓存技术将相关文件放置在主存中,所以如若重复度量程序性能,那么后续运行该程序所得到的计时很可能好于第一次。
- 第一次 运行使得文件进入系统高速缓存,后续各次运行一般从系统高速缓存访问文件,无需读、写磁盘。
- 图中不同缓冲区长度的各次运行使用不同的文件副本,所以后一次运行不会在前一次运行的高速缓存中找到它需要的数据。这 些文件都足够大,不可能全部保留在高速缓存中。
三、文件共享
内核使用3种数据结构表示打开文件,之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描 述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
- 文件描述符标志。
- 指向一个文件表项的指针。
内核为所有打开文件维持一张文件表。每个文件表项包含:
- 文件状态标志(读、写、添写、同步和非阻塞等);
- 当前文件偏移量;
- 指向该文件
v节点
表项的指针。
- 指向该文件
- 当前文件偏移量;
-
每个打开文件(或设备)都有一个 v 节点(v-node)结构。v 节点包 含了文件类型和对此文件进行各种操作函数的指针,v节点还 包含了该文件的i节点(i-node,索引节点)。
-
这些信息是在打开文件时从磁盘 上读入内存的,所以,文件的所有相关信息都是随时可用的。
-
i 节点包含 了文件的所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等。
-
Linux没有使用v节点,而是使用了通用i节点结构。虽然两种实现有所不 同,但在概念上, v节点与i节点是一样的。两者都指向文件系统特有的i节点结构。
-
打开文件描述符表可存放 在用户空间(作为一个独立的对应于每个进程的结构,可以换出)。
-
非进程表中,这些表也可以用多种方式实现,不必一定是数组,例如:结构的链表。不考虑实现细节的话,通用概念是相同的
下图显示了一个进程对应的3张表之间的关系。该进程有两个不同的打开文件:一个文件从标准输入打开(文件描述符0),另一个从标准输出打开(文件描述符为1)。
两个独立进程打开同一个文件,如下所示:
假定第一个进程在文件描述符3上打开该文件,而另一个进程在文件描述符4上打开该文件。打开该文件的每个进程都获得各自的一个文件表项,但对 一个给定的文件只有一个v节点表项。之所以每个进程都获得自己的文件表项, 是因为这可以使每个进程都有它自己的对该文件的当前偏移量。
详细说明:
-
在完成每个
write
后,在文件表项中的当前文件偏移量即增加所写入的字节 数。如果这导致当前文件偏移量超出了当前文件长度,则将i节点表项中的当前文件长度设置为当前文件偏移量。 -
如果用
O_APPEND
标志打开一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有追加写标志的文件执行写操作时,文件表项 中的当前文件偏移量首先会被设置为i节点表项中的文件长度。这就使得每次写 入的数据都追加到文件的当前尾端处。 -
若一个文件用
lseek
定位到文件当前的尾端,则文件表项中的当前文件偏移 量被设置为i节点表项中的当前文件长度(注:这与用O_APPEND
标志打开文 件是不同的)。 -
lseek
函数只修改
文件表项中的当前文件偏移量,不进行任何I/O
操作。
四、原子操作
if(lseek(fd,OL,2),<0)
err_sys("lseek error");
if(write(fd,buf,100)!=100)
err_sys("write error");
-
原子操作:表示在多个线程访问同一个全局资源的时候,能够确保所有其他的线程都不在同一时间内访问相同的资源。也就是他确保了在同一时刻只有唯一的线程对这个资源进行访问。这有点类似互斥对象对共享资源的访问的保护,但是原子操作更加接近底层,因而效率更高。
-
例如上代码:两个进程都对同一文件进行写操作。每个进程都已打开文件,但未使用
O_APPEND
标准。两个进程都调用lseek和write函数进行偏移量的设置,结果会造成后一个进程将覆盖前一个进程写到文件的数据。- 解决方法:使用原子操作:即打开文件时设置
O_APPEND
标准。
- 解决方法:使用原子操作:即打开文件时设置
-
1.函数pread和pwrite
//允许原子性地定位并执行I/O,同样能解决上面问题,但偏移量不能更新。
#include<unistd.h>
//调用pread相当于调用lseek后调用read。
//但是pread又与这种顺序调用有下列重要区别
//调用pread时,无法中断其定位和读操作
//不更新当前文件偏移量
ssize_t pread(int fd,void *buf,size_t nbytes,off_t offset);
//调用pwrite相当于调用lseek后调用write,但也与它们有类似的区别。
ssize_t pwrite(int fd,const void *buf,size_t nbytes,off_t offset);
五、函数dup和dup2、dup3
//用来复制一个现有的文件操作符。
#include<unistd.h>
int dup(int fd);//复制一个打开的文件描述符fd,并返回一个新描述符,
int dup2(int fd,int fd2);//fd2指定新描述符的值,fd语句打开,则先将其关闭。
//fd等于fd2,则返回fd2,且不关闭,否则fd2的FD_CLOEXEC文件描述符被清除,这样fd2在进程调用exec时已是打开状态。
int dup3(int fd,int fd2,int flags);
//与dup2相同,只增加了参数flag,用来修改系统调用行为的位掩码。
//只支持一个标志O_CLOEXEC,促使内核新文件描述符设置标志(FD_CLOEXEC),类似于对open函数调用O_CLOEXEC标志。
函数返回的新文件描述符与参数fd共享同一个文件表项,如下所示:
//上图若进程启动时执行:
newfd = dup(1);
//开始执行时,若下一个可用的描述符为3(0,1和2都是由shell打开)。
//两个描述符指向同一个文件表项,所以共享同一个文件状态,以及同一文件偏移量。
//
//复制描述符另一种方法:
dup(fd);
//等价于:
fctl(fd,FD_DUPFD,0);
dup2(fd,fd2);
等价于:
close(fd2);
fcntl(fd,F_DUPFD,fd2);
//dup2并不完全等价于close加上fcntl,因为:
//1.dup2是个原子操作。而clsoe和fcntl包含两个函数调用,可能在之间调用信号捕获函数,可能修改文件描述符。如果不不同的线程改不了文件描述符也会出现相同问题。
//dup2和fcntl有些不同的errno。
六、函数sync、fsync和fdatasync
传统的UNIX系统实现在内核中设有缓冲区高速缓存或页高速缓存,大多数 磁盘I/O
都通过缓冲区进行。
当我们向文件写入数据时,内核通常先将数据复制 到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式被称为延迟写
。
//当内核需要重用缓冲区来存放其他磁盘块数据时,它会把所有延迟写数据块写入磁盘。
//为了保证磁盘上实际文件系统与缓冲区中内容的一致性。
#include<unistd.h>
int fsync(int fd);//只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操 作结束才返回。
int fdatasync(int fd);//数类似于fsync,但它只影响文件的数据部分。而除数据外, fsync还会同步更新文件的属性。
void sync(void);//将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待 实际写磁盘操作结束。
七、函数fcntl
//改变已经打开的文件属性
#include<fcntl.h>
int fcntl(int fd,int cmd,.../*int arg*/);
//返回值与命令有关,出错返回-1,成功返回某个其他值。
//F_DUPFD命令返回新的文件描述符
//F_GETFD、F_GETFL命令返回相 应的标志
//F_GETOWN命令返回一个正的进程ID或负的进程组ID。
fcntl函数有以下5种功能:
-
复制一个已有的描述符(
cmd=F_DUPFD或F_DUPFD_CLOEXEC
)。 -
获取/设置文件描述符标志(
cmd=F_GETFD或F_SETFD
)。 -
获取/设置文件状态标志(
cmd=F_GETFL或F_SETFL
)。 -
获取/设置异步I/O所有权(
cmd=F_GETOWN或F_SETOWN
)。 -
获取/设置记录锁(
cmd=F_GETLK、F_SETLK或F_SETLKW
)。 -
F_DUPFD
:复制文件描述符fd。新文件描述符作为函数值返回。 -
F_DUPFD_CLOEXEC
:复制文件描述符,设置与新描述符关联的FD_CLOEXEC
文件描述符标志的值,返回新文件描述符。 -
F_GETFD
:对应于fd的文件描述符标志作为函数值返回。当前只定义了一个 文件描述符标志FD_CLOEXEC
。 -
F_SETFD
:对于fd设置文件描述符标志。新标志值按第3个参数设置。 -
F_GETFL
: 对应于fd的文件状态标志作为函数值返回。open函数 时,已描述了文件状态标志。如下图所示:
-
F_SETF
:将文件状态标志设置为第3个参数的值(取为整型值)。可以更改的几个标志是:O_APPEND、O_NONBLOCK、O_SYNC、O_DSYNC、 O_RSYNC、O_FSYNC和O_ASYNC
。 -
F_GETOWN
:获取当前接收SIGIO和SIGURG
信号的进程ID或进程组ID。 -
F_SETOWN:设置接收
SIGIO和SIGURG
信号的进程ID或进程组ID。
例:第1个参数指定文件描述符,并对于该描述符打印所选择的文件标志说明:
#include "apue.h"
#include <fcntl.h>
int
main(int argc, char *argv[])
{
int val;
if (argc != 2)
err_quit("usage: a.out <descriptor#>");
if ((val = fcntl(atoi(argv[1]), F_GETFL, 0)) < 0)
err_sys("fcntl error for fd %d", atoi(argv[1]));
switch (val & O_ACCMODE) {
case O_RDONLY:
printf("read only");
break;
case O_WRONLY:
printf("write only");
break;
case O_RDWR:
printf("read write");
break;
default:
err_dump("unknown access mode");
}
if (val & O_APPEND)
printf(", append");
if (val & O_NONBLOCK)
printf(", nonblocking");
if (val & O_SYNC)
printf(", synchronous writes");
#if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC) && (O_FSYNC != O_SYNC)
if (val & O_FSYNC)
printf(", synchronous writes");
#endif
putchar('\n');
exit(0);
}
fcntl函数设置O_SYNC
会增加系统时间和时钟闹钟。
1.set_fl函数
//在修改文件描述符标志或文件状态标志时必须谨慎,先要获得现在的标志 值,然后按照期望修改它,最后设置新标志值。
//不能只是执行F_SETFD或 F_SETFL命令,这样会关闭以前设置的标志位。
//set_fl函数是对于一个文件描述符设置一个或多个文件状态标志的函数。
#include "apue.h"
#include <fcntl.h>
void set_fl(int fd, int flags)
{
int val;
if ((val = fcntl(fd, F_GETFL, 0)) < 0)
err_sys("fcntl F_GETFL error");
val |= flags;
if (fcntl(fd, F_SETFL, val) < 0)
err_sys("fcntl F_SETFL error");
}
//使当前文件状态标志值val与flags的反码进行逻辑“与”运算。
#include "apue.h"
#include <fcntl.h>
void clr_fl(int fd, int flags)
{
int val;
if ((val = fcntl(fd, F_GETFL, 0)) < 0)
err_sys("fcntl F_GETFL error");
val &= ~flags;
if (fcntl(fd, F_SETFL, val) < 0)
err_sys("fcntl F_SETFL error");
}
//在I/O效率标题中代码增加set_fl,则开启了同步1写标志:
set_fl(STDOUT_FILENO,0_SYNC);
//使每次write都要等待,直到已写到磁盘再返回。
八、函数ioctl
ioctl函数一直是I/O操作的杂物箱。不能用本章中其他函数表示的I/O
操作通 常都能用ioctl
表示。终端I/O是使用ioctl
最多的地方。
#include<unistd.h>//System V
#include<sys/ioctl.h>//BSD and Linux
int ioctl(int fd,int request,...);
//fd:某个设备或文件已经打开的文件描述符
//request:指定了将在fds上执行的控制操作。
每个设备驱动程序可以定义它自己专用的一组 ioctl
命令,系统则为不同种 类的设备提供通用的ioctl命令。下图中总结了FreeBSD支持的通用ioctl命令的 一些类别。
磁带操作使我们可以在磁带上写一个文件结束标志、倒带、越过指定个数的文件或记录等,用本章中的其他函数(
read、write、lseek
等)都难于表示这 些操作,所以,对这些设备进行操作最容易的方法就是使用ioctl
。
九、/dev/fd
较新的系统都提供名为/dev/fd
的目录,其目录项是名为 0、1、2
等的文 件。打开文件/dev/fd/n
等效于复制描述符n
(假定描述符n是打开的)。
fd = open("/dev/fd/0", mode);
//大多数系统忽略它所指定的mode,而另外一些系统则要求 mode必须是所引用的文件(在这里是标准输入)初始打开时所使用的打开模式的一个子集。
//如:O_WRONLY
//等价于:
fd=dup(0);
//描述符0和fd共享同一文件表项
//描述符0先前被 打开为只读,那么我们也只能对fd进行读操作。
//即使系统忽略打开模式,而且 下列调用是成功的:
fd = open("/dev/fd/0", O_RDWR);
//我们仍然不能对fd进行写操作
//注:Linux实现中的/dev/fd是个例外,把文件描述符映射成指向底层物理文件的符号链。
//例:当打开/dev/fd/0时,事实上正在打开与标准输入关联的文件,因此返回的新文件描述符的模式与/dev/fd文件描述符的模式其实并不相 关。
/dev/fd
也可以作为路径名参数调用creat
,这与调用open时用O_CREAT
作为第2个参数作用相同。例:若一个程序调用creat
,并且路径名 参数是/dev/fd/1
,那么该程序仍能工作。- 某些系统提供路径名
/dev/stdin、/dev/stdout
和/dev/stderr
,这些等效 于/dev/fd/0、/dev/fd/1和/dev/fd/2
。
/dev/fd
文件允许使用路径名作为调用参数的程序,能处理其他路径名的相同方式处理标准输入和输出。
例:cat(1)
命令对其命令行参数采取了一种特殊处理,将单独的一个字符“-”解释为标准输入。
filter file2 | cat file1 - file3 | lpr
// 首先cat读file1
//接着读其标准输入(也就是filter file2命令的输出)
//然后读file3
//若支持/dev/fd,则可以删除cat对“-”的特殊处理,
filter file2 | cat file1 /dev/fd/0 file3 | lpr
//命令行参数的“-”特指标准输入或标准输出
//问题:
//用“-”指定第一个文件,那么看来就像指定了命令行的一个选项。/dev/fd则提高了文件名参数的一致性,也更加清晰。