APUE 文件I/O

《UNIX环境高级编程》第2章 文件I/O 笔记

引言

UNX系统中的大多数文件只需用到5个函数:open、read、 write、 lseek以及close。

本章描述的函数经常被称为不带缓冲的I/O( unbuffered I/O,与将在第5章中说明的标准I/O函数相对照)。术语不带缓冲指的是每个read和 write都调用内核中的一个系统调用。

只要涉及在多个进程间共享资源,原子操作的概念就变得非常重要。我们将通过文件IO和open函数的参数来讨论此概念。

文件描述符

对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。当打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。

按照惯例,UNIX系统shell把文件描述符0与进程的标准输入关联,文件描述符1与标准输出关联,文件描述符2与标准错误关联。这是各种shel以及很多应用程序使用的惯例.

函数open和openat

用于打开或者创建一个文件

1
2
3
4
#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 */ );
//Both return: file descriptor if OK, −1 on error

我们将最后一个参数写为…,ISO C用这种方法表明余下的参数的数量及其类型是可变的。对于open函数而言,仅当创建新文件时才使用最后这个参数(稍后将对此进行说明)。

path参数是要打开或创建文件的名字。oflag参数可用来说明此函数的多个选项。用一个或者多个常量“或”运算构成oflag参数

使用的常量可以参考原书

函数create

可以调用create函数创建一个文件:

1
2
3
4
5
#include <fcntl.h> 

int creat(const char *path, mode_t mode);

//Returns: file descriptor opened for write-only if OK, −1 on error

此函数等效于open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);

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

函数close

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

1
2
3
4
#include <unistd.h> 
int close(int fd);

//Returns: 0 if OK, −1 on error

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

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

函数lseek

每个打开文件都有一个与其相关联的“当前文件偏移量”( current file offset)。它通常是一个非负整数,用以度量从文件开始处计算的字节数。

调用1seek显式地为一个打开文件设置偏移量:

1
2
3
4
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);

//Returns: new file offset if OK, −1 on error

对参数offet的解释与参数 whence的值有关。

  • 若 whence是 SEEK_SET,则将该文件的偏移量设置为距文件开始处 offset个字节。
  • 若 whence是 SEEK_CUR,则将该文件的偏移量设置为其当前值加 offset, offset可为正或负。
  • 若 whence是 SEEK_END,则将该文件的偏移量设置为文件长度加 offset, offset可正可负。

通常,文件的当前偏移量应当是一个非负整数,但是,某些设备也可能允许负的偏移量。但对于普通文件,其偏移量必须是非负值。

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "apue.h"
#include <fcntl.h>

char buf1[] = "abcdefghij";
char buf2[] = "ABCDEFGHIJ";

int
main(void)
{
int fd;

// 创建文件
if ((fd = creat("file.hole", FILE_MODE)) < 0)
err_sys("creat error");

// 写入a-j,此时文件偏移量为10
if (write(fd, buf1, 10) != 10)
err_sys("buf1 write error");
/* offset now = 10 */

// 改变文件偏移量到16384
if (lseek(fd, 16384, SEEK_SET) == -1)
err_sys("lseek error");
/* offset now = 16384 */

// 在新的文件偏移量下写入A-J
if (write(fd, buf2, 10) != 10)
err_sys("buf2 write error");
/* offset now = 16394 */

exit(0);
}

因为1seek使用的偏移量是用off_t类型表示的,所以允许具体实现根据各自特定的平台自行选择大小合适的数据类型。

函数read

调用read函数从打开文件中读数据

1
2
3
4
#include <unistd.h> 
ssize_t read(int fd, void *buf, size_t nbytes);

//Returns: number of bytes read, 0 if end of file, −1 on error

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

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

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

函数write

调用write函数想打开文件写数据

1
2
3
4
#include <unistd.h> 
ssize_t write(int fd, const void *buf, size_t nbytes);

//Returns: number of bytes written if OK, −1 on error

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

I/O效率

BUFFSIZE 一般选取与磁盘块相同大小的字节数,大多数情况问4096

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

文件共享

UNIX系统支持在不同进程间共享打开文件。在介绍dup函数之前,先要说明这种共享。为此先介绍内核用于所有I/O的数据结构。

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

  1. 每个进程在进程表中都有一个记录项,记录项中包含一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是文件描述符标志和指向一个文件表项的指针。
  2. 内核为所有打开文件维持一张文件表。每个文件表项包含文件状态标志(读、写、添写、同步和非阻塞等)、当前文件偏移量、指向该文件v节点表项的指针
  3. 每个打开文件(或设备)都有一个v节点( v-node)结构。v节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点(i-node,索引节点)。

创建v节点结构的目的是对在一个计算机系统上的多文件系统类型提供支持。把与文件系统无关的i节点部分称为v节点。Linux没有将相关数据结构分为i节点和ⅴ节点,而是采用了一个与文件系统相关的i节点和个与文件系统无关的i节点。

两个独立进程打开同一个文件

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

原子操作

  1. 追加到一个文件

多个进程同时使用这种方法将数据追加写到同一文件,则会产生问题

问题出在逻辑操作“先定位到文件尾端,然后写”,它使用了两个分开的函数调用。解决问题的方法是使这两个操作对于其他进程而言成为一个原子操作。

  1. 函数pread和pwrite
1
2
3
4
5
6
7
8
#include <unistd.h> 
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);

//Returns: number of bytes read, 0 if end of file, −1 on error

ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);

//Returns: number of bytes written if OK, −1 on error

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

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

函数dup和dup2

下面两个用来复制一个文件描述符

1
2
3
4
5
6
7
#include <unistd.h>

int dup(int fd);

int dup2(int fd, int fd2);

//Both return: new file descriptor if OK, −1 on error

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

dup(1)以后的内核数据结构:

函数sync、 fsync和 fdatasync

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

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

1
2
3
4
5
6
7
8
9
#include <unistd.h> 

int fsync(int fd);

int fdatasync(int fd);

//Returns: 0 if OK, −1 on error

void sync(void);

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

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

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

函数fcntl

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

1
2
3
4
#include <fcntl.h> 
int fcntl(int fd, int cmd, ... /* int arg */ );

//Returns: depends on cmd if OK (see following), −1 on error

fcnt1函数有以下5种功能

  1. 复制一个已有的描述符(cmd= F_DUPFD或 F_DUPFD_CLOEXEC)。
  2. 获取/设置文件描述符标志(cmd= E_GETED或 F_SETED)。
  3. 获取/设置文件状态标志(cmd= F_GETEL或 F_SETEL)
  4. 获取/设置异步IO所有权(cmd= F_GETOWN或F_SETOWN)。
  5. 获取/设置记录锁(cmd= F_GETLK、 F_SETLK或 F_SETLKW)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "apue.h"
#include <fcntl.h>

void
set_fl(int fd, int flags) /* flags are file status flags to turn on */
{
int val;

// 获取现在的文件标志位
if ((val = fcntl(fd, F_GETFL, 0)) < 0)
err_sys("fcntl F_GETFL error");

// 计算新的标志值
val |= flags; /* turn on flags */

// 设置新的标志值
if (fcntl(fd, F_SETFL, val) < 0)
err_sys("fcntl F_SETFL error");
}

函数ioctl

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

/dev/fd

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

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

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

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