由于在内核测试中需要验证一些容灾故障场景,所以研究了一下Linux下的错误注入方法。目前我们的测试脚本中只用了些非常原始粗暴的命令如去移除scsi device,感觉很不专业!搜索了下万能的谷歌,找到了一篇质量很高的StackOverflow回答!然后就是逐项尝试了:
libfiu
简介
libfiu是一个开源的C语言库,可以直接用在项目中添加可控制的错误注入点,也可以直接用它提供的共享库来做POSIX API
的错误注入(无需修改已有代码)。从文档来看fiu-run
和fiu-ctrl
使用起来挺方便的,就照样子试一下吧!
安装
下载源码,解压,然后
1
| $ make && sudo make install
|
没什么特别的,另外还可以安装python binding
1
| $ make python2 && sudo make python2_install
|
首次运行如果提示找不到so文件可以跑一下
使用
直接用文档里的例子来看。
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-run
和fiu-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 $ echo -1 | sudo tee times $ echo 10 | sudo tee interval $ echo 100 | sudo tee probability $ echo 2 | sudo tee verbose
|
接下来就是开启错误注入的激动人心时刻!
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:
- 可以直接嵌入C代码,然后就有很多神奇的用法比如霸爷的这篇以及这篇博文。
- SystemTap的脚本语言也比较直观,可以用-I来加载lib,方便组织代码重用。
- Probe的注入点非常灵活,所以对于错误注入的控制粒度也是最细的了,感觉甚至可以用它来帮助观察内核的瞬时call stack以增进对kernel工作原理的理解,比直接看代码更快捷。
这个项目也有些年头了,代码一直没有更新,所以拿来直接跑会碰到不少问题:
- 在SystemTap 1.7之后嵌入C代码时函数的参数从
THIS->arg
形式改为了STAP_ARG_arg
,另外返回值也从THIS->__retvalue
变成了STAP_RETVALUE
。碰到临时编译的C代码报错问题时可以在运行stap时加上-k
参数,这样自动生成的C代码会在/tmp
下保留,方便进一步排查问题。
- 原来的代码适应的内核版本较老,在2.6.32以后的版本中会报有些struct的成员变量已经不存在的错误,比如
drivers/scsi/scsi.c
中的$cmd->request->sector
就找不到了,打开lxr查找scsi_cmnd以及request struct的定义,可以看到sector被改成了__sector。更严重的是timeout的错误注入,drivers/scsi/scsi_error.c
这个文件中的各个函数都被改得面目全非,所以暂时没有尝试这种错误注入。
- 我在跑脚本的时候还碰到了找不到那些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_params
和fij_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; scmd->sense_buffer[0] = 0x70; scmd->sense_buffer[2] = (unsigned char)(long)STAP_ARG_sensekey; scmd->sense_buffer[7] = 0x13; scmd->sense_buffer[12] = (unsigned char)(long)STAP_ARG_asc; scmd->sense_buffer[13] = (unsigned char)(long)STAP_ARG_ascq; %}
|
一头雾水的感觉啊有没有!这又是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上的SDN与The Hardware/Software Interface这两门课。有兴趣的同学也可以一起组团,共同进步!
Happy Hacking Everyday!