FIO中的插件式设计



  • 插件式设计模板[GITHUB]

    一、设计来源

    fio的源代码,学习了一波fio中关于IO引擎的插件式设计,对重要的一些部分做了摘要。以下是我的总结,其中有一些是我的猜测,有待验证

    名词解释:

    • 主程序:调用插件的程序
    • 插件:可以被方便地替换地部分

    二、插件与主程序的结构关联

    主程序如果想使用插件中的函数,则需要知道插件中对应函数的地址,所以我们可以定义一个结构体集合,用来保存插件提供函数的地址,和其它相关内容。在使用插件时我们只要找到了该结构体的位置,就相当于找到了插件中提供的函数的位置。例如

    struct PluginStruct {
        char *plugin_name;
        int plugin_version;
    
        int (*init)(struct thread_data *);
        int (*uninit)(struct thread_data *);
    
        int (*io_write)(struct thread_data *, struct io_unit*);
        int (*io_read)(struct thread_data *, struct io_unit*);
    };
    

    在本例中,我们准备做一个读写文件的插件,插件一使用write/read方式读写;插件二使用pwrite/pread方式读写。如上述结构体中:inituninit是插件的初始化和反初始化,io_writeio_read是对读写接口的封装。

    插件可能是由多线程来调用的,为了表明这一点,插件接口的参数中使用结构体struct thread_data来表示每个线程的私有数据(fio中也是用的thread_data)。

    插件中也可能想保存私有数据,但是由于不知道有多少线程会使用该插件,所以只能将数据保存到结构体struct thread_dataplugin_data,然后在插件中做类型转换,结构体struct thread_data只是作为私有数据的plugin_data承载作用,私有数据plugin_datainituninit中分别申请空间和释放空间,对于外部来说可以做到‘神不知,鬼不觉’。

    为了能够做到基本的数据读写,结构体struct thread_datastruct io_unit定义如下

    /* 文件属性,用于保存每个线程操作的文件信息 */
    struct FileAttr {
        char *file_name;
        int fd;
    };
    
    /* 线程数据,保存一个线程用到的所有数据 */
    struct thread_data {
        /* some thread private data */
        int thread_id;
        pthread_t thread_handle;
    
        struct PluginStruct *plugin_struct; // 通过该变量访问插件中的函数地址
        void *plugin_private_data;
    
        struct FileAttr *file_attr;			// 保存该线程用到的文件信息
    
        void *plugin_dll_handle;			// 共享库句柄
    };
    
    /* io操作单元,一次IO操作必要的信息 */
    struct io_unit {
        void *buffer;	// 缓冲区地址
        uint64_t size; 	// 读写大小
        uint64_t offset;// 读写偏移
    };
    

    以上的结构体均定义在主程序头文件中,插件只是使用这些结构体类型,或访问内存中的值,或定义变量。

    完整的插件需要包含的主程序头文件plugin.h定义如下

    // plugin.h
    
    // 插件在加载和关闭时,自动调用的构造函数和析构函数 标识?
    #define plugin_init	__attribute__((constructor))
    #define plugin_exit	__attribute__((destructor))
    
    /* 文件属性,用于保存每个线程操作的文件信息 */
    struct FileAttr {
        char *file_name;
        int fd;
    };
    
    /* 线程数据,保存一个线程用到的所有数据 */
    struct thread_data {
        /* some thread private data */
        int thread_id;
        pthread_t thread_handle;
    
        struct PluginStruct *plugin_struct; // 通过该变量访问插件中的函数地址
        void *plugin_private_data;
    
        struct FileAttr *file_attr;			// 保存该线程用到的文件信息
    
        void *plugin_dll_handle;			// 共享库句柄
    };
    
    /* io操作单元,一次IO操作必要的信息 */
    struct io_unit {
        void *buffer;	// 缓冲区地址
        uint64_t size; 	// 读写大小
        uint64_t offset;// 读写偏移
    };
    
    struct PluginStruct {
        char *plugin_name;
        int plugin_version;
    
        int (*init)(struct thread_data *);
        int (*uninit)(struct thread_data *);
    
        int (*io_write)(struct thread_data *, struct io_unit*);
        int (*io_read)(struct thread_data *, struct io_unit*);
    };
    
    // 插件向主程序注册和反注册接口
    extern void plugin_register(struct PluginStruct *);
    extern void plugin_unregister(struct PluginStruct *);
    

    三、插件向主程序注册/主程序主动加载插件

    fio中IO引擎的注册包含了两种,(猜测是为了防止其中一种失败,然后采用另一种方式)。

    主程序主动加载插件方式是主程序通过dlopen()dlsym()dlclose()系列函数完成插件中struct PluginStruct结构体变量的加载,从而获取到插件中相应函数的地址;插件向主程序注册方式是主程序在加载插件动态库时,自动调用某些"构造函数",而在构造函数中调用主程序的注册函数可以完成插件的注册。

    插件程序定义如下:

    // plugin_1.c
    
    /* 包含定义插件必须的头文件 */
    #include "plugin.h"
    
    // 插件私有变量定义
    struct plugin_private_data {
        // some private data
        int write_call_times;
        int read_call_times;
    }
    
    static int plugin_1_init(struct thread_data *td)
    {
    }
    
    static int plugin_1_uninit(struct thread_data *td)
    {
    }
    
    static int plugin_1_io_read(struct thread_data *td, struct io_unit *io_u)
    {
    }
    
    static int plugin_1_io_write(struct thread_data *td, struct io_unit *io_u)
    {
    }
    
    // 注册所有本插件的相关函数到插件结构体中
    struct PluginStruct plugin = {
        .plugin_name 		= "plugin_1",
        .plugin_version 	= 1,
        .init				= plugin_1_uninit,
        .uninit 			= plugin_1_uninit,
    	  .io_write 			= plugin_1_io_write,
        .io_read			= plugin_1_io_read,
    };
    
    // 插件动态库在加载时会自动调用该函数,因为plugin_init的原因
    static void plugin_init plugin_1_auto_register(){
        plugin_register(&plugin);
    }
    // 插件动态库在关闭时会自动调用该函数
    static void plugin_exit plugin_1_auto_unregister(){
        plugin_unregister(&plugin);
    }
    

    3.1 主程序主动加载插件

    主程序通过dlopen()dlsym()dlclose()系列函数完成插件中plugin变量的加载,从而完成插件的注册,这种方式的前提是插件中一定定义了plugin变量,否则无法完成插件的加载。

    主动加载示例如下:

    // plugin.c
    
    static PluginStruct* load_plugin(struct thread_data *td, char *plugin_dll_path){
        struct PluginStruct *plugin;
        void *dll_handle = dlopen(plugin_dll_path, RTLD_LAZY);
    	if (!dll_handle) {
    		return NULL;
    	}
    
    	plugin = dlsym(dll_handle, plugin_dll_path); // 这是啥?
    	if (!plugin){
            plugin = dlsym(dll_handle, "plugin");
      }
    	return plugin;
    }
    

    3.2 插件向主程序注册

    插件中需要定义的代码如下:

    // plugin_1.c
    
    // 插件动态库在加载时会自动调用该函数,因为plugin_init的原因
    static void plugin_init plugin_1_auto_register(){
        plugin_register(&plugin);
    }
    // 插件动态库在关闭时会自动调用该函数
    static void plugin_exit plugin_1_auto_unregister(){
        plugin_unregister(&plugin);
    }
    

    四、动态库文件的装载(插件设计原理)

    动态符号表(.dynsym)是一个符号集,保存动态链接相关的符号,这些符号对于运行时的动态对象是可见的。在动态链接过程中,如果发现未定义的动态符号,链接器会把动态符号加入到动态符号表。但是我们的插件是显示地运行时链接的(为了减少与主程序的耦合),不可能在编译过程中就动态链接到对应的插件库,所以只能主动导出插件中使用的未定义的符号(注册与反注册符号)到动态符号表中,所以在编译主程序时,使用-Wl,-E(-Wl,–export-dynamic)链接器参数将主程序中所有的符号导出到动态符号表中。这样在使用dlopen打开插件动态库时,插件动态库中相关的注册和反注册接口符号在主程序的动态符号表中就有了定义,于是就可以正常运行了。

    使用readelf --dyn-syms + 可执行文件或动态库可以查看可执行文件或者动态库中的动态符号表。本例中动态库中动态符号表部分内容如下:

    Symbol table '.dynsym' contains 22 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         1: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _Z19unregister_ioengineP1
         2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (2)
        11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _Z17register_ioengineP11I
        13: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND pthread_self@GLIBC_2.2.5 (3)
        14: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND pread64@GLIBC_2.2.5 (3)
        15: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND pwrite64@GLIBC_2.2.5 (3)
        16: 0000000000202080    80 OBJECT  GLOBAL DEFAULT   24 ioengine
    

    从中可以看出注册和反注册接口、以及调用glic库中的符号的是未定义(UND)状态,需要在加载时确定这些符号的位置,glic库相关的符号在加载glic库时完成确定,注册与反注册则需要在主程序中确定位置,所以主程序的动态符号表必须有这两个符号的定义。

    主程序在未加-Wl,-E参数编译时,动态符号表内容如下:

    Symbol table '.dynsym' contains 87 entries:
       Num:    Value          Size Type    Bind   Vis      Ndx Name
         0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
         1: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _ZNSsaSEPKc@GLIBCXX_3.4 (2)
         2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _ZNSsC1Ev@GLIBCXX_3.4 (2)
         3: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND printf@GLIBC_2.2.5 (3)
         4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND _ZSt21__throw_runtime_err@GLIBCXX_3.4 (2)
         5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND snprintf@GLIBC_2.2.5 (3)
    

    其中只包含一些调用其它动态库的动态符号内容,像glibc库这些第三方库中的符号。

    主程序在添加-Wl,-E参数编译时,动态符号表内容过多,添加grep过滤(readelf --dyn-syms main_prj | grep register)可得:

    240: 0000000000410dd5   218 FUNC    GLOBAL DEFAULT   14 _Z17register_ioengineP11I
    596: 0000000000410eaf   211 FUNC    GLOBAL DEFAULT   14 _Z19unregister_ioengineP1
    

    可以看出在使用了-Wl,-E参数后,动态符号表中多出了一些主程序中内部函数的符号,包含插件注册与反注册接口的符号(符号修饰名与插件中的修饰名一致)。


 

Copyright © 2018 bbs.dian.org.cn All rights reserved.

与 Dian 的连接断开,我们正在尝试重连,请耐心等待