Linux C语言实现硬盘读写性能测试:完整代码与速度计算

发布时间:2026/10/17 2:46:18
Linux C语言实现硬盘读写性能测试:完整代码与速度计算 一、前言在 Linux 系统开发、嵌入式软件测试以及存储设备验证过程中硬盘读写性能是一项重要的测试指标。无论是 SATA 硬盘、SSD、NVMe 固态硬盘还是嵌入式设备中的 eMMC、SD 卡都可以通过文件读写测试对存储系统的基本性能进行评估。在 Linux 环境下可以使用dd、fio等工具进行磁盘性能测试也可以使用 C 语言编写测试程序将文件创建、数据写入、数据读取、计时和结果统计集成到自己的测试流程中。使用 C 语言实现测试程序的主要优势包括可以自定义测试文件大小和数据块大小。可以控制文件打开方式、写入流程和数据同步策略。可以将性能测试集成到自动化测试程序中。可以统一输出测试日志和 PASS/FAIL 结果。可以根据实际需求扩展循环测试、异常处理和长时间稳定性测试。本文以 Linux 系统为例使用标准 C 文件接口和 Linux 系统调用实现一个基础的文件读写性能测试程序并介绍读写速度的计算方法及测试结果的注意事项。需要说明的是本文测量的是文件系统层面的读写性能。由于操作系统缓存、文件系统实现和存储设备自身缓存等因素的影响测试结果不一定等于存储介质的物理读写速度。二、测试原理2.1 测试流程整个测试分为写入测试和读取测试两个阶段。写入阶段创建测试文件。分配固定大小的内存缓冲区。将测试数据分块写入文件。累计实际写入字节数。调用fsync()请求同步文件数据。统计写入耗时并计算写入速度。读取阶段重新打开测试文件。将文件内容分块读取到内存缓冲区。累计实际读取字节数。检查读取过程中是否出现错误。统计读取耗时并计算读取速度。整体流程如下开始 | v 分配读写缓冲区 | v 创建测试文件 | v 循环写入测试数据 | v 检查写入结果 | v fsync() 同步文件数据 | v 计算写入速度 | v 重新打开文件 | v 循环读取文件数据 | v 计算读取速度 | v 输出测试结果 | v 释放资源并结束2.2 读写速度计算公式文件读写速度可以通过实际完成的数据量除以对应的耗时计算。VSTV\frac{S}{T}其中VV读写吞吐量。SS实际完成的字节数。TT对应操作的耗时单位为秒。如果使用 MiB/s 作为单位则VMiB/sST×1024×1024V_{\mathrm{MiB/s}} \frac{S}{T\times 1024\times 1024}例如实际写入 512 MiB耗时 4 秒则平均写入速度为V5124128 MiB/sV\frac{512}{4}128\ \mathrm{MiB/s}这里需要区分 MB 和 MiB1 MB 1,000,000 字节。1 MiB 1,048,576 字节。如果测试程序使用1024 * 1024进行单位换算输出时应使用 MiB/s而不是严格意义上的 MB/s。2.3 为什么使用CLOCK_MONOTONICLinux 下可以使用clock_gettime()获取时间。clock_gettime(CLOCK_MONOTONIC, ts);CLOCK_MONOTONIC用于测量单调递增的时间间隔不会因为系统日历时间被调整而产生普通墙上时钟那样的跳变因此适合用于性能测试。测试程序应使用同一个单调时钟记录操作开始时间和结束时间再计算两者之间的差值。2.4 为什么需要fsync()调用fwrite()或write()后数据可能暂时停留在操作系统的页缓存中。程序完成写入调用并不意味着所有数据已经同步到存储设备。在写入测试中调用fsync(fd);可以请求将文件数据及相关元数据同步到存储设备并等待操作完成。因此本文将写入耗时定义为“写入数据并完成fsync()同步所需的总耗时”。这与只测量写入系统调用的耗时不同。即使fsync()成功底层设备和硬件缓存的具体持久化行为仍取决于设备及其配置。三、完整 C 语言测试代码下面的程序适用于 Linux 环境可用于 x86-64 和 aarch64 Linux 系统。编译工具链、C 库和内核需要提供相应接口。程序默认创建一个 512 MiB 的测试文件每次读写 1 MiB 数据。主要功能可配置测试文件大小和数据块大小。检查每次写入和读取的实际字节数。使用fsync()测量包含数据同步的写入耗时。使用CLOCK_MONOTONIC进行计时。使用stat()检查文件实际大小。通过数据校验确认读取内容与写入内容一致。输出读写速度和最终测试结果。#define _POSIX_C_SOURCE 200809L #include stdio.h #include stdlib.h #include stdint.h #include string.h #include errno.h #include time.h #include fcntl.h #include unistd.h #include sys/stat.h #define TEST_FILE ./disk_test.bin #define BUFFER_SIZE (1024U * 1024U) #define TEST_FILE_SIZE (512ULL * 1024ULL * 1024ULL) static double get_time_sec(void) { struct timespec ts; if (clock_gettime(CLOCK_MONOTONIC, ts) ! 0) { perror(clock_gettime); exit(EXIT_FAILURE); } return (double)ts.tv_sec (double)ts.tv_nsec / 1000000000.0; } static void fill_buffer(unsigned char *buffer, size_t size, uint64_t offset) { for (size_t i 0; i size; i) { buffer[i] (unsigned char)((offset i) % 251); } } static int check_buffer(const unsigned char *buffer, size_t size, uint64_t offset) { for (size_t i 0; i size; i) { unsigned char expected (unsigned char)((offset i) % 251); if (buffer[i] ! expected) { fprintf(stderr, Data mismatch at offset %llu\n, (unsigned long long)(offset i)); return -1; } } return 0; } static int write_all(int fd, const unsigned char *buffer, size_t size, uint64_t *total_written) { size_t offset 0; while (offset size) { ssize_t n write(fd, buffer offset, size - offset); if (n 0) { if (errno EINTR) { continue; } perror(write); return -1; } if (n 0) { fprintf(stderr, write returned 0\n); return -1; } offset (size_t)n; *total_written (uint64_t)n; } return 0; } static int read_all(int fd, unsigned char *buffer, size_t size, uint64_t *total_read) { size_t offset 0; while (offset size) { ssize_t n read(fd, buffer offset, size - offset); if (n 0) { if (errno EINTR) { continue; } perror(read); return -1; } if (n 0) { fprintf(stderr, Unexpected EOF at offset %llu\n, (unsigned long long)*total_read); return -1; } offset (size_t)n; *total_read (uint64_t)n; } return 0; } int main(void) { int fd -1; int read_fd -1; unsigned char *write_buffer NULL; unsigned char *read_buffer NULL; uint64_t total_written 0; uint64_t total_read 0; uint64_t position 0; struct stat st; double write_start, write_end; double read_start, read_end; double write_elapsed, read_elapsed; int result EXIT_FAILURE; write_buffer malloc(BUFFER_SIZE); read_buffer malloc(BUFFER_SIZE); if (write_buffer NULL || read_buffer NULL) { perror(malloc); goto cleanup; } fd open(TEST_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (fd 0) { perror(open for write); goto cleanup; } write_start get_time_sec(); while (position TEST_FILE_SIZE) { uint64_t remaining TEST_FILE_SIZE - position; size_t chunk remaining BUFFER_SIZE ? (size_t)remaining : BUFFER_SIZE; fill_buffer(write_buffer, chunk, position); if (write_all(fd, write_buffer, chunk, total_written) ! 0) { goto cleanup; } position chunk; } if (fsync(fd) ! 0) { perror(fsync); goto cleanup; } write_end get_time_sec(); write_elapsed write_end - write_start; if (close(fd) ! 0) { fd -1; perror(close after write); goto cleanup; } fd -1; if (stat(TEST_FILE, st) ! 0) { perror(stat); goto cleanup; } printf( Write Test \n); printf(Expected size : %llu bytes\n, (unsigned long long)TEST_FILE_SIZE); printf(Written bytes : %llu bytes\n, (unsigned long long)total_written); printf(Actual size : %lld bytes\n, (long long)st.st_size); if (total_written ! TEST_FILE_SIZE || (uint64_t)st.st_size ! TEST_FILE_SIZE) { fprintf(stderr, Write size check FAILED\n); goto cleanup; } printf(Write time : %.6f s\n, write_elapsed); if (write_elapsed 0.0) { printf(Write speed : %.2f MiB/s\n, (double)total_written / (1024.0 * 1024.0 * write_elapsed)); } read_fd open(TEST_FILE, O_RDONLY); if (read_fd 0) { perror(open for read); goto cleanup; } read_start get_time_sec(); while (total_read TEST_FILE_SIZE) { uint64_t remaining TEST_FILE_SIZE - total_read; size_t chunk remaining BUFFER_SIZE ? (size_t)remaining : BUFFER_SIZE; if (read_all(read_fd, read_buffer, chunk, total_read) ! 0) { goto cleanup; } if (check_buffer(read_buffer, chunk, total_read - chunk) ! 0) { goto cleanup; } } read_end get_time_sec(); read_elapsed read_end - read_start; if (close(read_fd) ! 0) { read_fd -1; perror(close after read); goto cleanup; } read_fd -1; printf(\n Read Test \n); printf(Read bytes : %llu bytes\n, (unsigned long long)total_read); printf(Read time : %.6f s\n, read_elapsed); if (read_elapsed 0.0) { printf(Read speed : %.2f MiB/s\n, (double)total_read / (1024.0 * 1024.0 * read_elapsed)); } if (total_read ! TEST_FILE_SIZE) { fprintf(stderr, Read size check FAILED\n); goto cleanup; } printf(\nData integrity: PASS\n); printf(Disk I/O test : PASS\n); result EXIT_SUCCESS; cleanup: if (fd 0) { if (close(fd) ! 0) { perror(close during cleanup); } } if (read_fd 0) { if (close(read_fd) ! 0) { perror(close read_fd during cleanup); } } free(write_buffer); free(read_buffer); return result; }四、编译与运行将代码保存为disk_test.c。4.1 编译gcc -O2 -Wall -Wextra disk_test.c -o disk_test如果使用交叉编译工具链应将gcc替换为对应的交叉编译器。例如针对 Linux aarch64 的交叉编译环境aarch64-linux-gnu-gcc -O2 -Wall -Wextra \ disk_test.c -o disk_test交叉编译时需要确保目标系统的 C 库和运行环境兼容。4.2 运行./disk_test程序会在当前目录创建disk_test.bin。默认文件大小为 512 MiB因此运行前应确认当前目录所在文件系统有足够的可用空间和写入权限。注意程序使用O_TRUNC打开测试文件。如果同名文件已经存在原有内容会被清空。请在专用测试目录运行避免覆盖重要数据。五、如何理解测试结果假设程序输出如下 Write Test Expected size : 536870912 bytes Written bytes : 536870912 bytes Actual size : 536870912 bytes Write time : 4.000000 s Write speed : 128.00 MiB/s Read Test Read bytes : 536870912 bytes Read time : 1.500000 s Read speed : 341.33 MiB/s Data integrity: PASS Disk I/O test : PASS以上仅为演示数据不代表任何实际硬盘的性能结果。5.1 为什么写入速度和读取速度不同读写速度不一致并不一定说明硬盘存在故障常见原因包括存储设备本身的读写性能不同。文件写入过程中包含了fsync()同步等待。文件读取命中了 Linux 页缓存。测试文件大小不足以反映持续读写性能。系统负载、文件系统状态和设备缓存影响测试结果。因此在分析结果时应首先明确测试目标是文件系统吞吐量、包含同步等待的写入性能还是尽可能接近物理介质的读写性能。5.2 为什么读取速度可能特别高Linux 使用页缓存减少重复访问存储设备的开销。刚写入的文件可能仍有部分或全部数据保留在内存中。如果随后立即读取相同文件读取操作可能主要从内存完成因而速度明显高于存储介质本身的持续读取速度。因此本文程序得到的读取速度应理解为当前系统状态下的文件读取吞吐量而不能直接视为硬盘的真实物理读取带宽。5.3 为什么写入测试需要统计fsync()如果只统计write()调用的耗时操作系统可能仅将数据复制到页缓存就返回导致结果明显高于实际持久化写入性能。本文把fsync()放在写入计时区间内因此写入速度包含了同步等待时间。这是一种有明确测试定义的方法但并不代表所有存储测试都应该采用相同策略。比较不同设备时应确保同步策略一致。六、如何进一步提高测试准确性如果只是验证文件读写功能上面的程序已经包含了基本的字节数检查和数据完整性检查。如果要用于正式的硬盘性能测试建议继续改进以下方面。6.1 增加不同块大小的测试分别使用 4 KiB、64 KiB、256 KiB 和 1 MiB 等块大小进行测试。块大小会影响系统调用次数、缓存行为和实际吞吐量。不同块大小的测试结果可以帮助分析存储设备在不同访问模式下的表现。6.2 进行多轮测试单次测试可能受到后台任务、缓存和系统负载影响。建议进行多轮测试记录每轮的写入速度和读取速度并计算平均值、中位数及波动范围。6.3 区分缓存测试与存储设备测试如果目标是测量应用程序的常规文件访问性能应保留正常的操作系统缓存行为。如果目标是尽可能减少页缓存对结果的影响则需要设计单独的测试方案例如在 Linux 中评估直接 I/O。使用O_DIRECT时还必须满足缓冲区地址、偏移和长度的对齐要求并处理文件系统的具体限制。不应将不同缓存策略下的结果直接进行比较。6.4 关注测试文件大小如果测试文件太小结果可能主要反映内存缓存性能。测试文件大小应结合物理内存容量、设备容量和测试目的确定。对于嵌入式设备还应考虑可用存储空间避免测试文件占满系统分区或影响正常业务。6.5 区分顺序 I/O 和随机 I/O本文实现的是顺序文件读写测试。随机读写需要设计不同的访问位置和请求分布并分别统计吞吐量和 IOPS每秒输入输出操作次数。因此不能仅凭本文的顺序读写速度判断设备的随机读写性能。七、总结本文使用 Linux C 语言实现了一个基础的硬盘文件读写性能测试程序通过write()、read()、fsync()、stat()和clock_gettime()等接口实现了实际读写字节数统计、数据完整性校验以及读写吞吐量计算。进行存储性能测试时需要明确计时范围、同步策略、缓存状态和访问模式。只有在测试条件一致的前提下不同设备或不同配置下的结果才具有可比性。对于嵌入式 Linux 系统可以在此基础上进一步扩展块大小测试、多轮稳定性测试、随机 I/O 测试和自动化 PASS/FAIL 判定将基础文件读写程序逐步发展为完整的存储设备测试工具。

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询