open/read/write/lseek 与 errno
掌握四个最核心的系统调用与 errno 错误处理,写出健壮的文件 IO 程序
- 来源
- 纠错
- 补充
标记说明:【来源】来自上传资料 · 【补充】课程新编 · 【纠错】按勘误表修正 · 【更新】过时内容已现代化 · 【待确认】无法可靠还原
完成标准(本章)
- 📖 已阅读:滚动 ≥ 80% 且有效阅读 ≥ 240 秒
- ✏️ 已练习:小练习正确率 ≥ 60%
- 📝 已通过测验:分数 ≥ 60 分
- 🛠️ 已掌握还需完成实践任务
5.1.4 open/read/write/lseek 与 errno
本章来源:核心用法来自《文件io讲义》PAGE 7-10 与《Linux IO》PAGE 25-27【来源】(两份讲义示例代码在 PDF 提取中丢失,本章代码全部重写并实测);含 3 条【纠错】(lseek 语义 / EACCES 拼写 / mode 表述)与 O_* 标志族等【补充】;平台差异与迁移训练见文末专块。
① 学习目标
- 说出 open/read/write/lseek 的返回值约定与 errno 的正确使用时机;
- 解释"read 返回 0 = EOF、-1 = 出错"与"部分写"两个关键细节;
- 说出 lseek 修改的是文件偏移量,对 read/write 都生效(并知道 O_APPEND 例外);
- 列举常用 O_* 标志(O_CREAT/O_EXCL/O_TRUNC/O_APPEND/O_NONBLOCK)并说明 mode 参数何时生效;
- 独立完成实践任务 mycp,能演示至少 3 种 errno 错误路径。
② 前置知识
- 必修:5.1.3 系统调用与文件描述符(fd 概念、0/1/2、ulimit);
- 建议:1.3.1 指针(read/write 的缓冲区传参)。
③ 核心概念【来源】
| 概念 | 要点 |
|---|---|
| open | 打开/创建文件,返回 fd;失败返回 -1 并设置 errno |
| read | 从 fd 读入缓冲区,返回实际字节数;0=EOF,-1=出错 |
| write | 把缓冲区写入 fd;返回值可能小于请求长度(部分写) |
| lseek | 修改文件当前偏移量;对后续 read/write 都生效 |
| errno | 全局错误码变量;只在出错时才有意义,成功调用不会清零它 |
| O_* 标志 | 打开方式(RDONLY/WRONLY/RDWR)与行为标志(CREAT/EXCL/TRUNC/APPEND/NONBLOCK…) |
| mode 参数 | 仅当 flags 含 O_CREAT 时才生效;实际权限 = mode & ~umask |
④ 通俗解释【补充】
把文件想象成一本"游标书":
- open = 借出这本书,拿到借书卡(fd);
- read = 从游标位置往后读几页,读完了返回 0(EOF);书缺页了返回 -1(查 errno);
- write = 在游标位置写内容,写完游标自动前移;有时一次写不完(部分写),要继续写剩下的;
- lseek = 直接把游标翻到第 N 页——翻页这个动作对"读"和"写"都生效(写也是写在游标处);但如果是"只能追加"模式(O_APPEND),写操作永远发生在书的最后一页;
- errno = 图书管理员留下的"故障单"——只有出事了才有内容,且不会因为你没出错就被清空,所以先判断返回值,失败才看 errno。
⑤ 示例代码【代码全部重写并实测(gcc 15.2.0,2026-08-15)】
代码示例与验证记录
- examples/ex1-open-read.cmycp:open/read/write 最小文件拷贝✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64, 2026-08-15(含 O_BINARY 平台差异修复与字节级一致性验证))编译:
gcc ex1-open-read.c -o mycp -Wall -Wextra适用环境:LinuxWindows(MinGW)展开预期输出(实测)
拷贝完成: ex1-open-read.c -> copied.txt (随后哈希对比源与目标:SHA256 一致)
差异说明:错误路径示例:源不存在时输出 "open xxx 失败: No such file or directory (errno=2)"
完整源码见 /code 代码示例页
- examples/ex2-lseek-hole.clseek 制造空洞文件✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64, 2026-08-15)编译:
gcc ex2-lseek-hole.c -o hole -Wall -Wextra适用环境:LinuxWindows(MinGW)展开预期输出(实测)
lseek 后文件偏移 = 1048576 当前偏移 = 1048579(= 文件逻辑大小,中间 1MB-5B 为空洞) hole.dat 大小: 1048579 字节
差异说明:Linux 专属行为未实测:ls -l 显示 1048579,du 显示远小于此(稀疏文件);Windows 上不明显
完整源码见 /code 代码示例页
- examples/ex3-errno-handling.cerrno/perror/strerror 错误处理演示✓ 已实测(gcc 15.2.0 / MinGW-w64 x86_64, 2026-08-15)编译:
gcc ex3-errno-handling.c -o errdemo -Wall -Wextra适用环境:LinuxWindows(MinGW)展开预期输出(实测)
open("no-such-file.txt") 失败: errno = 2 strerror = No such file or directory perror : No such file or directory差异说明:errno 数值由标准规定(2=ENOENT);strerror 文本随系统 locale 可能为中文
完整源码见 /code 代码示例页
⑥ 编译与运行方法
每个示例的编译命令、适用环境与实测输出见上方示例卡(数据源为 examples.yaml)。要点:ex1(mycp)在 Windows 下必须加 O_BINARY(CRT 文本模式会把 \n 转成 \r\n,已实测发现并修复);ex2 的空洞文件行为在 Linux 上可用 ls -l 与 du 对比观察(稀疏文件);ex3 的 errno 数值由标准规定(2=ENOENT),文本随系统 locale 可能为中文。
⑦ 常见错误
| 症状 | 原因 | 解决 |
|---|---|---|
| 【纠错】以为 lseek 只影响 read | 原讲义(Linux IO PAGE 26)表述错误 | lseek 修改文件偏移量,对 read/write 都生效;O_APPEND 下 write 仍写末尾 |
| 编译报 EACCESS 未定义 | 原讲义(文件io PAGE 8)拼写错误 | 正确常量是 EACCES |
| 【纠错】把"只写""644""存在则报错"混为一谈 | 原讲义(文件io PAGE 9)表述混淆 | 访问方式=O_WRONLY;权限=mode(仅 O_CREAT 时生效);"存在则报错"=O_EXCL |
| 拷贝出的文件换行被改写(Windows) | CRT 文本模式自动转换 \n→\r\n | open 加 O_BINARY(本平台差异实测发现) |
| read 返回 0 被当成错误 | 0=EOF 是正常结束 | 循环条件 >0 继续、==0 结束、<0 才查 errno |
| write 返回值 < 请求长度被忽略 | 部分写 | 循环写入剩余部分(见 ex1 内层 while) |
| errno 读晚了 | 中间调用了 printf 等可能覆盖 errno | 失败后立即保存 errno 再处理 |
⑧ 小练习
小练习
学习自测:提交后才显示答案与解析(前端判分,不作为正式考试)ex-b-1.open() 成功时的返回值是?(单选)
◌ 未作答ex-b-2.read(fd, buf, n) 返回 0 表示?(单选)
◌ 未作答ex-b-3.判断:lseek() 修改的文件偏移量只对后续的 read() 生效,不影响 write()。(判断)
◌ 未作答
⑨ 章节测验
章节测验
⑩ 实战任务
实践任务
mycp:带完整 errno 处理的文件拷贝工具
用 open/read/write/lseek 实现 cp 命令的简化版,重点不是"能拷贝",而是"每条错误路径都有 正确的 errno 处理",并体验空洞文件与平台差异。
输入与输出
命令行:./mycp <源文件> <目标文件>。成功输出"拷贝完成: …",失败输出"open/read/write 失败: <strerror> (errno=<n>)"并以非零码退出。支持把源文件设为目录观察 EISDIR,目标路径不可写观察 EACCES。
功能要求
- 源文件用 O_RDONLY 打开,目标文件用 O_WRONLY|O_CREAT|O_TRUNC 打开(Windows 加 O_BINARY)
- read 循环处理;write 循环处理部分写;read 返回 0 视为正常结束,-1 才是错误
- 每个 open/read/write/close 失败都打印 strerror(errno) 与 errno 数值
- 支持 lseek 演示:加一个 -s 选项,先 lseek 到指定偏移再写,制造空洞文件
限制条件
- 单文件、≤ 200 行;-Wall -Wextra 零警告
- 必须显式检查所有系统调用返回值;不得使用 gets()
- 普通 Linux 环境即可完成;Windows(MinGW) 亦可编译运行(需 O_BINARY)
验收步骤(自检清单 0/5)
验收标准
- 零警告编译通过
- 字节级拷贝一致(验收步骤 2)
- 至少演示 3 种不同 errno(如 ENOENT/EISDIR/EACCES)且输出格式正确(验收步骤 3/5)
- 能解释空洞文件"ls -l 大、du 小"的原因(验收步骤 4)
- 代码提交 Git 仓库并打 tag
常见失败原因
- 忘了判 open 返回值,fd=-1 时继续 read 导致 EBADF
- errno 读取太晚,被后续 printf 覆盖(应先保存 errno 再打印)
- Windows 下忘记 O_BINARY,拷贝出的文件换行被改写、字节数对不上
- 以为 read 返回 0 是出错,写出错误的退出逻辑
可选扩展
- 支持多文件一次拷贝(argv 循环)
- 用 stat 判断源是目录并递归拷贝(衔接 5.1.5 章)
- 对比自己 mycp 与系统 cp 的性能(不同 BUF_SIZE 的影响)
完成必要清单后才能计入"已完成实践"(学习状态自动推导,不提供一键完成)
⑪ 面试问题
面试问题
read() 的返回值分别代表什么?高频Linux 系统编程 · medium
要点:>0 为实际读到的字节数;0 表示到达文件末尾(EOF);-1 表示出错,需查 errno。
read(fd, buf, n) 三种返回:①返回 >0:实际读入 buf 的字节数,可能小于 n(管道/网络尤其常见),应继续读; ②返回 0:EOF,正常结束,不是错误;③返回 -1:出错,errno 给出原因——其中 EINTR(被信号打断)通常可重试, EAGAIN 表示非阻塞模式下暂无数据。常见错误:把 0 当出错、把 -1 当 EOF。
追问:- 追问:为什么一次 read 不一定读到 n 个字节?
- 追问:EINTR 场景如何处理?
评分要点:- 三种返回值各说清语义
- 区分 EOF 与错误
lseek() 的作用是什么?对 write 生效吗?高频Linux 系统编程 · medium
要点:lseek 修改文件当前偏移量,对后续的 read 和 write 都生效;O_APPEND 打开时 write 仍追加到末尾。
偏移量是文件对象的属性,不区分读写方向——"lseek 只对 read 生效"是原讲义的错误说法(本课程已勘误, 见 err-7.1-1)。两个要点:①lseek 可以越过文件末尾(SEEK_SET 超过 EOF),再写入会形成"空洞文件", 空洞区域读出来是全 0(ls -l 显示逻辑大小、du 显示实际占用,Linux 稀疏文件行为); ②以 O_APPEND 打开的文件,write 忽略偏移量、始终写在末尾。
追问:- 追问:什么是空洞文件?ls 与 du 为什么不一致?
评分要点:- 说清偏移量对 read/write 都生效
- 说出 O_APPEND 例外
- 能提空洞文件加分
errno 的正确使用方式是什么?高频Linux 系统编程 · easy
要点:先判断函数返回值,失败时才读 errno;成功调用不会清零 errno;用 perror/strerror 输出。
三个规则:①errno 只在"某次调用失败"后才有意义——正确姿势是检查返回值(如 open 返回 -1),失败再读 errno; ②任何成功调用都不会把 errno 清零,所以"先调用再判断 errno 是否为 0"是错误写法; ③errno 应尽快读取(后续函数调用可能覆盖),并用 perror 或 strerror(errno) 转成可读文本。 另外:常量名必须写对,例如权限错误是 EACCES(两个 C 一个 S),不存在 EACCESS(本课程勘误 err-7.1-3)。
追问:- 追问:EINTR 与 EAGAIN 分别怎么处理?
评分要点:- 返回值判成败、失败才看 errno
- 说出"成功不清零"
- perror/strerror 用法
⑫ 延伸阅读
man 2 open/man 2 read/man 2 lseek(CONFORMING TO 节可查各标志的内核版本要求);- 《UNIX 环境高级编程(APUE)》第 3 章:文件 IO;
- Linux man-pages:errno(3) 全部错误码列表;
- 下一章:5.1.5 stat 与目录操作。
平台适用性(platform block)
- platform_scope:universal——open/read/write/lseek 的语义是 POSIX 通用知识;
- kernel-version-specific:O_CLOEXEC(2.6.23+)、O_DIRECT(2.4+)、O_TMPFILE(3.11+),使用前查
man 2 open; - 【平台差异】 Windows 对照:CRT 文本模式(必须 O_BINARY)、API 为 CreateFile/ReadFile/WriteFile/SetFilePointer(本章不展开);32 位系统处理大文件需
_FILE_OFFSET_BITS=64。
迁移训练(migration training)
完成本章后,请书面回答:"把 mycp 从 x86_64 Linux 迁移到 ①aarch64 板卡 Linux、②macOS、③Windows,哪些原理不变?哪些要改?"
- 不变的:open/read/write/lseek 的语义、fd 模型、errno 用法、空洞文件原理;
- 要改的:①交叉编译器与运行环境(衔接 7.2);②个别标志与 off_t 宽度核对;③API 全套替换为 Win32 并处理文本模式。
内容来源映射
| 内容部分 | 资料 | 位置 | 标记 | 说明 |
|---|---|---|---|---|
| open/read/write/lseek 用法、文件描述符、errno 概念 | 文件io讲义 | 文件io讲义 PAGE 7-10(与 Linux IO(1) PAGE 25-27 交叉印证) | 【来源】 | 两份讲义示例代码在 PDF 提取中丢失,本章代码全部重写并实测 |
| lseek 语义 | Linux IO(1) | PAGE 26 | 【纠错】 | 原文"lseek 只对 read 生效"→对 read/write 均生效(见 corrections.yaml err-7.1-1) |
| 错误常量名 | 文件io讲义 | PAGE 8 | 【纠错】 | 原文"EACCESS"→正确常量 EACCES(见 err-7.1-3) |
| open 的 mode 参数与访问方式表述 | 文件io讲义 | PAGE 9 | 【纠错】 | 原文"以所有者只写方式打开文件(mode=644)"混淆了访问标志与权限位(见 err-5.1.4-mode) |
| O_* 标志族完整表、部分写处理、平台差异与迁移训练 | 无 | 【补充】 | 资料仅提及个别标志,完整语义与迁移内容为新编 | |
| 练习 / 测验 / 实践任务 / 面试问题 | 文件io讲义 | PAGE 9-10(拷贝/连接文件练习为雏形) | 【补充】 | 在资料练习雏形上扩展为可客观验收的任务 |
本章勘误与更新记录(【纠错】/【更新】)
err-7.1-1 · 技术错误 · 出处 PAGE 26
原文:lseek: 定位光标的位置——只对 read 生效
正确:lseek 修改的是文件的当前偏移量(file offset),对随后的 read 和 write 都生效;例外:以 O_APPEND 打开的文件,write 始终写到文件末尾(偏移量被忽略)。
原因:偏移量是文件对象的属性,不区分读写;'只对 read 生效'会让学习者在追加写/随机写场景写出错误代码。
err-7.1-3 · 技术错误 · 出处 PAGE 8
原文:错误值定义为 'EXXX' 形式,如 EACCESS
正确:正确常量名为 EACCES(Permission denied,两个 C 一个 S)。EACCESS 不存在,照抄会编译失败。
原因:拼写错误,属第一步报告 7.1 勘误表第 3 条。
err-5.1.4-mode · 表述修正 · 出处 PAGE 9
原文:以所有者只写方式打开文件(mode=644,不存在则创建、存在则报错)
正确:访问方式(O_WRONLY 只写)与权限位(mode=0644,所有者 rw)是两个不同维度:前者是 open 的 flags 参数,后者是仅当 flags 含 O_CREAT 时才生效的第三个参数(且受 umask 影响)。'存在则报错'对应的是 O_EXCL 标志,与 mode 无关。
原因:原文把三个概念(访问方式、权限位、O_EXCL)混在一句,易误导初学者。