LinuxでIO Errorをエミュレーション

アプリケーションを開発してるとエラー発生時の処理とか面倒なところですが、 特に一部のIO Errorって再現し辛くて、テストがおろそかになってました。

調べてみたら、Linuxのデバイスマッパーを使えば簡単にread/writeのIO Errorを再現できることが分かったので、簡単な手順をまとめました。

# やりたいこと

ファイルのオープン時ではなく、途中までread/writeしてからIO Errorを発生させたい。 (オープン時のエラーは、ファイルの代わりにディレクトリ置いとけば簡単にできるからね)

# 大まかな手順

手順の概要です。

  1. Readしたいファイルを作っておく
  2. ループバックデバイスで作ったファイルを/dev/loopXにマップする
  3. デバイスマッパーで/dev/loopAを/dev/mapper/xxxxにマップする
  4. なにかプログラムを書いて/dev/mapper/xxxxを読み込む

読み込み前提の順になっていますが、書き込みも同様の手順で可能です。 ですがこの手順だとファイルサイズに上限がありますので、上限エラーも試すことができます。

# 詳細な手順

# Readしたいファイルを作っておく

適当にファイル作りましょう。

$ dd if=/dev/zero of=TEMPFILE bs=512 count=20480
20480+0 records in
20480+0 records out
10485760 bytes (10 MB, 10 MiB) copied, 0.317796 s, 33.0 MB/s
$ ls -l
total 10240
-rw-r--r-- 1 yogi yogi 10485760 Jul  7 00:52 TEMPFILE

上記は/dev/zeroからとってるので0クリアされた10MBのファイルになります。 デバイスマッパーでのエラーエミュレーションはセクタ単位でしかできないので、512バイト以上にしておいたほうが良いです。 そうじゃないと最初のreadでえらーになっちゃいます。

# ループバックデバイスで/dev/loopXにマップ

$ sudo losetup -f TEMPFILE
$ losetup -l
NAME       SIZELIMIT OFFSET AUTOCLEAR RO BACK-FILE            DIO LOG-SEC
/dev/loop0         0      0         0  0 /home/user/TEMPFILE    0     512

/dev/loop?の空いてるデバイスファイルとしてマップされます。('?'の部分は数字になります) 上の例では/dev/loop0に割り当てられたようです。

この状態でも通常ファイルのようにread/writeできますが、デバイスファイルの扱いです、writeしても元ファイルサイズ以上には書き込めません。

# デバイスマッパーで/dev/mapper/xxxxにマップする

まずはマップファイルを用意。 テキストエディタで作ってもいいのですが、下の例ではcatコマンドに標準入力で手入力した例です。 最後まで入力したらCtrl-Dでファイルが出来上がります。

$ cat > MAPFILE
0 10240 linear /dev/loop0 0
10240 1 error
10241 10239 linear /dev/loop0 10241

作ったマップファイルを使って、デバイスマッパーのデバイスを作成します。

$ sudo dmsetup create errdev0 < MAPFILE
$ ls -l /dev/mapper
total 0
crw------- 1 root root 10, 236 Jul  6 14:31 control
lrwxrwxrwx 1 root root       7 Jul  7 01:03 errdev0 -> ../dm-0

errdev0の部分は任意の名前を指定できます。
必要に応じて、シンボリックリンク先(/dev/dm-0)のパーミッションは変更してください。デフォルトではrootしかアクセスできないはずです。

このマップファイルにより、以下のようにセクタが割り当てられます。

errdev0セクタ 対象 対象セクタ
0 - 10239 /dev/loop0 0 - 10239
10240 エラー -
10241 - 20480 /dev/loop0 10241 - 20480

つまり、セクタ0からセクタ10239までは/dev/loop0(=MAPFILE)と同じデータ。
セクタ10240にアクセスするとIO Error。
セクタ10241からセクタ20479までは/dev/loop0(=MAPFILE)と同じデータ。
ということになります。

# エラー発生確認

例としてPythonなら以下のようなコードでエラーの発生を確認できます。

import time

with open('/dev/mapper/errdev0', mode="r") as fo:
    size = 0
    while True:
        b = fo.read(10240)
        size += len(b)
        print(f"{size}")
        time.sleep(0.1)

1セクタが512バイトなので(なんでそうなってるのかは未確認ですが)、 10240*512バイト以降をreadしたところでIO Errorとなります。

$ sudo python3 read.py
10240
20480
30720
40960
  :
5222400
5232640
5242880
Traceback (most recent call last):
  File "read.py", line 6, in <module>
    b = fo.read(10240)
OSError: [Errno 5] Input/output error

# 参考

いろいろ参考になりました!