[SPDK/NVMe存儲技術分析]012 - 用戶態ibv_post_send()源碼分析


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. | 驚濤駭浪,方顯英雄本色。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM