《深入理解计算机系统》读书笔记11: 系统级 I/O

📅 2026/7/16 10:33:15
《深入理解计算机系统》读书笔记11: 系统级 I/O
作者: andylin02学习章节: 第 10 章 系统级 I/O关键词 Unix I/O文件描述符RIO包带缓冲的输入输出共享文件I/O重定向标准I/O库stat目录操作套接字Shell Lab引言为什么程序员要学习系统级 I/O“输入/输出I/O是在主存和外部设备之间复制数据的过程。”—— CSAPP 作者尽管在日常编程中我们总是使用高级的I/O函数如C语言的printf/scanfC的/运算符大多数时候它们运行良好但深入学习 Unix I/O 仍有几个至关重要的原因。理解I/O与其他系统概念的循环依赖I/O 是系统操作不可分割的一部分只有深入理解I/O才能真正掌握进程、虚拟内存、网络编程等核心概念。处理标准I/O库无法胜任的场景例如标准I/O库无法读取文件的元数据并且在处理网络套接字时可能因复杂的缓冲机制引发致命问题。编写更健壮、更底层的代码系统级I/O是构建所有高级I/O函数的基础掌握它有助于编写出性能更高、更健壮的程序。本章将带领你从内核的视角出发探索文件系统、I/O操作以及它们与进程之间的交互让你真正理解程序是如何与外部世界交换数据的。一、Unix I/O一切皆文件在Linux系统中一个文件就是一个m个字节的序列。Unix I/O提供了统一的模型将所有I/O设备如网络、磁盘和终端都抽象为文件输入和输出都被当作对相应文件的读和写操作来执行。这使得所有输入和输出都能以一种统一的方式执行。1.1 打开与关闭文件应用程序通过调用open函数“打开”一个文件内核会返回一个小的非负整数——文件描述符用于在后续操作中标识这个文件。当操作完成时再调用close函数关闭文件。// 打开文件#includesys/types.h#includesys/stat.h#includefcntl.hintopen(char*filename,intflags,mode_tmode);// 返回若成功则为新文件描述符若出错则为 -1flags参数指明访问模式O_RDONLY只读、O_WRONLY只写、O_RDWR读写。mode参数指定新文件的访问权限位。// 关闭文件#includeunistd.hintclose(intfd);// 返回若成功则为 0若出错则为 -11.2 读写文件每个打开的文件都有一个内核维护的文件位置k初始为 0表示下一个读写操作的起始字节偏移量。#includeunistd.hssize_tread(intfd,void*buf,size_tn);// 返回若成功则为读的字节数若 EOF 则为 0若出错则为 -1ssize_twrite(intfd,constvoid*buf,size_tn);// 返回若成功则为写的字节数若出错则为 -1read从当前文件位置k开始复制最多n个字节到buf然后k增加实际读到的字节数write从buf复制最多n个字节到当前文件位置k。1.3 文件类型Linux中的文件有多种类型内核通过stat和fstat函数可以获取这些信息。文件类型说明普通文件包含任意数据的文件。应用程序会区分文本文件和二进制文件但内核一视同仁。目录包含一组链接的文件每个链接将一个文件名映射到一个文件。套接字用于与另一个进程进行跨网络通信的文件。其他还有命名管道、符号链接、字符和块设备等特殊文件类型。二、RIO 包健壮的 I/OUnix I/O 的read和write函数在读取终端或网络时可能会因信号中断或网络延迟导致不足值即未读完所有请求字节。RIO 包Robust I/O封装了read和write通过循环读写自动处理了这些情况确保传输完所有请求数据。RIO 提供了两类函数2.1 RIO 的无缓冲输入输出函数这些函数直接在内存和文件之间传输数据适合在网络套接字上读写二进制数据。#includecsapp.hssize_trio_readn(intfd,void*usrbuf,size_tn);ssize_trio_writen(intfd,void*usrbuf,size_tn);// 返回若成功则为传送的字节数若 EOF 则为 0若出错则为 -12.2 RIO 的带缓冲输入函数这些函数是线程安全的内部维护了一个应用程序级的缓冲区可以高效地从文件中读取文本行和二进制数据。#includecsapp.h// 初始化缓冲区voidrio_readinitb(rio_t*rp,intfd);// 读取一个文本行包括结尾的换行符ssize_trio_readlineb(rio_t*rp,void*usrbuf,size_tmaxlen);// 读取 n 个字节的二进制数据ssize_trio_readnb(rio_t*rp,void*usrbuf,size_tn);2.3 RIO 包的实现原理其核心是rio_read函数它会智能地管理内部缓冲区。当缓冲区为空时才调用read系统调用填满它当用户请求数据时优先从缓冲区复制而不是每次都陷入内核极大地提高了效率。// RIO 包的简化实现rio_readn 的核心逻辑ssize_trio_readn(intfd,void*usrbuf,size_tn){size_tnleftn;ssize_tnread;char*bufpusrbuf;while(nleft0){if((nreadread(fd,bufp,nleft))0){if(errnoEINTR)/* 被信号中断重试 */nread0;elsereturn-1;/* 真实错误返回 */}elseif(nread0){break;/* EOF */}nleft-nread;bufpnread;}return(n-nleft);/* 返回实际读取的字节数 */}2.4 RIO 包的使用场景与性能分析使用场景推荐函数理由网络套接字读写rio_readn,rio_writen直接处理不足值避免数据粘包问题。读取文本文件/终端输入rio_readlineb高效读取行数据且应用级缓冲减少系统调用。读取二进制数据块rio_readnb确保读取完整的二进制数据块。性能提示在读取文本文件时直接使用read函数一次读取一个字节查找换行符会导致大量的内核陷入开销巨大每次调用约20000-40000个时钟周期。而rio_readlineb通过内部缓冲区将多次read调用合并为少数几次性能显著提升。三、共享文件与 I/O 重定向3.1 内核表示打开文件的三个数据结构Unix 内核使用三个关键数据结构来表示打开的文件。数据结构作用域内容描述符表每个进程独有记录该进程打开的文件描述符每个表项指向文件表的一个表项。文件表所有进程共享记录文件位置、引用计数以及指向 v-node 表项的指针。v-node 表所有进程共享包含stat结构中的大部分信息如文件大小、类型等。3.2 I/O 重定向dup2函数Shell 通过dup2函数实现 I/O 重定向其核心原理是修改描述符表项使其指向不同的文件表项。#includeunistd.hintdup2(intoldfd,intnewfd);// 返回若成功则为非负的描述符若出错则为 -1dup2函数将描述符表项newfd指向oldfd所指向的文件表项实现了重定向。3.3 理解文件共享和重定向的示例intmain(){intfd1,fd2,fd3;charc1,c2,c3;fd1open(abcde.txt,O_RDONLY,0);fd2open(abcde.txt,O_RDONLY,0);fd3open(abcde.txt,O_RDONLY,0);// 演示不同描述符各自拥有独立的文件位置read(fd1,c1,1);// fd1 读一个字节文件位置变为 1read(fd2,c2,1);// fd2 也读一个字节但其位置也变为 1printf(c1 %c, c2 %c\n,c1,c2);// 输出: c1 a, c2 a// 演示使用 dup2 进行重定向dup2(fd2,fd3);// fd3 现在指向 fd2 指向的文件表项read(fd3,c3,1);// fd2 和 fd3 共享同一个文件位置printf(c3 %c\n,c3);// 输出: c3 b (因为 fd2 的位置已经是 1)return0;}四、标准 I/O 库ANSI C 定义了一组高级输入输出函数称为标准 I/O 库libc为程序员提供了 Unix I/O 的更高级别替代。4.1 标准 I/O 与系统级 I/O 对比特性Unix I/O (系统级)标准 I/O (库函数)抽象层次内核级系统调用用户级函数缓冲机制无缓冲直接在内核空间操作有缓冲通过用户空间缓冲区操作核心概念文件描述符int流FILE *适用场景所有 I/O 设备本地磁盘文件、终端网络编程更适用通常不适用存在限制4.2 标准 I/O 的缓冲类型标准 I/O 库使用了三种类型的缓冲。全缓冲当缓冲区填满或显式刷新时才进行实际I/O操作通常用于磁盘文件。行缓冲遇到换行符时进行I/O操作通常用于终端交互。无缓冲立即进行I/O操作通常用于标准错误以确保错误信息能即时输出。五、本章配套实验Shell Lab第 8 章的Shell Lab要求学生实现一个支持作业控制的 Unix shell 程序它完美地串联了第 8 章的进程控制、信号处理和第 10 章的 I/O 重定向等核心概念。5.1 Shell Lab 实验背景与目标该实验要求实现一个简单的shell (tsh)它能解析命令行并执行程序支持以下核心功能。支持前后台运行。处理SIGINTCtrlC和SIGTSTPCtrlZ信号。实现作业控制内建命令jobs,fg,bg。支持输入输出重定向和。正确回收僵尸进程。5.2 Shell Lab 关键技术点I/O 重定向的实现在 Shell Lab 中实现 I/O 重定向是本章知识的直接应用。Shell 程序需要解析命令行识别其中的和符号然后通过dup2函数完成重定向。// 在 fork 后的子进程中执行命令时处理重定向intin_fd-1,out_fd-1;// 解析命令行发现重定向符号if(存在输入重定向){// 以只读方式打开输入文件in_fdopen(infile,O_RDONLY);// 将标准输入重定向到该文件dup2(in_fd,STDIN_FILENO);close(in_fd);}if(存在输出重定向){// 以写方式打开输出文件如果不存在则创建存在则截断out_fdopen(outfile,O_WRONLY|O_CREAT|O_TRUNC,0644);// 将标准输出重定向到该文件dup2(out_fd,STDOUT_FILENO);close(out_fd);}// 重定向完成后再执行程序execve(argv[0],argv,environ);六、本章知识点思维导图第 10 章 系统级 I/O │ ├── 1. Unix I/O一切皆文件 │ ├── 文件描述符0: stdin, 1: stdout, 2: stderr │ ├── open / close 系统调用 │ ├── read / write 系统调用 │ ├── 文件类型普通文件、目录、套接字 │ └── 不足值Short Counts的处理 │ ├── 2. RIO 包健壮的 I/O │ ├── 无缓冲 I/Orio_readn / rio_writen │ ├── 带缓冲的 I/Orio_readinitb, rio_readlineb, rio_readnb │ └── RIO 包的实现原理与性能优势 │ ├── 3. 读取文件元数据与目录内容 │ ├── stat / fstat 读取文件元数据 │ └── opendir / readdir / closedir 读取目录 │ ├── 4. 共享文件与 I/O 重定向 │ ├── 描述符表 / 文件表 / v-node 表 │ ├── 父子进程共享文件 │ └── dup2 函数实现 I/O 重定向 │ ├── 5. 标准 I/O 库 │ ├── 标准 I/O vs Unix I/O 的对比 │ └── 缓冲类型全缓冲、行缓冲、无缓冲 │ └── 6. Shell Lab ├── I/O 重定向在 Shell 中的实现 └── 作业控制与信号处理七、本章小结第 10 章深入探讨了程序与外部世界交互的底层机制为理解复杂系统构建了坚实的基础。✅Unix I/O 模型理解了“一切皆文件”的设计哲学掌握了open、close、read和write等基础系统调用。✅RIO 健壮 I/O 包掌握了如何通过封装系统调用来处理“不足值”并提供带缓冲的高效输入这是编写网络服务器等复杂程序的关键技术。✅文件共享与 I/O 重定向深入理解了内核用于管理打开文件的三个数据结构以及dup2如何实现 I/O 重定向。✅标准 I/O 库对比了标准I/O库与系统级 I/O 的差异理解了它们的适用场景和局限。✅配套实验通过 Shell Lab你将理论应用于实践亲手构建一个支持 I/O 重定向和作业控制的 Shell这将是综合运用进程、信号和 I/O 知识的最佳实践。本章最核心的三个洞察“一切皆文件”是 Unix 设计的核心哲学它统一了程序与所有外部设备磁盘、终端、网络的交互方式使得同一套接口可以适用于各种场景。不足值的处理是编写健壮网络程序的关键与本地文件不同网络 I/O 由于延迟和信号中断很容易出现不足值这正是 RIO 包的核心价值所在。理解内核数据结构是掌握系统编程的钥匙描述符表、文件表和 v-node 表是理解文件共享、I/O 重定向和父子进程关系的基础为深入理解操作系统内核奠定了基础。 下一篇预告下一章我们将进入第 11 章网络编程。这一章将揭开网络通信的神秘面纱让你掌握构建分布式程序的核心技能客户端-服务器模型理解网络应用的基本架构IP 地址、域名与 DNS网络中的“定位系统”套接字接口网络通信的系统级 APIHTTP 协议与 Web 服务器应用层协议的实现并发服务器如何处理多个客户端的同时连接配套实验Proxy Lab——实现一个多线程的 Web 代理服务器为并发编程和网络应用打下坚实基础第 11 章将帮助你理解网络通信的本质掌握构建 Web 服务器和代理等网络应用的编程模型。敬请期待本文为个人学习笔记仅用于知识分享。如有错误欢迎指正。 点赞 收藏 分享让更多开发者看到这篇深度解析❤️ 如果觉得有用请给个赞支持一下作者