文件I/O

来源:互联网 发布:生成雪碧图 for mac 编辑:程序博客网 时间:2024/05/23 02:04

文件I/O

简介

对UNIX系统来说,可用的文件I/O函数主要有:打开文件、读文件、写文件等。涉及到的函数主要有:open、read、write、lseek、close。而其中read和write函数受不同缓冲长度影响。上面提到的函数常常被称为不带缓冲的I/O操作。其中不带缓冲指每个read和write都调用内核中的一个系统调用。涉及多个进程间共享资源时,原则操作就非常重要

文件描述符

对内核而言,所有打开的文件都通过文件描述符引用文件描述符是一个非负整数。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时,使用open或create返回的文件描述符标识该文件,将其作为参数传递给read和write。

UNIX系统shell把文件描述符0与进程的标准输入关联,文件描述符1与标准输出关联,文件描述符2与标准错误关联

在符合POSIX.1的应用程序中,幻数0、1、2虽然已被标准化,但应当把它们替换成富豪常量STDIN_FIFLNO、STDOUT_FILENO和STDERR_FILENO以提高可读性。这些常量在头文件

函数open和openat

调用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 */);//两个函数的返回值:若成功,返回文件描述符;若出错,返回-1。最后一个参数...表示余下的参数的数量及其类型是可变的。path参数是要打开或创建文件的名字。oflag参数可用来说明此函数的多个选项。用下列一个或多个常量进行“或”运算构成oflag参数。O_RDONLY:只读打开;O_WEONLY:只写打开;O_RDWR:读、写打开;O_EXEC:只执行打开;O_SEARCH:只搜索打开(应用于目录)。上面5个常量中必须指定一个且只能指定一个。下列常量则是可选的。O_APPEND:每次写时追加到文件的尾端。O_CLOEXEC:把FD_CLOEXEC常量设置为文件描述符标志。O_CREAT:若此文件不存在则创建它。使用此选项时,open函数需同时说明第3个参数mode(openat函数需说明第4个参数mode),用mode指定该新文件的访问权限位。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_TTY_INIT:如果打开一个还未打开的终端设备,设置非标准termios参数值,使其符合Single UNIX Specification(以及POSIX.1)中同步输入和输出选项的一部分。O_DSYNC:使每次write要等待物理I/O操作完成,但是如果该写操作并不影响读取刚写入的数据,则不需要等待文件属性被更新。O_RSYNC:使每一个以文件描述符作为参数进行的read操作等待,直至所有对文件同一部分挂起的写操作都完成。

open和openat函数返回的文件描述符一定是最小的未用描述符数值。这一点被某些应用程序用来在标准输入、标准输出或标准错误上打开新的文件。如一个应用程序可以先关闭标准输出(通常是文件描述符1),然后打开另一个文件,执行打开操作前就能了解到该文件一定会在文件描述符1上打开。

fd参数把open和openat函数区分开,共有3种可能性
- path参数指定的是绝对路径名,在这种情况下,fd参数被忽略,openat函数就相当于open函数;
- path参数指定的是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址。fd参数是通过打开相对路径名所在的目录来获取;
- path参数指定了相对路径名,fd参数具有特殊值AT_FDCWD。在这种情况下,路径名在当前工作中获取。openat函数在操作上与open函数类似。

openat函数是POSIX.1最新版本中新增的一类函数之一,希望解决两个问题。第一,让线程可以使用相对路径名打开目录中的文件,而不再只能打开当前工作目录。第二,可以避免time-of-check-to-time-of-use(TOCTTOU)错误。

TOCTTOU错误的基本思想是:如果有两个基于文件的函数调用,其中第二个调用依赖于第一个调用结果,那么程序是脆弱的。如果两个调用并不是原子操作,在两个函数调用之间文件可能改变了,这样也就造成了第一个调用的结果就不再有效,使得程序最终的结果是错误的。文件系统命名空间中的TOCTTOU错误通常处理的就是那些覆盖文件系统权限的小把戏,这些小把戏通过骗取特权程序降低特权文件的权限控制或者让特权文件打开一个安全漏洞等方式进行。

文件名和路径名截断:如果NAME_MAX是14,则在早期的System V版本中,会直接将文件名截断为14个字符,而不给出任何信息。BSD类的系统,则会返回出错状态。在POSIX.1中,常量——POSIX_NO_TRUNC决定是要截断过长的文件名或路径名,还是返回一个出错。可以通过fpathconf或pathconf来查询目录具体支持何种行为,到底是截断过长的文件名还是返回出错。若_POSIX_NO_TRUNE有效,则在整个路径名超过PATH_MAX,或路径名中的任一文件名超过NAME_MAX时,出错返回,并将errno设置为ENAMETOOLONG。

函数creat

调用creat函数创建一个新文件。

#include <fcntl.h>int creat(const char *path, mode_t mode);返回值:若成功,返回只写打开的文件描述符;若出错,返回-1。此函数等效于:open(path, O_WRONLY | O_CREAT | O_TRUNE, mode);

creat的一个不足之处是它以只写方式打开所创建的文件。在提供open的新版本之前,如果要创建一个临时文件,并要先写该文件,然后又读该文件,则必须先调用creat、close,然后在调用open。可用下列方式调用open实现:

open(path, O_RDWR | O_CREAT | O_TRUNE, mode);

函数close

调用close函数关闭一个打开文件。

#include <unistd.h>int close(int fd);返回值:若成功,返回0;若出错,返回-1。

关闭一个文件时还会释放该进程加载该文件上的所有记录锁。当一个进程终止时,内核自动关闭它所有的打开文件。很多程序都利用这一功能而不显示地用close关闭打开文件。

函数lseek

每个打开文件都有一个与其相关联的“当前文件偏移量”。它通常是一个非负整数,用以度量从文件开始处计算的字节数。通常,读、写操作都从当前文件偏移量处开始,并使偏移量增加所读写的字节数。按系统默认的情况,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0。可以地阿婆用lseek显示地为一个打开文件设置偏移量。

#include <unistd.h>off_t lseek(int fd, off_t offset, int whence_;返回值:若成功,返回新的文件偏移量;若出错,返回-1

对参数offset的解释与参数whence的值有关。
- 若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。

3个符号常量SEEK_SET、SEEK_CUR、SEEK_END是System V中引入的。在System V之前,whence被指定为0(绝对偏移量)、1(相对于当前位置的偏移量)或2(相对文件尾端的偏移量)

通常文件的当前偏移量应当是一个非负整数,但是,某些设备也可能允许负的偏移量。但对于普通文件,其偏移量必须是非负值。因为偏移量可能是负值,所以在比较lseek的返回值时应当谨慎,不要测试它是否小于0,需要测试它是否等于-1。

lseek仅将当前的文件偏移量记录在内核中,它并不引起任何I/O操作。然后,该偏移量用于下一个读或写操作。

文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为0。

文件中的空洞并不要求在磁盘上占用存储区。具体吃力方式与文件系统的实现有关,当定位到超过文件尾端之后写时,对于新写的数据需要分配磁盘块,但是对于原文件尾端和新开始写位置之间的部分则不需要分配磁盘块。

lseek使用的偏移量使用off_t类型,故允许具体实现根据各自特定的平台自行选择大小合适的数据类型。现今大多数平台提供两组接口以处理文件偏移量。一组使用32位文件偏移量,另一组使用64位文件偏移量。

注意:尽管可以实现64位文件偏移量,但是能否创建一个大于2GB(2^31-1字节)的文件则依赖于底层文件系统的类型。

函数read

调用read函数可以从打开的文件中读取数据。

#include <unistd.h>ssize_t read(int fd, void *buf, size_t nbytes);返回值:读到的字节数,若已到文件尾,则返回0;若出错,返回-1

如果read成功,则返回读到的字节数。如已到达文件的尾端,则返回0。

很多种情况可使实际读到的字节数少于要求读的字节数。

  • 读普通文件时,在读到要求字节数之前已经到达了文件尾端。
  • 当从终端设备读时,通常一次最多读一行。
  • 当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数。
  • 当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数。
  • 当从某些面向记录的设别(如磁带)读时,一次最多返回一个记录。
  • 当一信号造成中断,而已经读了部分数据量时。

**读操作从文件的当前偏移量处开始,在成功返回之前,该偏移量将增加实际读到的字节数。**POSIX.1从几个方面对read函数的原型做了更改。经典的原型定义是:

int read(int fd, char *buf, unsigned nbytes);为与ISO C一致,将第2个参数char *改为void *。其中void *为通用指针。返回值必须是一个带符号整型(ssize_t),以保证能够返回正整数字节数、0(表示文件尾端)或-1(出错)。第3个参数在历史上是一个无符号整型,这允许一个16位的实现一次读或写的数据可以多达65534个字节。

函数write

调用write函数向打开文件写数据。

#include <unistd.h>ssize_t write*(int fd, const void *buf, size_t nbytes);返回值:若成功,返回已写的字节数;若出错,返回-1。

其返回值通常与nbytes的值相同,否则表示出错。write出错的一个常见原因是磁盘已写满,或者超过了一个给定进程的文件长度限制。

对于普通文件,写操作从文件的当前偏移量处开始。如果在打开文件时,指定了O_APPEND选项,则在每次写操作之前,将文件偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。

I/O的效率

大多数文件系统为改善性能都采用某种预读(read ahead)技术。当检测到正进行顺序读取时,系统就试图读入比应用所要求的更多数据,并假想应用很快就会读这些数据。

操作系统试图用告诉缓存技术将相关文件放置在主存中,所以如若重复度量程序性能,那么后续运行该程序所得到的计时很可能好于第一次。原因是,第一次运行使得文件进入系统高速缓存,后续各次运行一般从系统高速缓存访问文件,无需读、写磁盘。

文件共享

UNIX系统支持在不同进程间共享打开文件。

内核使用3种数据结构表示打开文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。

1)每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
- 文件描述符标志;
- 指向一个文件表项的指针。

2)内核为所有打开文件维持一张文件表。每个文件表项包含:
- 文件状态标志(读、写、添写、同步和非阻塞等);
- 当前文件偏移量;
- 指向该文件v节点表项的指针。

3)**每个打开文件(或设备)都有一个v节点(v-node)结构。v节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点(i-node,索引节点)。这些信息是打开文件时从磁盘上读入内存的,所以,文件的所有相关信息都是随时可用的。如i节点包含了文件的所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等。
**
对于以上数据结构,其相关操作的说明如下:

  • 在完成每个write后,在文件表项中的当前文件偏移量即增加所写入的字节数。如果这导致当前文件偏移量超出了当前文件长度,则将i节点表项中的当前文件长度设置为当前文件偏移量(即该文件增加了);
  • 如果用O_APPEND标志打开一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有追加写标志的文件执行写操作时,文件表现中的当前文件偏移量首先会被设置为i节点表现中的文件长度。这就使得每次写入的数据都追加到文件的当前尾端处;
  • 若一个文件用lseek定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为i节点表项中的当前文件长度;
  • lseek函数只修改文件表项中的当前文件偏移量,不进行任何I/O操作。

可能有多个文件描述符项指向同一文件表项。在fork后就出现此情况,此时父进程、子进程各自的每一个打开文件描述符共享同一个文件表项。

注意:文件描述符标志和文件状态标志在作用范围方面的区别,前者只用于一个进程的一个描述符,而后者则应用于指向该给定文件表项的任何进程中的所有描述符。

对于多个进程读取同一个文件几乎都能正确工作。每个进程都由它自己的文件表项,其中也有它自己的当前文件偏移量。但是,当多个进程写同一个文件时,则可能产生预想不到的结果。为了说明如何避免这种情况,需要理解原子操作的概念。

原子操作

追加到一个文件

当一个进程时,可以将数据追加到一个文件尾端。但若有多个进程同时将数据追加写到同一个文件,则会产生问题(如此程序由多个进程同时执行,各自将消息追加到一个日志文件中,就会产生这种情况)。

两个进程同时对同一文件进行追加写操作,可能因为“先定位到文件尾端,然后写”单只出现某一进程的写被覆盖,从而出现错误,其解决办法是使这两个操作对于其他进程而言成为一个院子操作。任何要求多于一个函数调用的操作都不是原子操作,因为在两个函数调用之间,内核有可能会临时挂起进程。

函数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;若出错,返回-1ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);返回值:若成功,返回已写的字节数;若出错,返回-1

调用pread相当于调用lseek后调用read,但是pread又与这种顺序调用由下列重要区别。
- 调用pread时,无法中断其定位和读操作。
- 不更新当前文件偏移量。

调用pwrite相当于调用lseek后调用write,但也与他们有类似的区别。

创建一个文件

在open和creat之间,另一个进程创建了文件,就会出现问题。若在两个函数调用之间,另一个进程创建了该文件,并写入了一些数据,然后原先进程执行这段程序中的creat,此时,刚由另一个进程写入的数据就会被擦去。如若将这两者合并在一个原子操作中,这种问题不会出现。

一般而言,原子操作指的是由多步组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。

函数dup和dup2

dup和dup2两个函数都用来复制一个现有文件描述符。

#include<unistd.h>int dup(int fd);int dup2(int fd, int fd2);两个函数的返回值:若成功,返回新的文件描述符;若出错,返回-1

由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。对于dup2,可以用fd2参数指定新描述符的值。如果fd2已经打开,则先将其关闭。如若fd等于fd2,则dup2返回fd2,而不关闭。否则,fd2的FD_CLOEXEC文件描述符标志就被清除,这样fd2在进程调用exec时是打开状态。

函数sync、fsync和fdatasync

传统UNIX系统实现在内核中设有缓冲区高速缓存或页面高速缓存,大多数磁盘I/O都通过缓冲区进行。当我们向文件写入数据时,内核通过先将数据复制到缓冲区中,然后排入队列,晚些时候在写入磁盘。这种方式被称为延迟写。

通常内核需要重用缓冲区来存放其他磁盘块数据时,它会把所有延迟写数据块写入磁盘。为保证磁盘上世纪文件系统与缓冲区中内容一致,UNIX系统提供了sync、fsync和fdatasync三个函数。

#include <unistd.h>int fsync(int fd);int fdatasync(int fd);返回值:若成功,返回0;若出错,返回-1void sync(void);

sync只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。

通常,称为updata的系统守护进程周期性地调用(一般每隔30秒)sync函数。这就保证了定期冲洗(flush)内核的块缓冲区。命令sync(l)也调用sync函数。

**fsync函数只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束才返回。**fsync可用于数据库这样的应用程序,这种应用程序需要确保修改过的块立即写到磁盘上。

fdatasync函数类似于fsync,但它只影响文件的数据部分。而除数据外,fsync换回同步更新文件的属性。

函数fcntl

fcntl函数可以改变已经打开文件的属性。

#include <fcntl.h>int fcntl(int fd, int cmd, ... /* int arg */);返回值:若成功,则依赖于cmd;若出错,返回-1

fcntl函数有以下5种功能。
- 复制一个已有的描述符(cmd=F_DUPFD或F_DUPFD_CLOEXEC)
- 获取/设置文件描述符标志(cmd=F_GETFD或F_SETFL)
- 获取/设置文件状态标志(cmd=F_GETFL或F_SETFL)
- 获取/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN)
- 获取/设置记录锁(cmd=F_GETLK、F_SETLK或F_SETLKW)

下面是进程表项中各文件描述符相关联的文件描述符标志以及每个文件表项中的文件状态标志。

  • F_DUPFD:复制文件描述符fd。新文件描述符作为函数值返回。它是尚未打开的各描述符中大于或等于第3个参数值中各值的最小值。新描述符与fd共享同一文件表项。但是,新描述符有它自己的一套文件描述符标志,其FD_CLOEXEC文件描述符标志被消除(这表示该描述符在exec时扔保持有效)。
  • F_DUPFD_CLOEXEC:复制文件描述符,设置与新描述符关联的FD_CLOEXEC文件描述符标志的值,返回新文件描述符。
  • F_GETFD:对应于fd的文件描述符标志作为函数值返回。当前只定义了一个文件描述符标志FD_CLOEXEC。
  • F_SETFD:对于fd设置文件描述符标志。新标志值按第3个参数(取为整型值)设置。
  • F_GETFL:对应于fd的文件状态标志作为函数值返回。下面是open函数时描述的文件状态标志。
文件状态标志 说明 O_RDONLY 只读打开 O_WRONLY 只写打开 O_RDWR 读、写打开 O_EXEC 只执行打开 O_SEARCH 只搜索打开目录 O_APPEND 追加写 O_NONBLOCK 非阻塞模式 O_SYNC 等待写完成(数据和属性) O_DSYNC 等待写完成(仅数据) O_RSYNC 同步读和写 O_FSYNC 等待写完成(仅FreeBSD和Mac OS X) O_ASYNC 异步I/O(仅FreeBSD和Mac OS X)

遗憾的是,5个访问方式标志(O_RDONLY、O_WRONLY、O_RDWR、O_EXEC以及O_SEARCH)并不各占1位。因此首先必须用屏蔽字O_ACCMODE取得访问方式位,然后将结果与这5个值的每一个相比较。

F_SETFL:将文件状态标志设置为第3个参数的值(取为整型值)。可以更改的几个标志是:O_APPEND、O_NONBLOCK、O_SYNC、O_DSYNC、O_RSYNC、O_FSYNC和O_ASYNC。

F_GETOWN:获取当前接收SIGION和SIGURG信号的进程ID或进程组ID。

F_SETOWN:设置接收SIGION和SIGURG信号的进程ID和进程组ID。正的arg指定一个进程ID,负的arg表示等于arg绝对值的一个进程组ID。

fcntl的返回值与命令有关。如果出错,所有命令都返回-1,如果成功则返回某个其他值。下列4个命令有特定返回值:F_DUPFS、F_GETFD、F_GETFL以及F_GETOWN。第一个命令返回新的文件描述符,第二个和第三个命令返回相应的标志,最后一个命令返回一个正的进程ID或负的进程组ID。

函数ioctl ##

ioctl函数一直是I/O操作的杂物箱。终端I/O是使用ioctl最多的地方。

#include <unistd.h>#include <sys/ioctl.h>int ioctl(int fd, int request, ...);返回值:若出错,返回-1;若成功,返回其他值

每个设备驱动程序可以定义它自己专用的一组ioctl命令,系统则为不同种类的设别提供通用的ioctl命令。下面是FreeBSD支持的通用ioctl命令的一些类别。

类别 常量名 头文件 ioctl数 盘标号 DIOxxx

/dev/fd

较新的系统都名为/dev/fd的目录,其目录项是名为0、1、2等的文件。打开文件/dev/fd/n等效于复制描述符n(假定描述符n是打开的)。

在下列函数调用中:

fd = open(“/dev/fd/0", mode);

大多数系统忽略它所指定的mode,而另外一些系统则要求mode必须是所引用的文件(在这里是标准输入)初始打开时所使用的打开模式的一个子集。上面打开等效于:fd = dup(0);

所以描述符0和fd共享同一文件表现。如若描述符0先前被打开为只读,那么我们也只能对fd进行读操作。即使系统忽略打开模式,而且下面调用是成功的:

fd = open(“/dev/fd/0”, O_RDWR);

我们可以用/dev/fd作为路径名参数调用create,这与调用open时用O_CREAT作为第2个参数作用相同。

某些系统提供路径名/dev/stdin、/dev/stdout和/dev/stderr,这些等效于/dev/fd/0、/dev/fd/1和/dev/fd/2。

/dev/fd文件主要由shell使用,它允许使用路径名作为调用参数的程序,能处理其他路径名的相同方式处理标准输入和输出。如cat(1)命令对其命令行参数采取了一种特殊处理,它将单独的一个字符”-“解释为标准输入。

总结

以上介绍了UNIX系统提供的基本I/O函数。因为read和write都在内核执行,故这些函数是不带缓冲区的I/O函数。在只使用read和write情况下,可以看不同的I/O长度对读文件所需时间的影响,以及数据冲洗到磁盘上的方法和对应用程序性能的影响。

多个进程对同一文件进行追加写操作和创建同一个问价时,为防止出错,需满足原子操作。

本章还分析了ioctl和fcntl函数。

  • 参考文献:UNIX环境高级编程
1 0