Group Details Private

Group Leaders

在任项目组组长

  • 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参数后,动态符号表中多出了一些主程序中内部函数的符号,包含插件注册与反注册接口的符号(符号修饰名与插件中的修饰名一致)。

    posted in C系
  • RE: 蓝牙广播

    很好,学习了

    posted in 嵌入式方向
  • RE: yyj的STM32学习笔记

    1、是同相放大器、反相放大器。“xiang”指的是相位,不是方向。
    2、为什么要设置为推挽输出?
    3、如何计算定时器的周期?
    4、在这个过程中遇到了什么问题?又是如何解决的?

    posted in 嵌入式方向
  • 蓝牙广播

    一、什么是扫描?

    蓝牙从机在位建立连接时会对外广播自身的信息,主机寻找、发现从机的过程就叫做扫描。在扫描时,主机可以获取到从机的地址、广播数据、RSSI灯信息,并可以据此决定是否与从机建立连接

    二、扫描的分类

    扫描分为主动扫描和被动扫描两种,此处先拿出两个概念:扫描请求和扫描响应

    • 扫描请求:由链路层处于扫描态的设备发送,链路层处于广播态的设备接收
    • 扫描响应:由链路层处于广播态的设备发送,链路层处于扫描态的设备接收

    注:扫描请求和扫描响应都属于广播包,不是连接包

    区别

    • 被动扫描只接收广播包,不发送扫描请求
    • 主动扫描接收广播包后向广播态设备发起扫描请求,并通过返回的扫描相应获取额外数据

    三、被动扫描流程

    0_1600483783142_1025b5a1-2b27-460b-9fd5-653149936f5a-image.png image-20200919104255413

    四、主动扫描流程

    0_1600483749516_7f95466e-9d85-4dbd-8ef5-ef853c57228e-image.png image-20200919104339025

    扫描请求和扫描响应均由协议栈完成,应用程序只参与到扫描的配置和启动,以及信息的处理

    五、扫描参数

    • 扫描窗口:一次扫描进行的时间宽度
    • 扫描间隔:两个连续扫描窗口的起始时间之间的时间间隔,包括扫描休息的时间和扫描进行的时间
    • 扫描超时时间:即扫描持续时间,可配置为不超时

    0_1600483723455_c5c912c7-f303-40f7-87c4-6e0b62b92992-image.png

    posted in 嵌入式方向
  • RE: STM32学习笔记(二)

    0_1599896287701_75cded8d-16b1-4269-8025-ef8c6681d9b3-image.png
    对于这一处的说法我不是很理解。对于查询来说,他要不断地去查询相应的寄存器,但如果是中断,我可以不管他的计数,直到中断的产生,中断产生之后的操作和定时器查询完成之后要做的操作应该是大同小异的。这么看的话,中断可以把查询的时间节省出来去做其他的事情,效率更高才对

    posted in 嵌入式方向

Looks like your connection to Dian was lost, please wait while we try to reconnect.