概要
我打算用C++写一个direct IO的demo程序,运行的时候报了个错误:EINVAL,于是用trace工具深入内核源码看看这个错误是怎么报出来的,最终修改好demo程序。下面展示了这个过程。
原始direct IO实例代码
#include <cstring>
#include <fcntl.h>
#include <format>
#include <iostream>
#include <unistd.h>
#ifndef _GNU_SOURCE
#error "NO GNU SOURCE defined\n"
#endif
int main(int argc, char *argv[]) {
const char *file_name{"/mnt/ext4/hello.txt"};
int fd = open(file_name, O_DIRECT | O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
std::cerr << std::format("failed to open file: {}, error: {}\n", file_name,
std::strerror(errno));
return 1;
}
std::string file_content{"hello world\n"};
if (write(fd, file_content.data(), file_content.size()) < 0) {
std::cerr << std::format("failed to write file: {}, error: {}\n", file_name,
std::strerror(errno));
}
close(fd);
return 0;
}
运行结果
[root@zpol82 cpp_progs]# ./direct
failed to write file: /mnt/ext4/hello.txt, error: Invalid argument
可以看到在ext4文件系统上面运行出错了。
Ivalid argument意味着错误码是EINVAL。
如果在xfs上运行,也是这样的结果;如果在tmpfs或者btrfs上面运行,则不会报错。说明这个报错是文件系统层报出来的。
分析出错原因
初步分析
如果你很有经验,应该会看出,这个错误是由于buf缓冲区没有内存对齐导致的。
这点在open()的man page有说明:
In Linux 2.4, most filesystems based on block devices require that the file offset and the length and memory address of all I/O segments be multiples of the filesystem block
size (typically 4096 bytes).
In Linux 2.6.0, this was relaxed to the logical block size of the block device (typically 512 bytes). A block device’s logical block size can be determined using the ioctl(2)
BLKSSZGET operation or from the shell using the command: blockdev --getss
简单概括,就是要求buf缓冲区是512字节对齐。
但是呢,文档的描述是描述,纸上得来终觉浅,也没法把这个要求和运行出错代码强关联起来,那我们能否亲眼追查到其中的因果关系呢?
下面我们来trace一把上面的报错。
trace这个write()
调用究竟中哪里出错了
最简单的trace办法是使用trace-cmd工具,如果没有,dnf install一下就可以拥有了。
安装完后,使用办法如下:
# 列出可trace函数
[root@zpol82 cpp_progs]# trace-cmd list -f sys_write
__x64_sys_writev
__ia32_sys_writev
__ia32_compat_sys_writev
ksys_write
__x64_sys_write # <-----这个就是write()调用在x64平台上的实现
__ia32_sys_write
proc_sys_write
drm_fb_helper_sys_write [drm_kms_helper]
# 只trace上面实例代码中的write()调用
[root@zpol82 cpp_progs]# trace-cmd record -p function_graph -g __x64_sys_write ./direct
plugin 'function_graph'
failed to write file: /mnt/ext4/hello.txt, error: Invalid argument
CPU0 data recorded at offset=0x60a000
4096 bytes in size
...
# 打印trace结果
[root@zpol82 cpp_progs]# trace-cmd report > direct.trace
trace-cmd的输出
可以看到write()调用经过一连串函数,来到ext4_direct_IO(),其实就是ext4_direct_IO_write(),在这个函数中,它调用完毕__blockdev_direct_IO()
(请使用宽屏浏览)
direct-8330 [002] 1976.840109: funcgraph_exit: 1.591 us | }
direct-8330 [002] 1976.840109: funcgraph_entry: 0.334 us | __blockdev_direct_IO(); // 这个函数调用完成
direct-8330 [002] 1976.840110: funcgraph_entry: | wake_up_bit() {
direct-8330 [002] 1976.840110: funcgraph_entry: 0.045 us | __wake_up_bit();
direct-8330 [002] 1976.840110: funcgraph_exit: 0.223 us | }
direct-8330 [002] 1976.840110: funcgraph_entry: | down_write() {
direct-8330 [002] 1976.840110: funcgraph_entry: | _cond_resched() {
direct-8330 [002] 1976.840110: funcgraph_entry: 0.029 us | rcu_all_qs();
direct-8330 [002] 1976.840111: funcgraph_exit: 0.212 us | }
direct-8330 [002] 1976.840111: funcgraph_exit: 0.398 us | }
direct-8330 [002] 1976.840111: funcgraph_entry: | truncate_inode_pages() {
direct-8330 [002] 1976.840111: funcgraph_entry: 0.055 us | truncate_inode_pages_range();
direct-8330 [002] 1976.840111: funcgraph_exit: 0.232 us | }
direct-8330 [002] 1976.840111: funcgraph_entry: | ext4_truncate() { // 进入了这个函数
direct-8330 [002] 1976.840111: funcgraph_entry: 0.034 us | ext4_can_truncate();
direct-8330 [002] 1976.840112: funcgraph_entry: | ext4_writepage_trans_blocks() {
direct-8330 [002] 1976.840112: funcgraph_entry: 0.029 us | jbd2_journal_blocks_per_page();
direct-8330 [002] 1976.840112: funcgraph_entry: | ext4_meta_trans_blocks() {
direct-8330 [002] 1976.840112: funcgraph_entry: 0.036 us | ext4_ext_index_trans_blocks();
direct-8330 [002] 1976.840112: funcgraph_exit: 0.223 us | }
direct-8330 [002] 1976.840112: funcgraph_exit: 0.614 us | }
相关内核代码
对照一下内核中的ext4_direct_IO_write()的源码:
ext4_direct_IO_write() {
...
ret = __blockdev_direct_IO(iocb, inode, inode->i_sb->s_bdev, iter,
get_block_func, ext4_end_io_dio, NULL,
dio_flags);
if (ret > 0 && !overwrite && ext4_test_inode_state(inode,
EXT4_STATE_DIO_UNWRITTEN)) {
int err;
/*
* for non AIO case, since the IO is already
* completed, we could do the conversion right here
*/
err = ext4_convert_unwritten_extents(NULL, inode,
offset, ret);
if (err < 0)
ret = err;
ext4_clear_inode_state(inode, EXT4_STATE_DIO_UNWRITTEN);
}
inode_dio_end(inode);
/* take i_mutex locking again if we do a ovewrite dio */
if (overwrite)
inode_lock(inode);
if (ret < 0 && final_size > inode->i_size) // 结合trace输出可知本条件为真
ext4_truncate_failed_write(inode);
...
}
从trace的输出来看,运行的时候执行了ext4_truncate(),而它是从ext4_truncate_failed_write()调用过去的,所以证明推断它上面的if分支条件为真,所以得出ret < 0
。
这个ret就是最终的返回码,结合程序运行出错信息,我们知道这个值是EINVAL。
往上查代码,ret 变量是__blockdev_direct_IO()的返回值,所以我们查这个函数在什么情况返回EINVAL即可。
再翻翻__blockdev_direct_IO()源码,可知它实际调用的是do_blockdev_direct_IO(),我们看后者即可,见下面注释
static inline ssize_t
do_blockdev_direct_IO(struct kiocb *iocb, struct inode *inode,
struct block_device *bdev, struct iov_iter *iter,
get_block_t get_block, dio_iodone_t end_io,
dio_submit_t submit_io, int flags)
{
unsigned i_blkbits = READ_ONCE(inode->i_blkbits);
unsigned blkbits = i_blkbits;
unsigned blocksize_mask = (1 << blkbits) - 1;
ssize_t retval = -EINVAL; // 看这里
const size_t count = iov_iter_count(iter);
loff_t offset = iocb->ki_pos;
const loff_t end = offset + count;
struct dio *dio;
struct dio_submit sdio = { 0, };
struct buffer_head map_bh = { 0, };
struct blk_plug plug;
unsigned long align = offset | iov_iter_alignment(iter);
/*
* Avoid references to bdev if not absolutely needed to give
* the early prefetch in the caller enough time.
*/
if (align & blocksize_mask) {
if (bdev)
blkbits = blksize_bits(bdev_logical_block_size(bdev));
blocksize_mask = (1 << blkbits) - 1;
if (align & blocksize_mask)
goto out; // 应该是从这里返回了,错误码是EINVAL
}
...
}
查到这里,原因就比较清晰了,就是内存对齐的问题了。
具体说来,它要求write()系统调用中用到的buf的offset和len都必须是bdev_logical_block_size()的倍数,那这个值究竟是多少呢?
如man page所说,这个值可以使用blockdev --getss
命令查询FS下面的裸设备而得到。
我们不妨来查查看。
查看设备的logical_block_size
root@zpol82 cpp_progs]# blockdev --getss /dev/vgdata/lv1
512
原来是512,既然知道了ext4的要求是512字节对齐,那么我们可以修改代码了。
另外,网上还有很多文章说这个对齐要求是page_size,即4096,其实这点man page也提到过,这是比较老的要求了。
修改后的代码
#include <cstring>
#include <fcntl.h>
#include <format>
#include <iostream>
#include <unistd.h>
#ifndef _GNU_SOURCE
#error "NO GNU SOURCE defined\n"
#endif
int main(int argc, char *argv[]) {
const char *file_name{"/mnt/ext4/hello.txt"};
int fd = open(file_name, O_DIRECT | O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
std::cerr << std::format("failed to open file: {}, error: {}\n", file_name,
std::strerror(errno));
return 1;
}
// 准备direct IO的buffer
#define SECTORSIZE 512
alignas(512) char buf[SECTORSIZE];
bzero(buf, SECTORSIZE);
// 开始direct IO
std::string file_content{"hello world\n"};
strcpy(buf, file_content.data());
if (write(fd, buf, SECTORSIZE) < 0) {
std::cerr << std::format("failed to write file: {}, error: {}\n", file_name,
std::strerror(errno));
goto out;
}
// 把文件大小修改为真实的逻辑大小
if (ftruncate(fd, file_content.size()) < 0) {
std::cerr << std::format("failed to ftruncate file: {}, error: {}\n",
file_name, std::strerror(errno));
}
out:
close(fd);
return 0;
}
代码运行通过
补充信息
- 本文所涉的内核代码版本是:oraclelinux8.9 所带4.18.0-513.24.1.el8_9.x86_64
- 本文所涉用户态代码运行环境是oraclelinux8.9,编译环境是g++ 13版本并使用C++20标准。
- 理论上oraclelinux8.9和centos stream8.9是一样的,但未验证。