本章导读

文件中章讲三个高阶能力。内存映射(xrtFileMap):把文件映射进地址空间,指针即文件内容——随机访问大文件零拷贝、零系统调用。文件锁(xrtFileLock 整文件 + xrtFileLockRange 区间):共享/独占两模式,多进程协调读写同一文件——从“整个文件互斥”到“字节区间细粒度”两档粒度。原子交付(xrtFileTemp + 重命名):临时文件写完 + 原子改名——读者永远看到完整文件,“写一半”状态结构性消失。三者各解一个并发与性能的真实问题。

46.1引入

三个场景。场景一:一个 2GB 的索引文件要随机访问——Read/Seek 循环每次读都是系统调用(内核拷贝到用户缓冲),热路径上百万次访问的 syscall 开销成了大头;映射进内存后访问就是普通的指针解引用——内核按页惰性加载,零拷贝。场景二:两个进程同时更新同一个数据文件——不加协调互相覆盖;整文件锁太粗(读多写少的场景读端全串行),范围锁让读端共享、写端独占、互不重叠的区间并行。场景三:配置文件更新时读者恰好在读——读到写了一半的内容,配置解析崩溃;写临时文件再原子改名,读者要么看到旧文件要么看到新文件,永不见中间态。

46.2概念

内存映射:指针即文件(随机访问的主场)

1建立映射xrtFileMap(句柄, 偏移, 长度, 标志)——长度 0 = 映射到文件尾;视图的 Data/Size 即访问面
2访问指针解引用即读文件;可写标志下写入同步到文件
3生命周期Unmap 归还映射;写入的落盘时机由内核与 flush 控制
4适用随机访问的大文件(索引/数据库/只读资源)——顺序流读写反而不如 Read

映射的收益按访问模式分化:随机访问——每次访问是一次缺页(首次)或内存读(后续),热区驻留后接近内存速度;顺序流——映射没有预读优势,分块 Read 反而更简单可控。所以“映射更快”是半真半假——随机快、顺序无优势,选型先看访问模式(第 45 章三宿主的延续)。

范围锁:字粒度的协调

两档粒度:xrtFileLock(句柄, 共享/独占, 是否等待) 整文件锁(单实例守护、任务队列互斥的标准姿势);xrtFileLockRange(句柄, 模式, 偏移, 长度, 是否等待) 字节区间锁(记录级协调)——Unlock/UnlockRange 对应释放,区间锁解除时参数必须与加锁完全一致。两档的选择看数据形状:整文件锁适合“文件即单元”的互斥(单实例守护进程的经典用法——进程存活期间持有、退出自动释放);区间锁适合记录级并行(读端对区间 A 加共享锁、写端对区间 B 加独占锁,不重叠则并行)。锁的语义纪律:锁是协调协议不是魔法——所有访问方都遵守同一锁协议才有意义(一个进程不加锁直接写,锁保护不了任何人);锁范围与数据结构的边界对齐(记录级锁按记录区间)。

原子交付:临时文件 + 原子重命名(中间态消灭者)

第 45 章“三步收尾”的工业完全体:① 临时文件写入(同目录——重命名跨文件系统会失败);② Flush 语义确保数据落盘;③ 原子重命名到最终名——rename 在同一文件系统内是原子的,读者在任何时刻看到的都是完整文件。锁 范例之外,temp 与 dir_temp 范例演示了临时文件的命名与清理配合(xrtFileTemp 创建唯一命名的临时文件(目录/前缀参数化)、用完删除或改名)。

三件套的组合(按场景按需搭配)

映射与锁组合:映射的写端用范围锁协调——每个写者锁自己的记录区间再通过映射写入。原子交付与锁组合:更新频繁的文件用“版本文件 + 原子切换”(写 v2 临时文件、rename 到主名)避免长持锁。三件套没有强制组合——按并发场景按需取用,这正是它们各自独立成节的原因。

46.3示例

完整程序:内存映射读取

来自仓库范例 examples/file/map/main.c:

examples/file/map/main.c源码 ↗
#include <stdio.h>

#include <xrt.h>





















 */


/* 展示使用只读映射检查文件内容。 */
int main(void)
{
	static const char sPath[] = "xrt-file-map-example.tmp";
	xfile File;
	xfilemap Map;
	int iResult = 1;

	File = xrtOpen(sPath, XFILE_READ | XFILE_WRITE |
		XFILE_CREATE | XFILE_TRUNCATE);
	if ( (File == NULL) ||
		 !xrtWriteFull(File, "mapped", 6u, NULL) ) {
		(void)xrtClose(File);
		return 1;
	}
	Map = xrtFileMap(File, 0u, 0u, XFILE_MAP_READ);
	if ( Map != NULL ) {
		printf("%.*s\n", (int)xrtFileMapSize(Map),
			(const char*)xrtFileMapData(Map));
		if ( xrtFileUnmap(Map) && xrtClose(File) &&
			 xrtFileDelete(sPath) ) {
			iResult = 0;
		}
	}
	return iResult;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/file/map/main.c -lws2_32 -liphlpapimapped

刚才发生了什么。 ① 文件先写入内容再建立映射——参数 (file, offset=0, size=0, READ) 的 size 0 表示映射到文件尾;映射不创建文件,它映射文件的当前字节。② 映射建立后通过指针读取内容并输出 mapped——这次读取没有 Read 调用,就是内存访问。③ xrtFileMapData/xrtFileMapSize 取视图的指针与长度、xrtFileUnmap 解除映射——顺序纪律:先 Unmap 后 Close(顺序不可反)。只读映射的额外红利:多进程共享同一物理页——词典/索引/只读资源文件的加载方式。可写映射(标志声明)的写入会同步回文件——脏页由内核择机刷盘,显式落盘需求配 flush 语义。

完整程序:文件锁

来自 examples/file/lock/main.c:

examples/file/lock/main.c源码 ↗
#include <stdio.h>

#include <xrt.h>




















 */


/* 展示对整个文件使用非阻塞排他锁。 */
int main(void)
{
	static const char sPath[] = "xrt-file-lock-example.tmp";
	xfile File = xrtOpen(sPath, XFILE_READ | XFILE_WRITE |
		XFILE_CREATE);

	if ( File == NULL ) {
		return 1;
	}
	if ( !xrtFileLock(File, XFILE_LOCK_EXCLUSIVE, false) ) {
		(void)xrtClose(File);
		return 1;
	}
	printf("locked\n");
	if ( !xrtFileUnlock(File) || !xrtClose(File) ) {
		return 1;
	}
	return xrtFileDelete(sPath) ? 0 : 1;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/file/lock/main.c -lws2_32 -liphlpapilocked

刚才发生了什么。 ① 对目标区间加独占锁——成功后输出 locked;锁持有期间其他进程对同区间的加锁请求将等待或失败(按标志)。② 范例是单进程演示锁的 API 形态;真实的多进程协调要所有参与方都走锁协议——这是“协调协议”本义的实践面。③ Unlock 释放——锁的生命周期也可以挂在句柄关闭上(进程退出自动释放),但显式 Unlock 是纪律写法。把 link 与 report 范例留作进阶:符号链接操作与文件状态报告(大小/时间/属性)属于文件元数据族,与本章三件套互补。

46.4契约

  • 映射纪律:先建映射后用、先 Unmap 后 Close;随机访问是它的主场,顺序流用 Read。
  • 锁协议:所有访问方遵守同一协议才有保护;锁范围与数据边界对齐;显式 Unlock。
  • 原子交付:临时文件同目录、写完落盘、rename 原子切换——“写一半”状态结构性消失。
  • 临时文件:唯一命名(xrtFileTemp)、成功路径改名、失败路径删除——两种结局都不留垃圾。
  • 组合自由:三件套按并发场景按需组合,无强制配套。

从示例到工程:三件套的三个宿主

索引与资源加载(映射的主场):词典、只读配置大文件、数据库索引——启动映射、全程只读访问、退出 Unmap;多进程共享同一物理页的福利让"每进程一份内存"变成"每进程一份视图"。多进程协调(锁的主场):单实例守护进程用整文件锁互斥(拿到锁才服务、第二个实例退出);任务队列文件、账本文件的记录级更新用区间锁——锁协议写进文件的"使用说明"(哪个字段区间、什么模式、持锁时长上限)。配置与状态交付(原子交付的主场):一切"读者随时可能读"的文件——配置、清单、检查点。三宿主也互相组合:账本文件 = 区间锁(记录更新)+ 原子交付(整档轮换)。

崩溃一致性:原子交付的盲区

原子交付保证"读者不见中间态",但有一个盲区要认清:重命名成功不等于数据已落盘——rename 原子切换的是目录项,数据块可能还在页缓存里;此时断电,目录指向新名但数据不全。对崩溃一致性要求高的场景(账本、检查点),完整链是"写临时文件 → fsync 语义(数据落盘)→ rename → fsync 目录(现代文件系统)"——XRT 的文件层在 flush 语义上提供对应能力,重命名的目录 fsync 属于宿主文件系统的细节(多数场景省略、账本级场景补上)。把这条链写进你的"持久化清单":中间态由 rename 消灭、断电窗口由 fsync 消灭——两个保证不同层,缺谁都留窗口。

一个选型提醒:什么时候不用锁

文件锁不是唯一的并发协调方案,选型前先问三个问题。同机多进程? 是 → 文件锁合理;跨机 → 文件锁无效(网络文件系统的锁语义不可靠),改用应用层协议。访问粒度? 文件级 → 整文件锁(最简单);记录级 → 区间锁;更细 → 考虑单写者模型(一个进程独管文件、其他进程经它访问——第 21 章队列传消息)绕开锁。竞争频率? 低频更新 → 原子交付直接替代锁(版本切换无锁);高频竞争 → 锁的等待开销显现,考虑内存共享 + 单写者。锁是协调的最后手段之一,不是第一反应——与第 23 章"先上锁保平安"反选型问题同源。

常见错误

坑 1:映射当顺序读用

症状:用映射逐字节顺序扫大文件——性能没有提升甚至更差(缺页开销 + 无预读),代码却更难写。

原因:映射的收益模型是“随机访问热区驻留”——顺序流它的缺页与 Read 的分块相比毫无优势。

错误写法
xfilemap Map = xrtFileMap(File, 0, Size, XFILE_MAP_READ);
for ( uint64 i = 0; i < Size; i++ ) {
	Consume(Map.Data[i]);   /* 顺序扫 2GB——每页一次缺页,无预读 */
}
正确写法
/* 顺序流:回到分块 Read(第 45 章流式宿主) */
char Buffer[65536];
uint64 Done = 0;
while ( Done < Size ) {
	uint64 Chunk = Min(sizeof(Buffer), Size - Done);
	xrtReadFull(File, Buffer, Chunk);
	Consume(Buffer, Chunk);
	Done += Chunk;
}
坑 2:锁了但别人不锁

症状:加了锁还是出现数据竞争——写坏记录、读半新半旧;偶发且难复现。

原因:锁是参与方之间的协议——有一个写者绕过锁直接写,所有守规矩方的锁都是摆设。

错误写法
/* 进程 A(守规矩):先锁后写 */
xrtFileLockRange(File, XFILE_LOCK_EXCLUSIVE, Off, Len, true);
WriteRecord(File, Off, Data);
xrtFileUnlockRange(File, Off, Len);

/* 进程 B(老代码,没上锁协议):直接写 */
WriteRecord(File, Off, OtherData);   /* A 的锁对不加锁的 B 无约束——竞争依旧 */
正确写法
/* 全部访问方走同一锁协议——B 的改法与 A 相同 */
xrtFileLockRange(File, XFILE_LOCK_EXCLUSIVE, Off, Len, true);
WriteRecord(File, Off, OtherData);
xrtFileUnlockRange(File, Off, Len);
/* 协议写进模块文档:此文件的一切写入必须先加锁 */

编程练习

基础:映射与读回对照(两种访问路径的一致性)

同一段内容分别用映射与 ReadFull 读出——字节一致;再用映射改写(可写映射)后读回验证同步。

进阶:双进程锁实验

写一个“锁持有者”与一个“锁等待者”两个程序:前者加锁睡眠数秒、后者尝试加锁并报告等待/成功——体会锁的协调语义(等待者改用共享锁与独占锁各试一次)。

挑战:原子配置更新器(并发读者的零中间态验证)

实现 save_config(路径, 内容):同目录临时文件(唯一名)写入 → 落盘 → 原子重命名;并发读者循环读配置并解析,验证任何时刻读到的都是完整合法内容。验收标准:100 次更新期间读者零解析失败;失败注入(重命名前杀进程)后原文件完好;无临时文件残留。

小结

知识点速查
映射xrtFileMap 指针即文件;随机访问主场、顺序流无优势;先 Unmap 后 Close
两档锁整文件 Lock(单实例互斥)/ 区间 LockRange(记录级并行);协议约束所有访问方
原子交付同目录临时文件(唯一名)→ 落盘 → 原子重命名;中间态结构性消失
临时文件xrtFileTemp(目录, 前缀, ...) 唯一命名;成功改名、失败删除——两态零垃圾
组合映射+锁(写端协调)/ 原子切换(免长持锁)——按场景取用
元数据族link/report 范例:符号链接与文件状态报告——三件套的配套
三宿主索引资源加载(映射)/ 多进程协调(锁)/ 随读文件交付(原子改名)
崩溃盲区rename 消灭中间态、fsync 消灭断电窗口——两层保证缺一留窗
锁的边界跨机无效、低频可被原子交付替代、高频考虑单写者——最后手段而非第一反应

📖 本章涉及的 API 参考手册:file(132 个条目)

点击查看完整签名、参数约束、返回值与错误说明