[3 文件I/O(不带缓冲的I/O)]

3.1 引言

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

不带缓冲的I/O指的是read和write都调用内核中的一个系统调用。

3.2 文件描述符

对于内核而言,所有打开的文件都通过文件描述符引用。惯例,Unix系统shell把文件描述符0与进程的标准输入关联,文件描述符1与进程的标准输出关联,文件描述符2与进程的标准错误关联。

<unistd.h>包含STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO的定义。

3.3 函数open和openat

打开或创建一个文件:

#include <fcntl.h>

// 若成功返回文件描述符,若失败返回-1
int open(const char *path, int oflag, ... /* mode_t mode */);
int openat(int fd, const char *path, int oflag, ... /* mode_t mode */);

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

oflag参数可用来说明多个选项。在下面5个常量中必须指定一个且只能指定一个:

O_RDONLY:只读打开

O_WRONLY:只写打开

O_RDWR:读写打开

O_EXEC:只执行打开

O_SEARCH:只搜索打开

下列选项可以用一个或多个常量进行或运算构成oflag参数:

O_APPEND:追加到文件尾,3.11

O_CLOEXEC:把FD_CLOEXEC常量设置为文件描述符标志,3.14

O_CREAT:若文件不存在则创建。若使用此选项,open函数需要同时说明第3个参数mode,用mode指定新文件的文件访问权限,4.5

O_DIRECTORY:若path引用的不是目录,则报错

O_EXCL:若同时指定了O_CREATE,若文件已经存在则出错;若文件不存在,则创建此文件。使得测试和创建两者成为一个原子操作,3.11

O_NOCTTY:若path引用的是终端设备,则不将该设备分配作为此进程的控制终端,9.6

O_NOFOLLOW:若path引用的是一个符号链接,则出错。4.17

O_NONBLOCK:若path引用的是一个FIFO、一个块特殊文件或一个字符特殊文件,则此选项为文件的本次打开操作和后续的I/O操作设置为非阻塞方式。14.2

O_SYNC:使每次write等待物理I/O操作完成,包括由该write操作引起的文件属性更新所需的I/O。3.14

O_TRUNC:若此文件存在,且为只写或读写成功打开,则将其长度截断为0

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

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

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

3 path参数指定的是相对路径名,fd参数为AT_FDCWD。这种情况下,路径名在当前工作目录下获取。

3.4 函数creat

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

#include <fcntl.h>

// 若成功,返回只写打开的文件描述符;若出错,返回-1
int creat(const char *path, mode_t mode);

等效于:

open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);

3.5 函数close

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

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

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

当一个进程终止时,内核自动关闭它所有的打开文件。很多程序利用这一点,不显示地调用close关闭文件。

3.6 函数lseek

每个打开文件都有一个对应的"当前文件偏移量"。默认情况下,除非指定O_APPEND选项,否则打开文件时偏移量为0。lseek函数可以为一个打开文件设置偏移量:

#include <unistd.h>
// 若成功,返回新的文件偏移量;若失败,返回-1
off_t lseek(int fd, off_t offset, int whence);    //whence,从何处

1 若whence是SEEK_SET,则将文件的偏移量设置为距离文件开始处offset个字节。

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

3 若whence是SEEK_END,则讲文件的偏移量设置为为文件长度加offset,offset可为正或负。

可用下列方式确定打开文件的文件偏移量:

off_t currpos;
currpos = lseek(fd, 0 , SEEK_CUR);

下面程序可以测试对其标准输入是否可以设置偏移量:

#include "apue.h"

int main(void)
{
	if (lseek(STDIN_FILENO, 0, SEEK_CUR) == -1)
		printf("cannot seek\n");
	else
		printf("seek OK\n");
	exit(0);
}

测试结果:

./lseek < /etc/passwd
seek OK
cat /etc/passwd | ./lseek
cannot seek

因为偏移量可能是负值,所以在比较lseek的返回值时不要测试它是否小于0,而要测试它是否等于-1。lseek仅将当前的文件偏移量记录在内核中,它并不引起任何I/O操作。

文件偏移量可以大于文件的当前长度,这时,对该文件的下一次写将加长该文件,并在文件中构成一个空洞。位于文件中但没有写过的字节被读为0。文件中的空洞并不在磁盘上占用存储区。

如下程序用于创建一个具有空洞的文件:

#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");

	if (write(fd, buf1, 10) != 10)
		err_sys("buf1 write error");
	/* offset now = 10 */

	if (lseek(fd, 16384, SEEK_SET) == -1)
		err_sys("lseek error");
	/* offset now = 16384 */

	if (write(fd, buf2, 10) != 10)
		err_sys("buf2 write error");
	/* offset now = 16394 */

	exit(0);
}

测试结果:

od -c file.hole

od命令用于观察文件的实际内容,-c表示以字符方式打印文件内容。

ls -ls file.hole file.nohole
8 -rw-r--r-- 16394 file.hole
20 -rw-r--r-- 16394 file.hole

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

3.7 函数read

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

读操作从当前偏移量开始。注意,有多种情况可使实际读到的字节数少于要求读的字节数:

1 读普通文件时,在读到要求字节数之前已经到达了文件尾端。

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

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

4 当从管道或FIFO读时,如果管道包含的字节少于所需的数量时,只返回实际可用的字节数。

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

POSIX.1中,引入了新的基本数据类型:

ssize_t 带符号的

size_t 不带符号的

3.8 函数write

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

3.10 文件共享

Unix系统支持在不同进程间共享打开文件。先介绍内核用于所有I/O的数据结构:

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

1 每个进程在进程表中都有一个进程表项,进程表项中包含一张打开文件描述符表,包含:

a. 文件描述符标志fd

b. 指向一个文件表项的指针

2 内核为所有打开文件维持一张文件表项。每个文件表项包含:

a. 文件状态标志,3.14

b. 当前文件偏移量

c. 指向该文件v节点表项的指针

3 每个打开文件都有一个v节点表项。包含文件的i节点。

下图显示了一个进程对应的3张表之间的关系。该进程有两个不同的打开文件:一个文件从标准输入打开(fd0),一个文件从标准输出打开(fd1)。

todo

文件共享,两个独立进程打开了同一文件,如图3-8:

todo

图3-8 两个独立进程打开了同一文件

我们假定进程1在fd3上打开该文件,进程2在fd4上打开该文件。打开该文件的每个进程都获得各自的一个文件表项,但对一个给定的文件只有一个v节点表项。之所以每个进程都有自己的文件表项,是因为这可以使每个进程都有它自己的对该文件的当前偏移量。

现在对前面所述的操作进一步说明:

1 write后,在文件表项的当前文件偏移量即增加所写入的字节数。如果这导致当前文件偏移量超过了当前文件长度,则将i节点表项中的当前文件长度设置为当前文件偏移量。

2 如果用O_APPEND标志打开一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种标志的文件执行写操作时,文件表项的当前文件偏移量首先会被设置为i节点表项中的文件长度。

3 若一个文件用lseek定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为i节点表项中的当前文件长度。

4 lseek函数只修改文件表项中的当前文件偏移量,不进行任何I/O操作。

可能有多个文件描述符fd指向同一文件表项的情况。如:dup,3.12;fork,8.3。

注意:文件描述符fd是对进程而言的,即每个进程有对应的fd(0,1,2...);文件状态标志是对文件表项而言的,一个文件表项对应一个文件状态标志,这个标志可以对应多个进程。

3.11 原子操作

1 追加到一个文件

考虑一个进程将数据追加到文件尾端。早期Unix系统不支持open的O_APPEND选项,可能写出如下程序:

if (lseek(fd, 0, 2) < 0)
    err_sys("lseek error");
if (write(fd, buf, 100) != 100)
    err_sys("write error");

对于多个进程同时追加写文件,会产生问题。

假设有两个进程对同一文件追加写,每个进程都已经打开了文件,如图3-8。每个进程都有自己的文件表项,但是共享一个v节点表项。假如进程A调用了lseek,它将进程A的当前文件偏移量设置为1500字节(文件末尾)。然后内核切换进程,进程B调用lseek(1500字节)并write了100字节,这时进程B的当前文件偏移量设置为1600字节。因为文件长度增加,内核将v节点中的当前文件长度更新为1600。这时,内核切换为进程A,A调用write时是从1500字节开始写的,这就会导致覆盖进程B刚才写到文件的数据。

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

2 函数pread和pwrite

XSI扩展允许原子性地定位并执行I/O。

#include <unistd.h>
// 返回读到的字节数,若已经到文件尾,返回0;若出错,返回-1
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
// 若成功,返回已写的字节数;若出错,返回-1
ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);

调用pread相当于调用lseek后调用read。

调用pwrite相当于调用lseek后调用write。

3 创建一个文件

我们提及检查文件是否存在和创建文件这两个操作是作为一个原子操作执行的。即open函数的O_CREAT和O_EXCL选项。如果没有这样一个原子操作,可能会编写下列程序:

if (fd = open(pathname, O_WRONLY) < 0) {
    // ENOENT:"No such file"
    if (errno == ENOENT) {
        if ((fd = creat(path, mode)) < 0)
            err_sys("creat error");
    } else {
        err_sys("open error");
    }
}

如果在open和creat之间,另一个进程创建了该文件,就会出现问题。若另一个进程在open和creat之间创建了文件,并写入了一些数据。然后原来进程执行creat,则另一个进程写入的数据会被擦除。

3.12 函数dup和dup2

用来复制一个现有的文件描述符。

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

这些函数返回的新文件描述符与参数fd共享同一个文件表项,如图3-9:

todo

图3-9 dup(1)后的内核数据结构

new fd = dup(1);

因为两个文件描述符指向同一文件表项,所以它们共享同一文件状态标志(读、写、追加等)以及同一当前文件偏移量。

3.13 函数sync、fsync和fdatasync

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

为保证磁盘上文件系统与缓冲区上内容一致,Unix系统提供了sync、fsync和fdatasync三个函数。

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

void sync(void);

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

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

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

3.14 函数fcntl

fcntl函数用来改变已经打开文件的属性。

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

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的前8项:

F_DUPFD:复制文件描述符fd。新描述符与fd共享同一文件表项(图3-9)。新描述符有它自己的一套文件描述符标志,其FD_CLOEXEC文件描述符标志被清除。

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

F_GETFD:对于fd的文件描述符标志作为函数值返回。

F_SETFD:对于fd设置文件描述符标志。

F_GETFL:对于fd的文件状态标志作为函数值返回。文件状态标志如下:

文件状态标志 说明
O_RDONLY 只读打开
O_WRONLY 只写打开
O_RDWR 读写打开
O_EXEC 只执行打开
O_SEARCH 只搜索打开目录
O_APPEND 追加写
O_NONBLOCK 非阻塞模式
O_SYNC 等待写完成(数据和属性)
O_DSYNC 等待写完成(仅数据)
O_RSYNC 同步读和写

5个访问方式标志(O_RDONLY、O_WRONLY、O_RDWR、O_EXEC以及O_SEARCH)互斥,一个文件的访问方式只能取这5个值之一。

F_SETFL:设置文件状态标志。可以更改的标志:O_APPEND,O_NONBLOCK,O_SYNC,O_DSYNC和O_RSYNC。

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

F_SETOWN:设置接收SIGIO和SIGURG信号的进程ID和进程组ID。

如下程序的第一个参数指定文件描述符fd,并打开该fd的文件标志:

#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);
}

测试:

./fileflags 0 </dev/tty
read only
./fileflags 1 >temp
cat temp
write only
./fileflags 2 >>temp
write only, append

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

如下函数对于一个文件描述符设置一个或多个文件状态标志:

#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 */
    // val &= ~flags;		/* turn off flags */

	if (fcntl(fd, F_SETFL, val) < 0)
		err_sys("fcntl F_SETFL error");
}

3.15 函数ioctl

ioctl函数是I/O操作的杂物箱,不能用其他函数表示的I/O操作通常都能用ioctl表示。

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

每个设备驱动程序可以定义它自己专用的一组ioctl命令。

18.12节将使用ioctl函数获取和设置终端窗大小;19.7节将使用ioctl函数访问伪终端。

3.16 /dev/fd

/dev/fd目录下有0、1、2等文件,打开文件/dev/fd/n等价于复制文件描述符n。

0,stdout;1,stdin;2,stderr;

猜你喜欢

转载自blog.csdn.net/u012906122/article/details/120214792