# 《UNIX 环境高级编程》笔记 - 文件 IO

本章描述的函数经常被称为 **不带缓冲** 的 I/O（unbuffered I/O）。术语 **不带缓冲** 指的是每个 `read` 和 `write` 函数都会调用内核中的一个系统调用。

## 文件描述符

对于内核而言，所有打开的文件都通过 **文件描述符** 引用。文件描述符是一个非负整数，当打开一个现有文件或者创建一个新文件时，内核向进程返回一个文件描述符。当读、写一个文件时，使用 `open` 或者 `create` 返回的文件描述符用于标识该文件，将其作为参数传给 `read` 或者 `write`。

UNIX 系统把文件描述符 0 与进程的标准输入关联，文件描述符 1 与标准输出关联，文件描述符 2 与标准错误关联。

在符合 POSIX 的应用程序中，魔法值 0、1、2 虽然已经被标准化，但应当把它们替换为符号常量 `STDIN_FILENO`、`STDOUT_FILENO`、`STDERR_FILENO` 以提高可读性。这些常量都在头文件 `<unistd.h>` 中定义。

## 函数 open 和 openat

调用 `open` 函数或 `openat` 函数可以打开或创建一个文件。

```c
#include <sys/stat.h>
#include <fcntl.h>

int open(const char *path, int oflag, ...);
int openat(int fd, const char *path, int oflag, ...);
```

path 参数是要打开或创建文件的名字，oflag 参数用来说明此函数的多个选项。用下列一个或多个常量进行「或」运算来构成 oflag 参数：

* `O_RDONLY` 只读打开；
* `O_WRONLY` 只写打开；
* `O_RDWR` 读写打开；
* `O_EXEC` 只执行打开；
* `O_APPEND` 每次 `write` 时都追加内容到文件的尾端；
* `O_CREAT` 若文件不存在，则会创建；
* `O_TRUNC` 如果文件存在，而且为只写或者读写打开时，则会将其长度截断为 0；
* `O_NONBLOCK` 当以只写或者读写打开 FIFO（进程间通信）时，或者当打开支持以非阻塞方式打开的块特殊文件和字符特殊文件时，后续的 I/O 操作将会被设置为非阻塞的；
* `O_SYNC` 每次 `write` 等待物理 I/O 操作完成，包括由该写操作引起的更新文件属性所需的 I/O；
* `O_DSYNC` 每次 `write` 等待物理 I/O 操作完成，忽略由该写操作引起的更新文件属性所需的 I/O；
* ……

`openat` 函数时 POSIX 最新版本中新增的函数，支持让线程可以使用相对路径名打开目录中的文件，而不再只能打开当前工作目录中的文件。

## 函数 creat

调用 `creat` 函数可以创建一个新的文件。

```c
#include <sys/stat.h>
#include <fcntl.h>

int creat(const char *path, mode_t mode);
```

`creat` 函数等同于调用 `open(path, O_WRONLY|O_CREAT|O_TRUNC, mode)`。

## 函数 close

调用 `close` 函数可以关闭一个打开的文件。

```c
#include <unistd.h>

int close(int fildes);
```

关闭一个文件时，还会释放该进程加在该文件上的所有记录锁。当一个进程终止时，内核自动关闭它所有的打开文件。

## 函数 lseek

调用 `lseek` 函数可以显式地为一个打开的文件设置偏移量。

```c
#include <unistd.h>

off_t lseek(int fildes, off_t offset, int whence);
```

每个打开的文件都有一个与其关联的「当前文件偏移量（current file offset）」。它通常是一个非负整数，用以度量文件开始处计算的字节数。通常，读、写操作都从当前文件偏移量处开始，并使偏移量增加所读写的字节数。

当打开一个文件时，除非指定 `O_APPEND` 选项，否则该偏移量被设置为 0。

如果文件描述符指向的是管道、FIFO、网络套接字，则 `lseek` 函数返回 -1，并将 errno 设置为 `ESPIPE`。

## 函数 read

调用 `read` 函数可以从打开的文件中读取数据。

```c
#include <unistd.h>

ssize_t pread(int fildes, void *buf, size_t nbyte, off_t offset);
ssize_t read(int fildes, void *buf, size_t nbyte);
```

如果 `read` 函数执行成功，则返回实际读取的字节数，如果已到达文件的尾端，则返回 0。

## 函数 write

调用 `write` 函数可以向打开的文件中写入数据。

```c
#include <unistd.h>

ssize_t pwrite(int fildes, const void *buf, size_t nbyte,
           off_t offset);
ssize_t write(int fildes, const void *buf, size_t nbyte);
```

## I/O 的效率

大多数文件系统为改善性能，都会采用某种预读（read ahead）技术。当检测到正进行顺序读取时，系统就会试图读入比应用所需的更多数据，并假设应用很快就会读取到这些数据。

## 原子操作

### 追加文件内容

早期版本的 UNIX 系统不支持 `open` 函数的 `O_APPEND` 选项，因此追加文件内容的逻辑会被写成：

```c
if (lseek(fd, OL, 2) < 0) {
  err_sys("lseek error");
}
if (write(fd, buf, 100) != 100) {
  err_sys("write_error");
}
```

被拆分成两个函数调用的「先定位到文件尾端，然后写入内容」逻辑代码是非原子性的，因为在两个函数调用之间，内核有可能会临时挂起进程。

UNIX 系统为这样的操作提供了一种保证原子性的方法，即在打开文件时设置 `O_APPEND` 选项。

### 函数 pread 和 pwrite

调用 `pread` 函数和 `pwrite` 函数可以原子性地定位并执行 I/O 操作。

```c
#include <unistd.h>

ssize_t pread(int fildes, void *buf, size_t nbyte, off_t offset);
ssize_t pwrite(int fildes, const void *buf, size_t nbyte,
           off_t offset);
```

调用 `pread` 函数或 `pwrite` 函数相当于先调用 `lseek` 函数再调用 `read` 函数或 `write` 函数，但又有一些区别：

* 调用 `pread` 函数或 `pwrite` 函数时，无非中断其定位和读写操作；
* 不会更新当前文件的偏移量。

### 创建一个文件

`open` 函数的 `O_CREAT` 选项支持原子性的「检查文件是否存在，当文件不存在时则创建文件」的逻辑操作。

## 函数 dup 和 dup2

调用 `dup` 函数或 `dup2` 函数可以复制一个现有的文件描述符。

```c
#include <unistd.h>

int dup(int fildes);
int dup2(int fildes, int fildes2);
```

## 函数 sync、fsync 和 fdatasync

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

通常，当内核需要重用缓冲区来存放其它磁盘的数据块时，它会把所有延迟写的数据块写入磁盘。为了保证磁盘上实际文件系统与缓冲区中的内容一致，UNIX 系统提供了 `sync`、`fsync`、`fdatasync` 三个函数。

```c
#include <unistd.h>

void sync(void);
int fsync(int fildes);
int fdatasync(int fildes);
```

`sync` 函数只是将所有修改过的在缓冲区中的数据块排入写队列，然后就返回，它并不等待实际写磁盘的操作结束。通常，称为 update 的系统守护进程会周期性地调用（一般间隔 30s）`sync` 函数。

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

`fdatasync` 函数类似于 `fsync` 函数，但它只影响文件的数据部分。而 `fsync` 函数除了对数据部分，还会同步更新文件的属性。

## 函数 fcntl

调用 `fcntl` 函数可以修改已经打开文件描述符/文件状态的标志。

```c
#include <fcntl.h>

int fcntl(int fildes, int cmd, ...);
```

`fcntl` 函数支持以下 5 种功能：

1. 复制一个已有的描述符；
2. 获取/设置文件描述符的标志；
3. 获取/设置文件状态的标志；
4. 获取/设置异步 I/O 所有权；
5. 获取/设置记录锁。

在 UNIX 系统中，通常 write 只是将数据排入队列，而实际的写磁盘操作则可能在之后的某个时刻进行。数据库系统则会需要使用 `O_SYNC` 选项，当它从调用 `write` 函数返回时就可以确保数据已经的确写入到磁盘上了，以系统在异常时导致数据丢失。

程序在运行时，设置 `O_SYNC` 标志会增加系统时间和时钟时间，其原因是内核需要从进程中复制数据，并将数据排入队列以便由磁盘驱动器将其写到磁盘上。当使用同步写入时，系统时间和时钟时间便会显著增加。

## 函数 ioctl

`ioctl` 函数一直是 I/O 操作的杂物箱，不能用本章中其它函数表示的 I/O 操作通常都能用 `ioctl` 函数来完成。终端 I/O （例如树莓派的 GPIO）是使用 `ioctl` 最多的地方。

```c
#include <stropts.h>

int ioctl(int fildes, int request, ... /* arg */);
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://gitbook.fantasticmao.cn/tech/c-and-unix/unix-like/unix-huan-jing-gao-ji-bian-cheng-bi-ji-wen-jian-io.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
