3.1 引言
可用的文件I/O函数——打开文件、读文件、写文件等,UNIX系统中的大多数文件I/O只需要用到5个函数:
open
read
write
lseek
close
这些函数经常被称为不带缓冲
的I/O(unbuffered I/O),术语不带缓冲指的是每个read和write都调用内核中的一个系统调用。
3.2 文件描述符
- 文件描述符是一个非负整数;
- 所有打开的文件都通过文件描述符引用;
- UNIX系统shell把文件描述符0与进程的标准输入关联,文件描述符1与标准输出关联,文件描述符2与标准错误关联;
- 0、1、2虽然已经被POSIX1标准化,但应当把它们替换成符号常量
STDIN_FILENO
、STDOUT_FILENO
和STDERR_FILENO
以提高可读性; - 文件描述符的变化范围是0~OPEN_MAX-1。
3.3 函数open和openat
调用open
或openat
函数可以打开或创建一个文件:
#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 */);
// 两函数的返回值:若成功,返回文件描述符;若出错,返回-1
- path参数是要打开或创建文件的名字;
- oflag参数用来说明此函数的多个选项,用下列一个或多个常量进行“
或
”运算构成:
oflag参数选项 | ||
---|---|---|
选项 | 含义 | 备注 |
O_RDONLY | 只读打开 | 必选,且只能指定一个 |
O_WRONLY | 只写打开 | |
O_RDWR | 读、写打开 | |
O_EXEC | 只执行打开 | |
O_SEARCH | 只搜索打开(应用于目录) | |
O_APPEND | 每次写时都追加到文件的尾端 | 可选 |
O_CLOEXEC | 将FD_CLOEXEC设置为文件描述符的标志,在执行exec时关闭该文件描述符 | |
O_CREAT | 若此文件不存在则创建它 | |
O_EXCL | 如果同时指定了O_CREAT,而文件已经存在,则报错; 用此可以测试一个文件是否存在,如果不存在,则创建此文件,这使测试和创建两者成为一个原子操作 | |
O_TRUNC | 如果此文件存在,而且为只写或读-写成功打开,则将其长度截断为0 | |
... | ... |
- open和openat函数返回的文件描述符一定是最小的未用描述符数值;
- fd参数把open和openat函数区分开,共有3种可能性:
(1)若path参数指定的是绝对路径名,fd参数被忽略,openat函数就相当于open函数;
(2)若path参数指定的是相对路径名,fd参数指出了相对路径名在文件系统中的开始地址;
(3)若path参数指定的是相对路径名,fd参数具有特殊值AT_FDCWD,路径名在当前工作目录中获取,openat函数在操作上与open函数类似。
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);
3.5 函数close
可调用close
函数关闭一个打开文件:
#include <unistd.h>
int close(int fd);
// 返回值:若成功,返回0;若出错,返回-1
- 关闭一个文件时还会释放该进程加在该文件上的所有记录锁;
- 当一个进程终止时,内核自动关闭它所有的打开文件,所以很多程序而不显示用close关闭文件。
3.6 函数lseek
- 每个打开文件都有一个与其相关联的“
当前文件偏移量
”,它是一个非负整数,用以度量从文件开始处计算的字节数; - 读、写操作都从当前文件偏移量处开始,并使偏移量增加所读写的字节数;
- 当打开一个文件时,除非指定O_APPEND选项,否则该偏移量被设置为0。
可调用lseek
函数为一个打开文件设置偏移量:
#include <unistd.h>
off_t lseek(int fd, off_t offset, int whence);
// 返回值:若成功,返回新的文件偏移量;若出错,返回-1
对参数offset的解释与参数whence的值有关:
- 若whence是SEEK_SET,则将该文件的偏移量设置为距文件开始处offset个字节;
- 若whence是SEEK_CUR,则将该文件的偏移量设置为其当前值加offset,offset可为正或负;
- 若whence是SEEK_END,则将该文件的偏移量设置为文件长度加offset,offset可正可负。
3.7 函数read
调用read
函数从打开文件中读数据:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t nbytes);
// 返回值:读到的字节数,若已到文件尾,返回0;若出错,返回-1
3.8 函数write
调用write
函数向打开文件写数据:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t nbytes);
// 返回值:若成功,返回已写的字节数;若出错,返回-1
3.9 I/O的效率
大多数文件系统为改善性能都采用某种预读
技术,当检测到正进行顺序读取时,系统就试图读入比应用所要求的更多数据,并假想应用很快就会读这些数据。
3.10 文件共享
UNIX系统支持在不同进程间共享打开文件,内核使用3种数据结构表示打开文件,它们之间的关系决定了在文件共享方面一个进程对另一个进程可能产生的影响。
- 每个进程在
进程表
中都有一个记录项,记录项中包含一张打开文件描述符表
,可将其视为一个矢量,每个描述符占用一项。与每个文件描述符相关联的是:
a. 文件描述符标志;
b. 指向一个文件表项的指针。 - 内核为所有打开的文件维持一张
文件表
。每个文件表项包含:
a. 文件状态标志(读、写、添写、同步和非阻塞等);
b. 当前文件偏移量;
c. 指向该文件v节点表项的指针。 - 每个打开文件(或设备)都有一个
v节点
(v-node)结构。v节点包含了文件类型和对此文件进行各种操作函数的指针。对于大多数文件,v节点还包含了该文件的i节点
(i-node,索引节点),i节点包含了文件的所有者、文件长度、指向文件实际数据块在磁盘上所在位置的指针等。
如果两个独立进程各自打开了同一文件,则有下图中的关系:
- 每个进程都有独立的文件描述符表;
- 每个进程都获得各自的一个文件表项;
- 两个文件表项指向同一个v节点表项,因为一个给定的文件只有一个v节点表项。
可能有多个文件描述符项指向同一文件表项,比如:
- 使用
dup
函数复制文件描述符; fork
调用后,父进程、子进程各自的每一个打开文件描述符共享同一个文件表项。
文件描述符标志
和文件状态标志
在作用范围方面的区别:
- 文件描述符标志只用于一个进程的一个描述符;
- 文件状态标志应用于指向该给定文件表项的任何进程中的所有描述符。
3.11 原子操作
原子操作:由多步组成一个操作,要么执行完所有步骤,要么一步也不执行。
pread
和pwrite
允许原子性地定位并执行I/O:
#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
3.12 函数dup和dup2
下面两个函数都可用来复制一个现有的文件描述符:
#include <unistd.h>
int dup(int fd);
int dup2(int fd, int fd2);
// 两函数的返回值:若成功,返回新的文件描述符;若出错,返回-1
- 由
dup
返回的新文件描述符一定是当前可用文件描述符中的最小值; - 对于
dup2
,可用fd2参数指定新描述符的值; - 这些函数返回的新文件描述符与参数fd共享同一个文件表项。
3.13 函数sync、fsync和fdatasync
- 传统的UNIX系统实现在内核中设有缓冲区高速缓存或页高速缓存,大多磁盘I/O都通过缓冲区进行;
延迟写
:向文件写入数据时,内核先将数据复制到缓冲区中,然后排入队列,晚些时候再写入磁盘;- 当内核需要重用缓冲区来存放其他磁盘块数据时,它会把所有延迟写数据块写入磁盘,保证磁盘上实际文件系统与缓冲区中内容的一致性,UNIX系统提供了以下三个函数:
#include <unistd.h>
int fsync(int fd);
int fdatasync(int fd);
// 返回值:若成功,返回0;若出错,返回-1
void sync(void);
(1)sync
只是将所有修改过的块缓冲区排入写队列,然后就返回,并不等待实际写磁盘操作结束,通常update守护进程周期性地调用sync函数来冲洗内核的块缓冲区;
(2)fsync
函数只对由文件描述符fd指定的一个文件起作用,并且等待写磁盘操作结束才返回,可用于数据库这样的应用程序,确保修改过的块立即写到磁盘上;
(3)fsyncdata
函数类似于fsync
,但它只影响文件的数据部分,而fsync还会同步更新文件的属性。
3.14 函数fcntl
fcntl
函数可以改变已经打开文件的属性:
#include <fcntl.h>
int fcntl(int fd, int cmd, .../* int arg */);
// 返回值:若成功,则依赖于cmd;若出错,返回-1
fcntl
函数有以下5种功能:
- 复制一个已有的描述符(cmd=F_DUPFD或F_DUPFD_CLOEXEC);
- 获取/设置文件描述符标志(cmd=F_GETFD或F_SETFD);
- 获取/设置文件状态标志(cmd=F_GETFL或F_SETFL);
- 获取/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN);
- 获取/设置记录锁(cmd=F_GETLK、F_SETLK或F_SETLKW);
文件描述符标志 | 含义 |
---|---|
F_DUPFD | 复制文件描述符fd,返回尚未打开的各描述符中大于或等于第3个参数值的最小值; 新描述符与fd共享同一文件表项; 新描述符有自己的一套文件描述符标志,其FD_CLOEXEC标志被清除 |
F_DUPFD_CLOEXEC | 复制文件描述符,设置与新描述符关联的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; 正的arg指定一个进程ID,负的arg表示等于arg绝对值的一个进程组ID |
对于fcntl的文件状态标志 | |
---|---|
文件状态标志 | 说明 |
O_RDONLY | 只读打开 |
O_WRONLY | 只写打开 |
O_RDWR | 读、写打开 |
O_EXEC | 只执行打开 |
O_SEARCH | 只搜索打开目录 |
O_APPEND | 追加写 |
O_NONBLOCK | 非阻塞模式 |
O_SYNC | 等待写完成(数据和属性) |
O_DSYNC | 等待写完成(仅数据) |
O_RSYNC | 同步读和写 |
O_FSYNC | 等待写完成(仅FreeBSD和Mac OS X) |
O_ASYNC | 异步I/O(仅FreeBSD和Mac OS X) |
3.15 函数iocntl
ioctl
函数一直是I/O操作的杂物箱,不能用其他函数表示的I/O操作通常都能用ioctl表示,终端I/O是使用ioctl最多的地方:
#include <unistd.h>
#include <sys/ioctl.h>
int ioctl(int fd, int request, ...);
// 返回值:若出错,返回-1;若成功,返回其他值
3.16 /dev/fd
- 较新的系统都提供名为
/dev/fd
的目录,其目录项是名为0、1、2等的文件; - 打开文件/dev/fd/n等效于复制描述符n(假定描述符n是打开的);
- /dev/fd文件主要由shell使用;
- 作为命令行参数的“
-
”特指标准输入和标准输出。