概述
所有执行 IO 的系统调用均以文件描述符为目标,包括管道、FIFO、socket、终端设备和普通文件。每个进程中的文件描述符是互不相关的。 各个进程中默认会有三个标准文件描述符:
其中 POSIX 名称定义在头文件 <unistd.h>
中。
通用 I/O
UNIX I/O 模型的特点为 I/O 的通用性概念。针对系统所有的文件,包括设备、管道、socket 等可用文件描述符表示的资源,都可以通过同一套系统调用执行 I/O 操作。
open 打开文件
#include <sys/stat.h> // 文件属性信息结构体的相关申明
#include <fcntl.h> // 文件操作的相关声明
/*
@brief 打开一个文件
@param pathname 要打开文件的路径,如果是符号链接会解引用
@param flags 位掩码,指定文件访问模式
@param mode 可忽略,标识文件的所有权限,一般在创建文件时使用
- S_IRUSR | S_IWUSR 属主的读写权限
- S_IRGRP | S_IWGRP 数组的读写权限
- S_IROTH | S_IWOTH 其它的去写权限
@return 成功返回文件描述符,失败返回 -1
*/
int open(const char *pathname, int flags, ... /* mode_t mode*/);
flags 可取
O_RDONLY 只读
O_WRONLY 只写
O_RDWR 读写
以上三个必须选择一个。此外还有一下标志:
O_CLOEXEC 设置 close-on-exec 标志,在执行 exec() 后关闭文件描述符
O_CREAT 不存在则创建
O_DIRECT 无缓冲 I/O
O_DIRECTORY 目标不是目录则失败
O_EXCL 文件存在则返回错误,配合 O_CREAT 使用用于创建文件
O_LARGEFILE 32 位系统使用,用于打开大文件
O_NOATIME 不修改文件最近访问时间
O_NOFOLLOW 符号链接不要解引用
O_NOCTTY 目标不能成为控制终端
O_TRUNC 截断文件,使其长度为 0,清空文件
---------- 以下允许通过 fcntl 函数修改 ----------
O_APPEND 尾部追加
O_ASYNC I/O 可操作时,使用信号通知进程
O_DSYNC 提供同步 I/O 数据的完整性
O_SYNC 文件同步方式写入
O_NONBLOCK 非阻塞打开文件
打开文件失败会返回 -1,并更新错误号 errno,可能的错误号如下所示:
EACCES 文件权限原因不能打开文件
EISDIR 文件为目录
EMFILE 打开的文件描述符已经达到进程资源限制的上限
ENFILE 文件打开数量达到系统允许的上限
ENOENT 文件不存在
EROFS 目标文件只读,却以写方式打开
ETXTBSY 目标文件为可执行文件且正在运行,不能修改
read 读取文件内容
#include <unistd.h>
/*
@brief 从 fd 文件描述符指定的文件读取 count 字节数据到 buffer 中
@return 失败返回 -1,读到文件结束返回 0,否则返回读取的字节数
*/
ssize_t read(int fd, void *buffer, size_t count)
write 写入文件
#include <unistd.h>
/*
@brief 将 buffer 中 count 字节的数据写入 fd 文件
@return 失败返回 -1,否则返回写入的字节
*/
ssize_t write(int fd, void *buffer, size_t count)
close 关闭文件
#include <unistd.h>
/*
@brief 关闭指定的文件
@return 失败返回 -1,成功返回 0
*/
int close(int fd)
lseek 改变文件偏移量
#include <unistd.h>
/*
@brief 改变文件 fd 偏移量
@param offset 相对于 where 的偏移量
@param where 可取:
- SEEK_SET: 文件开头
- SEEK_CUR: 当前文件位置
- SEEK_END: 文件尾部
@return 失败返回 -1,读到文件结束返回 0,否则返回读取的字节数
*/
off_t read(int fd, off_t offset, int where)
可以在文件任意位置写入,但当偏移量跨度很大的时候,这部分数据并不会占用磁盘空间,称为文件空洞。
深入探究文件 I/O
所有的系统调用均为原子操作,在编写 I/O 代码时要注意进程间可能的竞争关系。
文件控制 fcntl
#include <fcntl.h>
/*
@brief 对打开的文件描述符 fd 执行一系列操作(cmd 表示)
@return 失败返回 -1,成功返回值取决于 cmd
*/
int fcntl(int fd, int cmd, ...);
// 一些常用的 cmd
int old_flags = fcntl(fd, F_GETFL); // 获取文件状态标志
fcntl(fd, F_SETFL, new_flags); // 设置文件状态标志
文件描述符与文件的关系
对于文件,内核维护以下三个数据结构:
上面的结构揭示了:
不同的文件描述符,若指向同一个文件句柄,则共享同一文件偏移量
复制文件描述符
#include <unistd.h>
// 复制已打开的文件描述符 oldfd,成功则返回新的描述符,指向同一文件句柄,失败则返回 -1
int dup(int oldfd);
// 可指定复制后的描述符为 newfd,若存在则先关闭原来的 newfd
int dup2(int oldfd, int newfd);
// flags 只能为 O_CLOEXEC, Linux 特有函数
#define _GNU_SOURCE
int dup3(int oldfd, int newfd, int flags);
特定偏移量处的 I/O
#include <unistd.h>
// 读写特定偏移量的数据,且不改变当前文件偏移量
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);
// 相当于将下面的代码封装为系统调用
off_t orig = lseek(fd, 0, SEEK_CUR);
lseek(fd, offset, SEEK_SET);
s = read(fd, buf, count);
lseek(fd, orig, SEEK_CUR);
可方便多线程应用,因为系统调用为原子操作。
分散输入和集中输出
#include <sys/uio.h>
struct iovec {
void *iov_base;
size_t iov_len;
};
// 读写数据到 iovec 链表声明的所有数据块中
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
截断文件
#include <unistd.h>
// 设置文件到指定大小,文件大小大于 length 则删除多余部分,若小于,则添加空字节或文件空洞
int truncate(const char *pathname, off_t length);
int ftruncate(int fd, off_t length);
非阻塞 I/O
一般情况下文件 I/O 默认为非阻塞的,除非使用强制文件锁。对于管道、FIFO、套接字、设备要启用非阻塞需要通过 fcntl 函数设置 O_NONBLOCK 标志。
/dev/fd 目录
此目录为虚拟目录,其中 /dev/fd/n 对应进程打开的各个文件描述符。打开/dev/fd 目录中的一个文件等同于复制相应的文件描述符。
创建临时文件
此功能为 GNU C 提供:
#include <stdlib.h>
// template 为临时文件路径,最后 6 个字符必须为 XXXXXX,这几个字符会被替换以保证唯一
// 成功返回文件描述符,否则返回 -1
int mkstemp(char *template);
#include <stdio.h>
// 返回的是 C 文件指针
FILE *tmpfile(void);
文件 I/O 缓冲
文件 I/O 内核缓冲
read 和 write 系统调用在操作磁盘文件时不会直接发起磁盘访问,而是仅仅在用户空间缓冲区与内核缓冲区高速缓存之间复制数据。可大大减少与磁盘交互的时间。内核缓冲区由内核设置,不能修改。
stdio 库的缓冲
#include <stdio.h>
/*
@brief 修改文件流 stream 的缓冲为 buf 指向的 size 大小区域
@param mode 指定缓冲类型
- _IONBF: 不进行缓冲
- _IOLBF: 行缓冲,标准输入输出一般使用这个
- _IOFBF: 全缓冲,磁盘 IO 用的较多
@return 成功返回 0, 失败返回 -1
*/
int setvbuf(FILE *stream, char *buf, int mode, size_t size);
// 相当于 setvbuf(stream, buf, (buf != NULL) ? _IOFBF : _IONBF, BUFSIZ);
void setbuf(FILe *stream, char *buf);
#define _BSD_SOURCE
void setbuffer(FILE *stream, char *buf, size_t size);
// 刷新 stream 的缓冲区到内核缓冲区中,stream 为 NULL,则刷新所有缓冲区
int fflush(FILE *stream);
控制文件 I/O 的内核缓冲
有以下两种同步 I/O 类型:
同步 I/O 数据完整性:数据已成功全部写到磁盘,或全部从磁盘读入
同步 I/O 文件完整性:是数据完整性的超级,还将同步文件元数据
#include <unistd.h>
// 使得文件处于 同步 I/O 文件完整性 状态
int fsync(int fd);
// 使得文件处于 同步 I/O 数据完整性 状态
int fdatasync(int fd);
// 刷新所有内核缓冲区
void sync(void)
直接 I/O
通过 O_DIRECT 绕过内核缓冲区直接与磁盘 I/O 需要注意:
用于传递数据的缓冲区,其内存边界必须对齐为块大小的整数倍
数据传输的开始点,亦即文件和设备的偏移量,必须是块大小的整数倍
混合系统调用与库文件 I/O
#include <stdio.h>
// 获取 stream 的为文件描述符
int fileno(FILE *stream);
// 转换为文件流,mode 同 fopen 函数
FILE *fdopen(int fd, const char* mode);