第11.13章: POSIX 文件 API

English

概述

在每个符合 POSIX 的系统上(Linux、FreeBSD、macOS 等),文件都是通过一小批系统调用来访问的:open、openat、read、write、close 等等。这些调用作用于一个原始的整数文件描述符(int)。

fast_io 把这些系统调用包装在两个类型背后:

用 posix_file 打开文件

最常见的情形:按名字打开文件,然后用 print 和 println 读写。


#include <fast_io.h>
#include <fast_io_device.h>

int main()
{
    using namespace ::fast_io::iomnp;
    // Open a file for writing. The file is closed automatically
    // when `pf` goes out of scope.
    ::fast_io::posix_file pf(u8"hello_posix.txt", ::fast_io::open_mode::out);

    // fast_io's print/println work directly on posix_file.
    println(pf, u8"Hello from posix_file!");

    ::std::size_t const n{42zu};
    println(pf, u8"The answer is: ", n);
}
      

posix_file 的构造函数会以由 ::fast_io::open_mode 推导出的标志转发到 open。析构函数在文件描述符有效时调用 close。

包裹已有的文件描述符

有时你会从遗留代码、某个库,或者你自己直接发起的系统调用那里拿到一个 int fd。你可以在不复制、不接管所有权的情况下把它包起来:


#include <fast_io.h>
#include <fast_io_device.h>
#include <unistd.h>

int main()
{
    using namespace ::fast_io::iomnp;
    // A raw file descriptor from somewhere else in your program.
    int fd{::open("legacy.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644)};

    // Non-owning observer: does NOT close the fd on destruction.
    ::fast_io::posix_io_observer observer{fd};
    println(observer, u8"Written through a posix_io_observer\n");

    // If you want ownership, construct a posix_file. It will close
    // the fd when it goes out of scope.
    ::fast_io::posix_file owned{::fast_io::from_fd, fd};
    println(owned, u8"Written through an owning posix_file\n");
}
      

当文件描述符的生命周期由别处管理时,选择 posix_io_observer;当你想要 RAII 清理时,选择 posix_file。

用 posix_file 读取

输入通过 scan 完成,它用 read 从底层文件描述符读取:


#include <fast_io.h>
#include <fast_io_device.h>

int main()
{
    using namespace ::fast_io::iomnp;
    ::fast_io::posix_file inpf(u8"input.txt", ::fast_io::open_mode::in);

    ::std::size_t value{};
    scan(inpf, value);

    ::fast_io::posix_file outpf(u8"output.txt", ::fast_io::open_mode::out);
    println(outpf, "Read the value: ", value);
}
      

相对于某个目录 open-at

与前面章节的 obuf_file 一样,posix_file 可以配合 at(dir) 相对于某个目录文件描述符打开文件,底层使用 openat:


#include <fast_io.h>
#include <fast_io_device.h>

int main(int argc, char** argv)
{
    using namespace ::fast_io::iomnp;
    ::fast_io::posix_file dir{::fast_io::mnp::os_c_str(argv[1]),
                              ::fast_io::open_mode::directory};
    ::fast_io::posix_file pf(at(dir), u8"relative.txt",
                             ::fast_io::open_mode::out);
    println(pf, u8"This file is opened with openat()\n");
}
      

原始的 POSIX I/O 系统调用

在使用 fast_io 的包装之前,先了解底层的 POSIX 系统调用会有帮助。它们是真正的内核入口(手册第 2 节):

系统调用 用途
open(path, flags, mode) 按路径打开文件,返回一个 int 文件描述符。
openat(dirfd, path, flags, mode) 相对于某个目录文件描述符打开文件。比 open 更安全,因为它避免了路径解析带来的竞态。
read(fd, buf, count) 从 fd 最多读取 count 个字节到 buf。返回实际读取的字节数,出错时返回 -1。
write(fd, buf, count) 把 buf 中的 count 个字节写到 fd。返回实际写入的字节数,出错时返回 -1。
close(fd) 关闭文件描述符。永远需要 — 文件描述符是有限资源。
lseek(fd, offset, whence) 改变文件偏移。whence 是 SEEK_SET、SEEK_CUR 或 SEEK_END。
fdopen(fd, mode) 把一个 C 的 FILE* 流关联到已有的文件描述符上,返回 FILE*。

这些系统调用不带缓冲 — 每次调用都直接进入内核。这使得它们在操作很小或很频繁时很慢。fast_io 的带缓冲类型(如 ibuf_file 与 obuf_file)用用户态缓冲包装这些调用,性能好得多。

即使不用 fast_io 的 I/O,也可以用 posix_file 做 RAII

即使你更愿意直接调用原始 POSIX API(例如 read 与 write)而不使用 fast_io 的 print 与 scan,你仍然应该用 ::fast_io::posix_file 来做 RAII。它的析构函数保证调用 close,所以你永远不会忘记关闭文件描述符。

当涉及异常或提前返回时,这一点尤其重要 — 没有 RAII,你必须在每一条退出路径上手动调用 close,这很容易出错。


#include <fast_io.h>
#include <fast_io_device.h>
#include <unistd.h>

int main()
{
    // Open with posix_file for RAII, but use raw POSIX read/write
    ::fast_io::posix_file pf(u8"data.bin", ::fast_io::open_mode::out);

    // Get the raw file descriptor
    int fd{pf.native_handle()};

    // Use raw POSIX write (unbuffered, goes directly to kernel)
    char const buffer[]{"Hello from raw write(2)\n"};
    ::ssize_t written{::write(fd, buffer, sizeof(buffer) - 1)};

    if (written < 0) {
        // handle error
    }

    // pf's destructor calls close(fd) automatically when it goes out of scope.
    // No need to manually call close() here, even if we return early or throw.
}

同样的原则也适用于读取:


#include <fast_io.h>
#include <fast_io_device.h>
#include <unistd.h>

int main()
{
    ::fast_io::posix_file pf(u8"data.bin", ::fast_io::open_mode::in);
    int fd{pf.native_handle()};

    char buffer[256];
    ::ssize_t nread{::read(fd, buffer, sizeof(buffer))};

    if (nread > 0) {
        // Process the buffer
    }

    // close(fd) is called automatically
}

要定位,就在原生句柄上使用 lseek:


#include <fast_io.h>
#include <fast_io_device.h>
#include <unistd.h>

int main()
{
    ::fast_io::posix_file pf(u8"data.bin",
        ::fast_io::open_mode::in | ::fast_io::open_mode::out);
    int fd{pf.native_handle()};

    // Seek to offset 100 from the beginning of the file
    ::off_t result{::lseek(fd, 100, SEEK_SET)};
    if (result == static_cast<::off_t>(-1)) {
        // handle error
    }

    // Now read or write from the new position
}

小结