第11.13章: POSIX 文件 API
概述
在每个符合 POSIX 的系统上(Linux、FreeBSD、macOS 等),文件都是通过一小批系统调用来访问的:open、openat、read、write、close 等等。这些调用作用于一个原始的整数文件描述符(int)。
fast_io 把这些系统调用包装在两个类型背后:
-
::fast_io::posix_file— RAII 拥有式包装。它通过open(或openat)打开文件,并保证对象离开作用域时调用close。 -
::fast_io::posix_io_observer— 对已有文件描述符的非拥有视图。它可平凡复制、没有析构函数,按值传递是安全的。
用 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
}
小结
-
::fast_io::posix_file是对 POSIXopen/close的 RAII 包装。 -
::fast_io::posix_io_observer是对已有文件描述符的轻量、非拥有视图。 -
两个类型都能直接接入
::fast_io::io::print、::fast_io::io::println与::fast_io::io::scan。 -
你可以包裹任何原始的
int fd,而不用改动代码的其他部分。 -
原始 POSIX 系统调用有
open、openat、read、write、close、lseek和fdopen。它们不带缓冲,直接进入内核。 -
即使你直接调用原始 POSIX API(例如
read与write),也仍然要用::fast_io::posix_file做 RAII,以保证close被自动调用。这能防止资源泄漏,在有异常或提前返回时尤其如此。 - 关于 POSIX API 的更多细节,见 Linux man pages 或你的系统文档。