Contents
  1. 1. libfiu
    1. 1.1. 简介
    2. 1.2. 安装
    3. 1.3. 使用
    4. 1.4. 总结
  2. 2. fsdisk
  3. 3. Linux Kernel fault injection infrastructure
    1. 3.1. 简介
    2. 3.2. 配置
    3. 3.3. 使用
    4. 3.4. 总结
  4. 4. SystemTap
    1. 4.1. 简介
    2. 4.2. 配置
    3. 4.3. 使用
    4. 4.4. 工作原理
    5. 4.5. SCSI错误类型
    6. 4.6. 总结
  5. 5. 未来

由于在内核测试中需要验证一些容灾故障场景,所以研究了一下Linux下的错误注入方法。目前我们的测试脚本中只用了些非常原始粗暴的命令如去移除scsi device,感觉很不专业!搜索了下万能的谷歌,找到了一篇质量很高的StackOverflow回答!然后就是逐项尝试了:

libfiu

简介

libfiu是一个开源的C语言库,可以直接用在项目中添加可控制的错误注入点,也可以直接用它提供的共享库来做POSIX API的错误注入(无需修改已有代码)。从文档来看fiu-runfiu-ctrl使用起来挺方便的,就照样子试一下吧!

安装

下载源码,解压,然后

1
$ make && sudo make install

没什么特别的,另外还可以安装python binding

1
$ make python2 && sudo make python2_install

首次运行如果提示找不到so文件可以跑一下

1
$ sudo ldconfig

使用

直接用文档里的例子来看。

1
2
$ fiu-run -x -c "enable_random name=posix/io/rw/read,probability=1" cat test_fault_injection
cat: test_fault_injection: File descriptor in bad state

有趣的是重复执行它出来的错误还不一样:

1
2
3
4
5
$ fiu-run -x -c "enable_random name=posix/io/rw/read,probability=1" cat test_fault_injection
cat: test_fault_injection: Is a directory
$ fiu-run -x -c "enable_random name=posix/io/rw/read,probability=1" cat test_fault_injection
cat: test_fault_injection: Value too large for defined data type

来用strace看一下

1
2
3
4
5
6
7
8
9
10
11
12
13
15:02:53.637634 clone(Process 10821 attached child_stack=0x41310240, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x413109d0, tls=0x41310940, child_tidptr=0x413109d0) = 10821 <0.000064>
...
[pid 10821] 15:02:53.638372 open("/tmp/fiu-ctrl-10820.in", O_RDONLY <unfinished ...>
[pid 10820] 15:02:53.638433 mmap(NULL, 56408368, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7ffc9c686000 <0.000035>
[pid 10820] 15:02:53.639478 close(3) = 0 <0.000028>
[pid 10820] 15:02:53.639684 fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0 <0.000026>
[pid 10820] 15:02:53.639835 open("test_fault_injection", O_RDONLY) = 3 <0.007911>
[pid 10820] 15:02:53.647831 fstat(3, {st_mode=S_IFREG|0664, st_size=130, ...}) = 0 <0.000020>
[pid 10820] 15:02:53.648029 write(2, "cat: ", 5cat: ) = 5 <0.000023>
[pid 10820] 15:02:53.648123 write(2, "test_fault_injection", 20test_fault_injection) = 20 <0.000022>
...
[pid 10820] 15:02:53.649916 write(2, ": Invalid argument", 18: Invalid argument) = 18 <0.000021>
[pid 10820] 15:02:53.650022 write(2, "\n", 1) = 1 <0.000022>

好像看不出所以然……大致看了下源码,感觉应该是直接fork了一个thread然后在这个thread中设置了注入的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
exit_fail:
pthread_setspecific(last_failinfo_key,
pf->failinfo);
failnum = pf->failnum;
if (pf->flags & FIU_ONETIME) {
pf->failed_once = true;
pthread_mutex_unlock(&pf->lock);
}
ef_runlock();
rec_count--;
return failnum;

作为C语言和Linux系统编程小白,艰难地读了这个项目的代码,感觉还是挺有趣的。他写了个python脚本从man page中获取libc和posix中各种API的错误返回等信息,然后再通过这些信息来生成wrapper代码。恩,等我读了APUE再来细细学习研究!

文档中还有一个fiu-runfiu-ctrl配合使top命令输出为空的例子,体现了这个工具的灵活性。另外在libjio项目中有使用它的python binding来做一些测试的实例,值得学习!

总结

主要优势就是简单好用,不用改代码就可以跑了!还提供python的接口。缺点就是它是跑在user space的,而我们主要是想测试kernel本身的稳定性……唔

fsdisk

这个项目的主页在这里。用谷歌一搜基本都是问是不是要找fdisk,好像没什么别人用啊,对于Linux下的驱动还不太了解,所以目前还没有试用它。有兴趣的同学可以按照项目文档用用看。看起来还是满足我们的需要的,只是不清楚它本身会不会带来一些bug之类的问题。

Linux Kernel fault injection infrastructure

简介

这个是merge进主干内核的项目,所以质量应该还是有保证的,文档地址

配置

用这个工具的最大问题是要在内核里打开它然后重新编译安装。下载完源码之后修改.config文件中的如下部分:

1
2
3
4
5
6
CONFIG_FAULT_INJECTION=y
CONFIG_FAILSLAB=y
CONFIG_FAIL_PAGE_ALLOC=y
CONFIG_FAIL_MAKE_REQUEST=y
CONFIG_FAIL_IO_TIMEOUT=y
CONFIG_FAULT_INJECTION_DEBUG_FS=y

然后编译安装内核,修改grub,重启系统

起来之后mount一下debugfs

1
$ sudo mount -t debugfs none /sys/kernel/debug/

看看/sys/kernel/debug下各种fail*的目录有没有出现,有了应该就是配置成功啦!

使用

让我们来试用感受一下高大上的原生错误注入!写了个土土的python脚本模拟文件写入动作:

1
2
3
4
5
6
7
8
9
10
import time
raw_input('press enter to continue')
f = open('test_fault_injection.txt', 'w')
for x in xrange(0, 1000):
f.write("test fault injection\n")
f.flush()
time.sleep(1)
f.close()

原生的fault injection控制粒度只能精确到device+pid 配置方法如下:

1
2
3
4
5
6
$ cd /sys/kernel/debug/fail_make_request
$ echo Y | sudo tee task-filter # 开启pid过滤
$ echo -1 | sudo tee times # 错误注入次数,-1表示无限
$ echo 10 | sudo tee interval # 错误注入的间隔,比如每5秒1次
$ echo 100 | sudo tee probability # 错误注入的概率,范围从0到100
$ echo 2 | sudo tee verbose # 输出log的级别,范围0到2

接下来就是开启错误注入的激动人心时刻!

1
2
$ echo 1 | sudo tee /sys/block/sda/sda2/make-it-fail
$ echo 1 | sudo tee /proc/14255/make-it-fail

在我的测试中写到第554行脚本就挂了,报Input/output error

查看一下/var/log/messages日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
FAULT_INJECTION: forcing a failure
Pid: 14255, comm: python Not tainted 2.6.32-358.23.2.ali_zijie.el5.x86_64
Call Trace:
[<ffffffff8126dc06>] ? should_fail+0xd6/0x130
[<ffffffff8123cb8f>] ? generic_make_request+0x19f/0x480
[<ffffffff8123cee9>] ? submit_bio+0x79/0x120
[<ffffffff811ad483>] ? submit_bh+0x103/0x180
[<ffffffff811b0083>] ? __sync_dirty_buffer+0x53/0xe0
[<ffffffff811b0123>] ? sync_dirty_buffer+0x13/0x20
[<ffffffffa00cb604>] ? journal_dirty_data+0x114/0x210 [jbd]
[<ffffffffa00e40f4>] ? ext3_journal_dirty_data+0x24/0x60 [ext3]
[<ffffffffa00e4a8c>] ? journal_dirty_data_fn+0x1c/0x30 [ext3]
[<ffffffffa00e3a86>] ? walk_page_buffers+0x56/0xa0 [ext3]
[<ffffffffa00e4a70>] ? journal_dirty_data_fn+0x0/0x30 [ext3]
[<ffffffffa00e8529>] ? ext3_ordered_write_end+0xa9/0x1a0 [ext3]
[<ffffffff81116016>] ? generic_file_buffered_write+0x186/0x2b0
[<ffffffff811177b2>] ? __generic_file_aio_write+0x272/0x430
[<ffffffff811179ed>] ? generic_file_aio_write+0x7d/0xf0
[<ffffffff8117a312>] ? do_sync_write+0xf2/0x130
[<ffffffff811404a4>] ? handle_mm_fault+0x234/0x2d0
[<ffffffff81095dd0>] ? autoremove_wake_function+0x0/0x40
[<ffffffff810a0175>] ? ktime_get_ts+0xb5/0xf0
[<ffffffff8118fc6b>] ? poll_select_copy_remaining+0xdb/0x140
[<ffffffff81216dc6>] ? security_file_permission+0x16/0x20
[<ffffffff8117ba2c>] ? vfs_write+0xcc/0x1a0
[<ffffffff8117bbe5>] ? sys_write+0x55/0x90
[<ffffffff8100cf72>] ? system_call_fastpath+0x16/0x1b
Buffer I/O error on device sda2, logical block 11844973
lost page write due to I/O error on sda2
EXT3-fs: ext3_journal_dirty_data: aborting transaction: IO failure in ext3_journal_dirty_data
EXT3-fs (sda2): error in ext3_orphan_add: Readonly filesystem
EXT3-fs (sda2): error in ext3_ordered_write_end: IO failure
JBD: Detected IO errors while flushing file data on sda2

Kernel级别的错误注入!很好很强大!

总结

总的来说这个方法还是挺好用的,最大的问题是要重新build内核,此外就是错误注入的控制还欠灵活些(相对下面的SystemTap来说)。

SystemTap

简介

SystemTap的简介,只需要7个字!狂拽酷炫屌炸天!自从读了Brendan Gregg大神的<Systems Performance>,DTrace,SystemTap这类的动态追踪工具就成了我眼中的大杀器!第一次实际使用它,我就被其缓慢的启动速度,诡异的报错信息以及强大的破坏力深深地打动了……下面就来详细说一下吧。

配置

在RHEL或者CentOS下的安装应该都比较顺利,可以直接yum install或者从源码编译(版本会比较新)。然后要装一下kernel的debuginfo包,否则直接在地址信息上插入probe有点暴力的……

装完试一下经典的Hello World!

1
$ sudo stap -ve 'probe begin { println("Hello World") exit () }'

还可以试试这个:

1
$ sudo stap -l 'kernel.function("scsi_*")'

列出了各种可以获取到的kernel function,之后就可以以此来注入probe啦!

使用

由于对Linux SCSI驱动层面几乎一无所知,所以基本沿用了这个开源项目中的SystemTap代码。从这个项目中也学习到了很多SystemTap的优秀feature:

  1. 可以直接嵌入C代码,然后就有很多神奇的用法比如霸爷的这篇以及这篇博文。
  2. SystemTap的脚本语言也比较直观,可以用-I来加载lib,方便组织代码重用。
  3. Probe的注入点非常灵活,所以对于错误注入的控制粒度也是最细的了,感觉甚至可以用它来帮助观察内核的瞬时call stack以增进对kernel工作原理的理解,比直接看代码更快捷。

这个项目也有些年头了,代码一直没有更新,所以拿来直接跑会碰到不少问题:

  1. 在SystemTap 1.7之后嵌入C代码时函数的参数从THIS->arg形式改为了STAP_ARG_arg,另外返回值也从THIS->__retvalue变成了STAP_RETVALUE。碰到临时编译的C代码报错问题时可以在运行stap时加上-k参数,这样自动生成的C代码会在/tmp下保留,方便进一步排查问题。
  2. 原来的代码适应的内核版本较老,在2.6.32以后的版本中会报有些struct的成员变量已经不存在的错误,比如drivers/scsi/scsi.c中的$cmd->request->sector就找不到了,打开lxr查找scsi_cmnd以及request struct的定义,可以看到sector被改成了__sector。更严重的是timeout的错误注入,drivers/scsi/scsi_error.c这个文件中的各个函数都被改得面目全非,所以暂时没有尝试这种错误注入。
  3. 我在跑脚本的时候还碰到了找不到那些function的错误,后来发现是因为我们的kernel是把scsi模块直接编译进内核的,所以要在kernel.function里找。默认的编译方式好像是把这些东西编译成module的,因而原先的脚本都是在module("*").function里找。

由于原脚本的逻辑看起来异常复杂,难以调试,所以我把其中的代码一块一块剥离出来自己拼接使用,为了更好地控制错误注入的方式,比如通过pid,inode,device来过滤需要错误注入的目标,加上失败次数,失败概率等各种常见调控参数,我又以自己拙劣的SystemTap代码能力……历经千辛万苦……从网上找了另一个fault injection的framework来用…………组装成功后真是简单好用,想怎么注入就怎么注入啊!

先来看下效果:

准备工作

简单起见,我就用读写一个文本文件来做测试了。随便建立一个文件

1
$ echo "test fault injection" > test_fault_injection

在跑脚本时需要指定一下错误注入的目标也就是这个文件的inode号或block号,可以用下面的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ sudo debugfs -R "stat /home/admin/zijie/test_fault_injection" /dev/sda2
debugfs 1.41.12 (17-May-2010)
Inode: 2992033 Type: regular Mode: 0664 Flags: 0x0
Generation: 2438482565 Version: 0x00000000
User: 505 Group: 505 Size: 130
File ACL: 0 Directory ACL: 0
Links: 1 Blockcount: 8
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x53745b2d -- Thu May 15 14:14:05 2014
atime: 0x5390017b -- Thu Jun 5 13:34:51 2014
mtime: 0x53745b2d -- Thu May 15 14:14:05 2014
Size of extra inode fields: 4
BLOCKS:
(0):11982918
TOTAL: 1

最后还要获取一下device的major, minor id,我用的是

1
2
3
4
5
6
7
8
9
$ ls -l /dev | grep sda
brw-r----- 1 root disk 8, 0 May 15 02:46 sda
brw-r----- 1 root disk 8, 1 May 14 18:46 sda1
brw-r----- 1 root disk 8, 2 May 15 02:46 sda2
brw-r----- 1 root disk 8, 3 May 15 02:46 sda3
brw-r----- 1 root disk 8, 4 May 15 02:46 sda4
brw-r----- 1 root disk 8, 5 May 14 18:46 sda5
brw-r----- 1 root disk 8, 6 May 14 18:46 sda6
brw-r----- 1 root disk 8, 7 May 15 02:46 sda7

可以看到major id是8,minor id从0到7。

注入起来

错误注入前:

1
2
3
4
5
6
7
8
$ cat test_fault_injection
test
$ echo "test" >> test_fault_injection
$ cat test_fault_injection
test
test

跑一下脚本命令:

1
$ sudo stap -g scsi_fij.stp -I ./lib/ debug=1 inode=2992033 exec="cat" exec="swapper"

测试读取文件:

1
2
$ cat test_fault_injection
cat: test_fault_injection: Input/output error

看一下/var/log/message

1
2
3
4
sd 0:0:0:0: [sda] Sense Key : Medium Error [current]
sd 0:0:0:0: [sda] Add. Sense: Unrecovered read error - auto reallocate failed
sd 0:0:0:0: [sda] CDB: Read(10): 28 00 05 be 62 30 00 00 00 00
Buffer I/O error on device sda2, logical block 11982918

写入也是一样:

1
2
$ echo "test_after_fault_inject" >> test_fault_injection
-bash: echo: write error: Input/output error

把脚本停了之后,文件中原来的内容还是没有任何损坏。

另外可以在脚本里加print_backtrace()来查看错误注入时的call stack。如果对内核代码非常了解的话,就可以几乎在任意的位置注入错误了!

工作原理

详细的SystemTap使用可以看官方教程,另外它本身就自带了许多示范脚本,已经可以满足很多日常的监控分析工作,推荐大家试试!另外其工作原理可以参考这篇文章

在这个SCSI错误注入脚本里基本上就是在scsi_decide_disposition这个function上加probe,然后进去后获取一系列变量信息以此来控制是否进行错误注入。在控制是否错误注入时使用了fij的库,在代码里看就是那些fij_paramsfij_should_fail()等。截取一个代码片段来看下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
probe kernel.function("scsi_decide_disposition@drivers/scsi/scsi_error.c")
{
scmd_direction = $scmd->sc_data_direction
major = $scmd->request->rq_disk->major
minor = $scmd->request->rq_disk->first_minor
block = $scmd->request->__sector
struct_bio= $scmd->request->bio
if (struct_bio != 0)
{
page = $scmd->request->bio->bi_io_vec->bv_page
if (page != 0)
{
inode = get_inode(page)
}
}
if (fij_params["inode_filter"] == 1 && !(inode in fij_inodes_to_fail)) next
if (fij_should_fail() == 1) {
printf("scsi_decide_disposition : %s(%d) cmd (%d)\n", execname(), pid(), $scmd->cmnd[0])
printf("scsi_decide_disposition : major=%d minor=%d scmd=%d \n", major, minor, $scmd)
printf("scsi_decide_disposition : scmd-direction = %d\n", scmd_direction)
printf("scsi_decide_disposition : start sector = %d \n", block)
printf("scsi_decide_disposition : inode = %d \n", inode)
print_backtrace()
printf("scsi_decide_disposition : !!!!!! set sense buf !!!!!!\n")
set_sense_buf($scmd, 0x02, 0x03, 0x11, 0x04)
fij_done_fail()
}
}

类似C的语法的脚本语言!还是很直观好用的吧!

SCSI错误类型

在脚本中有两个关键的错误注入点,一个是set_sense_buf函数

1
2
3
4
5
6
7
8
9
10
11
function set_sense_buf:long (cmd:long, result:long, sensekey:long, asc:long, ascq:long )
%{
struct scsi_cmnd * scmd = (struct scsi_cmnd *)(long)STAP_ARG_cmd;
scmd->result = (int)(long)STAP_ARG_result; /* case DID_BUS_BUSY: 0x02 */
scmd->sense_buffer[0] = 0x70; /* current, fixed format */
scmd->sense_buffer[2] = (unsigned char)(long)STAP_ARG_sensekey; /* 0x03 */
scmd->sense_buffer[7] = 0x13; /* length */
scmd->sense_buffer[12] = (unsigned char)(long)STAP_ARG_asc; /* 0x11 */
scmd->sense_buffer[13] = (unsigned char)(long)STAP_ARG_ascq; /* 0x04 */
%}

一头雾水的感觉啊有没有!这又是SCSI驱动编程的新领域了……去查文档。终于大致理解了上面这段代码:

第0个字节默认设置为0x70或0x71
第2个字节为Sense Key,脚本中设置为0x03,意为medium error,与系统报错一致
第7个字节为长度,不知道为何设置成0x13,看代码应该大于十进制的13才能读取后面用到的两个field
第12,13字节组成了一套复杂的状态
脚本中设置的0x11, 0x04含义为:UNRECOVERED READ ERROR - AUTO REALLOCATE FAILED,与系统报错一致

我们可以利用这些信息返回任意我们想要的SCSI错误。

另外一个是修改scsi command,脚本中有如下代码:if (($cmd->cmnd[0] == 0x28) || ($cmd->cmnd[0] == 0x2a))
参考维基百科
可以看到这两个命令是:28 READ(10)2A WRITE(10)读或者写10个字节
但是后面设置了$cmd->cmnd[7] = 0, $cmd->cmnd[8] = 0这两个,苦苦搜索都没找到相关文档,看到内核源码中有这么一段:

1
2
3
4
5
6
7
} else if (cmd->cmnd[0] == WRITE_10) {
cmnd_lba = ((u64)cmd->cmnd[2] << 24) |
(cmd->cmnd[3] << 16) |
(cmd->cmnd[4] << 8) |
cmd->cmnd[5];
cmnd_count = (cmd->cmnd[7] << 8) |
cmd->cmnd[8];

所以这两个应该是scsi command的数量,在这里强制设为了0,可能是直接取消了后续的任何读写操作?还望大牛指点迷津啊!

总结

SystemTap果然是极其强大啊!当然对使用者要求也比较高,需要对内核本身的各种function,运作流程比较了解。使用中感觉它最大的优势是控制点非常细,几乎可以在任意的代码路径上注入想要的错误。如果我们能取得kernel panic时候的call stack,就可以准确地在原本出错的函数返回处注入错误,以此来进行bug的修复验证,而不用长时间反复跑压力测试来期望硬件问题的复现,可以极大地提高效率!

未来

写了第一篇博文,感觉还是有点激动的!目前对于Linux内核方面我还是完全的小白,希望以后能多向大牛学习,争取早日入门!计划接下来看一些Robert Love大神的书,然后6月跟一下Coursera上的SDNThe Hardware/Software Interface这两门课。有兴趣的同学也可以一起组团,共同进步!

Happy Hacking Everyday!

Contents
  1. 1. libfiu
    1. 1.1. 简介
    2. 1.2. 安装
    3. 1.3. 使用
    4. 1.4. 总结
  2. 2. fsdisk
  3. 3. Linux Kernel fault injection infrastructure
    1. 3.1. 简介
    2. 3.2. 配置
    3. 3.3. 使用
    4. 3.4. 总结
  4. 4. SystemTap
    1. 4.1. 简介
    2. 4.2. 配置
    3. 4.3. 使用
    4. 4.4. 工作原理
    5. 4.5. SCSI错误类型
    6. 4.6. 总结
  5. 5. 未来