引言
Linux系统的核心哲学是一切皆文件,文件是操作系统对所有可I/O资源的统一逻辑抽象,而文件I/O就是Linux系统编程的基石,也是C++后端开发的必备能力。很多开发者仅会使用C++标准库中的fstream或C语言stdio缓冲I/O,却对底层系统调用的原理,竞态风险,性能优化与高频坑点一无所知,最终写出的代码出现数据覆盖,fd泄露,并发异常等问题。
本文将从基础API,底层内核原理,进阶与原子操作,大文件与临时文件,高效I/O能力五个维度系统拆解Linux/C++文件与I/O操作的全链路内容。本文面向有C++基础,想入门Linux系统编程的开发者,所有代码均可直接复现,读完你将彻底搞懂Linux文件I/O的核心逻辑,写出更健壮,高效,完全的I/O代码。
本文目录
- 前置铺垫:Linux系统I/O vs 标准库缓冲I/O
- 基础文件I/O API:核心五件套
open/read/write/lseek/close - 底层核心原理:文件描述符,打开文件表与i-node表的关系
- 原子文件I/O API:解决竞态问题的核心能力
- 大文件支持(LFS)API全解:64位系统下的标准
- 临时文件安全管理:
temfile/mkstemp - 文件状态控制API:万能的
fcntl与核心用法 - 文件描述符复制:
dup/dup2/dup3的原理与实战 - 文件截断API:
truncate/ftruncate与空洞文件特性 - 分散输入与集中输出:
readv/writev高效I/O实战 - 非阻塞I/O极简入门:核心概念与基础用法
- 高效避坑指南与核心总结
- 拓展学习路径
1. 前置铺垫:Linux系统I/O vs 标准库缓冲I/O
在正式讲解API之前,先明确两个核心概念的区别,避免后续混淆:
-
本文核心讲解的「系统调用I/O」 :直接与Linux内核交换的无缓冲I/O,操作对象是文件描述符(fd),每次调用都会触发用户态与内核态的切换,是所有I/O能力的底层基础,符号POSIX标准,跨UNIX/Linux平台兼容。
-
C++标准库I/O (
fstream/ifstream/ofstream等):在系统调用的基础上,封装了用户态缓冲区,通过减少系统调用次数,操作对象是流对象,但底层调用的还是open/read/write等。
简单来说:标准库I/O是系统调用的上层封装,只有吃透底层系统调用,才能真正理解I/O的本质,解决各种复杂问题。
2. 基础文件I/O API:核心五件套open/read/write/lseek/close
这五个API是Linux文件I/O的基础,涉及打开,读取,写入,关闭,方位五个方面,所有进阶操作均基于它们展开,每个API我读会讲清函数原型,参数含义,返回值,常见错误与避坑点 ,最后给出两个程序案例,分别通过系统调用I/O 与C++标准库I/O 去实现copy程序与seek_io程序,并综述系统调用与C++标准库所解决的问题 与局限性
2.1 open:打开/创建文件,获取文件描述符
open是I/O操作的起点,用于打开一个已存在的文件,或创建一个新文件,并返回一个非负整数的文件描述符(fd),后续的所有操作基于fd完成。
点击查看代码
cpp
//函数原型
#include <fcntl.h>
int open(const char* pathname, int flags, mode_t mode);
- 核心参数说明:
pathname:文件的路径(相对/绝对),又叫符号链接flags:文件打开模式,必选参数+可选参数组合- 必选(三选一):
O_RDONLY(只读),O_WRONLY(只写),O_RDWR(读写) - 高频可选:
O_CREAT(文件不存在则创建),O_EXCL(配合O_CREAT,文件已存在则报错),O_TRUNC(打开文件时清空原有内容),O_APPEND(每次写入都定位到文件末尾)O_NONBLOCK(非阻塞模式)
3.mode:仅当使用O_CREAT是必需传入,指定新文件的权限,常使用0644(所有者读写,其他用户只读),
注意不要写成644(八进制前置必需加前缀0)
- 必选(三选一):
- 返回值:成功返回非负整数fd(从3开始,0/1/2默认对应标准输入/标准输出,标准错误),失败返回-1,并设置errno标识错误原因。
2.2 read:从文件描述符读取数据
点击查看代码
//函数原型
#include <unistd.h>
ssize_t read(int fd, void* buffer, size_t count);
-
核心说明 :从fd对应的文件中,读取至多
count字节的数据到buffer指向的缓冲区 -
返回值:
- 大于0:实际读取到的字节数(可能小于count,比如读到文件末尾)
- 等于0:已达到文件末尾(EOF)
- 等于-1:读取失败,设置
errno
-
避坑点:
- 类型混用风险:ssize_t是有符号整数,size_t是无符号整数,不要混用;
- 不保证读满 :不要假设
read每次都能读取到count字节,必需严格处理返回值; - 无字符串语义 :
read是纯字节流读取 ,它:- 不会自动在缓冲区末尾添加C++字符串的
\0结束符; - 不会区分"文本数据"和"二进制数据",文件里的换行符,\0都会被当做普通字节读取;
- 如果直接把
buffer当作char*字符串用(比如直接printf("%s",buffer)),会导致内存越界访问,乱码甚至程序崩溃; \n与\0:\n会在文本层进行特殊处理,\0在文本层没有合法语义,会导致文本展示异常,\0是C/C++ 字符串的 "私有终止符"\n是合法的文本语义标记,所以对于\0需谨慎处理。
- 不会自动在缓冲区末尾添加C++字符串的
2.3 write:向文件描述符写入数据
点击查看代码
cpp
//函数原型
#include<unistd.h>
ssize_t write(int fd, const void* buffer,size_t count);
- 核心说明 :将
buffer中count字节的数据写入fd对应的文件 - 返回值 :成功返回实际写入的字节数,失败返回-1,设置
errno - 避坑点 :返回值小于count不是错误 ,比如硬盘满,信号中断等场景会出现部分写入,必需循环重试,不能只调用一次;谨慎将
\0写入文件中。
2.4 lseek:改变文件偏移量
每个打开的文件都有一个当前读写偏移量 (存储在系统打开文件表中,详见第三章底层原理),默认从文件开头(偏移量0)开始,每次read/write操作后,偏移量会自动向后移动实际读取的字节数。lseek用于显示调整这个偏移量,实现任意位置读写。
点击查看代码
cpp
#include<unisted.h>
#include<sys/types.h>
off_t lseek(int fd, off_t offset, int whence);
-
核心参数说明:
offset:偏移量(字节数),有符号整数,正数后移,负数前移whence:偏移量的基准值,必要以下三个值之一:
whence值 基准位置 实例说明 SEEK_SET文件开头 lseek(fd, 100, SEEKSET)-> 定位到文件第100字节处(绝对偏移100)SEEK_CUR当前偏移量位置 lseek(fd, -50, SEEK_CUR)-> 从当前位置向前偏移50字节SEEK_END文件末尾 lseek(fd, 0, SSEEK_END)->定位到文件末尾 -
返回值:
- 成功:返回调整后的文件绝对偏移量 (从文件开头计算的字节数,
off_t类型) - 失败:返回-1,并设置
errno标识错误原因
- 成功:返回调整后的文件绝对偏移量 (从文件开头计算的字节数,
-
避坑点:
- 文件偏移量不能为负 :调整后的最终偏移量如果是负数,
lseek会失败并设置errno=EINVAL - 仅普通文件支持随机读写 :管道、套接字、终端、FIFO 等设备文件不支持
lseek,调用会失败并设置errno=ESPIPE lseek不会触发 I/O 操作:它只是修改系统级打开文件表中的偏移量,不会实际读写磁盘,性能很高- 32 位系统默认偏移量限制:32 位系统中 off_t 默认是 32 位有符号整数,最大支持 2GB 偏移,超过会溢出,解决方法见第 5 章 LFS API
- 多个 fd 共享偏移量 :如果多个 fd 指向同一个打开文件表(如
dup复制的 fd),一个 fd 的lseek会影响所有其他 fd 的读写位置,详见第 3 章底层原理
- 文件偏移量不能为负 :调整后的最终偏移量如果是负数,
2.4 close:关闭文件描述符
点击查看代码
cpp
#include <unistd.h>
int close(int fd);
- 核心说明:关闭fd,释放对应的系统资源,进程退出时,内核会自动关闭所有未关闭的fd
- 返回值:成功返回0,失败返回-1
- 避坑点 :必须检查
close的返回值(比如NFS场景下,close可能返回IO错误);fd关闭后不能再使用,否则会触发异常;避免重复关闭fd。
2.5 可运行示例:用基础API实现文件复制
点击查看代码
cpp
#include<unistd.h>
#include<fcntl.h>
#include<sys/stat.h>
#include<sys/errno.h>
#include<iostream>
#include<string>
#include<stdexcept>
#include<utility>
#include <cstring>
#include <cstdlib>
constexpr size_t BUF_SIZE=BUFSIZ;
class FileDescriptor{
public:
FileDescriptor(const std::string& path,int flags,mode_t mode=0){
_fd=open(path.c_str(),flags,mode);
if(_fd==-1){
throw std::runtime_error("打开文件失败:"+path+" - "+strerror(errno));
}
}
FileDescriptor(FileDescriptor&& oth) noexcept:_fd(oth._fd){
oth._fd=-1;
}
FileDescriptor(const FileDescriptor&)=delete;
FileDescriptor& operator=(const FileDescriptor&)=delete;
~FileDescriptor(){
if(_fd!=-1){
if(close(_fd)==-1){
std::cerr<<"关闭文件描述符失败:"<<strerror(errno)<<std::endl;
}
}
}
int get() const{ return _fd; }
private:
int _fd=-1;
};
void copy_file(const std::string& src_path,const std::string& dst_path){
try{
FileDescriptor src_fd(src_path,O_RDONLY);
FileDescriptor dst_fd(dst_path,O_WRONLY|O_CREAT|O_TRUNC,0644);
char buf[BUF_SIZE];
ssize_t n_read;
while((n_read=read(src_fd.get(),buf,BUF_SIZE))>0){
char* buf_ptr = buf;
ssize_t remain = n_read;
while(remain > 0){
ssize_t n_written = write(dst_fd.get(), buf_ptr, remain);
if(n_written == -1){
throw std::runtime_error("写入目标文件失败:"+dst_path+" - "+strerror(errno));
}
remain -= n_written;
buf_ptr += n_written;
}
}
if(n_read==-1){
throw std::runtime_error("读取源文件失败:"+src_path+" - "+strerror(errno));
}
}catch(const std::runtime_error& e){
std::cerr<<"错误:"<<e.what()<<std::endl;
throw;
}
}
int main(int argc,char* argv[]){
if(argc!=3){
std::cerr<<"用法:"<<argv[0]<<" 源文件 目标文件"<<std::endl;
return EXIT_FAILURE;
}
std::string src_path=argv[1];
std::string dst_path=argv[2];
try{
copy_file(src_path,dst_path);
std::cout<<"文件复制成功!"<<std::endl;
return EXIT_SUCCESS;
}catch(const std::exception& e){
return EXIT_FAILURE;
}
}
2.6 可运行示例:用C++标准库API实现SEEK_IO
点击查看代码
cpp
#include<iostream>
#include<fstream>
#include<vector>
#include<cstdlib>
#include<cctype>
#include<iomanip>
#include<cstring>
using namespace std;
int main(int argc,char* argv[]){
if(argc<3){
cerr<<"Usage: "<<argv[0]
<<" file {r<length>|R<length>|w<string>|s<offset>}..."<<endl;
return 1;
}
fstream fs;
const char* filename=argv[1];
fs.open(filename,ios::in|ios::out|ios::binary);
if(!fs){
fs.open(filename,ios::out|ios::binary);
fs.close();
fs.open(filename,ios::in|ios::out|ios::binary);
}
if (!fs) {
cerr << "Cannot open/create file: " << filename << endl;
return 1;
}
for(int i=2;i<argc;++i){
char* arg=argv[i];
switch (arg[0]){
case 's':{
off_t offset=atoi(&arg[1]);
fs.seekg(offset,ios::beg);
fs.seekp(offset,ios::beg);
if (!fs) {
cerr << arg << ": seek failed" << endl;
return 1;
}
cout << arg << ": seek succeeded" << endl;
break;
}
case 'r':{
size_t len=atoi(&arg[1]);
vector<char> buf(len);
fs.read(buf.data(),len);
size_t read_len=fs.gcount();
if(!fs&&!fs.eof()){
cerr << arg << ": read failed" << endl;
return 1;
}
if(read_len==0){
cout << arg << ": end-of-file" << endl;
}else{
cout << arg <<": ";
for(size_t j=0;j<read_len;++j){
unsigned char c = static_cast<unsigned char>(buf[j]);
cout << (isprint(c) ? static_cast<char>(c) : '?');
}
cout<<endl;
}
break;
}
case 'R':{
size_t len=atoll(&arg[1]);
vector<char> buf(len);
fs.read(buf.data(),len);
size_t read_len=fs.gcount();
if(!fs&&!fs.eof()){
cerr << arg << ": read failed" << endl;
return 1;
}
if(read_len==0){
cout << arg << ": end-of-file" << endl;
}else{
cout << arg <<": ";
for(size_t j=0;j<read_len;++j){
unsigned char c = static_cast<unsigned char>(buf[j]);
cout<<hex<<setw(2)<<setfill('0')
<<static_cast<unsigned int>(c)<<" ";
}
cout<<dec<<endl;
}
break;
}
case 'w':{
const char* str=&arg[1];
size_t len=strlen(str);
fs.write(str,len);
if(!fs){
cerr<<arg<<": write failed"<<endl;
return 1;
}
cout<<arg<<": wrote"<<len<<" bytes"<<endl;
break;
}
default:
cerr<<"Argument must start with [rRws]: "<<arg<<endl;
return 1;
}
}
return 0;
}
2.7 综述
1. 系统调用I/O
核心解决的问题:
- 作为用户态程序访问内核I/O资源、硬件设备的唯一合法入口,由内核统一调度,保障系统安全与资源全局管理。
- 开放内核级极致控制能力,支持直接I/O、零拷贝、异步I/O、IO多路复用等高级特性,可完全定制I/O全链路行为。
- 无额外用户态封装开销,可精准控制I/O时机,满足数据库、高性能服务器等对延迟、吞吐量的极致要求。
核心局限性:
- 强依赖操作系统,接口不通用,完全不具备跨平台可移植性,跨系统开发维护成本极高。
- 使用门槛高,需手动管理句柄生命周期、缓冲区、竞态条件等全链路细节,极易出现资源泄漏、数据错误。
- 无内置格式化与类型安全能力,频繁小数据读写会因用户态/内核态切换产生巨大性能开销。
2. C++标准库I/O
核心解决的问题:
- 彻底解决跨平台痛点,一套符合ISO C++标准的接口,可在全平台编译运行,大幅降低跨平台开发成本。
- 提供类型安全的面向对象流抽象,统一文件、控制台、字符串等不同I/O目标的读写语法,内置缓冲区、格式化、本地化能力,大幅降低普通I/O场景的开发门槛。
- 基于C++ RAII机制自动管理资源,即使发生异常也能避免资源泄漏,适配C++的编程范式,提升代码健壮性。
核心局限性:
- 为保证跨平台兼容性,仅封装了全平台通用的基础能力,大量操作系统专属的高级I/O特性不支持,底层控制能力严重不足。
- 默认配置下有显著的额外开销,对非阻塞I/O、异步I/O、零拷贝等高性能I/O模型完全不支持,无法用于极致性能要求的底层系统。
- 二进制I/O支持不友好,复杂格式化语法繁琐,错误处理不够灵活,问题定位难度较高。
关键误区纠正
很多人认为「标准库I/O一定比系统调用慢」,实则不然:关闭与C stdio的同步后,标准库I/O在普通大文件顺序读写等场景下,性能与手动优化的系统调用基本持平。仅在需要极致定制化、使用内核高级I/O特性的场景下,系统调用才有不可替代的优势。
3.底层核心原理:文件描述符,打开文件表与i-node表的关系
只会用API只是入门,只有搞懂内核的三个核心表,才能真正理解文件I/O的底层逻辑,后续的fd复制、原子操作、竞态问题都会迎刃而解。
Linux内核中,文件I/O的核心是三个层级的表,关系如下:
3.1 三个表的核心定义
| 表类型 | 所属范围 | 核心存储内容 |
|---|---|---|
| 进程级文件描述符表 | 每个进程独立维护 | 每个条目对应一个fd,存储:1. fd标志(仅FD_CLOEXEC);2. 指向系统级打开文件表的指针 |
| 系统级打开文件表 | 内核全局维护,所有进程共享 | 每个条目对应一个「打开的文件实例」,存储:1. 文件当前偏移量;2. 文件打开状态(flags);3. 访问模式;4. 指向inode表的指针 |
| 文件系统级i-node表 | 磁盘文件系统+内核缓存 | 每个条目对应一个唯一的文件(i-node是文件的唯一标识),存储:1. 文件元数据(权限、大小、创建/修改时间);2. 文件数据的磁盘块位置;3. 硬链接数;4. 文件所有者等 |
3.2 核心关系与关键结论
- fd只是一个索引:进程拿到的fd,只是进程级文件描述符表的下标,内核通过这个下标找到对应的打开文件表,再通过打开文件表找到inode,最终操作文件。
- 多个fd可以指向同一个打开文件表 :比如
dup复制fd、父子进程继承fd,此时多个fd共享同一个打开文件表的偏移量、打开状态,一个fd修改偏移量,会影响所有其他fd。 - 多个打开文件表可以指向同一个inode :比如多个进程/同一个进程多次
open同一个文件,会创建多个独立的打开文件表条目,各自有独立的偏移量,互不影响,但最终操作的是同一个文件。 - 文件的删除逻辑:只有当inode的硬链接数为0,且所有指向该inode的打开文件表条目都被关闭,文件才会被真正删除。
3.3 直观示例
- 场景1:进程两次
open同一个文件,得到fd1和fd2 → 两个独立的fd,对应两个独立的打开文件表,指向同一个inode → 各自的偏移量独立,fd1的lseek不会影响fd2。 - 场景2:进程用
dup复制fd1得到fd2 → 两个fd指向同一个打开文件表 → 共享偏移量,fd1的lseek会影响fd2的读写位置。
4. 原子文件I/O API:解决竞态问题的核心能力
原子操作的核心定义:不可被内核调度中断的操作,要么全部执行完成,要么完全不执行,不会出现中间态 。
原子I/O是解决并发场景下竞态问题、安全漏洞的核心手段。
4.1 为什么需要原子I/O?两个高频竞态场景
- TOCTOU安全漏洞:「检查文件是否存在」和「创建文件」分为两个操作,中间可能被其他进程打断,导致安全问题。
- 多进程写文件覆盖:「定位到文件末尾」和「写入数据」分为两个操作,中间被打断,导致多个进程的写入内容互相覆盖。
4.2 核心原子I/O API与用法
4.2.1 open的原子标志:O_CREAT | O_EXCL
-
作用:原子完成「检查文件是否存在+不存在则创建」的操作,文件已存在则直接报错,彻底解决TOCTOU漏洞。
-
错误示例(非原子,有竞态):
点击查看代码
cpp// 错误写法:检查和创建分为两个操作,中间可能被打断 if (access("test.txt", F_OK) == -1) { // 此处可能被其他进程抢占,创建了test.txt int fd = open("test.txt", O_WRONLY | O_CREAT, 0644); // 会覆盖已有文件,引发安全问题 } -
正确示例(原子操作):
点击查看代码
cpp// 正确写法:原子完成检查+创建 int fd = open("test.txt", O_WRONLY | O_CREAT | O_EXCL, 0644); if (fd == -1) { if (errno == EEXIST) { printf("文件已存在\n"); } else { perror("创建文件失败"); } return 1; }
4.2.2 open的原子标志:O_APPEND
-
作用:每次
write操作前,原子地将文件偏移量定位到文件末尾,再执行写入,彻底解决多进程并发写文件的覆盖问题。 -
错误示例(非原子,有竞态):
点击查看代码
cpp// 错误写法:lseek+write分为两个操作,中间可能被打断 lseek(fd, 0, SEEK_END); // 定位到末尾 // 此处可能被其他进程打断,其他进程写入数据,文件末尾发生变化 write(fd, buf, len); // 写入的位置已经不是最新的末尾,导致内容覆盖 -
正确示例(原子操作):
点击查看代码
// 正确写法:打开时加O_APPEND,每次write都是原子的末尾写入 int fd = open("test.txt", O_WRONLY | O_CREAT | O_APPEND, 0644); // 无需lseek,每次write都会原子定位到末尾,不会被打断 write(fd, buf, len);
4.2.3 原子读写API:pread/pwrite
点击查看代码
// 函数原型
#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);
- 核心作用:原子完成「定位到指定offset + 读写操作」,不会修改文件当前的偏移量,完美适配多线程并发读写同一个文件的场景,无需加锁。
- 核心优势:对比
lseek + read/write,pread/pwrite是原子操作,不会被打断,且不改变全局偏移量,多线程操作互不影响。 - 适用场景:多线程读写固定位置的文件(比如数据库文件、配置文件),无需互斥锁。
5. 大文件支持(LFS)API全解:64位系统下的标准
很多开发者会有疑问:现在都是64位系统,LFS是不是已经没用了?
答案是否定的:LFS(Large File Support)是POSIX.1b标准定义的、跨UNIX/Linux平台的大文件支持官方规范 ,64位系统只是默认开启了它的基础能力(off_t默认64位),而非让它过时。在跨平台开发、嵌入式场景等代码兼容中,LFS依然是保证代码健壮性、可移植性的必备方案。
传统POSIX文件I/O接口使用off_t类型表示文件偏移量,32位系统中默认是32位有符号整数,取值范围仅为-2^31 ~ 2^31-1,对应最大文件偏移量为2GB,超过该大小的文件会出现偏移溢出、读写异常、元数据获取错误等问题。LFS通过编译宏、64位偏移类型、兼容API扩展,彻底解决了这一问题,最大支持8EB的文件操作。
5.1 最佳实践
无论你是32位还是64位系统,所有Linux系统编程代码,都建议在所有头文件之前定义#define _FILE_OFFSET_BITS 64 。
这行代码零侵入、零成本、无副作用,能让你的代码在所有POSIX兼容环境下,原生支持超大文件,彻底规避2GB偏移溢出的坑,是工业级代码的默认规范。
5.2 LFS核心编译宏
LFS能力通过编译宏启用,必须在包含所有头文件之前定义,否则宏不生效。核心宏分为三类,对应两种使用模式:
| 宏定义 | 核心作用 | 适用场景 |
|---|---|---|
_FILE_OFFSET_BITS=64 |
【推荐首选】所有标准I/O函数和off_t类型自动替换为64位版本,无需修改代码函数名 |
跨平台兼容、代码无侵入,99%的业务场景首选 |
_LARGEFILE_SOURCE |
启用LFS扩展函数(fseeko/ftello)的声明 |
配合标准库缓冲I/O使用大文件 |
_LARGEFILE64_SOURCE |
启用显式带64后缀的LFS API声明,强制使用off64_t64位偏移类型 |
需要显式区分32位/64位操作的特殊底层场景(如数据库存储引擎、文件系统工具) |
编译方式:g++/gcc编译时必须显式指定宏(代码中定义+编译命令中定义双重保险),示例:
bash
g++ -o lfs_demo lfs_demo.cpp -D_FILE_OFFSET_BITS=64
# 32位兼容模式编译(验证LFS效果)
g++ -m32 -o lfs_demo_32 lfs_demo.cpp -D_FILE_OFFSET_BITS=64
5.3 推荐模式:隐式兼容LFS API
定义_FILE_OFFSET_BITS=64后,前文讲解的所有标准I/O系统调用,会自动升级为64位大文件版本,函数名、参数格式完全不变,无需修改原有业务代码,即可支持超大文件操作。
5.4 显式模式:专用64位LFS API
需提前定义_LARGEFILE64_SOURCE宏,函数名带64后缀,强制使用off64_t类型(固定64位文件偏移),不受系统默认字长影响,适用于底层跨架构兼容场景。
5.4.1 open64:打开/创建超大文件
点击查看代码
cpp
// 函数原型
#define _LARGEFILE64_SOURCE
#include <fcntl.h>
int open64(const char *pathname, int flags, mode_t mode);
- 核心说明:功能与标准
open完全一致,底层使用64位inode结构,支持打开>2GB的文件 - 参数、返回值规则与标准
open完全一致,必选/可选flags完全兼容 - 避坑点:禁止与标准
open混用操作同一个文件,会导致偏移量异常
5.4.2 lseek64:超大文件偏移量调整
点击查看代码
cpp
// 函数原型
#define _LARGEFILE64_SOURCE
#include <unistd.h>
#include <sys/types.h>
off64_t lseek64(int fd, off64_t offset, int whence);
- 核心说明:调整文件描述符的当前读写偏移量,支持64位超大偏移,是大文件随机读写的核心API
- 核心参数:
offset:64位有符号整数偏移量,支持超过2GB的超大偏移定位whence:与标准lseek完全兼容,可选SEEK_SET(绝对偏移)、SEEK_CUR(当前位置偏移)、SEEK_END(文件末尾偏移)
- 返回值:成功返回调整后的文件绝对偏移量(
off64_t类型),失败返回-1并设置errno
5.4.3 ftruncate64:超大文件截断/扩容
点击查看代码
cpp
// 函数原型
#define _LARGEFILE64_SOURCE
#include <unistd.h>
#include <sys/types.h>
int ftruncate64(int fd, off64_t length);
- 核心说明:将文件截断/扩容到指定的64位长度,支持创建TB级空洞文件
- 参数、返回值规则与标准
ftruncate完全一致,仅length参数为off64_t类型 - 经典场景:大文件下载、数据库文件预分配、虚拟机镜像创建
5.4.4 stat64/fstat64:获取超大文件元数据
点击查看代码
cpp
// 函数原型
#define _LARGEFILE64_SOURCE
#include <sys/stat.h>
int stat64(const char *pathname, struct stat64 *buf);
int fstat64(int fd, struct stat64 *buf);
- 核心说明:获取文件元数据,
struct stat64中的st_size字段为64位整数,可正确显示>2GB文件的大小,解决标准stat获取超大文件大小溢出为负数的问题 - 核心场景:超大文件大小校验、文件元数据合规检查
5.4.5 pread64/pwrite64:超大文件原子随机读写
点击查看代码
cpp
// 函数原型
#define _LARGEFILE64_SOURCE
#include <unistd.h>
ssize_t pread64(int fd, void *buf, size_t count, off64_t offset);
ssize_t pwrite64(int fd, const void *buf, size_t count, off64_t offset);
- 核心说明:原子完成「64位偏移量定位+读写操作」,不修改文件当前全局偏移量,完美适配超大文件的多线程并发随机读写场景,无需加锁
- 与标准
pread/pwrite的唯一区别:offset参数为off64_t类型,支持超过2GB的偏移量定位
5.5 完整可运行实战示例
示例基于推荐的隐式兼容模式,实现10GB空洞文件创建、5GB偏移位置写入数据、读取验证全流程,代码可直接编译复现:
点击查看代码
cpp
// 必须在所有头文件之前定义LFS核心宏(双重保险:代码+编译命令)
#define _FILE_OFFSET_BITS 64
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
int main() {
const char *file_path = "lfs_large_file.data";
// 10GB文件大小(字节):必须先强制转换为off_t,避免整数溢出
const off_t file_size = (off_t)10 * 1024 * 1024 * 1024;
// 写入位置:5GB偏移处
const off_t write_offset = (off_t)5 * 1024 * 1024 * 1024;
const char *write_data = "LFS大文件API测试数据:5GB偏移位置写入";
// 1. 创建/打开大文件
int fd = open(file_path, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("打开大文件失败");
return 1;
}
// 2. 扩容文件到10GB(创建空洞文件)
if (ftruncate(fd, file_size) == -1) {
perror("文件扩容失败");
close(fd);
return 1;
}
printf("成功创建10GB空洞文件\n");
// 3. 定位到5GB偏移位置
off_t new_offset = lseek(fd, write_offset, SEEK_SET);
if (new_offset == -1) {
perror("lseek定位失败");
close(fd);
return 1;
}
printf("成功定位到5GB偏移位置,当前偏移:%lld\n", (long long)new_offset);
// 4. 写入测试数据
ssize_t data_len = strlen(write_data);
ssize_t written = write(fd, write_data, data_len);
if (written != data_len) {
perror("写入数据失败");
close(fd);
return 1;
}
printf("成功写入%zd字节数据\n", written);
// 5. 重新定位到写入位置,读取验证
new_offset = lseek(fd, write_offset, SEEK_SET);
if (new_offset == -1) {
perror("重新定位失败");
close(fd);
return 1;
}
char read_buf[1024] = {0};
ssize_t read_len = read(fd, read_buf, sizeof(read_buf)-1);
if (read_len == -1) {
perror("读取数据失败");
close(fd);
return 1;
}
printf("读取到%zd字节数据:%s\n", read_len, read_buf);
// 6. 验证文件大小
struct stat st;
if (fstat(fd, &st) == -1) {
perror("获取文件元数据失败");
close(fd);
return 1;
}
printf("文件实际大小:%lld 字节\n", (long long)st.st_size);
// 7. 关闭文件
if (close(fd) == -1) {
perror("关闭文件失败");
return 1;
}
// 8. 清理测试文件(可选)
// if (unlink(file_path) == -1) { perror("删除测试文件失败"); return 1; }
printf("LFS大文件API测试完成\n");
return 0;
}
5.6 LFS高频避坑指南
- 宏定义顺序错误 :LFS宏必须在包含所有头文件之前定义,否则宏替换不生效,编译器仍会使用32位版本函数
- 混用32位与64位API :禁止用标准
open打开文件、用lseek64操作偏移,会导致文件偏移量错乱、数据丢失 - 类型安全问题 :禁止用
int/long/int64_t直接替代off_t,必须通过_FILE_OFFSET_BITS=64保证off_t的字长一致性,避免跨平台编译异常 - 编译时未指定宏 :代码中定义了宏,但编译命令中未加
-D_FILE_OFFSET_BITS=64,会导致部分头文件宏替换失效,仍触发2GB限制 - 文件系统上限限制:即使使用LFS API,也受文件系统本身的最大文件大小限制(如ext3单文件最大2TB,ext4单文件最大16TB)
- 偏移量算术溢出 :超大文件偏移量计算时,必须先强制转换为
off_t再做运算,避免整数溢出,示例:off_t offset = (off_t)5 * 1024 * 1024 * 1024; - 标准库I/O兼容问题 :使用
fopen/fseek等标准库函数操作大文件时,同样需要定义LFS宏,否则仍有2GB限制;推荐优先使用fseeko/ftello替代fseek/ftell
6. 临时文件安全管理:temfile/mkstemp
临时文件用于存储进程运行时的中间数据(如日志缓存、排序临时块、下载临时分片),需保证创建安全(无TOCTOU竞态)、自动清理、权限可控三个核心要求,否则会引发数据泄露、文件名冲突、安全漏洞等问题。
6.1 匿名临时文件:tmpfile
tmpfile是Linux系统提供的最安全的临时文件创建方式,它创建一个匿名的临时文件(无文件名,无法通过路径访问),文件关闭后自动删除,无需手动清理,彻底避免了TOCTOU竞态、文件名冲突、权限泄露等问题。
点击查看代码
cpp
// 函数原型
#include <stdio.h>
FILE *tmpfile(void);
- 返回值 :成功返回
FILE*指针(可通过fileno()获取对应的文件描述符,用于系统调用I/O),失败返回NULL - 优势 :完全匿名、自动删除、权限默认
0600(仅所有者读写)、无竞态风险 - 局限 :只能通过
FILE*或fileno()操作,无法获取文件路径,无法与其他进程共享
实战示例:
点击查看代码
cpp
#include <stdio.h>
#include <unistd.h>
#include <cstring>
int main() {
// 1. 创建匿名临时文件
FILE *tmp = tmpfile();
if (tmp == NULL) {
perror("tmpfile创建失败");
return 1;
}
printf("匿名临时文件创建成功\n");
// 2. 获取对应的文件描述符(可选,用于系统调用I/O)
int fd = fileno(tmp);
if (fd == -1) {
perror("fileno获取失败");
fclose(tmp);
return 1;
}
// 3. 写入测试数据(标准库I/O)
const char *write_data = "匿名临时文件测试数据";
size_t data_len = strlen(write_data);
if (fwrite(write_data, 1, data_len, tmp) != data_len) {
perror("fwrite写入失败");
fclose(tmp);
return 1;
}
printf("成功写入%zd字节数据\n", data_len);
// 4. 重新定位到文件开头,读取验证(系统调用I/O)
off_t new_offset = lseek(fd, 0, SEEK_SET);
if (new_offset == -1) {
perror("lseek定位失败");
fclose(tmp);
return 1;
}
char read_buf[1024] = {0};
ssize_t read_len = read(fd, read_buf, sizeof(read_buf)-1);
if (read_len == -1) {
perror("read读取失败");
fclose(tmp);
return 1;
}
printf("读取到%zd字节数据:%s\n", read_len, read_buf);
// 5. 关闭文件,自动删除
fclose(tmp);
printf("临时文件已自动删除\n");
return 0;
}
6.2 命名临时文件:mkstemp
如果需要临时文件的路径(比如与其他进程共享、需要持久化到进程退出后),使用mkstemp原子创建命名临时文件,它是POSIX标准定义的命名临时文件创建方式,彻底避免了TOCTOU竞态漏洞。
点击查看代码
cpp
// 函数原型
#include <stdlib.h>
int mkstemp(char *template);
- 核心参数 :
template是文件名模板,必须是可修改的字符数组 (不能是字符串常量),且必须以XXXXXX结尾(6个X会被内核随机替换为唯一的字符组合,保证文件名不冲突) - 返回值 :成功返回文件描述符(权限默认
0600,仅所有者读写),失败返回-1 - 注意 :命名临时文件不会自动删除,需手动调用
unlink();可在创建后立即unlink(),文件描述符仍可正常使用,关闭后自动删除(结合了匿名和命名的优势)
实战示例:
点击查看代码
cpp
#include <fcntl.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <cstring>
int main() {
// 1. 构造文件名模板(必须是可修改的字符数组,必须以XXXXXX结尾)
char template[] = "/tmp/my_named_temp_file_XXXXXX";
// 2. 原子创建命名临时文件
int fd = mkstemp(template);
if (fd == -1) {
perror("mkstemp创建失败");
return 1;
}
printf("命名临时文件创建成功,路径:%s\n", template);
// 3. 立即unlink(可选,推荐):文件描述符仍可使用,关闭后自动删除
if (unlink(template) == -1) {
perror("unlink预删除失败");
close(fd);
return 1;
}
printf("临时文件已预删除,关闭后自动清理\n");
// 4. 写入测试数据
const char *write_data = "命名临时文件测试数据(已预删除)";
size_t data_len = strlen(write_data);
ssize_t written = write(fd, write_data, data_len);
if (written != data_len) {
perror("write写入失败");
close(fd);
return 1;
}
printf("成功写入%zd字节数据\n", written);
// 5. 重新定位到文件开头,读取验证
off_t new_offset = lseek(fd, 0, SEEK_SET);
if (new_offset == -1) {
perror("lseek定位失败");
close(fd);
return 1;
}
char read_buf[1024] = {0};
ssize_t read_len = read(fd, read_buf, sizeof(read_buf)-1);
if (read_len == -1) {
perror("read读取失败");
close(fd);
return 1;
}
printf("读取到%zd字节数据:%s\n", read_len, read_buf);
// 6. 关闭文件,自动删除
close(fd);
printf("临时文件已自动删除\n");
return 0;
}
6.3 临时文件高频避坑指南
- 禁止使用
tmpnam/tempnam:这两个函数已被POSIX标准标记为废弃,它们只生成临时文件名,不创建文件,存在严重的TOCTOU竞态漏洞,可能导致数据泄露或覆盖 - 禁止使用字符串常量作为
mkstemp的模板:字符串常量存储在只读内存区,修改会导致程序崩溃,必须使用可修改的字符数组 - 命名临时文件必须注意权限 :
mkstemp默认权限是0600,不要手动修改为更宽松的权限,否则会导致数据泄露 - 命名临时文件推荐立即
unlink:这样可以避免进程异常退出时临时文件残留,同时文件描述符仍可正常使用,关闭后自动删除 - 临时文件路径选择 :推荐使用
/tmp或/var/tmp目录(Linux系统默认的临时文件目录),不要使用当前工作目录,避免污染项目目录
7. 文件状态控制API:万能的fcntl与核心用法
fcntl(file control)是Linux文件I/O的万能控制函数,用于对已打开的文件描述符执行各类控制操作,核心用法如下:
点击查看代码
cpp
// 函数原型
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
7.1 高频核心用法
7.1.1 获取/设置文件状态标志:F_GETFL / F_SETFL
-
作用:
F_GETFL获取当前fd的打开标志,F_SETFL设置标志,仅支持修改O_APPEND、O_NONBLOCK、O_ASYNC等标志,无法修改O_RDONLY/O_WRONLY/O_RDWR等核心模式。 -
示例:给fd设置非阻塞模式
点击查看代码
cpp// 获取当前标志 int flags = fcntl(fd, F_GETFL); if (flags == -1) { perror("fcntl F_GETFL failed"); return 1; } // 追加非阻塞标志 flags |= O_NONBLOCK; if (fcntl(fd, F_SETFL, flags) == -1) { perror("fcntl F_SETFL failed"); return 1; }
7.1.2 获取/设置文件描述符标志:F_GETFD / F_SETFD
-
唯一常用标志:
FD_CLOEXEC,设置后,进程执行exec系列函数时,会自动关闭该fd,避免子进程继承不必要的fd,造成资源泄露和安全问题。 -
示例:给fd设置
FD_CLOEXEC标志点击查看代码
cppint flags = fcntl(fd, F_GETFD); if (flags == -1) { perror("fcntl F_GETFD failed"); return 1; } flags |= FD_CLOEXEC; if (fcntl(fd, F_SETFD, flags) == -1) { perror("fcntl F_SETFD failed"); return 1; } -
优化提示:
open时直接传入O_CLOEXEC标志,原子完成fd创建与FD_CLOEXEC设置,避免多线程场景下的竞态问题。
7.1.3 复制文件描述符:F_DUPFD / F_DUPFD_CLOEXEC
- 作用:复制fd,与
dup/dup2功能一致,F_DUPFD_CLOEXEC会给新fd设置FD_CLOEXEC标志。
8. 文件描述符复制:dup/dup2/dup3的原理与实战
8.1 复制的本质
基于第3节的三个表原理,fd复制的本质是:创建一个新的fd,指向同一个打开文件表条目,新老fd共享文件偏移量、打开状态、文件锁等,仅fd编号不同。
8.2 核心API详解
8.2.1 dup:复制到最小可用fd
点击查看代码
cpp
// 函数原型
#include <unistd.h>
int dup(int oldfd);
- 作用:复制
oldfd,返回系统当前可用的最小fd编号,新fd与oldfd指向同一个打开文件表。
8.2.2 dup2:指定目标fd复制
-
作用:复制
oldfd到指定的newfd,如果newfd已经打开,会先原子关闭newfd,再完成复制。 -
经典实战场景:实现shell的输出重定向(把标准输出重定向到文件)
点击查看代码
cpp#include <fcntl.h> #include <unistd.h> #include <cstdio> #include <cstdlib> int main() { // 打开目标文件 int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd == -1) { perror("open failed"); return 1; } // 把标准输出(1)重定向到fd,关闭原来的标准输出 if (dup2(fd, STDOUT_FILENO) == -1) { perror("dup2 failed"); close(fd); return 1; } close(fd); // 复制完成,原fd可以关闭 // 后续printf的内容,都会写入output.txt,而不是终端 printf("这段内容会被重定向到文件中\n"); return 0; }
8.2.3 dup3:Linux特有的扩展API
点击查看代码
cpp
// 函数原型
#include <fcntl.h>
#include <unistd.h>
int dup3(int oldfd, int newfd, int flags);
- 核心扩展:支持传入
O_CLOEXEC标志,给新fd设置close-on-exec属性,避免额外的fcntl调用,原子操作更安全。
8.3 关键注意事项
- 复制后的fd共享打开文件表,因此
lseek修改偏移量,会影响所有共享的fd; - 只有当所有指向该打开文件表的fd都被关闭,才会释放对应的文件资源;
- fd的标志(
FD_CLOEXEC)是每个fd独立的,老fd的标志不会继承给新fd,需要单独设置。
9. 文件截断API:truncate/ftruncate与空洞文件特性
文件截断用于修改文件的大小,将文件缩容或扩容到指定的长度,核心API有两个:
点击查看代码
cpp
// 函数原型
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *pathname, off_t length);
int ftruncate(int fd, off_t length);
9.1 核心区别与用法
truncate:通过文件路径操作,无需打开文件,仅需对文件所在目录有执行权限、对文件有写权限;ftruncate:通过已打开的fd操作,fd必须有写权限。
9.2 两种核心场景
- 缩容场景 :
length小于文件原有大小,文件超出length的部分会被丢弃,数据不可恢复,操作前务必备份; - 扩容场景 :
length大于文件原有大小,文件会被扩容,超出原有大小的部分会被填充为\0,形成空洞文件。
9.3 空洞文件特性(Linux核心特性)
-
空洞文件的扩容部分,不会实际占用磁盘空间,只有当真正写入数据时,内核才会分配磁盘块;
-
用
ls -l查看会显示扩容后的完整大小,用du -h查看会显示实际占用的磁盘空间; -
经典应用场景:下载大文件时提前预分配空间、虚拟机磁盘镜像、数据库文件预分配。
-
示例:创建1G的空洞文件
点击查看代码
cppint fd = open("hole_file", O_WRONLY | O_CREAT | O_EXCL, 0644); if (fd == -1) { perror("open failed"); return 1; } // 截断到1G大小 if (ftruncate(fd, 1024 * 1024 * 1024) == -1) { perror("ftruncate failed"); close(fd); return 1; } close(fd);
10. 分散输入与集中输出:readv/writev高效I/O实战
readv/writev也叫Scatter-Gather I/O(分散-聚集I/O),是Linux提供的高效I/O能力,核心解决多缓冲区读写的性能与原子性问题。
点击查看代码
cpp
// 函数原型
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
// iovec结构体:定义一个缓冲区
struct iovec {
void *iov_base; // 缓冲区起始地址
size_t iov_len; // 缓冲区长度
};
10.1 核心能力
- 分散输入
readv:一次性从fd中读取数据,按顺序分散填充到iovcnt个不连续的缓冲区中,无需多次调用read; - 集中输出
writev:一次性将iovcnt个不连续的缓冲区中的数据,按顺序连续写入fd中,无需多次调用write。
10.2 核心优势
- 性能提升 :仅需一次系统调用,减少用户态与内核态的切换开销,对比多次
read/write性能更优; - 原子性保证 :
writev是原子操作,多个缓冲区的内容会连续写入文件,不会被其他进程打断,完美适配日志写入、网络发包等场景,避免内容错乱; - 减少内存拷贝:无需将多个不连续的缓冲区拼接成一个大缓冲区,直接写入,节省内存与拷贝开销。
10.3 实战示例:HTTP响应一次性写入
点击查看代码
cpp
#include <fcntl.h>
#include <sys/uio.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>
int main() {
int fd = open("http_response.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) {
perror("open failed");
return 1;
}
// 三个不连续的缓冲区,分别存储状态行、头部、响应体
const char *status_line = "HTTP/1.1 200 OK\r\n";
const char *header = "Content-Type: text/plain\r\nContent-Length: 12\r\n\r\n";
const char *body = "Hello World\n";
// 构造iovec数组
struct iovec iov[3];
iov[0].iov_base = (void *)status_line;
iov[0].iov_len = strlen(status_line);
iov[1].iov_base = (void *)header;
iov[1].iov_len = strlen(header);
iov[2].iov_base = (void *)body;
iov[2].iov_len = strlen(body);
// 一次性写入所有缓冲区
ssize_t written = writev(fd, iov, 3);
if (written == -1) {
perror("writev failed");
close(fd);
return 1;
}
printf("成功写入%zd字节\n", written);
close(fd);
return 0;
}
10.4 注意事项
iovcnt的最大值为IOV_MAX(Linux下一般为1024),超出会报错;readv/writev会按顺序处理iovec数组,先处理完第一个缓冲区,再处理第二个,以此类推;- 同样需要处理部分写入的情况,循环重试。
11. 非阻塞I/O极简入门:核心概念与基础用法
11.1 什么是非阻塞I/O?
Linux文件I/O默认是阻塞模式:
- 调用
read时,如果文件没有数据(比如管道、套接字、终端),进程会阻塞等待,直到有数据可读; - 调用
write时,如果内核缓冲区满了,进程会阻塞等待,直到有空闲空间。
而非阻塞模式 :调用I/O函数时,如果操作无法立即完成,函数会立即返回-1,并设置errno为EAGAIN(或EWOULDBLOCK,Linux下两者等价),不会阻塞进程,进程可以继续执行其他任务。
11.2 如何设置非阻塞模式?
两种方式,推荐第一种原子操作:
-
open时直接传入O_NONBLOCK标志:cppint fd = open("/dev/tty", O_RDWR | O_NONBLOCK); -
用
fcntl给已打开的fd追加O_NONBLOCK标志,参考第7.1.1节的示例。
11.3 核心适用场景与注意事项
-
适用场景 :仅对管道、套接字、终端、FIFO等设备文件有效,对普通磁盘文件无效(普通文件的读写不会阻塞,内核会通过页缓存保证立即返回);是多路复用(select/poll/epoll)、高并发网络编程的基础。
-
核心注意事项 :非阻塞I/O必须循环处理
EAGAIN错误,不能只调用一次,示例如下:点击查看代码
cppchar buf[1024]; ssize_t n; while (1) { n = read(fd, buf, sizeof(buf)); if (n == -1) { if (errno == EAGAIN) { // 无数据可读,稍后重试,可先执行其他任务 printf("无数据,稍后重试\n"); sleep(1); continue; } // 真正的错误 perror("read failed"); break; } if (n == 0) { printf("文件结束\n"); break; } // 处理读取到的数据 printf("读取到%zd字节\n", n); }
12. 高频避坑指南与核心总结
12.1 高频避坑清单
- 不检查系统调用的返回值 :尤其是
close、write的部分写入、fcntl的设置结果,导致异常无法定位; - 不处理
EINTR错误 :系统调用被信号中断时会返回-1并设置errno=EINTR,必须循环重试,不能直接判定为失败; - 混淆系统调用I/O与标准库I/O:混用两者操作同一个文件,导致缓冲不一致,数据错乱;
- 不用原子操作导致竞态问题 :多进程/多线程场景下,不用
O_APPEND、O_EXCL、pread/pwrite,导致数据覆盖、安全漏洞; - 忘记设置
FD_CLOEXEC:子进程继承不必要的fd,造成资源泄露、安全问题; - 非阻塞I/O不处理
EAGAIN:直接判定为读取失败,导致数据丢失; - 误以为普通文件支持非阻塞I/O :
O_NONBLOCK对普通磁盘文件无效,不要做无用功; - 重复关闭fd、使用已关闭的fd:导致程序崩溃、文件描述符泄露。
- LFS宏定义顺序错误 :必须在所有头文件之前定义
_FILE_OFFSET_BITS 64,否则宏替换不生效 - 临时文件禁止使用
tmpnam/tempnam:这两个函数已废弃,存在严重的TOCTOU竞态漏洞 - 命名临时文件推荐立即
unlink:避免进程异常退出时临时文件残留,同时文件描述符仍可正常使用 - 超大文件偏移量计算必须先强制转换为
off_t:避免整数溢出
12.2 核心总结
Linux文件I/O的核心逻辑,始终围绕「文件描述符-打开文件表-inode表」三个核心表展开,所有API都是基于这个底层模型的能力延伸。
- 基础API是操作的入口,掌握
open/read/write/close是入门的基础; - 原子API是并发安全的核心,彻底解决竞态问题与安全漏洞;
fcntl是万能控制工具,覆盖fd的各类状态管理;- fd复制是重定向、进程间通信的基础;
readv/writev是高效I/O的优化手段,减少系统调用与内存拷贝;- 非阻塞I/O是高并发编程的基础,配合多路复用实现高性能I/O。
- LFS API是超大文件处理的标准方案,通过
_FILE_OFFSET_BITS=64实现无侵入式64位兼容,突破2GB文件大小限制,是工业级代码的默认规范; - 临时文件首选
tmpfile(匿名、自动删除、无竞态),需要路径时用mkstemp+立即unlink的组合。
11. 拓展学习路径
- 经典权威书籍:《UNIX环境高级编程(APUE)》第3、5、14章,是Linux系统I/O的圣经;
- Linux官方手册:
man 2 open、man 2 read、man 2 writev等,查看API的完整细节与最新特性; - 进阶学习方向:内存映射I/O(
mmap)、零拷贝技术(sendfile、splice)、多路复用(select/poll/epoll)、异步I/O(io_uring)。
尾声
以上就是Linux/C++ 文件与I/O操作的全链路内容,从基础API到底层原理,再到进阶实战与避坑指南,覆盖了生产环境中90%的使用场景。
如果有任何疑问,或者有想补充的场景,欢迎在评论区留言交流。如果本文对你有帮助,欢迎推荐、收藏、关注、转发,后续会继续分享Linux系统编程、C++后端开发的核心内容。