UNIX环境高级编程:第三章(文件I/O)

3.1引言

大多数文件I/O只需要5个函数:open、read、write、lseek以及close

不带缓冲的I/O(unbuffered i/o):每个read和write都调用内核中的一个系统调用。

在描述了这些特征后,将说明dup、fcntl、sync、fsync和ioctl函数。

3.2文件描述符

对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。

当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时,使用open或creat返回的文件描述符标识该文件,将其作为参数传送给read或write。

幻数0、1、2虽然已被标准化,但应当把它们替换成符号常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO以提高可读性。这些常量都在头文件<unistd.h>中定义。

 

3.3函数open 和 openat

#include<fcntl.h>
int open  (const char *path, int oflag , .../*mode_t mode*/);
intopenat(int fd,const char *path,int oflag , .../*mode_tmode*/);
//返回值:成功返回文件描述符,出错返回-1

path参数是要打开或创建文件的名字。

oflag参数可用来说明此函数的多个选项。用下列一个或多个常量进行“或”运算构成oflag参数(这些常量在头文件<fcnt1.h>中定义)。

O_RDONLY 只读打开    大多数定义为 0

O_WRONLY 只写打开                          1

O_RDWR 读、写打开                           2

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参数值,使其符合SingleUNIXSpecification。

O_DSYNC使每次write要等待物理I/O操作完成,但是如果该写操作并不影响读取刚写入的数据,则不需等待文件属性被更新。

O_RSYNC 使每一个以文件描述符作为参数进行的read操作等待,直至所有对文件同一部分挂起的写操作都完成。

注意:

O_DSYNC和O_SYNC标志有微妙的区别。仅当文件属性需要更新以反映文件数据变化(例如,更新文件大小以反映文件中包含了更多的数据)时,O_DSYNC标志才影响文件属性。而设置O_SYNC标志后,数据和属性总是同步更新。当文件用O_DSYN标志打开,在重写其现有的部分内容时,文件时间属性不会同步更新。与此相反,如果文件是用O_SYNC标志打开,那么对该文件的每一次write都将在write返回前更新文件时间,这与是否改写现有字节或追加写文件无关。

 

由open和openat函数返回的文件描述符一定是最小的未用描述符数值。这一点被某些应用程序用来在标准输入、标准输出或标准错误上打开新的文件。

fd参数把open和openat函数区分开,共有3种可能性。

(1)path参数指定的是绝对路径名,在这种情况下,fd参数被忽略,openat函数就相当于open函数。

(2)path参数指定的是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址。fd参数是通过打开相对路径名所在的目录来获取。

(3)path参数指定了相对路径名,fd参数具有特殊值AT_FDCWD。在这种情况下,路径名在当前工作目录中获取,openat函

数在操作上与open函数类似。

openat函数是POSIX.1最新版本中新增的一类函数之一,希望解决两个问题。第一,让线程可以使用相对路径名打开目录中的文件,而不再只能打开当前工作目录。在第11章我们会看到,同一进程中的所有线程共享相同的当前工作目录,因此很难让同一进程的多个不同线程在同一时间工作在不同的目录中。第二,可以避免time-of-check-to-time-of-use(TOCTTOU)错误。

 

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

文件名和路径名截断: 现在大多数是255,一般不会出错了。

 

3.4函数 creat

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

#include<fcntl.h>
int creat(const char *path,mode_t mode);//返回值:成功返回为只写打开的文件描述符,出错返回-1

等效于:open(path,O_WRONLY|O_CREAT|O_TRUNC,mode);

reat的一个不足之处是它以只写方式打开所创建的文件。所以:

现在:open(path,O_RDWR|O_CREAT|O_TRUNC,mode);

3.5函数 close

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

#include<unistd.h>
int close(int file);//成功返回0,出错-1

关闭一个文件时还会释放该进程加在该文件上的所有记录锁。

当一个进程终止时,内核自动关闭它所有的打开文件。很多程序都利用了这一功能而不显式地用close关闭打开文件。

3.6函数lseek

"当前文件偏移量”(current file offset)。它通常是一个非负整数,用以度量从文件开始处计算的字节数。

按系统默认的情况,当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0。

//可以调用lseek显式地为一个打开文件设置偏移量。
#include<unistd.h>
off_t lseek(int fd,off_t offset,int whence);
//返回值:若成功,返回新的文件偏移量;若出错,返回为−1 对参数offset的解释与参数whence的值有关。

若whence是:

SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节。

SEEK_CUR,则将该文件的偏移量设置为其当前值加offset,offset可为正或负。

SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可正可负。

若lseek成功执行,则返回新的文件偏移量,为此可以用下列方式确

定打开文件的当前偏移量:

off_t currpos;

currpos = lseek(fd, 0, SEEK_CUR);

是一个管道、FIFO或网络套接字,则lseek返回−1,并 将errno设置为ESPIPE。

例子:创建一个具有空洞的文件

$ ./a.out

$ ls -l file.hole 检查其大小

-rw-r--r-- 1 sar 16394 Nov 25 01:01 file.hole

$ od -c file.hole 观察实际内容

 

$ ls -ls file.hole file.nohole 比较长度

8 -rw-r--r-- 1 sar 16394 Nov 25 01:01 file.hole

20 -rw-r--r-- 1 sar 16394 Nov 25 01:03 file.nohole

虽然两个文件的长度相同,但无空洞的文件占用了20个磁盘块,而 具有空洞的文件只占用8个磁盘块。

3.7函数read

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

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

•读普通文件时,在读到要求字节数之前已到达了文件尾端。例 如,若在到达文件尾端之前有30个字节,而要求读100个字节,则read返回30。下一次再调用read时,它将返回0(文件尾端)。

•当从终端设备读时,通常一次最多读一行。

•当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读 的字节数。

•当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那 么read将只返回实际可用的字节数。

•当从某些面向记录的设备(如磁带)读时,一次最多返回一个记录。

•当一信号造成中断,而已经读了部分数据量时。

 

3.8函数write

 

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

3.9io效率

例子:复制一个文件只适用write 和 read

大多数文件系统为改善性能都采用某种预读(read ahead)技术。

3.10文件共享

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

(1)每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。

(2)内核为所有打开文件维持一张文件表。

(3)每个打开文件(或设备)都有一个 v 节点(v-node)结构。v 节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点(i-node,索引节点)。

 

3.11原子操作

UNIX系统为这样的操作提供了一种原子操作方法,即在打开文件 时设置O_APPEND标志。正如前一节中所述,这样做使得内核在每次 写操作之前,都将进程的当前偏移量设置到该文件的尾端处,于是在每 次写之前就不再需要调用lseek。

#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时,无法中断其定位和读操作。

•不更新当前文件偏移量。

原子操作(atomic operation)指的是由多步组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不

执行,不可能只执行所有步骤的一个子集。

3.12函数dup 和 dup2

下面两个函数都可用来复制一个现有的文件描述符。

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

由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。 0 1 2 是标准的。

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

实际上,调用dup(oldfd)等效于,fcntl(oldfd, F_DUPFD, 0) 
           而调用dup2(oldfd, newfd)等效于,close(oldfd);fcntl(oldfd, F_DUPFD, newfd);

3.13函数sync fsync 和 fdatasync

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

#include<unistd.h>
int fsync(int fd);
int fdatasync(int fd);
返回值:若成功,返回0;若出错,返回−1
void sync(void);
//sync只是将所有修改过的块缓冲区排入写队列,然后就返回,它并
不等待实际写磁盘操作结束。

区别:

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

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

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

术语解释:

 脏页:linux内核中的概念,因为硬盘的读写速度远赶不上内存的速度,系统就把读写比较频繁的数据事先放到内存中,以提高读写速度,这就叫高速缓存,linux是以页作为高速缓存的单位,当进程修改了高速缓存里的数据时,该页就被内核标记为脏页,内核将会在合适的时间把脏页的数据写到磁盘中去,以保持高速缓存中的数据和磁盘中的数据是一致的。
      内存映射:内存映射文件,是由一个文件到一块内存的映射。Win32提供了允许应用程序把文件映射到一个进程的函数 (CreateFileMapping)。内存映射文件与虚拟内存有些类似,通过内存映射文件可以保留一个地址空间的区域,同时将物理存储器提交给此区域,内存文件映射的物理存储器来自一个已经存在于磁盘上的文件,而且在对该文件进行操作之前必须首先对文件进行映射。使用内存映射文件处理存储于磁盘上的文件时,将不必再对文件执行I/O操作,使得内存映射文件在处理大数据量的文件时能起到相当重要的作用。

 

3.14函数 fcntl

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

我们的程序在一个描述符 (标准输出)上进行操作,但是根本不知道由shell打开的相应文件的文件名。因为这是shell打开的,因此不能在打开时按我们的要求设置 O_SYNC标志。使用fcntl,我们只需要知道打开文件的描述符,就可以 修改描述符的属性。

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

fcntl函数有以下5种功能。

(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)。

cmd总共13种,先说8种:

1.F_DUPFD 复制文件描述符fd。新文件描述符作为函数值返回。但是,新描述符有它自己的一套文件描述符标志,其FD_CLOEXEC 文件描述符标志被清除(这表示该描述符在exec时仍保持有效)。

2.F_DUPFD_CLOEXEC 复制文件描述符,设置与新描述符关联的 FD_CLOEXEC文件描述符标志的值,返回新文件描述符。

3.F_GETFD 对应于fd的文件描述符标志作为函数值返回。当前只定 义了一个文件描述符标志FD_CLOEXEC。

4.F_SETFD 对于fd设置文件描述符标志。新标志值按第3个参数(取 为整型值)设置。 

   注意一点:FD_CLOEXEC:一般是将此标志设置为0(系统默认,在exec时不关闭)或 1(在exec时关闭)。

5.F_GETFL 对应于fd的文件状态标志作为函数值返回。

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

7.F_GETOWN 获取当前接收SIGIO和SIGURG信号的进程ID或进程组ID。

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

 

例子:对于指定的描述符打印文件标志(第1个参数指定文件描述符,并对于该描述符打 印其所选择的文件标志说明。)

$./a.out 0 < /dev/tty

read only

$./a.out 1 > temp.foo

$ cat temp.foo

write only

$./a.out 2 2>>temp.foo

write only, append

$./a.out 5 5<>temp.foo

read write

 

例子:对一个文件描述符开启一个或多个文件状态标志

在修改文件描述符标志或文件状态标志时必须谨慎,先要获得现在的标志值,然后按照期望修改它,最后设置新标志值。不能只是执行F_SETFD或F_SETFL命令,这样会关闭以前设置的标志位。

val &= ~flags; /* turn flags off */

就构成另一个函数,我们称为 clr_fl,并将在后面某些例子中用到它。此语句使当前文件状态标志值val与flags的反码进行逻辑“与”运算。

也可加:set_fl(STDOUT_FILENO, O_SYNC);

这就使每次write都要等待,直至数据已写到磁盘上再返回。
 

 

3.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, ...);
//返回值:若出错,返回−1;若成功,返回其他值

磁带操作使我们可以在磁带上写一个文件结束标志、倒带、越过指定个数的文件或记录等,用本章中的其他函数(read、write、lseek 等)都难于表示这些操作,所以,对这些设备进行操作最容易的方法就是使用ioctl。

 

3.16 /dev/fd

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

/dev/fd文件主要由shell使用,它允许使用路径名作为调用参数的程序,能用处理其他路径名的相同方式处理标准输入和输出。

3.17小结

因为read和write都在内 核执行,所以称这些函数为不带缓冲的I/O函数。在只使用read和write情况下,我们观察了不同的I/O长度对读文件所需时间的影响。我们也观察了许多将已写入的数据冲洗到磁盘上的方法,以及它们对应用程序性能的影响。

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值