使用trace-cmd跟踪directIO的调用过程

概要

我打算用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;
}

代码运行通过

补充信息

  1. 本文所涉的内核代码版本是:oraclelinux8.9 所带4.18.0-513.24.1.el8_9.x86_64
  2. 本文所涉用户态代码运行环境是oraclelinux8.9,编译环境是g++ 13版本并使用C++20标准。
  3. 理论上oraclelinux8.9和centos stream8.9是一样的,但未验证。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值