OFA定义了一组标准的Verbs,并提供了一个标准库libibvers。在用户态实现NVMe over RDMA的Host(i.e. Initiator)和Target, 少不了要跟OFA定义的Verbs打交道。但是,仅仅有libibverbs里的API是不够的,还需要对应的RDMA硬件的用户态驱动支持。在前文中,我们分析了内核态ib_post_send()的实现,理解了内核空间的回调函数post_send()是如何跟mlx5卡的设备驱动函数mlx5_ib_post_send()关联在一起的。本着“知其然更知其所以然”的精神,本文将继续以mlx5卡为例,分析用户态Verb API ibv_post_send()的实现原理。 分析用到的源码包有:

在用户态的libibverbs中, ibv_post_send()的源代码片段如下:

/* libibverbs-1.2.1/include/infiniband/verbs.h#1860 */

1860 /**
1861 * ibv_post_send - Post a list of work requests to a send queue.
....
1865 */
1866 static inline int ibv_post_send(struct ibv_qp *qp, struct ibv_send_wr *wr,
1867 struct ibv_send_wr **bad_wr)
1868 {
1869 return qp->context->ops.post_send(qp, wr, bad_wr);
1870 }

从L1869我们可以看出,post_send()是一个回调(callback)函数,跟RDMA硬件驱动密切相关。

而在mlx5卡的用户态驱动libmlx5的REDME中,我们可以看到libmlx5是一个为libibverbs准备的plug-in模块,允许应用程序在用户空间直接访问Mellanox的硬件mlx5 HCA卡。 当应用程序开发人员使用libibverbs的时候,用户态驱动libmlx5被自动加载。但是,必须首先加载mlx5卡的内核驱动(mlx5_ib.ko)以发现和使用HCA设备。那么,为什么必须率先加载mlx5_ib.ko模块?这是一个值得深究的问题。 (难道libmlx5用户态驱动没有发现HCA卡的能力?)

$ cat -n libmlx5-1.2.1/README
1 Introduction
2 ============
3
4 libmlx5 is a userspace driver for Mellanox ConnectX InfiniBand HCAs.
5 It is a plug-in module for libibverbs that allows programs to use
6 Mellanox hardware directly from userspace. See the libibverbs package
7 for more information.
8
9 Using libmlx5
10 ==============
11
12 libmlx5 will be loaded and used automatically by programs linked with
13 libibverbs. The mlx5_ib kernel module must be loaded for HCA devices
14 to be detected and used.

要搞清楚ibv_post_send()是如何将工作请求send_wr发送到mlx5硬件上去的,我们需要搞清楚下面4个问题。

  • 问题1:回调函数post_send()与struct ibv_qp的关系
  • 问题2:回调函数post_send()的初始化
  • 问题3:回调函数post_send()在mlx5用户态驱动中的实现
  • 问题4:为什么使用mlx5卡的用户态驱动还需要内核态驱动mlx5_ib.ko的支持

问题1:回调函数post_send()与struct ibv_qp的关系

1.1 struct ibv_qp

/* libibverbs-1.2.1/include/infiniband/verbs.h#837 */
837 struct ibv_qp {
838 struct ibv_context *context;
...
852 };

上面的结构体解释了ibv_post_send()函数实现中的qp->context。

1.2 struct ibv_context

/* libibverbs-1.2.1/include/infiniband/verbs.h#1185 */
1185 struct ibv_context {
1186 struct ibv_device *device;
1187 struct ibv_context_ops ops;
....
1193 };

上面的结构体解释了ibv_post_send()函数实现中的qp->context->ops。

1.3 struct ibv_context_ops

/* libibverbs-1.2.1/include/infiniband/verbs.h#1127 */
1127 struct ibv_context_ops {
....
1172 int (*post_send)(struct ibv_qp *qp, struct ibv_send_wr *wr,
1173 struct ibv_send_wr **bad_wr);
....
1183 };

上面的结构体解释了ibv_post_send()函数实现中的qp->context->ops.post_send(...)。 那么,回调函数指针post_send()是什么时候被赋值的(也就是初始化)?这是我们接下来需要探索的问题。

问题2:回调函数post_send()的初始化

2.1 注册mlx5用户态驱动的入口函数mlx5_register_driver() 调用verbs_register_driver()

/* libmlx5-1.2.1/src/mlx5.c#845 */
845 static __attribute__((constructor)) void mlx5_register_driver(void)
846 {
847 verbs_register_driver("mlx5", mlx5_driver_init);
848 }

注意: 函数mlx5_register_driver()在main()函数之前被调用,不是很容易理解。那么,有必要先写个demo解释一下__attribute__((constructor))

  • foo.c
 1 #include <stdio.h>
2
3 int main(int argc, char *argv[])
4 {
5 printf("Enter into %s()\n", __func__);
6 return 0;
7 }
8
9 static __attribute__((constructor)) void mlx5_register_driver(void)
10 {
11 printf("Enter into %s()\n", __func__);
12 }
  • 编译并运行
$ gcc -g -Wall -o foo foo.c
$ ./foo
Enter into mlx5_register_driver()
Enter into main()
$
$ gdb foo
GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.3) 7.7.1
...<snip>...
(gdb) b _start
Breakpoint 1 at 0x8048320
(gdb) b main
Breakpoint 2 at 0x8048426: file foo.c, line 5.
(gdb) b mlx5_register_driver
Breakpoint 3 at 0x8048447: file foo.c, line 11.
(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x08048320 <_start>
2 breakpoint keep y 0x08048426 in main at foo.c:5
3 breakpoint keep y 0x08048447 in mlx5_register_driver at foo.c:11
(gdb) r
Starting program: /tmp/foo Breakpoint 1, 0x08048320 in _start ()
(gdb) #
(gdb) c
Continuing. Breakpoint 3, mlx5_register_driver () at foo.c:11
11 printf("Enter into %s()\n", __func__);
(gdb) #
(gdb) c
Continuing.
Enter into mlx5_register_driver() Breakpoint 2, main (argc=1, argv=0xbffff084) at foo.c:5
5 printf("Enter into %s()\n", __func__);
(gdb) #
(gdb) c
Continuing.
Enter into main()
[Inferior 1 (process 14542) exited normally]
(gdb) q

从上面的输出可以看出,被__attribute__((constructor))限定的函数mlx5_register_driver()在主函数main()之前被调用。 更多解释请阅读__attribute__ ((constructor)) 用法解析

让我们暂时放下verbs_register_driver()不管,径直分析post_send()是如何被初始化的。

2.2 mlx5_driver_init()设置mlx5设备dev->verbs_dev.init_context为mlx5_init_context()

/* libmlx5-1.2.1/src/mlx5.c#791 */
791 static struct verbs_device *mlx5_driver_init(const char *uverbs_sys_path,
792 int abi_version)
793 {
794 char value[8];
795 struct mlx5_device *dev;
796 unsigned vendor, device;
797 int i;
798
799 if (ibv_read_sysfs_file(uverbs_sys_path, "device/vendor",
800 value, sizeof value) < 0)
801 return NULL;
802 sscanf(value, "%i", &vendor);
803
804 if (ibv_read_sysfs_file(uverbs_sys_path, "device/device",
805 value, sizeof value) < 0)
806 return NULL;
807 sscanf(value, "%i", &device);
808
809 for (i = 0; i < sizeof hca_table / sizeof hca_table[0]; ++i)
810 if (vendor == hca_table[i].vendor &&
811 device == hca_table[i].device)
812 goto found;
813
814 return NULL;
815
816 found:
817 if (abi_version < MLX5_UVERBS_MIN_ABI_VERSION ||
818 abi_version > MLX5_UVERBS_MAX_ABI_VERSION) {
...
824 return NULL;
825 }
826
827 dev = malloc(sizeof *dev);
...
834 dev->page_size = sysconf(_SC_PAGESIZE);
835 dev->driver_abi_ver = abi_version;
836 dev->verbs_dev.sz = sizeof(*dev);
837 dev->verbs_dev.size_of_context = sizeof(struct mlx5_context) -
838 sizeof(struct ibv_context);
839 dev->verbs_dev.init_context = mlx5_init_context;
840 dev->verbs_dev.uninit_context = mlx5_cleanup_context;
841
842 return &dev->verbs_dev;
843 }

在L839中, dev->verbs_dev.init_context被初始化为函数mlx5_init_context。

839    dev->verbs_dev.init_context = mlx5_init_context;

2.3 mlx5_init_context()设置context->ibv_ctx.ops为全局结构体变量mlx5_ctx_ops

/* libmlx5-1.2.1/src/mlx5.c#588 */
588 static int mlx5_init_context(struct verbs_device *vdev,
589 struct ibv_context *ctx, int cmd_fd)
590 {
591 struct mlx5_context *context;
...
611 context = to_mctx(ctx);
...
734 context->ibv_ctx.ops = mlx5_ctx_ops;
...
771 }

在L734中, context->ibv_ctx.ops被初始化为全局结构体变量mlx5_ctx_ops,而mlx5_ctx_ops的类型为struct ibv_context_ops。

2.4 在mlx5_ctx_ops中初始化回调函数post_send()

/* libmlx5-1.2.1/src/mlx5.c#90 */
90 static struct ibv_context_ops mlx5_ctx_ops = {
...
116 .post_send = mlx5_post_send,
...
122 };

在L116中,回调函数post_send()被静态地初始化为mlx5_post_send。也就是说,对于mlx5卡用户态驱动的消费者来说,调用ibv_post_send(),最终会落到mlx5_post_send()函数调用上

问题3:回调函数post_send()在mlx5用户态驱动中的实现

3.1 mlx5_post_send()调用_mlx5_post_send()

/* libmlx5-1.2.1/src/qp.c#897 */
897 int mlx5_post_send(struct ibv_qp *ibqp, struct ibv_send_wr *wr,
898 struct ibv_send_wr **bad_wr)
899 {
...
921 return _mlx5_post_send(ibqp, wr, bad_wr);
922 }

在L921调用_mlx5_post_send()。

3.2 _mlx5_post_send()驱动RDMA-Aware硬件(也就是mlx5卡)

/* libmlx5-1.2.1/src/qp.c#559 */
559 static inline int _mlx5_post_send(struct ibv_qp *ibqp, struct ibv_send_wr *wr,
560 struct ibv_send_wr **bad_wr)
561 {
562 struct mlx5_context *ctx;
563 struct mlx5_qp *qp = to_mqp(ibqp);
...
589 for (nreq = 0; wr; ++nreq, wr = wr->next) {
...
849 }
...
895 }

_mlx5_post_send()的代码很长,从上面的代码片段中我们不难发现,用户态驱动函数_mlx5_post_send()就是直接跟mlx5卡(硬件)打交道。 换言之,对mlx5卡的消费者来说,当用户空间的应用程序调用libibverbs中的API ibv_post_send()的时候,本质上就是通过_mlx5_post_send()去直接访问mlx5硬件。

问题4:为什么使用mlx5卡的用户态驱动还需要内核态驱动mlx5_ib.ko的支持

我们在一开始就提出了一个疑问:“难道libmlx5用户态驱动没有发现HCA卡的能力?” 这个问题可以问得更具体一些,“难道libmlx5用户态驱动没有直接通过PCIe发现HCA卡的能力?” 在回答这个问题之前,让我们回到2.1看看verbs_register_driver()的实现。libmlx5用户态驱动注册采用的代码如下:

/* libmlx5-1.2.1/src/mlx5.c#845 */
845 static __attribute__((constructor)) void mlx5_register_driver(void)
846 {
847 verbs_register_driver("mlx5", mlx5_driver_init);
848 }

我们在前面沿着mlx5_driver_init()的逻辑分析了post_send()在用户态驱动libmlx5中的具体实现。现在是时候一步一步分析用户态驱动libmlx5是如何注册到libibverbs中去的了。

4.1 verbs_register_driver()调用register_driver()

/* libibverbs-1.2.1/src/init.c#188 */
188 void verbs_register_driver(const char *name, verbs_driver_init_func init_func)
189 {
190 register_driver(name, NULL, init_func);
191 }

而verbs_dirver_init_func的定义是这样的:

/* libibverbs-1.2.1/include/infiniband/driver.h#96 */
96 typedef struct verbs_device *(*verbs_driver_init_func)(const char *uverbs_sys_path,
97 int abi_version);

mlx5_driver_init()的函数原型正好是:

791 static struct verbs_device *mlx5_driver_init(const char *uverbs_sys_path,
792 int abi_version)

那么,接下来我们看看mlx5_driver_init()被放置到什么地方去了。

4.2 register_driver()把mlx5_driver_init()放置到一个链表结点上

/* libibverbs-1.2.1/src/init.c#157 */

157 static void register_driver(const char *name, ibv_driver_init_func init_func,
158 verbs_driver_init_func verbs_init_func)
159 {
160 struct ibv_driver *driver;
161
162 driver = malloc(sizeof *driver);
...
168 driver->name = name;
169 driver->init_func = init_func;
170 driver->verbs_init_func = verbs_init_func;
171 driver->next = NULL;
172
173 if (tail_driver)
174 tail_driver->next = driver;
175 else
176 head_driver = driver;
177 tail_driver = driver;
178 }

L160: 定义一个类型为struct ibv_driver的结构体变量driver,该变量将作为一个链表结点。struct ibv_driver的定义如下:

/* libibverbs-1.2.1/src/init.c#70 */
70 struct ibv_driver {
71 const char *name;
72 ibv_driver_init_func init_func;
73 verbs_driver_init_func verbs_init_func;
74 struct ibv_driver *next;
75 };

L162: 为结构体变量driver申请内存空间
L168: 设置driver->name, e.g. "mlx5"
L169: 设置driver->init_func, e.g. NULL
L170: 设置driver->verbs_init_func, e.g. mlx5_driver_init
L171: 设置driver->next 为 NULL
L173-177: 维护全局链表head_driver, tail_driver可以理解为指向该链表的尾结点的指针,那么在L162申请的结点driver就是通过尾插法加入到链表head_driver中去的。

/* libibverbs-1.2.1/src/init.c#79 */
79 static struct ibv_driver *head_driver, *tail_driver;

接下来,我们需要去看看究竟是谁在消费全局链表head_driver。

4.3 消费全局链表head_driver的是try_drivers()函数

/* libibverbs-1.2.1/src/init.c#408 */
408 static struct ibv_device *try_drivers(struct ibv_sysfs_dev *sysfs_dev)
409 {
410 struct ibv_driver *driver;
411 struct ibv_device *dev;
412
413 for (driver = head_driver; driver; driver = driver->next) {
414 dev = try_driver(driver, sysfs_dev);
415 if (dev)
416 return dev;
417 }
418
419 return NULL;
420 }

在L413-417中,遍历全局链表head_driver, 针对单个结点driver在L414调用try_driver(driver, sysfs_dev)函数。如果匹配成功,则理解返回对应的ibv设备(struct ibv_device)。 接下来,我们从try_drivers()出发,逆向分析一下函数调用栈。

4.4 调用try_drivers()的是ibvers_init()

/* libibverbs-1.2.1/src/init.c#480 */
480 HIDDEN int ibverbs_init(struct ibv_device ***list)
481 {
...
510 ret = find_sysfs_devs();
...
514 for (sysfs_dev = sysfs_dev_list; sysfs_dev; sysfs_dev = sysfs_dev->next) {
515 device = try_drivers(sysfs_dev);
...
521 }
...
575 } /* libibverbs-1.2.1/src/ibverbs.h#55 */
55 #define HIDDEN __attribute__((visibility ("hidden")))

sysfs_dev是链表sysfs_dev_list上的一个结点。而sysfs_dev_list则是由L510调用find_sysfs_devs()创建的。 关于find_sysfs_devs()的实现,暂且不表。

4.5 调用ibverbs_init()的是count_devices()

/* libibverbs-1.2.1/src/device.c#56 */
53 static int num_devices;
54 static struct ibv_device **device_list;
55
56 static void count_devices(void)
57 {
58 num_devices = ibverbs_init(&device_list);
59 }

4.6 设置count_devices()的是__ibv_get_device_list()

/* libibverbs-1.2.1/src/device.c#61 */
52 static pthread_once_t device_list_once = PTHREAD_ONCE_INIT;
..
61 struct ibv_device **__ibv_get_device_list(int *num)
62 {
..
69 pthread_once(&device_list_once, count_devices);
..
88 }

在L69中,函数count_devices()被dispatch到一个线程中,当且仅当执行一次。 那么,是谁调用或设置__ibv_get_device_list()呢?

4.7 __ibv_get_device_list的别名被设置为ibv_get_device_list

/* libibverbs-1.2.1/src/device.c#89 */
89 default_symver(__ibv_get_device_list, ibv_get_device_list);

而default_symver的宏定义是

/* libibverbs-1.2.1/src/ibverbs.h#69 */
62 #ifdef HAVE_SYMVER_SUPPORT
63 # define symver(name, api, ver) \
64 asm(".symver " #name "," #api "@" #ver)
65 # define default_symver(name, api) \
66 asm(".symver " #name "," #api "@@" DEFAULT_ABI)
67 #else
68 # define symver(name, api, ver)
69 # define default_symver(name, api) \
70 extern __typeof(name) api __attribute__((alias(#name)))
71 #endif /* HAVE_SYMVER_SUPPORT */

于是,L89展开后(假设走#else分支)就是

extern __typeof(__ibv_get_device_list) ibv_get_device_list __attribute__((alias("__ibv_get_device_list")));

为了帮助理解 __attribute__((alias("FuncName"))), 下面给出一个demo。

  • foo.c
 1 #include <stdio.h>
2
3 int __ibv_xxx()
4 {
5 printf("Enter into %s\n", __func__);
6 return 0;
7 }
8
9 extern __typeof(__ibv_xxx) ibv_xxx __attribute__((alias("__ibv_xxx")));
10
11 int main(int argc, char *argv[])
12 {
13 return ibv_xxx();
14 }
  • 编译并运行
$ gcc -g -Wall -o foo foo.c
$ ./foo
Enter into __ibv_xxx
$
$ gdb foo
...<snip>...
(gdb) disas /m main
Dump of assembler code for function main:
12 {
0x0804843e <+0>: push %ebp
0x0804843f <+1>: mov %esp,%ebp
0x08048441 <+3>: and $0xfffffff0,%esp 13 return ibv_xxx();
0x08048444 <+6>: call 0x804841d <__ibv_xxx> 14 }
0x08048449 <+11>: leave
0x0804844a <+12>: ret End of assembler dump.
(gdb) q

通过反汇编,虽然在main()调用的是ibv_xxx(),但是本质上是调用__ibv_xxx()。

4.8 用户应用程序负责调用ibv_get_device_list()

ibv_get_device_list()是一个verbs API,调用ibv_post_send()之前必须先调用ibv_get_device_list去获取RDMA设备列表。 关于ibv_get_device_list的使用说明,请参见:

于是,我们可以得到如下函数调用栈:

0. ibv_get_device_list()        # start by User's Application
|
v
1. cout_devices() # @libibverbs-1.2.1/src/device.c#56
|
v
2. ibverbs_init() # @libibverbs-1.2.1/src/init.c#480
|
v
3. try_drivers() # @libibverbs-1.2.1/src/init.c#408
|
v
4. try_driver() # @libibverbs-1.2.1/src/init.c#349

接下来,我们将分析try_driver(), 搞清楚mlx5设备是如何被发现的。也就是说,接下来将进入最精彩的部分 -- 用户态驱动libmlx5为什么需要内核态驱动mlx5_ib.ko的支持。

4.9 find_sysfs_devs()负责发现所有RDMA设备

/* libibverbs-1.2.1/src/init.c#81 */
81 static int find_sysfs_devs(void)
82 {
83 char class_path[IBV_SYSFS_PATH_MAX];
84 DIR *class_dir;
85 struct dirent *dent;
86 struct ibv_sysfs_dev *sysfs_dev = NULL;
87 char value[8];
88 int ret = 0;
89
90 snprintf(class_path, sizeof class_path, "%s/class/infiniband_verbs",
91 ibv_get_sysfs_path());
92
93 class_dir = opendir(class_path);
94 if (!class_dir)
95 return ENOSYS;
96
97 while ((dent = readdir(class_dir))) {
98 struct stat buf;
99
100 if (dent->d_name[0] == '.')
101 continue;
102
103 if (!sysfs_dev)
104 sysfs_dev = malloc(sizeof *sysfs_dev);
105 if (!sysfs_dev) {
106 ret = ENOMEM;
107 goto out;
108 }
109
110 snprintf(sysfs_dev->sysfs_path, sizeof sysfs_dev->sysfs_path,
111 "%s/%s", class_path, dent->d_name);
112
113 if (stat(sysfs_dev->sysfs_path, &buf)) {
114 fprintf(stderr, PFX "Warning: couldn't stat '%s'.\n",
115 sysfs_dev->sysfs_path);
116 continue;
117 }
118
119 if (!S_ISDIR(buf.st_mode))
120 continue;
121
122 snprintf(sysfs_dev->sysfs_name, sizeof sysfs_dev->sysfs_name,
123 "%s", dent->d_name);
124
125 if (ibv_read_sysfs_file(sysfs_dev->sysfs_path, "ibdev",
126 sysfs_dev->ibdev_name,
127 sizeof sysfs_dev->ibdev_name) < 0) {
128 fprintf(stderr, PFX "Warning: no ibdev class attr for '%s'.\n",
129 dent->d_name);
130 continue;
131 }
132
133 snprintf(sysfs_dev->ibdev_path, sizeof sysfs_dev->ibdev_path,
134 "%s/class/infiniband/%s", ibv_get_sysfs_path(),
135 sysfs_dev->ibdev_name);
136
137 sysfs_dev->next = sysfs_dev_list;
138 sysfs_dev->have_driver = 0;
139 if (ibv_read_sysfs_file(sysfs_dev->sysfs_path, "abi_version",
140 value, sizeof value) > 0)
141 sysfs_dev->abi_ver = strtol(value, NULL, 10);
142 else
143 sysfs_dev->abi_ver = 0;
144
145 sysfs_dev_list = sysfs_dev;
146 sysfs_dev = NULL;
147 }
148
149 out:
150 if (sysfs_dev)
151 free(sysfs_dev);
152
153 closedir(class_dir);
154 return ret;
155 }

在L137,138,145,146中,函数finds_sysfs_devs()把发现的所有设备都通过头插法保存在全局链表sysfs_dev_list上。

/* libibverbs-1.2.1/src/init.c#77 */
77 static struct ibv_sysfs_dev *sysfs_dev_list;

而每一个设备的数据类型为:

/* libibverbs-1.2.1/src/init.c#55 */
55 struct ibv_sysfs_dev {
56 char sysfs_name[IBV_SYSFS_NAME_MAX];
57 char ibdev_name[IBV_SYSFS_NAME_MAX];
58 char sysfs_path[IBV_SYSFS_PATH_MAX];
59 char ibdev_path[IBV_SYSFS_PATH_MAX];
60 struct ibv_sysfs_dev *next;
61 int abi_ver;
62 int have_driver;
63 };

在find_sysfs_devs()中, 对于mlx5设备来说(假定只有一个mlx5卡),我们不难推导出:

  • L90-91: class_path为/sys/class/infiniband_verbs
  • L110-111: sysfs_dev->sysfs_path为/sys/class/infiniband_verbs/mlx5
  • L122-123: sysfs_dev->sysfs_name为mlx5
  • L125-131: sysfs_dev->ibdev_name为mlx5_0
  • L133-135: sysfs_dev->ibdev_path为/sys/class/infiniband/mlx5_0

无论是/sys/class/infiniband_verbs还是/sys/class/infiniband路径,都跟sysfs紧密相关。那么,谁有能力在/sys/class/infiniband下面创建设备信息?答案自然是RDMA卡的内核驱动,比如mlx5_ib.ko。因此,我们可以看到,libmlx5和libibverbs紧密配合发现mlx5设备,但是没有直接使用PCIe,而是借助于内核驱动mlx5_ib.ko在加载时创建的sysfs信息。到此为止,我们可以得到如下证据确凿的结论:

用户态驱libmlx5没有通过PCIe发现mlx5设备的能力,因为基于sysfs信息去发现mlx5设备,所以mlx5的Linux内核驱动是必须的。而mlx5的内核驱动,自然是通过PCIe去sysfs那里注册对应的mlx5设备的。

The good seaman is known in bad weather. | 惊涛骇浪,方显英雄本色。

[SPDK/NVMe存储技术分析]012 - 用户态ibv_post_send()源码分析的更多相关文章

  1. [SPDK/NVMe存储技术分析]015 - 理解内存注册(Memory Registration)

    使用RDMA, 必然关系到内存区域(Memory Region)的注册问题.在本文中,我们将以mlx5 HCA卡为例回答如下几个问题: 为什么需要注册内存区域? 注册内存区域有嘛好处? 注册内存区域的 ...

  2. 🏆【Alibaba微服务技术系列】「Dubbo3.0技术专题」回顾Dubbo2.x的技术原理和功能实现及源码分析(温故而知新)

    RPC服务 什么叫RPC? RPC[Remote Procedure Call]是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范.它允许程序调用另一个地址空间(通常是共享网络的另 ...

  3. 深度 Mybatis 3 源码分析(一)SqlSessionFactoryBuilder源码分析

    MyBatis消除了几乎所有的JDBC代码和参数的手工设置以及对结果集的检索封装.MyBatis可以使用简单的XML或注解用于配置和原始映射,将接口和Java的POJO(Plain Old Java ...

  4. 第十篇:Spark SQL 源码分析之 In-Memory Columnar Storage源码分析之 query

    /** Spark SQL源码分析系列文章*/ 前面讲到了Spark SQL In-Memory Columnar Storage的存储结构是基于列存储的. 那么基于以上存储结构,我们查询cache在 ...

  5. 第九篇:Spark SQL 源码分析之 In-Memory Columnar Storage源码分析之 cache table

    /** Spark SQL源码分析系列文章*/ Spark SQL 可以将数据缓存到内存中,我们可以见到的通过调用cache table tableName即可将一张表缓存到内存中,来极大的提高查询效 ...

  6. 源码分析系列1:HashMap源码分析(基于JDK1.8)

    1.HashMap的底层实现图示 如上图所示: HashMap底层是由  数组+(链表)+(红黑树) 组成,每个存储在HashMap中的键值对都存放在一个Node节点之中,其中包含了Key-Value ...

  7. $Django cbv源码分析 djangorestframework框架之APIView源码分析

    1 CBV的源码分析 #视图 class login (View): pass #路由 url(r'^books/$', views.login.as_view()) #阅读源码: #左侧工程栏--- ...

  8. [SPDK/NVMe存储技术分析]011 - 内核态ib_post_send()源码剖析

    OFA定义了一组标准的Verbs,并在用户态提供了一个标准库libibverbs.例如将一个工作请求(WR)放置到发送队列的Verb API是ibv_post_send(), 但是在Linux内核,对 ...

  9. [SPDK/NVMe存储技术分析]008 - RDMA概述

    毫无疑问地,用来取代iSCSI/iSER(iSCSI Extensions for RDMA)技术的NVMe over Fabrics着实让RDMA又火了一把.在介绍NVMe over Fabrics ...

随机推荐

  1. JAVA 对象的创建与克隆

    目录 一.对象的4种创建方式 二.通过new创建对象 三.反射 四.克隆对象 浅拷贝 深拷贝 五.反序列化 六.补充 一.对象的4种创建方式 new 创建 反射 克隆 反序列化 二.通过new创建对象 ...

  2. Solution -「洛谷 P4389」付公主的背包

    \(\mathcal{Description}\)   Link.   容量为 \(n\),\(m\) 种物品的无限背包,求凑出每种容量的方案数,对 \(998244353\) 取模.   \(n,m ...

  3. 用 JuiceFS 备份 Nginx 日志可以这么简单

    在我们线上的生产环境中要备份的东西很多,各种服务日志.数据库数据.用户上传数据.代码等等.用 JuiceFS 来备份可以节省你大量时间,我们会围绕这个主题写一系列的教程,整理出一套最佳实践,方便大家. ...

  4. validator参数校验

    目录 validator参数校验 validator参数校验 type Req struct { Sn string `json:"sn" binding:"requir ...

  5. 部署Nginx网站服务

    编译安装Nginx nginx 官方 : http://nginx.org/ yum -y install pcre-devel zlib-devel   ##安装相关依赖包 ./configure ...

  6. 使用讯飞tts+ffmpeg自动生成视频

    参考 FFmpeg 讯飞离线语音合成 起因 某日,看到一个营销号的视频说做视频日进斗金,大意是用软件识别文章小说,搭配一些图片转换成自己的视频.看完当时脑海里冒出一个念头,我也可以,于是有了这番尝试. ...

  7. 使用helm安装ingress,实现用域名的方式访问k8s内部应用

    k8s集群版本 k8s集群版本是1.22 提前部署好nginx服务和创建好svc deployment方式部署的nginx服务,1个副本 创建的服务 通过服务可以代理到nginx服务 curl 10. ...

  8. Windows server 2012安装vm-tools遇到的问题

    Windows server 2012安装VM tools异常解决办法 在VMWare虚拟机上安装Windows Server 2012之 后安装VMWare Tools时报如下错误信息: 问题:缺少 ...

  9. 学习Spring5必知必会(5)~Spring AOP

    一.学习 AOP 思想的准备工作: 1.横切面关注点 在开发中,为了给业务方法中增加日志记录,权限检查,事务控制等功能,此时我们需要在修改业务方法内添加这些零散的功能代码(横切面关注点). 这些零散存 ...

  10. linux系统挂载磁盘

    linux系统挂载磁盘 ## 将硬盘挂载到/mnt/disk1下 # 检查磁盘状态,确认分区 fdisk -l # 格式化磁盘(分区) mkfs.ext4 /dev/sda #根据机器上的命名 以sd ...