UNIX环境高级编程
第三章 文件I/O
3.1 引言
UNIX系统中的大多数文件I/O只需要用到5个函数:open、read、write、lseek以及close。本章描述的函数经常被称为不带缓冲的I/O。术语不带缓冲指的是每个read和write都调用内核中的一个系统调用。
3.2 文件描述符
对内核而言,所有打开的文件都通过文件描述符引用。文件描述符是一个非负整数。
- 文件描述符0:进程的标准输入
- 文件描述符1:进程的标准输入
- 文件描述符2:进程的标准错误
在头文件<unistd.h>将幻数0、1、2定义成符号常量STDIN_FILENO、STDOUT_FILENO和STDERR_FILENO。
文件描述符的变化范围是0~OPEN_MAX - 1。
3.3 函数open和openat
#include <fcntl.h>
/**
* \brief 打开或创建一个文件.
*
* \param fd 1.path参数指定的是绝对路径名,fd参数被忽略,openat相当于open函数;
* 2.path参数指定的是相对路径名,fd参数指出相对路径名在文件系统中的开始地址(通过打开相
* 对路径名所在的目录来获取);
* 3.path参数指定了相对路径名,fd参数具有特殊值AT_FDCWD。路径名在当前工作目录获取。
* \param path 要打开或创建文件的名字.
* \param oflag 用来说明此函数的多个选项.可用下列一个或多个常量进行“或”运算构成.
* O_RDONLY 只读打开。
* O_WRONLY 只写打开。
* O_RDWR 读、写打开
* O_EXEC 只执行打开。
* O_SEARCH 只搜索打开(应用于目录)本书涉及的操作系统目前不支持O_SEARCH。
* O_APPEND 每次写时都追加到文件的尾端。
* O_CLOEXEC 把FD_CLOEXEC常量设置为文件描述符标准。
* O_CREAT 若此文件不存在则创建它。使用此选项时,需要用mode指定文件的访问权限位。
* O_DIRECTORY 如果path引用的不是目录,则出错。
* O_EXCL 测试一个文件是否存在,如果同时指定了O_CREAT,而文件已经存在,则出错。
* O_NOCTTY 如果path引用的是终端设备,则不将该设备分配作为此进程的控制终端。
* O_TRUNC 如果此文件存在,而且为只读或读-写成功打开,则将其长度截断为0。
*
* \return 若成功,返回文件描述符.
* \return 若出错,返回-1.
*/
int open(const char *path, int oflag, ... /* mode_t mode */);
int openat(int fd, const char *path, int oflag, ... /* mode_t mode */);
将最后一个参数写为…,ISO C用这种方法表明余下的参数的数量及其类型是可变的。对于open函数而言,仅当创建新文件时才使用最后这个参数。
3.4 函数creat
#include <fcntl.h>
/**
* \brief 创建一个文件.
*
* \param path 要打开或创建文件的名字.
* \param mode 指定文件访问权限.
*
* \return 若成功,返回只写打开的文件描述符.
* \return 若出错,返回-1.
*/
int creat(const char *path, mode_t mode);
// 等效于:
open(path, O_WRONLY | O_CREAT | O_TRUNC, mode);
3.5 函数close
#include <unistd.h>
/**
* \brief 关闭一个打开文件.
*
* \param fd 文件描述符.
*
* \return 若成功,返回0.
* \return 若出错,返回-1.
*/
int close(int fd);
关闭一个文件时还会释放该进程加在该文件上的所有记录锁。
当一个进程终止时,内核自动关闭它所有的打开文件。
3.6 函数lseek
#include <unistd.h>
/**
* \brief 显式地为一个打开文件设置偏移量.
*
* \param fd 文件描述符.
* \param offset 该文件的偏移量.
* \param whence 偏移量设置的起始位置.
* SEEK_SET:文件的开始处.
* SEEK_CUR:文件的当前位置.
* SEEK_END:文件的末尾处,offset可正可负.
*
* \return 若成功,返回新的文件偏移量.
* \return 若出错,返回-1.
*/
off_t lseek(int fd, off_t offset, whence);
/* 可以用下列方式确定打开文件的当前偏移量,也可以用来确定文件是否可以设置偏移量。如果文件描述符指向的是一个
* 管道、FIFO或网络套接字,则lseek返回-1,并将errno设置为ESPIPE。
*/
off_t currpos;
currpos = lseek(fd, 0, SEEK_CUR);
通常,文件的当前偏移量应当是一个非负整数,但是,某些设备也可能允许负的偏移量。所以在比较lseek的返回值时,不要测试它是否小于0,而要测试它是否等于-1。
文件偏移量可以大于文件的当前长度,在这种情况下,对该文件的下一次写将加长该文件,并在文件中构成一个空洞,这一点是允许的。位于文件中但没有写过的字节都被读为0。
文件中的空洞并不要求在磁盘上占用存储区。当定位到超出文件尾端之后写时,对于新写的数据需要分配磁盘块,但是对于原文件尾端和新开始写位置之间的部分则不需要分配磁盘块。
3.7 函数read
#include <unistd.h>
/**
* \brief 从打开文件中读数据.
* \param fd 文件描述符.
* \param buf 读到的数据的缓冲区.
* \param nbytes 想要读的字节数.
* \return 读到的字节数,若已到文件尾,返回0.
* \return 若出错,返回-1.
*/
ssize_t read(int fd, void *buf, size_t nbytes);
以下情况可使实际读到的字节数少于要求读的字节数:
- 读普通文件时,在读到要求字节数之前已到达了文件尾端。
- 当从终端设备读时,通常一次最多读一行。
- 当从网络读时,网络中的缓冲机制可能造成返回值小于所要求读的字节数。
- 当从管道或FIFO读时,如若管道包含的字节少于所需的数量,那么read将只返回实际可用的字节数。
- 当从某些面向记录的设备(如磁带)读时,一次最多返回一个记录。
- 当一信号造成中断,而已经读了部分数据量时。
3.8 函数write
#include <unistd.h>
/**
* \brief 向打开文件写数据.
*
* \param fd 文件描述符.
* \param buf 要写数据的缓冲区.
* \param nbytes 想写的字节数.
*
* \return 如成功,返回已写的字节数.
* \return 如出错,返回-1.
*/
ssize_t write(int fd, const void *buf, size_t nbytes);
3.9 I/O的效率
- Linux探秘之I/O效率:
https://www.cnblogs.com/bakari/p/5532810.html
关于read和write的执行效率问题还存在一些疑问,先将此问题搁置,后续再进行说明。
3.10 文件共享
内核使用3种数据结构表示打开文件:
- 每个进程在进程表中都有一个记录项,记录项包含:
a. 文件描述符标志;
b. 指向一个文件表项的指针。 - 内核为所有打开文件维持一张文件表,每个文件表项包含:
a. 文件状态标志(读、写、添写,同步和非阻塞等);
b. 当前文件偏移量;
c. 指向该文件v节点表项的指针。 - 每个打开文件都有一个v节点结构。v节点包含:
a. 文件类型;
b. 对此文件进行各种操作的指针;
c. i节点(i-node,索引节点),i节点包含文件所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针。
注:v节点结构的目的是对在一个计算机系统上的多个文件系统类型提供支持,Linux没有使用v节点,而是使用了通用i节点结构,采用一个与文件系统相关的i节点和一个与文件系统无关的i节点。
- 在完成每个write后,在文件表项中的当前文件偏移量即为增加所写入的字节数。如果当前文件偏移量超出了当前文件长度,则将i节点表项中的当前文件长度设置为当前文件偏移量(也就是该文件加长了)。
- 如果用O_APPEND标志打开一个文件,则相应标志也被设置到文件表项的文件状态标志中。每次对这种具有追加写标志的文件执行写操作时,文件表项中的当前文件偏移量首先会被设置为i节点表项中的文件长度。这就使得每次写入的数据都追加到文件的当前尾端处。
- 若一个文件用lseek定位到文件当前的尾端,则文件表项中的当前文件偏移量被设置为i节点表项中的当前文件长度。
- lseek函数只修改文件表项中的当前文件偏移量,不进行任何I/O操作。
- 可能有多个文件描述符项指向同一个文件表项。
3.11 原子操作
一般而言,原子操作指的是由多步组成的一个操作。如果该操作原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。任何要求多余一个函数调用的操作都不是原子操作,因为在两个函数调用之间,内核有可能会临时挂起进程。
pread和pwrite是两个原子操作,调用pread相当于调用lseek后调用read,但是pread又与这种顺序调用有重要的区别:
- 调用pread时,无法中断其定位和读操作。
- 不更新当前文件偏移量。
#include <unistd.h>
/**
* \brief 原子性地定位并执行读操作.
* \param fd 文件描述符.
* \param buf 读到的数据的缓冲区.
* \param nbytes 想要读的字节数.
* \param offset 文件偏移量.
*
* \return 读到的字节数,若已到文件尾,返回0.
* \return 若出错,返回-1.
*/
ssize_t pread(int fd, void *buf, size_t nbytes, off_t offset);
#include <unistd.h>
/**
* \brief 原子性地定位并执行写操作.
*
* \param fd 文件描述符.
* \param buf 要写数据的缓冲区.
* \param nbytes 想写的字节数.
* \param offset 文件偏移量.
*
* \return 如成功,返回已写的字节数.
* \return 如出错,返回-1.
*/
ssize_t pwrite(int fd, const void *buf, size_t nbytes, off_t offset);
open函数的O_CREAT和O_EXCL选项是一个原子操作。当同时指定这两个选项,而该文件又已经存在时,open将失败。将检查文件是否存在和创建文件这两个操作作为一个原子操作来执行。如果没有这样一个原子操作,那么可能会编写下列程序段:
if ((fd = open(pathname, O_WRONLY)) < 0)
{
if (errno == ENOENT)
{
if ((fd = creat(path, mode)) < 0)
err_sys("creat error");
else
err_sys("open error");
}
}
如果在open和creat之间,另一个进程创建了该文件,就会出现问题。
3.12 函数dup和dup2
#include <unistd.h>
/**
* \brief 复制一个现有的文件描述符.
*
* \param fd 要复制的文件描述符.
* \param fd2 指定新描述符的值.
*
* \return 如成功,返回新的文件描述符.
* \return 如出错,返回-1.
*/
int dup(int fd);
int dup2(int fd, int fd2);
由dup返回的新文件描述符一定是当前可用文件描述符中的最小数值。对于dup2,可以用fd2参数指定新描述符的值。如果fd2已经打开,则先将其关闭。如若fd等于fd2,则dup2返回fd2,而不关闭它。否则,fd2的FD_CLOEXEC文件描述符标志就被清除,这样fd2在进程调用exec时是打开状态。
3.13函数sync、fsync和fdatasync
传统的UNIX系统实现在内核中设有缓冲区或页高速缓存,大多数磁盘I/O都通过缓冲区进行。当我们向文件写入数据时,内核通常先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘。这种方式被称为延迟写。
通常,当内核需要重用缓冲区来存放其他磁盘块数据时,它会把所有延迟写数据块写入磁盘。为了保证磁盘上实际文件系统与缓冲区中内容的一致性,UNIX系统提供了sync、fsync和fdatasync三个函数。
#include <unistd.h>
/**
* \brief 将缓冲区的数据写入磁盘.
*
* \param fd 要复制的文件描述符.
*
* \return 如成功,返回0.
* \return 如出错,返回-1.
*/
int fsync(int fd);
int fdatasync(int fd);
void sync(void);
sync只是将所有修改过的块缓冲区排队写入队列,然后就返回,它并不等待实际写磁盘操作结束。
fsync函数只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束才返回。
fdatasync函数类似于fsync,但它只影响文件的数据部分。而除数据外,fsync还会同步更新文件的属性。
3.14 函数fcntl
#include <fcntl.h>
/**
* \brief 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).
*
* \param fd 文件描述符.
* \param cmd 要打开或创建文件的名字.
* \param oflag F_DUPFD 复制文件描述符fd,新文件描述符作为函数值返回,它是尚未打开的各描述符中>=第3个参数值中各值的最小值.
* F_DUPFD_CLOEXC 复制文件描述符,设置与新描述符关联的FD_CLOEXEC文件描述符标志的值,返回新文件描述符.
* F_GETFD 对应于fd的文件描述符标志作为函数值返回.
* F_SETFD 对于fd设置文件描述符标志。新标志值按第3个参数设置.
* F_GETFL 对应于fd的文件状态标志作为函数值返回.
* F_SETFL 将文件状态标志设置为第3个参数的值.
* F_GETOWN 获取当前接收SIGIO和SIGURG信号的进程ID或进程组ID.
* F_SETOWN 设置接收SIGIO和SIGURG信号的进程ID或进程组ID.
*
* \return 若成功,则依赖于cmd.
* \return 若出错,返回-1.
*/
int fcntl(int fd, int cmd, ... /* int arg */);
3.15 函数ioctl
#include <unistd.h> /* System V */
#include <sys/ioctl.h> /* BSD and Linux */
/**
* \brief 标准IO扩展的操作.
*
* \param fd 文件描述符.
* \param request 指向一个变量或结构体的指针
*
* \return 如成功,返回其他值.
* \return 如出错,返回-1.
*/
int ioctl(int fd, int request, ...);