如何mock系统调用

2023-10-28

背景

​   Linux下开发存储系统、网络库的时候会用到一系列Linux的系统调用,每一个系统调用都有一些出错的场景,有些场景很极端,比如内存使用达到上限、磁盘写满等,如果对其进行测试的话,很难去构造这样的一个场景,这个时候集成测试就显得力不存心了,只能靠单元测试来覆盖这些场景。现在的问题就是如何去mock这些系统调用,然后通过程序返回对应场景的错误码来模拟各种场景。也就是将对系统函数的依赖注入到程序中。

系统函数的依赖注入

​   目前实现系统函数的依赖注入的手段有很多,分为编译期注入,和运行期注入,至于什么是依赖注入可以参考知乎的一篇文章如何用最简单的方式解释依赖注入,下面介绍几种依赖注入的方法:

  • 虚函数实现依赖注入(运行期注入)

​   使用传统的面向对象的手法,借助运行期的延迟绑定实现注入和替换,自己实现一个System接口类,把程序用到的系统调用都用虚函数封装一层,然后在调用的时候不直接调用系统调用,而是调用的System对应的方法。这样代码的主动权就交给了System接口类了。写单元测试的时候将这个System接口类替换成我们自己的mock对象就可以。完整的示例代码如下:

  // system.h
  class System {
   public:
    virtual int  open(const char *path, int oflag, ...) = 0;
    virtual ssize_t read(int fildes, void *buf, size_t nbyte) = 0;
    virtual ssize_t write(int fildes, const void *buf, size_t nbyte) = 0;
    virtual int close(int fildes) = 0;

    static System* GetInstance();
    static void set_instance(System* instance) {
      instance_ = instance;
    }

   private:
    static System* instance_;
  };
  // 具体的实现
  class FileOps : public System {
   public:
    int open(const char *path, int oflag, ...) override;
    ssize_t read(int fildes, void *buf, size_t nbyte) override;
    ssize_t write(int fildes, const void *buf, size_t nbyte) override;
    int close(int fildes) override;
    static FileOps* GetInstance();
  };

  // system.cc
  System* System::instance_ = nullptr;
  // 默认实现是FileOps,mock的时候通过改变这个默认实现从而把主动权从默认实现转到了mock的实现
  System* System::GetInstance() { 
    if (!instance_) {
      instance_ = FileOps::GetInstance();
    }
    assert(instance_);
    return instance_;
  }

  int FileOps::open(const char *path, int oflag, ...) {
    return ::open(path, oflag, 0777);
  }

  ssize_t FileOps::read(int fildes, void *buf, size_t nbyte) {
    return ::read(fildes, buf, nbyte);
  }

  ssize_t FileOps::write(int fildes, const void *buf, size_t nbyte) {
    return ::write(fildes, buf, nbyte);
  }

  int FileOps::close(int fildes) {
    return ::close(fildes);
  }

  FileOps* FileOps::GetInstance() {
    static FileOps sys;
    return &sys;
  }
  // 正常调用 main.cc
  int main() {
    assert(System::GetInstance() != nullptr);
    int fd = System::GetInstance()->open("txt", O_RDWR|O_CREAT, 0777);
    assert(fd > 0);
    int ret = System::GetInstance()->write(fd, "12345", 5);
    assert(ret > 0);
    ret = System::GetInstance()->close(fd);
    assert(ret == 0);

    return 0;
  }

  // 测试的时候调用如下,模拟一个IO错误

  // 一个mock版本的实现 test.cc
  class MockFileOps : public System {
  public:
   int open(const char *path, int oflag, ...) override;
   ssize_t read(int fildes, void *buf, size_t nbyte) override;
   ssize_t write(int fildes, const void *buf, size_t nbyte) override;
   int close(int fildes) override;
   static MockFileOps* GetInstance();
  };

  int MockFileOps::open(const char *path, int oflag, ...) {
    return ::open(path, oflag, 0777);
  }

  ssize_t MockFileOps::read(int fildes, void *buf, size_t nbyte) {
    return ::read(fildes, buf, nbyte);
  }
  // 模拟的一个IO错误
  ssize_t MockFileOps::write(int fildes, const void *buf, size_t nbyte) {
    errno = EIO;
    return -1;
  }

  int MockFileOps::close(int fildes) {
    return ::close(fildes);
  }

  MockFileOps* MockFileOps::GetInstance() {
    static MockFileOps sys;
    return &sys;
  }

  int main() {
    // 改变默认实现
    System::set_instance(MockFileOps::GetInstance());
    assert(System::GetInstance() != nullptr);
    int fd = System::GetInstance()->open("txt", O_RDWR|O_CREAT, 0777);
    assert(fd > 0);
    int ret = System::GetInstance()->write(fd, "12345", 5);
    assert(ret ==  -1); // 发生错误
    perror("write");
    ret = System::GetInstance()->close(fd);
    assert(ret == 0);

    return 0;
  }
  • 编译期延迟绑定(编译期注入)

​   创建一个命名空间,创建一系列和系统调用同名的方法,间接的调用系统调用,写测试代码的时候重新定义这些方法,这就相当于一份代码有了两份实现,根据编译的时候链接哪份代码来决定是否启用mock,这个看起来要比基于虚函数的要简单的多了。完整的示例代码如下:

  // file_ops.h
  namespace FileOps {
    int  open(const char *path, int oflag, ...);
    ssize_t read(int fildes, void *buf, size_t nbyte);
    ssize_t write(int fildes, const void *buf, size_t nbyte);
    int close(int fildes);
  }  // namespace FileOps

  // file_ops.cc
  namespace FileOps {

  int open(const char *path, int oflag, ...) {
    return ::open(path, oflag, 0777);
  }

  ssize_t read(int fildes, void *buf, size_t nbyte) {
    return ::read(fildes, buf, nbyte);
  }

  ssize_t write(int fildes, const void *buf, size_t nbyte) {
    return ::write(fildes, buf, nbyte);
  }

  int close(int fildes) {
    return ::close(fildes);
  }

  }  // namespace FileOps

  // mock_file_ops.cc

  namespace FileOps {

  int open(const char *path, int oflag, ...) {
    return ::open(path, oflag, 0777);
  }

  ssize_t read(int fildes, void *buf, size_t nbyte) {
    return ::read(fildes, buf, nbyte);
  }
  // 这里做了mock,改变了write的行为
  ssize_t write(int fildes, const void *buf, size_t nbyte) {
    errno = EIO;
    return -1;
  }

  int close(int fildes) {
    return ::close(fildes);
  }

  }  // namespace FileOps

  // 测试程序
  int main() {
    int fd = FileOps::open("txt", O_RDWR|O_CREAT, 0777);
    assert(fd > 0);
    int ret = FileOps::write(fd, "12345", 5);
    if (ret == -1) {
      perror("write:");
    }
    ret = FileOps::close(fd);
    assert(ret == 0);
    return 0;
  }

​   ​两种方法都比较好实现,前提是代码在一开始的时候就考虑过这些因素,并按照上述方式来编写,然后现实总是残酷的,面对一个已经编码完成的程序该如何为其编写系统调用的mock呢?就需要用到链接期垫片(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的对象了,完整代码如下:

//  一个待测试的对象
int main() {
  int fd = ::open("txt", O_RDWR|O_CREAT, 0777);
  assert(fd > 0);
  int ret = ::write(fd, "12345", 5);
  if (ret == -1) {
    perror("write:");
  }
  ret = ::close(fd);
  assert(ret == 0);
  return 0;
}

// 对目标进行mock,mock的对象是write系统调用
typedef ssize_t (*write_func_t)(int fildes, const void *buf, size_t nbyte);
// 通过dlsym的RTLD_NEXT获取write的下一个定义,也就是libc中的定义,如果想在mock中
// 调用真实的write系统调用不能直接用write,因为write已经被mock了,这样会导致一直递归下去
// 所以这里通过获取真实的write调用的地址,从而难道write的调用入口,这样既可以在mock中调用
// 真实的write调用了
write_func_t old_write_func =
    reinterpret_cast<write_func_t>(dlsym(RTLD_NEXT, "write"));

// 要mock的对象
extern "C" ssize_t write(int fildes, const void *buf, size_t nbyte) {
  errno = EIO;
  return -1;
}

​   另外一种就是Linux下独有的,通过gcc的–wrap选项可以指定要wrap的系统调用,那么相应的就回去调用带有__wrap前缀的对应系统调用实现,比如–wrap=write,那么在链接的时候就会链接到 __wrap_write,而真实的write调用变成了__real_write。完整代码例子如下:

// 测试程序
int main() {
  int fd = ::open("txt", O_RDWR|O_CREAT, 0777);
  assert(fd > 0);
  int ret = ::write(fd, "12345", 5);
  if (ret == -1) {
    perror("write:");
  }
  ret = ::close(fd);
  assert(ret == 0);
  return 0;
}

// mock对象
extern "C" ssize_t __real_write(int fildes, const void *buf, size_t nbyte);

extern "C" ssize_t __wrap_write(int fildes, const void *buf, size_t nbyte) {
  __real_write(fildes, buf, nbyte);
  errno = EIO;
  return -1;
}

​   最后一种就是给系统调用提供一份mock实现,并编译成动态库,然后通过LD_LIBRARY_PATH改变加载动态库的搜索路径让其优先搜索mock版本的动态库,或者是设置LD_PRELOAD环境变量,预先加载mock的动态库。

附录

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何mock系统调用 的相关文章

  • antd中Tabs切换控制其他地方的标签显隐(react)

    antd中Tabs切换控制其他地方的标签的显示与隐藏 过程如下 tab的回调函数 拿到key值之后就离成功不远了 然后在右边的标签下还有内容 也需要根据tab切换去控制显隐 和上面是一个道理 可以直接给里面的每个div类名 根据key值 去

随机推荐

  • Java写一个excel工具类_Java操作Excel工具类(poi)

    1 importorg apache commons lang3 exception ExceptionUtils 2 importorg apache poi hssf usermodel HSSFDataFormat 3 importo
  • STM32传感器外设集 -- 蓝牙(HC-05)+超声波(hc-sr04)

    前言 前言 蓝牙外设还没有给大家安排上 今天我就给大家安排上使用蓝牙传输超声波距离的例程 会给大家附带蓝牙的上位机的测试APP 一 模块介绍 1 连接图 蓝牙模块 引脚 超声波传感器 引脚 GND GND GND GND VCC 3 3 V
  • 基础IO(文件输入输出、标准IO接口、文件描述符和文件流指针)

    目录 基础IO 文件的输入输出操作 FILE fopen char filename char mode 文件名称 打开方式 size t fread char buf size t block size size t block coun
  • leetcode 1833 雪糕的最大数量 第一眼想到的是dp,其实只能排序加贪心

    夏日炎炎 小男孩 Tony 想买一些雪糕消消暑 商店中新到 n 支雪糕 用长度为 n 的数组 costs 表示雪糕的定价 其中 costs i 表示第 i 支雪糕的现金价格 Tony 一共有 coins 现金可以用于消费 他想要买尽可能多的
  • ioctl()函数

    include
  • 前端js、javascript 鼠标 框选 文件 功能

    一 DEMO
  • No module named ‘torch‘

    系统环境 Win10 Python3 6 一 此处推荐第三种方法到官网获取相关安装指令 1 默认安装新版本torch pip install torch 2 安装指定版本torch pip install torch 1 7 0 二 安装t
  • PIDNet: A Real-time Semantic Segmentation Network Inspired by PID Controllers翻译

    摘要 在实时语义分割任务中 双分支网络架构具有良好得效率和有效性 然而 高分辨率细节和低频上下文的直接融合存在细节特征容易被周围上下文信息淹没的缺点 这种超调现象限制了现有双分支模型分割精度的提高 在本文中 我们将卷积神经网络 CNN 和比
  • Python脚本,使用apikey查询OpenAi可用余额

    脚本如下 import openai import os 设置 OpenAI API key openai api key os environ OPENAI API KEY def get balance 获取当前 OpenAI 可用余额
  • 软件工程作业创建表

    设计表 4 按专业统计课程数量 sql SELECT Major COUNT AS Num FROM Course GROUP BY Major 5 按专业查询所有课程信息 sql SELECT FROM Course WHERE Majo
  • vue实现电梯锚点导航

    1 目标效果 最近喝了不少的咖啡 奶茶 有一个效果我倒是挺好奇怎么实现的 1 点击左侧分类菜单 右侧滚动到该分类区域 2 右侧滑动屏幕 左侧显示当前所处的分类区域 这种功能会出现在商城项目中或者分类数量较多的项目中 专业名称称电梯导航 目标
  • 管道符丨用法

    使用管道操作符 可以把一个命令的标准输出传送到另一个命令的标准输入中 连续的 意味着第一个命令的输出为第二个命令的输入 第二个命令的输入为第一个命令的输出 依次类推 最常用的就是配合grep来使用 ps ef grep pycharm ps
  • MATLAB怎么保存数据到当前M文件所在文件夹

    之前写人脸识别的时候遇到如上问题 然后发现解决方案一 local address pwd 这样会返回当前工作目录的路径 local address被赋值为字符串 就是下图里 的下面这个位置 但是对任意文件夹里的M文件需要先设置当前工作目录的
  • juc辅助类

    目录 减少计数 CountDownLatch 循环栅栏 CyclicBarrier 减少计数 CountDownLatch 循环栅栏 CyclicBarrier 两者区别 信号灯 Semaphore juc辅助类有 减少计数 CountDo
  • Doxygen文档生成工具

    Doxygen代码文档生成工具 文章目录 Doxygen代码文档生成工具 Doxygen Doxygen的注释 vscode插件 Doxygen实际使用 Doxygen 根据百度百科说法 Doxygen是一种开源的 跨平台的文档系统 支持C
  • 记一次由于外部K8S集群证书到期导致Jenkins无法生成动态agent节点错误解决(入坑出坑)...

    欢迎关注 WeiyiGeek 公众号 点击 下方卡片 即可关注我哟 设为 星标 每天带你 基础入门 到 进阶实践 再到 放弃学习 涉及 网络安全运维 应用开发 物联网IOT 学习路径 个人感悟 等知识 花开堪折直须折 莫待无花空折枝 作者主
  • Flask虚拟环境

    一 虚拟环境 1 为什么需要虚拟环境 虚拟环境作用是将每个项目所需要的包隔离开形成一个独立的整体 每个虚拟环境互不干涉 方便与运行 因为如果有包有升级的话 可能会运行不了 2 安装使用虚拟环境 pipenv 2 1 安装虚拟环境 在系统命行
  • uview2.0自定义u-count-down倒计时

    1 效果展示 2 思路需要后端返回一个结束的时间戳 注意是毫秒时间戳 如果是秒需要在后面加3个0转为毫秒 获取当当前时间戳 当前时间戳减去商品结束的时间戳得出要倒计时的时间戳 然后再进行值得处理 3 代码展示
  • 分享一个放烟花的特效

    先看效果 再看代码
  • 如何mock系统调用

    背景 Linux下开发存储系统 网络库的时候会用到一系列Linux的系统调用 每一个系统调用都有一些出错的场景 有些场景很极端 比如内存使用达到上限 磁盘写满等 如果对其进行测试的话 很难去构造这样的一个场景 这个时候集成测试就显得力不存心