我有一个运行 CentOS 7.4.1708 的系统,uname 报告内核为 3.10.0-693。我需要分析磁盘性能的某些方面,因此我编写了一个基本的 C 程序,该程序将使用 POSIX unistd 库中的函数打开磁盘设备路径并以随机偏移量执行一定大小的读取。
代码的基本结构如下:
int fd = open(devicePath, O_RDONLY | O_DIRECT);
while(1){
lseek(fd, offsetToSeekTo, SEEK_SET)
read(fd, readBuffer, numBytesToRead)
}
请注意,这使用了 O_DIRECT 标志,因此我已经处理了 readBuffer 的内存对齐,此代码问题的搜索在 512 字节偏移处对齐,并且 numBytesToRead 始终是 512 的倍数。被访问的设备的逻辑块大小为 512 字节,通过使用带有 BLKSSZGET 标志的 ioctl 函数获得。
我一直在使用启用了扩展磁盘属性的 iostat 来监控各种详细信息,包括平均请求大小(例如“avgrq-sz”)。此字段的单位是逻辑块,例如本例中的单位是 512 字节。
当我使用 numBytesToRead 作为 131072(例如 256 个扇区,每个扇区 512 字节)运行测试时,内核为设备发出的读取平均请求大小为 256 个扇区,例如内核向设备发出的读取大小与我的程序请求的相同。
当我的程序在一次读取中请求 131584 字节(例如 257 个扇区,每个扇区 512 字节)时,内核显示的设备平均请求大小为 128.5,这表明 256 个扇区/128KB 是内核在一次请求中可以从设备读取的最大数据量。我的程序可能针对 257 个扇区发出一个读取请求,但内核需要通过将其分成 2 次读取来处理,因为内核在一次操作中只能从设备读取 256 个扇区。
类似地,如果我的程序要求读取 261632 字节/511 个扇区,则平均请求大小显示为 255.5 个扇区。到目前为止,一切顺利,没有意外。
意想不到的是,当我的程序执行 256KB/512 个扇区的读取时,发生了这种情况。在这种情况下,Linux 内核发出 3 次磁盘设备读取,而请求的数据量可以放入 2 个请求中,每个请求 256 个块。iostat 输出显示平均请求大小为 170.67 个扇区。
我的问题是,为什么读 3 次?有人能提供一些见解,看看这是否是出于某种原因所期望的行为吗?
编辑:
我一直在使用 systemtap 进行调试。到目前为止,我可以看到,在我的程序调用 read(2) 之后,它最终进入内核的“submit_page_section@fs/direct-io.c”
反过来,该函数会针对 read(2) 所覆盖的每个 4096 字节范围调用“dio_send_cur_page@fs/direct-io.c”。一旦 dio 请求达到一定大小,“dio_bio_submit@fs/direct-io.c”函数会将其传递给“submit_bio@block/blk-core.c”,后者会将其传递给 generic_make_request,后者将通过块设备驱动程序执行 IO。submit_bio 函数会传递一个“struct bio”,其中包括“bi_sector”,用于跟踪 IO 请求的起始扇区。在我的系统上发出 128KB read(2) 时,submit_bio 只会被调用一次,其中 bio->bi_sector 设置为与我用 lseek 设置的偏移量相关联的起始扇区。发出 256KB read(2) 时,submit_bio 会被调用三次。第一个调用将 bio->bi_sector 设置为我上次用 lseek 设置的位置,随后的 submit_bio 调用将 bio->bi_sector 设置为高 255,然后最后一个 submit_bio 调用将 bio->bi sector 设置为高 256。换句话说,第一个 submit_bio 读取 255 个扇区,第二个读取 256 个扇区,最后一个读取 1 个扇区。因此,内核确实发出了 3 个读取请求,而本来应该在 2 个读取请求中完成。
我需要进行更多调试以了解为什么会出现这种情况,并查看是否已在较新的内核中修复了此问题。完成调试后,我将更新此内容。
答案1
在问题中,我提到我根据 512 字节的逻辑扇区大小对齐了内存、寻道和有效负载大小。我没有意识到直接 IO 以逻辑扇区大小为单位工作,但使用页面作为 IO 的内存单位。例如,IO 可以以逻辑扇区大小的倍数发生,并且这些扇区以页面的形式存储在内存中,每个页面包含多个扇区。在我的系统上,默认情况下,我的页面大小为 8 * 扇区大小 (512B) = 4KB。
为了创建问题中提到的 readBuffer,我最初是这样做的:
long sectorSizeBytes;
ioctl(fd, BLKSSZGET, sectorSizeBytes);
char *readBuffer = memalign(sectorSizeBytes, maxReadSizeInSectors * sectorSizeBytes);
这会使 readBuffer 地址对齐,使得地址 % sectorSizeBytes == 0。这对于 DMA/direct IO 是必需的,但不能保证良好的性能。
我应该做的是按页面大小对齐缓冲区。例如:
long pageSize = sysconf(_SC_PAGESIZE);
long sectorSizeBytes;
ioctl(fd, BLKSSZGET, sectorSizeBytes);
char *readBuffer = memalign(pageSizeBytes, maxReadSizeInSectors * sectorSizeBytes);
通过这种方式创建 readBuffer,内核能够执行 2 次 128KB 的磁盘读取,以满足 256KB 的读取(2)请求。