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. 1Python运行Appium测试的例子

    电脑系统配置:Windows7的64位 1.Python sample(Python示例)下载 https://github.com/appium/sample-code/tree/master/sa ...

  2. 分享学习linux网站

    1.实验楼 https://www.shiyanlou.com/     免费给你配置一台远端的linux电脑, 你可以根据步骤操作 2.鸟哥的Linux 私房菜 http://linux.vbird ...

  3. 文本处理命令(sort+uniq+cut+tr+wc)+三剑客之sed

    目录 文本处理命令+三剑客之sed 一.文本处理命令 1.排序命令 sort 2.检查/删除命令 uniq 3. cut 显示特定部分命令 4. 替换或删除命令 tr 5.统计 计算数字命令 wc 二 ...

  4. netty系列之:channel和channelGroup

    目录 简介 神龙见首不见尾的channel channel和channelGroup channelGroup的基本使用 将关闭的channel自动移出 同时关闭serverChannel和accep ...

  5. Nginx服务器SSL证书安装

    操作场景 本文档指导您如何在 Nginx 服务器中安装 SSL 证书. 说明: 本文档以证书名称 www.domain.com 为例. Nginx 版本以 nginx/1.16.0 为例. 当前服务器 ...

  6. python3批量统计用户电脑配置

    最近领导想统计一下用户电脑配置信息.好几百人难道让我一个一个的去弄吗? 想想还是写个程序接收一下吧. 客户端 # -*- coding: utf-8 -*- #author:Guoyabin impo ...

  7. 搭建开源跳板机——jumpserver

    搭建开源跳板机mobaxterm 官方文档:https://jumpserver.readthedocs.io/zh/master/ $ yum update -y # 防火墙 与 selinux 设 ...

  8. systemd配置文件填写了ExecStop=/usr/bin/kill -9 $MAINPID之后重启在messages发生了报错

    原因在于systemd模块需要增加自动化检测,检测有一项为检测messages日志内是否有systemd的failed 写了一个检测脚本,脚本的检测messages内容为/bin/cat /var/l ...

  9. 昨天面试被问到的 缓存淘汰算法FIFO、LRU、LFU及Java实现

    缓存淘汰算法 在高并发.高性能的质量要求不断提高时,我们首先会想到的就是利用缓存予以应对. 第一次请求时把计算好的结果存放在缓存中,下次遇到同样的请求时,把之前保存在缓存中的数据直接拿来使用. 但是, ...

  10. .NET Standard与BCL有什么区别?

    Net标准主要是为了改善代码共享,并使每个.Net实现中的API更加一致. .NET Standard 是.NET 平台(.net framework\.net core\.net mono)尚未在实 ...