C/C++插件式设计

Fio中的插件式设计分析,抽取出来一份模板。

一、设计来源

并不是我设计的哈哈哈哈~

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

名词解释:

名词 解释
主程序 调用插件的程序
插件 可以被方便地替换地部分

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

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

text
1
2
3
4
5
6
7
8
9
10
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定义如下

text
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
/* 文件属性,用于保存每个线程操作的文件信息 */
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定义如下

text
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
// 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结构体变量的加载,从而获取到插件中相应函数的地址;插件向主程序注册方式是主程序在加载插件动态库时,自动调用某些”构造函数”,而在构造函数中调用主程序的注册函数可以完成插件的注册。

插件程序定义如下:

text
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
// 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变量,否则无法完成插件的加载。

主动加载示例如下:

text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 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 插件向主程序注册

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

text
1
2
3
4
5
6
7
8
9
10
// 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);
}