如何mock系统调用
背景
Linux下开发存储系统、网络库的时候会用到一系列Linux的系统调用,每一个系统调用都有一些出错的场景,有些场景很极端,比如内存使用达到上限、磁盘写满等,如果对其进行测试的话,很难去构造这样的一个场景,这个时候集成测试就显得力不存心了,只能靠单元测试来覆盖这些场景。现在的问题就是如何去mock这些系统调用,然后通过程序返回对应场景的错误码来模拟各种场景。也就是将对系统函数的依赖注入到程序中。
系统函数的依赖注入
目前实现系统函数的依赖注入的手段有很多,分为编译期注入,和运行期注入,至于什么是依赖注入可以参考知乎的一篇文章如何用最简单的方式解释依赖注入,下面介绍几种依赖注入的方法:
- 虚函数实现依赖注入(运行期注入)
使用传统的面向对象的手法,借助运行期的延迟绑定实现注入和替换,自己实现一个System接口类,把程序用到的系统调用都用虚函数封装一层,然后在调用的时候不直接调用系统调用,而是调用的System对应的方法。这样代码的主动权就交给了System接口类了。写单元测试的时候将这个System接口类替换成我们自己的mock对象就可以。完整的示例代码如下:
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
|
- 编译期延迟绑定(编译期注入)
创建一个命名空间,创建一系列和系统调用同名的方法,间接的调用系统调用,写测试代码的时候重新定义这些方法,这就相当于一份代码有了两份实现,根据编译的时候链接哪份代码来决定是否启用mock,这个看起来要比基于虚函数的要简单的多了。完整的示例代码如下:
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
|
两种方法都比较好实现,前提是代码在一开始的时候就考虑过这些因素,并按照上述方式来编写,然后现实总是残酷的,面对一个已经编码完成的程序该如何为其编写系统调用的mock呢?就需要用到链接期垫片(link seam)的方法。
链接期垫片(link seam)
连接器垫片的方式一般情况有三种,如下:
- Shadowing functions through linking order (override functions in libraries with new definitions in object files)
- Wrapping functions with GNU's linker option -wrap (GNU Linux only)
- Run-time function interception* with the preload functionality of the dynamic linker for shared libraries (GNU Linux and Mac OS X only)
第一种就是通过链接顺序来改变链接的对象,将要mock的对象重新实现一遍,链接的时候链接器会优先使用我们自己实现的同名函数,这样就可以将目标替换为要mock的对象了,完整代码如下:
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 |
|
另外一种就是Linux下独有的,通过gcc的--wrap选项可以指定要wrap的系统调用,那么相应的就回去调用带有__wrap
前缀的对应系统调用实现,比如--wrap=write,那么在链接的时候就会链接到 __wrap_write
,而真实的write调用变成了__real_write
。完整代码例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
最后一种就是给系统调用提供一份mock
实现,并编译成动态库,然后通过LD_LIBRARY_PATH
改变加载动态库的搜索路径让其优先搜索mock版本的动态库,或者是设置LD_PRELOAD
环境变量,预先加载mock的动态库。