← ALL ARTICLES

文件IO

全库目录221
本文目录22

第一次读取慢、第二次快,可能只是同一批字节已经进入系统页缓存。文件IO需要沿着实际消费者追踪数据:文件版本给出字节身份,系统请求把字节送入应用缓冲,解码器把它变成资源,发布入口决定哪个版本开始对读者可见。某一步完成,并不意味着后面的使用者也已经结束。

以场景7加载一份材质为例:清单代次g7指向不可变版本A,材质记录位于文件偏移3000,总长3000字节,包含8字节头和2992字节负载。读取头时只收到3字节,加载器必须续读;负载完成后解码器可能仍在工作,而发布者已经把清单切到版本B。下文沿这次加载说明有效字节、缓冲引用和版本保护如何交接,并处理读取取消。发布冲突见不可变文件发布,服务重启与删除见文件版本代理与回收。

文件、缓存页与应用缓冲

缓冲读取的数据路径

应用先打开路径得到文件句柄,再以偏移和长度发起读取。路径只用于定位,后续请求绑定已打开的文件对象;发布者更换同名路径时,已有句柄是否仍读旧对象、替换是否允许,由平台共享与重命名语义决定。要求完整版本一致的加载器先取得不可变版本清单,再打开其中的文件,读取期间不重新寻找同名最新文件。

缓冲IO把文件字节按页保存在操作系统缓存中。一次请求覆盖的页已经有效时,系统从这些页复制到应用目标缓冲;缺页时先安排存储设备读取,页就绪后再完成请求。请求跨过多个页,一部分命中、一部分缺失,最终完成时间仍受缺失部分影响。系统可预读后续页,但预读的数据不等于应用已经请求或消费的数据。

例如教学页长4096字节,本例负载请求偏移3008、长度2992,覆盖[3008,6000),跨页0和页1。第一页已缓存、第二页缺失时,系统仍须取得第二页;应用收到完整2992字节后,解码器才可按实际字节范围处理。这里的4096只用于解释分段,设备块、文件系统块、内存页和应用请求长度分别由目标平台确定。

页缓存由操作系统管理,应用缓冲由请求所有者管理。释放应用缓冲不表示缓存页立刻退出内存;缓存页被回收也不使已复制的应用字节失效。因此连续读相同文件的加速可能来自缓存,不能当作设备带宽提升。冷读与热读测量记录实际缓存控制方法、文件集、请求大小和并发深度。

短读与完整输入

一次读取可能只返回请求的一部分。读取函数维护done,下一请求使用fileOffset+done和buffer+done,长度为expected-done;只把本次返回的有效字节计入done。读取普通文件时,成功返回零字节表示末尾;平台错误单独处理,不能把错误当作EOF。

本例固定头为8字节,第一次得到3、第二次得到2,下一次仍需3;三次偏移分别为3000、3003和3005。此时EOF应报告截断,缓冲剩余的旧值不是有效头部。头部完整后校验格式、版本和负载长度上限,再分配负载并继续循环;所有加法检查溢出。校验和或内容哈希覆盖实际完整负载,短读不能靠补零通过。

不可变文件解决边读边改的问题。普通可变文件即使每次读取都成功,也可能先读到旧头、再读到新负载;打开一次句柄并不自动提供内容快照。加载器使用版本文件,或在所有写入者遵守的锁内读取整份内容。长度检查只能识别部分损坏,不能替代一致版本。

直接IO与内存映射

直接IO绕过部分系统缓存,通常要求文件偏移、缓冲地址和长度满足平台对齐。应用按对齐区间发起请求,再从返回范围取所需子区间;最后不足一个对齐块的尾部按平台支持的读取路径处理。它减少一层缓存重复,不消除设备队列、完成通知和应用缓冲寿命。

内存映射把文件范围映射为虚拟地址。第一次访问未驻留页时产生缺页,由系统取得数据后继续执行;后续访问仍可能受页回收影响。解码器持有映射期间不能解除映射或让文件被不受控截断。映射避免显式复制接口,却把等待隐藏在普通加载指令内;需要可预测延迟的线程先准备驻留数据或将解码安排到允许等待的工作线程。

请求队列与消费者寿命

提交、完成与取消

加载器维护两层对象。逻辑读取保存不可变文件版本、句柄、目标缓冲、起始偏移、期望长度和累计有效长度done;系统子请求只负责其中一个尚未读取的区间。一个逻辑读取同时最多拥有一个在途子请求,子请求以逻辑请求ID、子请求代次识别。这样晚到或重复的通知不能被当作下一次读取的结果。

A的8字节头第一次成功返回3字节,系统子请求已经结束,逻辑读取仍处于Reading。完成处理器增加done=3,归还这一次系统请求槽,再把逻辑读取送回等待队列;下一次取得槽后提交偏移3003、长度5的子请求。整个8字节缓冲、文件版本引用和字节预算持续保留,直到完整输入交给消费者或逻辑读取失败后清理。短读期间归还字节预算会让调度器超额分配仍被占用的内存。

提交、完成和取消都经过同一逻辑请求锁。调度器在锁内检查取消标记和在途状态,再登记子请求并调用提交接口;立即失败也走同一终态处理。完成通知排队处理,不在提交接口内部重入这把锁。取消先设置永久取消标记:尚无在途子请求时直接结束逻辑读取;存在子请求时请求系统取消,并等它的唯一终态。Windows的CancelIoEx可能与成功完成竞争,取消返回本身不能证明系统已停止写缓冲。

完成处理器先核对子请求身份并清除在途登记,再判断逻辑请求。取消或业务代次失效时,即使系统成功也不重提;否则只累加有效字节,错误进入失败,未读满时零字节进入截断,正数短读重新排队。done == expected才结束逻辑读取并移交解码器。解码任务持有独立引用直到结束,系统完成只说明设备不再写入,不能让分配器覆盖解码器仍在读取的缓冲。

text
逻辑读取:Queued → Reading → Complete / Failed / Cancelled
系统子请求:Pending → 一个最终完成通知
提交前:锁内检查未取消、无在途子请求,登记本次代次
完成后:核对代次,收取终态并归还系统请求槽
取消已登记:禁止续读;无在途请求或已收取系统终态后结束逻辑读取
错误或提前EOF:结束逻辑读取,释放读取方持有
正数短读:更新done,保留缓冲与字节预算,重新排队
全部读满:把持有移交解码器,最后消费者退出后归还字节预算

并发深度与背压

异步让调用线程继续工作,并不创造无限吞吐。调度器分别限制在途请求数、在途字节和待解码字节。若磁盘每秒送入200MB、解码器只能消化100MB,持续五秒就可能多占500MB缓冲;仅限制设备请求数并不能限制完成队列的增长。

因此提交前预留缓冲预算,读取完成只归还请求槽,字节预算一直保留到最后消费者退出。待解码队列接近上限时暂停新的低优先级读取,消费者释放空间后再唤醒调度器。高优先级请求使用预留额度,防止大量后台资产占满全部槽位;等待时间参与优先级,避免大文件长期饥饿。取消尚未提交的低优先级工作可以立即释放其预算,已提交工作仍等终态。

可把同一不可变文件中相邻区间合成一个较大读取,减少小请求开销。合并记录保存每个子请求的偏移范围,完成后以共享缓冲切片交给消费者;单个子请求取消只撤销其消费资格,不能回收其他子请求正在使用的整块缓冲。合并过大也会让紧急的小区间等待无关数据,调度时限制合并跨度并保留单独高优先级通道。

解码与资源版本交接

请求发出时捕获资源ID、输入内容哈希、所有者代次。系统完成后先校验字节,再检查代次,随后在私有结果中解码并检查格式不变量。场景7的A请求在场景8才完成时,只结清旧请求,不把A装进新场景。即使场景仍为7,清单已经切到B也可能使A不再适合采用:加载任务固定A以确保读取一致,资源发布入口则比较该任务捕获的目标资源代次。热更新先推进目标代次,再安排B加载;A仍可完成读取和解码,但最终比较会拒绝它覆盖B。

逻辑读取完成到解码排队也有交接失败:创建解码任务之前,读取方继续持有缓冲引用;任务创建失败时,由读取方释放引用。任务创建成功后才把引用交给任务,再尝试入队;队列关闭或投递失败时,由提交方销毁任务并释放任务持有的引用。成功入队以后,由任务执行结束或队列取消确认统一释放,IO完成回调不再释放同一份持有。预算随最后引用归还,避免队列拒绝时遗留一块永远没有消费者的缓冲。

解码期间也可能发生热更新或取消,因此发布时在资源锁或主线程提交队列内再次比较输入版本和所有者代次,再替换当前资源指针;取消和热更新通过同一入口更新代次。这个共同入口消除了“检查通过、随后取消、最后仍发布”的空隙。失败保留旧资源,日志分别记录IO成功、解码成功和资源已发布,三者不互相代替。

新资源发布后,旧资源的使用者仍持有旧版本引用。渲染线程或GPU使用的对象另按其完成栅栏退休,不能由文件缓冲释放推断渲染资源已无读者。文件版本、压缩字节、解码对象和GPU对象各自拥有引用,交接时先建立下一消费者的持有,再释放上游持有。具体GPU退休见GPU资源生命周期。

不可变文件与持久清单

参见不可变文件发布。

文件准备与正式入口

参见不可变文件发布。

发布任务登记与续行

参见不可变文件发布。

发布冲突与恢复

参见不可变文件发布。

读取固定版本

参见不可变文件发布。

跨进程代理与回收

参见文件版本代理与回收。

令牌和在途读取

参见文件版本代理与回收。

服务重启与旧操作

参见文件版本代理与回收。

运行中的版本排空

参见文件版本代理与回收。

垃圾回收的状态转换

参见文件版本代理与回收。

验证方法

先逐步记录这份材质的状态:A头部3、2、3字节到齐,再读取2992字节负载,读取引用移交解码;g8/B发布使旧任务失去资源采用资格,A的消费者退出后才释放字节预算。随后关闭A的新读取入口,等待A系统请求、续读和发送消费者全退,再解除版本保护并进入Deleting。每一步分别检查字节预算、资源代次与版本状态,不能用一个“加载完成”标志代替。

用跨页和多次短读检查字节范围,给解码器降速检查提交背压是否限制内存,再在取消与成功完成竞争时确认缓冲仅在消费者结束后回收。版本测试让读者固定旧清单,发布新版本后继续读旧文件,确认不会混合依赖。

分别在文件同步、清单提交、资源指针替换和删除登记之后注入退出。重启检查已提交版本可读、旧令牌被拒绝、仍失联的旧代理版本保持隔离,Deleting能够继续收尾。冷缓存和平台持久性单独测量,协议数例不能替代这些运行结果。

实现与证据

2026-10-22,以本机 Python 3.11 运行 python 知识库/examples/revision_16_21/test_reference.py,17项检查通过。参考实现与测试输入可复现本页注明的机制。 上述运行覆盖临时目录里的真实文件写入、替换与替换前异常,验证最终内容和临时文件清理,对应不可变文件发布的单文件样例;本页的短读调度、背压、解码交接与异步取消仍是机制推演。未执行断电、网络文件系统或异步驱动取消测试。

相关主题

参考资料