跳到主要内容
🔍
5.1.4评审中intermediate · 约 4 课时 · P1

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_* 标志族等【补充】;平台差异与迁移训练见文末专块。

① 学习目标

  1. 说出 open/read/write/lseek 的返回值约定与 errno 的正确使用时机;
  2. 解释"read 返回 0 = EOF、-1 = 出错"与"部分写"两个关键细节;
  3. 说出 lseek 修改的是文件偏移量,对 read/write 都生效(并知道 O_APPEND 例外);
  4. 列举常用 O_* 标志(O_CREAT/O_EXCL/O_TRUNC/O_APPEND/O_NONBLOCK)并说明 mode 参数何时生效;
  5. 独立完成实践任务 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 -ldu 对比观察(稀疏文件);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\nopen 加 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()。(判断)

    ◌ 未作答

⑨ 章节测验

章节测验

5 题题库 · 随机抽 5 题 · 及格线 60 分 · 前端判分(学习自测)
开始测验 →

⑩ 实战任务

实践任务

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)混在一句,易误导初学者。