pread实现磁盘指定位置读取:扇区与字节偏移的底层实践

发布时间:2026/10/15 6:11:32
pread实现磁盘指定位置读取:扇区与字节偏移的底层实践 磁盘这个词在操作系统实验里一出现很多人的第一反应是打开文件、读一读、完事。等真正跑起来才发现文件系统把磁盘包装得太严实了你根本碰不到“指定的位置”。实验十一的标题写得很直白——读/写磁盘指定位置信息而且只需要完成读取。这意味着你得沉到块设备级别把一个扇区号换算成字节偏移然后请操作系统把那一块数据原样搬回来。这篇文章把这条链路的思路、代码和坑完整拉一遍适合正在做相关实验、或者想搞懂底层I/O流程的人参考。1. 实验思路先搞清楚你要在“哪一层”读磁盘1.1 三种读取姿态文件、块设备、驱动从用户进程视角看读取磁盘上数据至少有三种打开方式。第一种是你最熟悉的fopen/fread或者read/open一个普通文件。这看起来是“读磁盘”实际上你读的是文件系统已经编排好的逻辑视图。VFS会把文件转换成inode、逻辑块号、物理块号中间还要经过page cache最后才落到磁盘请求。这一层获得的“位置”是文件内的偏移而不是磁盘上的物理扇区。如果你想回答“磁盘第1024号扇区里存着什么”用这种方式绕了一大圈也到不了地方。第二种是打开块设备文件比如/dev/sdb或者一个磁盘镜像文件。你仍然用read/pread这些普通系统调用但这个设备节点背后没有文件系统来解释你的偏移。你传的偏移量会被直接当作设备上的字节位置从第0字节开始数。把扇区号乘以扇区大小就能精确定位到任意扇区。这才真正符合实验标题里的“磁盘指定位置”。第三种是在内核态直接写驱动通过request_queue、gendisk这类接口和设备交互。这是设备驱动课程的内容了普通教学实验一般不会要求到这个深度除非你是在内核模块实验里做扩展题。搞清楚这一点后面的一切都顺了这个实验要做的是第二层块设备级读取。1.2 扇区号和字节偏移怎么换算磁盘是按扇区组织的硬件做I/O的最小单位就是一个扇区传统上大多是512字节大容量磁盘和4K高级格式化盘是4096字节。系统把你的读请求映射到设备时最终还是要落到“从偏移多少字节开始读”。换算公式就一条字节偏移 扇区号 × 扇区大小比如要读第1024号扇区扇区大小512字节那么offset就是1024 × 512 524288。这里面有个容易搞混的点扇区号从0开始所以“第1个扇区”是0号扇区对应字节偏移0。有些文档还会说“第1扇区”但公式里跟着的编号是offset别在边界上差一整个扇区。实际写代码时我习惯把“要读的扇区号”直接放进变量然后用(off_t)sector * sector_size避免手算和字符串拼接带来的低级错误。另外还有CHS和LBA的差别。CHS柱面-磁头-扇区是老式寻址方式现在几乎不在应用层用了LBA逻辑块地址把磁盘看作一个连续的块数组从0开始编号。实验里用LBA就完了不要被一些老资料带偏。1.3 为什么建议用“磁盘镜像文件”而不是真设备测很多同学拿到题目第一反应是打开/dev/sda实际上在普通机器上你要么没权限需要root要么担心误操作把系统盘搞坏。虽然题目只要求读取但一旦把路径写错或者偏移算错很容易把磁盘搞乱尤其后续可能还会补“写”的部分。我的做法是先创建一个小镜像文件在文件里模拟磁盘布局然后对这个镜像文件做全部实验。镜像文件大小可以设成比如32MB里面自己写点标识数据。这样做的好处有三个权限随便给普通用户都能open数据随便搞读错了、写穿了都不会影响宿主机可以用hexdump之类工具交叉验证读到的内容。如果一定要用真设备也要挑一块空的U盘或者虚拟机里的虚拟盘不要在系统盘上跑。在虚拟机里挂一块新的虚拟磁盘是最稳的配合sudo访问/dev/vdb这类设备节点。2. 核心实现pread一条龙读取指定扇区2.1 为什么不用lseekread很多实验指导书喜欢写成open之后lseek到目标偏移然后read。这段模式在新手看来很直观但有两个毛病。一个是lseek和read是两个系统调用中间一旦有别的代码比如信号处理、多线程环境下的另一个线程也操作同一个文件描述符移动了文件偏移read就会读到错误位置。对单线程作业来说问题不大但它是一个坏习惯。另一个是每读一个扇区都要先lseek再read如果要连续扫很多扇区每次都两次系统调用性能白白浪费。pread这个接口就是专门解决这个问题的它把偏移和读取合并成一个调用并且不修改文件描述符的当前位置。ssize_t pread(int fd, void *buf, size_t count, off_t offset);它读取的内容等同于lseekread但操作是原子的也不改变fd的当前偏移。只要你指定count和offset内核直接给你搬数据。对于这种实验用pread就够了。2.2 最小可用的扇区读取代码下面这段代码是我实验里最终使用的版本目标是给定设备或镜像路径、起始扇区号、要读的扇区数量按十六进制打印每个扇区的内容。#include stdio.h #include stdlib.h #include fcntl.h #include unistd.h #include stdint.h #define SECTOR_SIZE 512 #define BYTES_PER_LINE 16 int main(int argc, char *argv[]) { if (argc ! 4) { fprintf(stderr, usage: %s device_or_image start_sector sector_count\n, argv[0]); return 1; } const char *path argv[1]; unsigned long long start strtoull(argv[2], NULL, 0); unsigned long count strtoul(argv[3], NULL, 0); int fd open(path, O_RDONLY); if (fd 0) { perror(open); return 1; } unsigned char buf[SECTOR_SIZE]; for (unsigned long i 0; i count; i) { unsigned long long sector_no start i; off_t offset (off_t)(sector_no * SECTOR_SIZE); ssize_t n pread(fd, buf, SECTOR_SIZE, offset); if (n 0) { perror(pread); break; } if (n 0) { printf(sector %llu: EOF, no more data\n, sector_no); break; } printf(---- sector %llu (offset 0x%llx) ----\n, sector_no, (unsigned long long)offset); for (ssize_t j 0; j n; j) { printf(%02x , buf[j]); if ((j 1) % BYTES_PER_LINE 0) { printf(\n); } } if (n % BYTES_PER_LINE ! 0) { printf(\n); } } close(fd); return 0; }从代码角度解释几个关键点。open用O_RDONLY因为这次实验只读。如果后面要做“写”的部分要追加O_WRONLY或者O_RDWR并且要考虑是否加O_DIRECT。O_DIRECT在这种场景下不是必需的后面会专门说缓存问题。offset类型是off_t严格来说是带符号的64位类型。我写(off_t)(sector_no * SECTOR_SIZE)有两个用意先让乘法发生在更宽的整数域避免32位整数溢出再显式转成off_t让编译器知道这个值可能超过int范围。在64位平台上sector_no是unsigned long long乘法结果不会溢出这个写法够用。pread的返回值是实际读到的字节数。对普通文件来说如果你请求512字节读到文件末尾时可能返回不足512字节对设备节点来说一般要么读满要么出错。代码里判断n0就是防止在镜像文件末尾读取时看到一堆空数据还以为是bug。2.3 一个进阶版本按字节偏移读取任意长度做完扇区级读取后你会发现更顺手的工具其实是“读取任意字节偏移”。这个能力在做文件系统手工解析时很有用。比如你想读一个FAT32镜像的DBR引导扇区它就在0号扇区你想读根目录区按照BPB里的参数算出偏移后直接按字节偏移读一段长度就行。进阶版本只需要把参数从“扇区号”换成“偏移量长度”#include stdio.h #include stdlib.h #include fcntl.h #include unistd.h #include stdint.h #include sys/stat.h int main(int argc, char *argv[]) { if (argc ! 4) { fprintf(stderr, usage: %s file byte_offset length\n, argv[0]); return 1; } const char *path argv[1]; off_t offset (off_t)strtoull(argv[2], NULL, 0); size_t length (size_t)strtoull(argv[3], NULL, 0); int fd open(path, O_RDONLY); if (fd 0) { perror(open); return 1; } unsigned char *buf malloc(length); if (buf NULL) { perror(malloc); close(fd); return 1; } ssize_t n pread(fd, buf, length, offset); if (n 0) { perror(pread); free(buf); close(fd); return 1; } for (ssize_t i 0; i n; i) { printf(%02x , buf[i]); if ((i 1) % 16 0) { printf(\n); } } if (n % 16 ! 0) { printf(\n); } printf(read %zd bytes at offset 0x%llx\n, n, (unsigned long long)offset); free(buf); close(fd); return 0; }这个版本删掉了“扇区”概念直接暴露字节偏移。你可以先用它验证自己的偏移计算再配合扇区版本确认边界对齐。两个版本一对照你会发现扇区只是一个“以512字节为单位的偏移折算”真正的底层操作对象就是字节范围。3. 让读取结果“可读”从十六进制到有意义的信息3.1 文本探针跨过文件系统直接找字符串直接把磁盘区块打印成十六进制实验就算完成了但很多同学做完依然一头雾水这堆数字到底证明了什么我建议做一个小验证在镜像文件里预先写入一个人类可读的文本串然后通过读取某个特定偏移把这个字符串从十六进制中还原出来。比如你写镜像时把字符串HELLO_DISK_12345放在偏移0x1000处然后运行读取程序在输出里找到对应的ASCII。看到48 45 4C 4C 4F 5F这样的字节还原成HELLO你对“磁盘上的数据就是原始字节序列”的理解会瞬间变得扎实。怎么预写数据用dd命令最直接echo -n HELLO_DISK_12345 | dd oftest.img bs1 seek$((0x1000)) convnotrunc这里dd的seek是字节偏移convnotrunc保证不会截断文件。之后再用读取程序去读偏移0x1000长度为16字节就能看到对应的ASCII码。这个自测方法比对着书本输出说“程序跑通了”有说服力得多。3.2 试一圈FAT32解析把“指定位置”用在真实磁盘结构上如果你想把这个实验拔高一点强烈推荐拿FAT32镜像试一圈。FAT32是可视化程度很高的文件系统结构规整用我们写的读取工具就能一层层剥开。基本思路是读0号扇区DBR解析BPB参数拿到每扇区字节数、每簇扇区数、保留扇区数、FAT表个数、FAT表大小、根目录起始簇号等信息。根据保留扇区数和FAT表大小计算FAT1、FAT2和根目录区的偏移。读根目录区的目录项每一项32字节其中文件名、扩展名、起始簇号、文件大小都按固定偏移排列。继续读该文件的起始簇号对应的簇然后到数据区读第一簇内容文件内容就出来了。FAT32里各种字段的位置表偏移0x0B每扇区字节数2字节偏移0x0D每簇扇区数1字节偏移0x0E保留扇区数2字节偏移0x10FAT表个数1字节偏移0x24FAT表大小4字节目录项的起始簇号在偏移0x14低2字节为簇号低位偏移0x1A为簇号高位共4字节。这些位置关系自己用读取程序扒一遍比看十遍文档都记得牢。你会发现前面写的“按字节偏移读取任意长度”工具在这种场景下就是一把万能钥匙先用扇区工具看DBR再按字节偏移读FAT表再读根目录每一步都在验证“偏移-内容”的映射关系。3.3 边界对齐问题为什么最好按扇区边界读在解析FAT32时我踩过一个坑偏移没有对齐到扇区边界读出来的内容本身是对的但底层I/O效率会受影响。Linux内核处理块设备I/O时会把请求合并、对齐到设备的逻辑块。如果你每次pread的长度或偏移不齐内核可能要拆成多次底层I/O有些设备驱动甚至会拒绝不对齐请求。这个实验只要求读取所以结果一定是对的但如果扩展到真实的高性能读写场景通常建议偏移对齐到扇区大小长度是扇区大小的整数倍缓冲区也尽量用对齐分配的方式申请内存比如posix_memalign。我在虚拟化环境里做过一次对比测试用非对齐长度读取速度大概会下降一截底层还会打warning级别日志。实验不做性能优化也可以但理解这个机制会让你对块设备的“最小寻址单位”有更深的认识。4. 调试与踩坑实录4.1 权限不够直接读/dev/sda报错怎么办第一次做实验很多人会直接open(/dev/sda)然后perror打印出Permission denied。Linux对块设备节点的访问默认要求root权限或者相应组权限。解决方式用sudo运行程序这是最简单的方式把当前用户加入disk组但不建议为做实验去改系统组权限更建议直接用磁盘镜像文件绕开权限问题。如果用虚拟机还要注意设备节点是否存在。虚拟化环境的控制器不同磁盘节点可能是/dev/sda也可能是/dev/nvme0n1如果没看到设备节点可能是内核模块和虚拟控制器类型不匹配。这个跟实验本身无关但排查起来确实容易让人怀疑人生。4.2 扇区大小不是512怎么办我有个朋友在做实验时直接把扇区大小写死成512拿一块4K高级格式化盘测试结果所有扇区内容都错位。现代“先进格式化”磁盘的逻辑扇区可以配置成4096字节但接口上仍会曝光512字节逻辑扇区来兼容老系统也就是常说的512e模式和真正的4Kn设备不一样。通用做法是在Linux里用ioctl去读设备的逻辑扇区大小int sector_size 512; ioctl(fd, BLKSSZGET, sector_size);返回值就是该设备的逻辑扇区字节数。对镜像文件来说ioctl不一定支持所以用512作为默认值仍然常见但你要清楚设备实际扇区可能不是512一旦读到的数据整体错位优先怀疑这个参数。4.3 读到的数据“是旧的”page cache的坑Linux对所有块设备I/O默认走page cache。第一次pread某个扇区内核会从磁盘读出来并缓存之后即使磁盘内容变了你再pread同一位置读到的可能还是缓存里的旧数据。实验里通常不会遇到这个问题因为没人会频繁改写磁盘。但如果你后面要做“读取刚写入的内容”这种验证就要考虑打开时加O_DIRECT标志绕过page cache但要求对齐入门阶段容易伤脑筋或者读之前调用posix_fadvise(fd, offset, len, POSIX_FADV_DONTNEED)丢弃缓存或者关闭后重新打开fd不过cache还是那层cache重启最保底。我在做镜像文件实验时因为经常用dd改数据再读被旧缓存坑过一次。后来形成流程每次改完镜像先重新执行一遍读取程序确认读到的是最新数据再做解析。4.4 字节序与“越界”恐惧为什么不要用struct直接读有的同学喜欢把读回来的缓冲区cast成结构体比如(struct DBR *)buf然后直接访问字段。这在x86小端机器上大多数时候能用但一旦涉及跨平台或者解析不是按C结构体对齐的数据就会翻车。磁盘上的格式是“字节序列”不是“C结构体”。MBR分区表里存的分区偏移是little-endian的多字节整型如果你按big endian去解释数值完全不对。我的建议是读取和解析分离。读取程序负责把原始字节搬回来用一个独立的解析函数手动拼多字节字段类似uint32_t le32(const unsigned char *p) { return (uint32_t)p[0] | ((uint32_t)p[1] 8) | ((uint32_t)p[2] 16) | ((uint32_t)p[3] 24); }这样后面想支持其它文件系统格式只需要加解析逻辑不用重写读盘部分。这种“读盘与解码解耦”的设计思想在真实存储工具里也是最常见的模式。4.5 越界读和短读怎么防pread一个越界偏移通常不会导致进程崩溃返回-1或者读到一个短长度但如果你在循环里没判断返回值把未初始化内存当数据打印什么怪事都可能发生。所以代码里一定要对offset做上界判断比如先fstat获取文件大小如果offset size就报错对返回值做短读处理没读满就补读或者break。块设备节点的fstat大小通常是0这时无法用文件大小判断只能靠设备的容量信息。对镜像文件来说fstat非常管用建议不管目标是不是设备节点都先查一下 size至少在普通文件场景能拦住大部分手滑。5. 常见问题速查表与复现流程5.1 一张表兜底常见故障把实验里最容易撞见的现象和解决办法收拢成一张表现象可能原因排解思路open返回Permission denied普通用户访问块设备节点无权限用sudo或改用镜像文件pread返回-1 errnoEINVAL偏移或长度不对齐对齐到扇区大小检查设备是否支持该请求pread返回-1 errnoENXIO偏移超出设备容量用fstat或设备容量信息校验偏移返回0字节镜像文件读到了末尾确认扇区号加扇区大小没有超过文件大小读到的数据和磁盘实际内容不一致page cache旧数据重新打开再读或加O_DIRECT或用posix_fadvise丢缓存十六进制里找不到期望的ASCII偏移计算错误或字节序解释错误用字节偏移版本工具逐段定位hexdump对照设备节点存在但open成功pread报Input/output error设备故障或接口参数错误换镜像文件方式确认代码逻辑再回头查设备程序在循环里打印出乱码缓冲区未清干净或短读未处理每次pread前memset缓冲区判断ncount再处理多字节数值明显不对端序解释错误用le32/le16函数手动拼装不要直接cast结构体这张表是给临时查用。真正跑实验时优先复现的最小路径是镜像文件 pread 按扇区读 hexdump对照。5.2 实验复现的标准流程我推荐按下面这套顺序把实验完整复现一遍顺着走完你对“读磁盘指定位置”的理解会非常扎实。1. 创建镜像文件大小自定建议32MB或64MB。 dd if/dev/zero oftest.img bs1M count32 2. 用echodd在指定偏移写入标记字符串验证定位。 echo -n HELLO_DISK_12345 | dd oftest.img bs1 seek$((0x1000)) convnotrunc 3. 用字节偏移版本工具读取偏移0x1000长度16确认输出与写入内容一致。 4. 用扇区版本工具读取0号扇区确认前512字节全是0x00因为初始镜像全零。 5. 手动写入一个MBR签名在偏移510和511处写55 AA再用工具读取0号扇区验证。 6. 如果做FAT32解析扩展用mkfs.fat格式化镜像然后从DBR到根目录逐层读取。每一步都有明确的“输入-输出-预期”任何一步对不上都可以快速定位是偏移计算问题、工具问题还是对文件系统结构的理解出了问题。6. 最后聊聊实验做完后的几个体会这个实验做完之后我最大的感受是不要只盯着API先问自己“我要读的位置在磁盘的地址空间里到底对应哪个字节”。如果这个心智模型建立了后面不管是用pread、dd还是写驱动都是一样的换算逻辑。我个人的习惯是先写一个最简版本用dd和hexdump做交叉验证确认偏移计算、输出格式都没问题再往里面加解析逻辑。别一上来就写完整的FAT32解析器拆一半卡住会非常难受。先读0扇区对着文档看DBR字段再读下一块区域每块都能对上整个实验就是水到渠成的事。另外分享一个小细节代码里的常量比如扇区大小、每行字节数尽量用宏或者const收拢在一起后面调试的时候你会疯狂感谢这个选择。我最初把512写死在打印循环里后来改成用ioctl动态获取再后来又为了镜像文件保留默认值这个演进过程恰好也对应了“从读扇区到读任意偏移”的能力升级。如果时间充裕强烈建议把FAT32解析那一节做个延伸先读DBR再读FAT表再读一个真实文件的目录项和簇链你会对“文件是怎么存储在磁盘上的”有一个从抽象到具体的完整体验。这种体验纯看书很难获得。做完这步再回头看文件系统相关的后续内容你会觉得顺很多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询