APUE - 文件 I/O
文件描述符
对于内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。
在依从 POSIX 的应用程序中,幻数 0、1、2 应当替换成符号常量 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO。这些常量都定义在头文件
文件描述符的变化范围是 0 ~ OPEN_MAX。
open 函数
调用 open 函数可以打开或创建一个文件。
#include <fcntl.h>
int open(const char *pathname, int oflag, ... /* mode_t mode */ );
由 open 返回的文件描述符一定是最小的未用描述符数值。
文件名和路径截短
在 POSIX.1 中,常量 _POSIX_NO_TRUNC 决定了是要截短过长的文件名或路径名,还是返回一个出错。
若 _POSIX_NO_TRUNC 有效,则在整个路径名超过 PATH_MAX,或路径名中的任一文件名超过 NAME_MAX 时,返回出错状态,并将 errno 设置为 ENAMETOOLONG。
creat 函数
调用 creat 函数创建一个新文件。
#include <fcntl.h>
int creat(const char *pathname, mode_t mode);
此函数等效于:
open(pathname, O_WRONLY | O_CREAT | O_TRUNC, mode);
creat 的一个不足之处是它以只写方式打开所创建的文件。
close 函数
调用 close 函数关闭一个打开的文件。
#include <unistd.h>
int close(int filedes);
关闭一个文件时还会释放该进程加在该文件上的所有记录锁。
当一个进程终止时,内核自动关闭它所有打开的文件。
lseek 函数
调用 lseek 显式地为一个打开的文件设置其偏移量。
#include <unistd.h>
off_t lseek(int filedes, off_t offset, int whence);
若 lseek 成功执行,则返回新的文件偏移量,可以用下列方式确定打开文件的当前偏移量:
off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);
这种方法也可用来确定所涉及的文件是否可以设置偏移量。如果文件描述符引用的是一个管道、FIFO 或网络套接字,则 lseek 返回 -1,并将 errno 设置为 ESPIPE。
通常,文件的当前偏移量应当是一个非负整数,但是,某些设备也可能允许负的偏移量。但对于普通文件,则其偏移量必须是非负值。
lseek 仅将当前的文件偏移量记录在内核中,它并不引起任何 I/O 操作。
文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞。位于文件中但没有写过的字节都被读为 0。
read 函数
调用 read 函数从打开的文件中读数据。
#include <unistd.h>
ssize_t read(int filedes, void *buf, size_t nbytes);
如 read 成功,则返回读到的字节数。如已到达文件结尾,则返回 0。
有多种情况可使实际读到的字节数少于要求读的字节数:
- 读普通文件时,在读到要求字节数之前已到达了文件尾端。
- 当从终端设备读时,通常一次最多读一行。
- 当从网络读时,网络中的缓冲机构可能造成返回值小于所要求读的字节数。
- 当从管道或 FIFO 读时,如若管道包含的字节少于所需的数量,那么 read 将只返回实际可用的字节数。
- 当从某些面向记录的设备(例如磁带)读时,一次最多返回一个记录。
- 当某一信号造成中断,而已经读了部分数据量时。
读操作从文件的当前偏移量处开始,在成功返回之前,该偏移量将增加实际读到的字节数。
write 函数
调用 write 函数向打开的文件写数据。
#include <unistd.h>
ssize_t write(int filedes, const void *buf, size_t nbytes);
其返回值通常与参数 nbytes 的值相同,否则表示出错。
对于普通文件,写操作从文件的当前偏移量处开始。如果在打开该文件时,指定了 O_APPEND 选项,则在每次写操作之前,将文件偏移量设置在文件的当前结尾处。在一次成功写之后,该文件偏移量增加实际写的字节数。
文件共享
UNIX 系统支持在不同进程间共享打开的文件。
内核使用三种数据结构表示打开的文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
- 每个进程在进程表中都有一个记录项,记录项中包含有一张打开文件描述符表,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
- 文件描述符标志。
- 指向一个表项的指针。
- 内核为所有打开文件维持一张文件表。每个文件表项包含:
- 文件状态标志。
- 当前文件偏移量。
- 指向该文件 v-node 表项的指针。
- 每个打开文件(或设备)都有一个 v-node 结构。v-node 包含了文件类型和对此文件进行各种操作的函数的指针。对于大多数文件,v-node 还包含了该文件的 i-node。这些信息是在打开文件时从磁盘上读入内存的,所以所有关于文件的信息都是快速可供使用的。
Linux 没有使用 v-node,而是使用了通用 i-node 结构。
- 在完成每个 write 后,在文件表项中的当前文件偏移量即增加所写的字节数。如果这使当前文件偏移量超过了当前文件长度,则在 i-node 表项中的当前文件长度被设置为当前文件偏移量(也就是该文件加长了)。
- 如果用 O_APPEND 标志打开了一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有添写标志的文件执行写操作时,在文件表项中的当前文件偏移量首先被设置为 i-node 表项中的文件长度。这就使得每次写的数据都添加到文件的当前尾端处。
- 若一个文件用 lseek 定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为 i-node 表项中的当前文件长度。
- lseek 函数只修改文件表项中的当前文件偏移量,没有进行任何 I/O 操作。
原子操作
添写至一个文件
在打开文件时设置 O_APPEND 标志,这就使内核每次对这种文件进行写之前,都将进程的当前偏移量设置到该文件的尾端处。
pread 和 pwrite 函数
#include <unistd.h>
ssize_t pread(int filedes, void *buf, size_t nbytes, off_t offset);
ssize_t pwrite(int filedes, const void *buf, size_t nbytes, off_t offset);
dup 和 dup2 函数
下面两个函数都可用来复制一个现存的文件描述符:
#include <unistd.h>
int dup(int filedes);
int dup2(int filedes, int filedes2);
由 dup 返回的新文件描述符一定是当前可用文件描述符中的最小数值。用 dup2 则可以用 filedes2 参数指定新描述符的数值。如果 filedes2 已经打开,则先将其关闭。如果 filedes 等于 filedes2,则 dup2 返回 filedes2,而不关闭它。
sync、fsync 和 fdatasync 函数
#include <unistd.h>
int fsync(int filedes);
int fdatasync(int filedes);
void sync(void);
sync 函数只是将所有修改过的块缓冲区排入写队列,然后就返回,它并不等待实际写磁盘操作结束。
fsync 函数只对由文件描述符 filedes 指定的单一文件起作用,并且等待写磁盘操作结束,然后返回。
fdatasync 函数类似于 fsync,但它只影响文件的数据部分。而除数据外,fsync 还会同步更新文件的属性。
fcntl 函数
fcntl 函数可以改变已打开的文件的性质。
#include <fcntl.h>
int fcntl(int filedes, int cmd, ... /* int arg */ );
fcntl 函数有 5 种功能:
- 复制一个现有的描述符(cmd=F_DUPFD)。
- 获得/设置文件描述符标记(cmd=F_GETFD 或 F_SETFD)。
- 获得/设置文件状态标志(cmd=F_GETFL 或 F_SETFL)。
- 获得/设置异步 I/O 所有权(cmd=F_GETOWN 或 F_SETOWN)。
- 获得/设置记录锁(cmd=F_GETLK、F_SETLK 或 F_SETLKW)。
ioctl 函数
#include <unistd.h> /* System V */
#include <sys/ioctl.h> /* BSD and Linux */
#include <stropts.h> /* XSI STREAMS */
int ioctl(int filedes, int request, ...);