文章目录
文件I/O
1.引言
本章介绍:
打开、读、写文件所用函数(open、read、write、lseek和close);
不带缓冲的I/O,即系统调用范畴的I/O,它们是由POSIX.1和Single Unix Specification的组成部分;
多进程间共享文件及dup、fcntl、sync、fsync和ioctl函数。
2.文件描述符
对于内核而言,所有打开的文件都通过文件描述符引用。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符,当读写一个文件时,使用open或create返回的文件描述符标识该文件,将其作为参数传送给read和write。一般系统将文件描述符0(STDIN_FILENO)、1(STDOUT_FILENO)和2(STDERR_FILENO)分别与标准输入、标准输出和标准错误相连。
3.函数open和openat
- 使用<fcntl.h>中的函数open(const char *path,int oflag,…)和openat(int fd,const char *path,int oflag,…)可以打开文件,…是可选参数,可用来指定新创建文件的访问权限,path参数是要打开或创建文件的名字,oflag选项可以用来说明文件的打开模式,共有5个必须指定1个且指定1个的常量(O_RDONLY、O_WRONLY、O_RDWR、O_EXEC和O_SEARCH)和以下可选常量:
O_APPEND、O_CLOEXEC、O_CREAT、O_DIREACTORY、O_EXCL、O_NOCTTY、O_NOFOLLOW、O_NONBLOCK、O_SYNC、O_TRUNC和O_TTY_INIT。以及两个Single Unix Specification(同时也是POSIX.1)中规定的同步输入和输出选项的两个可选常量:O_DSYNC和O_RSYNC。 - 由open或openat返回的文件描述符一定是最小的未用文件描述符。openat中当path为相对路径名时,fd为相对路径的起始地址,当fd为AT_FDCWD时,相对路径名从当前工作目录算起;
- 可以用_POSIX_NO_TRUNC决定当路径长度超过系统限制时要截断还是报错;
4.函数create
可用create(const char *path,mode_t mode)函数创建一个新文件,等同于open(path,O_WRONLY | O_CREAT | O_TRUNC,mode),create的问题是只能以可写方式打开所创建的文件,可以使用open(path,O_RDWR | O_CREAT | O_TRUNC,mode)来实现创建可读写的文件。
5.函数close
可用函数close关闭一个打开的文件,函数原型int close(int fd),关闭一个文件还将释放进程加在该文件上的所有记录锁。当一个进程终止时,内核自动关闭它所有的打开文件。
6.函数lseek
- 每个打开文件都有一个与其相关联的“当前文件偏移量”,读、写操作都从此开始,当不同O_APPEND指定时,此偏移量被指定为0。可以用lseek函数显式地为一个打开文件指定偏移量,off_t lseek(int fd,off_t offset,int whence),参数whence可以设置为SEEK_SET、SEEK_CUR和SEEK_END中的一个,分别表示设置新的偏移量为起始位置+offset、当前偏移量+offset和文件长度+offset。
- 该方法可以用来确定所涉及的文字是否可以设置偏移量,若文件描述符指向的是一个管道、FIFO或网络套接字,则lseek返回-1,且将ERRNO设置为ESPIPE。
- 文件偏移量可以大于文件长度,但写入时会形成文件空洞,空洞与具体系统实现有关,但读入空洞时应该会得到0。
- lseek使用的偏移量是用off_t来表示的,所以允许具体实现根据各自特定的平台自行选择大小合适的数据类型,Single Unix Specification向应用程序提供了sysconf函数来确定支持何种常量,C99编译器要求使用getconf(1)命令将所期望的数据大小模型映射为编译和链接程序所需的标志。
7.函数read
函数ssize_t read(int fd,void buf,size_t nbytes)用于从打开的文件中读数据,若读到数据返回读到的字节数,读到文件尾则返回0,有多种情况可以使得读到的字节数小于要求读的字节数:普通文件读到文件尾、终端设备一般一次读一行、从网络读时网络的缓冲机制可能造成返回值小于所要求读的字节数、从管道或FIFO读时若其中字节数小于要求字节数、从面向记录的设备(如磁带)则一次只能读一条记录、信号中断而已经读了一部分时。POSIX.1将函数原型int read(int fd,charbuf,unsigned nbytes)进行了更改,更改到现在的形式。
8.函数write
函数原型ssize_t write(int fd,const void *buf,size_t nbytes),其返回值通常与参数nbytes的值相同,否则表示出错。
9.I/O的效率
#include "apue.h"
#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);
}
大多数文件系统为改善性能都采用某种预读技术。
10.文件共享
Unix系统支持在不同进程间共享打开文件。系统内核使用3种数据结构表示打开文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
(1)每个进程在进程记录表中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个向量,每个描述符占用一项。与每个文件描述符相关联的是:
a.文件描述符标志(close_on_exec);
b.指向一个文件表项的指针。
(即对于每一个进程,其所打开的文件在其进程表中都有一条数据存储其文件描述符;)
(2)内核为所有打开文件维持一张文件表。每个文件表项包括:
a.文件状态标志(读、写、添写、同步和非阻塞等);
b.当前文件偏移量;
c.指向该文件v节点表项的指针。
(3)每个打开文件(或设备)都有一个v节点(v-node)结构。v节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点(i-node,索引节点)。这些信息在打开文件时读入内存,所以,文件所有相关信息都是可用的。(i节点包含了文件所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等)
(创建v节点结构的目的是对在一个计算机系统上多文件系统类型提供支持,由贝尔实验室和Sun公司分别独立完成,Sun将其称为虚拟文件系统,把与文件系统无关的 i 节点部分称为v节点。Linux没有将相关数据结构分为 i 节点和 v 节点,而是采用了一个与文件系统相关的 i 节点和一个与文件系统无关的 v 节点。)
当两个独立进程各自打开了同一文件,则有如下关系(每个进程对于该文件都有自己的文件表项,但对一个给定的文件只有一个v节点表项):
以下对操作进行说明:
- 在完成每个write后,在文件表项中的当前文件偏移量即增加所写入的字节数,如果这导致当前文件偏移量超出了当前文件长度,则将 i 节点表项中的当前文件长度设置为当前文件偏移量(也就是该文件加长了);
- 若用O_APPEND标志打开一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有追加写标志的文件执行写操作时,文件表项中的当前文件偏移量首先会被设置为 i 节点表项中的文件长度。这就使得每次写入的数据都追加到文件的当前尾端处;
- 若一个文件用 lseek 定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为 i 节点表项中的当前文件长度(注意,这与用O_APPEND标志打开文件是不同的,详见3.11节);
- lseek函数只修改文件表项中的当前文件偏移量,不进行任何 I/O 操作。
可能有多个文件描述符指向同一文件表项,如 dup 和 fork 之后。文件描述符只用于一个进程的一个描述符,而文件状态标志则应用于指向该给定文件表项的任何进程中的所有描述符。
本小节介绍了内核为一个进程所维护的打开文件的数据结构(进程表项、文件表项和v节点表项),注意每种操作的深度(达到了哪一个表)。
11.原子操作
1. 追加到一个文件:早期Unix没有open的O_APPEND标志,则追加文件将会被写成
if(lseek(fd,OL,2) < 0) /* position to EOF */
err_sys("lseek error");
if(write(fd,buf,100) != 100) /* and write */
err_sys("write error");
对于单进程,上述代码没有问题,但多进程情况下,内核可能执行完A进程的 lseek 后切换到B进程的 write 然后又返回到A进程的 write,这样的话对于A进程的写入可能会覆盖B进程的写入数据,造成不想要的问题。
2. 函数pread和pwrite:Single Unix Specification包括了XSI扩展,该扩展允许原子性地定位并执行 I/O。pread 和 pwrite 就是这种扩展。
#include <unistd.h>
ssize_t pread(int fd,void *buf,size_t nbytes,off_t offset); //返回值:读到的字节数,若读到文件尾返回0;出错返回-1
ssize_t pwrite(int fd,const void *buf,size_t nbytes,off_t offset); //返回值:若成功,返回已写的字节数;出错返回-1
调用 pread 相当于调用 lseek 后调用 read,但是 pread 又与这种顺序调用有下列重要区别。
- 调用 pread 时,无法中断其定位和读操作;
- 不更新当前文件偏移量。( pwrite 类似)
- 3. 创建一个文件:例如 open 函数的 O_CREATE 和 O_EXCL 选项就是一个原子操作的例子。当同时指定这两个选项而该文件又已经存在时,open将失败(检查文件是否存在和创建文件这两个操作是作为一个原子操作进行的)。若没有该原子操作,则可能会编写如下程序段:
if( (fd = open(pathname,O_WRONLY) ) < 0)
if(errno == ENOENT)
{
if( (fd = creat(path,mode) ) < 0)
err_sys("creat error");
}
else
{
err_sys(""open_error");
}
该段程序同样会在多进程时,如另一个进程在 open 和 creat 之间创建了该文件,就可能会出现某些覆盖之类的问题。
原子操作即指的是不可分的多步组成的一个操作。若该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能执行所有步骤的一个子集。
12.函数dup和dup2
下面两个函数都可以用来复制一个现有的文件描述符。
#include <unistd.h>
int dup(int fd);
int dup2(int fd,int fd2);
dup 将返回一个可用的最小的文件描述符。而 dup2 可以用 fd2 参数指定新描述符的值,若 fd2 已打开,则先将其关闭。若 fd = fd2,则 dup2 返回 fd2 而不关闭它,否则,清除 fd2 的 FD_CLOEXEC 文件描述符标志,这样 fd2 在进程调用 exec 时是打开状态。执行完后 fd2 与 fd 共享同一个文件表项,如图:
可以用 fcntl 函数来实现同样的复制文件描述符的操作,某种意义上 dup( fd ) 与 fcntl( fd,F_DUPFD,0 ) 等效,close( fd2 );fcntl( fd,F_DUPFD,fd2 ) 与 dup2( fd,fd2) 等效。但 dup2 与 close 加 fcntl 有一些区别,主要在于 dup2 是一个原子操作而 close 加 fcntl 不是,dup2 和 fcntl 有一些不同的 errno。
13.函数sync、fsync和fdatasync
传统Unix系统实现在内核中设有缓冲区高速缓存和页高速缓存,大多数磁盘I/O都通过缓冲区进行。当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘,这种方式被称为延迟写。当内核需要重用缓冲区来存放其它磁盘块数据时,它会把所有延迟写数据块写入磁盘,为了保证磁盘上实际文件系统与缓冲区中内容的一致性,Unix系统提供了 sync、fsync 和 fdatasync 三个函数
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
void sync(void);
sync 将所有修改过的块缓冲区排入写队列,然后返回,它不等待实际写磁盘操作结束;
通常,称为update的系统守护进程周期性地调用(一般每隔30s) sync 函数,这就保证了定期冲洗内核的缓冲区。fsync函数只对由文件描述符 fd 指定的一个文件起作用且它等待写磁盘操作结束才返回。fsync 可用于数据库这样的应用程序。fdatasync 只影响文件的数据部分,而 fsync 还影响文件的属性部分。
14.函数fcntl
函数 fcntl 可以改变已打开文件的属性。
#include <fcntl.h>
int fcntl(int fd,int cmd,... /* int arg */)
// 若成功,返回值依赖于cmd(见下),若出错,返回-1
本节中 fcntl 函数第三个参数总是一个整数,而在 14.3 节中说明记录锁时,该参数往往为一个指针。
fcntl 函数有以下 5 种功能:
(1) 复制一个已有的描述符(cmd = F_DUPFD 或 F_DUPFD_CLOEXEC );
(2) 获取/设置文件描述符标志(cmd = F_GETFD 或 F_SETFD );
(3) 获取/设置文件状态标志(cmd = F_GETEL 或 F_SETEL );
(4) 获取/设置异步 I/O 所有权(cmd = F_GETOEN 或 F_SETOWN );
(5) 获取/设置记录锁(cmd = F_GETLK、F_SETLK 或 F_SETLKW )。
以下对 8 种 cmd 进行说明:
F_DUPFD :复制文件描述符 fd,新文件描述符作为函数值返回。新描述符与 fd 共享一个文件表项,但有它自己的一套文件描述符标志,其 FD_CLOEXEC 标志被清除(表示该描述符在 exec 时仍保持有效);
F_DUPFD_CLOEXEC:复制文件描述符,设置与新描述符关联的 FD_CLOEXEC 文件描述符标志的值,返回新文件描述符;(此标志下执行 exec 函数族后本 fd 将会被关闭)
F_GETFD:对应于 fd 的文件描述符标志作为函数值返回。当前只定义了一个文件描述符标志 FD_CLOEXEC;(即返回 FD_CLOEXEC 标志 )
F_SETFD:对于 fd 设置文件描述符标志,新标志值按第 3 个参数(取为整型值)设置;
F_GETEL:对应于 fd 的文件状态标志作为函数值返回,我们在说明 open (第三节)函数时,已描述了文件状态标志。即下图:
F_SETEL:将文件状态标志设置为第 3 个参数的值(取为整数值),可以更改的几个标志是:O_APPEND、O_NONBLOCK、O_SYNC、O_DSYNC、O_RSYNC、O_FSYNC 和 O_ASYNC;
F_GETOEN:获取当前接收 SIGIO 和 SIGURG 信号的进程 ID 或进程组 ID;
F_SETOEN:设置接收 SIGIO 和 SIGURG 信号的进程 ID 或进程组 ID,正的 arg 指定一个进程 ID,负的 arg 表示等于 arg 绝对值的一个进程组 ID;
注意,修改文件描述符标志或文件状态标志时需要谨慎,先要获得现在的标志值,然后按照期望修改它,最后设置新标志值。不能只是执行 F_SETFD 或 F_SETFL 命令,这样会关闭以前设置的标志位。
实例:对于一个文件描述符设置一个或多个文件状态标志
#include "apue.h"
#include <fcntl.h>
void set_fl(int fd,int flags)
{
int val;
if( (val = fcntl(fd,F_GETEL,0) ) < 0)
err_sys("fcntl F_GETEL error");
val |= flags;
if(fcntl(fd,F_SETEL,val) < 0)
err_sys("fcntl F_SETEL error");
}
开启同步写标志实例:
set_fl(STDOUT_FILENO,O_SYNC);
这就使得每次 write 都要等待,直至数据已写到磁盘上才返回。
15.函数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)
ioctl 是 Single Unix Specification 标准的一个扩展部分,以便处理 STREAMS 设备。但在 SUSv4 中已处于弃用状态。Unix 系统实现用它进行很多杂项设备操作,有些实现甚至将其用于普通文件。
该函数原型是 POSIX.1 中所表示的函数原型,但在 FReeBSD8.0 和 Mac OS X 10.6.8 实现中将第 2 个参数类型设置为 unsigned long。ISO C 中常用…表示其余参数,但往往通常只有另外一个参数。
此原型中,我们表示的只是 ioctl 函数本身所要求的文件,通常还要求另外的设备专用头文件。例如终端 I/O 的 ioctl 命令往往都需要头文件 <termios.h>。每个设备驱动可以定义它自己专用的一组 ioctl 命令,系统为不同设备提供通用的 ioctl 命令。
16./dev/fd
较新的系统都提供名为 /dev/fd 的目录,其目录项是名为 0、1、2 等的文件。打开文件描述符 /dev/fd/n 等效于复制描述符 n 。通常大多数系统会忽略 fd = open("/dev/fd/0",mode)中指定的 mode,而另外一些系统则要求 mode 必须是所引用的文件(这里是标准输入)初始打开时所使用的打开模式的一个子集。
某些系统提供路径名 /dev/stdin、/dev/stdout 和 /dev/stderr,这些等效于 /dev/fd/0、/dev/fd/1 和 /dev/fd/3。
/dev/fd 文件主要由 shell 使用,它允许使用路径名作为调用参数的程序,能用处理其他路径名的相同方式处理标准输入和标准输出。例如:
filter file2 | cat file1 - file3 | lpr
首先 cat 读取 file1,接着读取标准输入(也就是 filter file2 命令的输出,即将单字符 ‘-’ 解释为标准输入),然后读 file3,若支持 /dev/fd,则我们可以使用下面语句代替上述语句:
filter file2 | cat file1 /dev/fd/0 file3 | lpr
本章说明了 Unix 系统提供的基本 I/O 函数,因为 read 和 write 都在内核执行,所以称这些函数为不带缓冲的 I/O。本章介绍了不带缓冲的 I/O 的一些常用函数以及相关用法。