天天看點

如何Mock系統調用

背景

​   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)的方法。

連結期墊片(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的動态庫。

附錄

繼續閱讀