1 /* SPDX-License-Identifier: BSD-3-Clause 2 * Copyright (C) 2017 Intel Corporation. 3 * All rights reserved. 4 */ 5 6 #include "spdk/stdinc.h" 7 8 #include "env_internal.h" 9 10 #include "spdk/version.h" 11 #include "spdk/env_dpdk.h" 12 #include "spdk/log.h" 13 #include "spdk/config.h" 14 15 #include <openssl/ssl.h> 16 #include <openssl/err.h> 17 18 #include <rte_config.h> 19 #include <rte_eal.h> 20 #include <rte_errno.h> 21 #include <rte_vfio.h> 22 23 #define SPDK_ENV_DPDK_DEFAULT_NAME "spdk" 24 #define SPDK_ENV_DPDK_DEFAULT_SHM_ID -1 25 #define SPDK_ENV_DPDK_DEFAULT_MEM_SIZE -1 26 #define SPDK_ENV_DPDK_DEFAULT_MAIN_CORE -1 27 #define SPDK_ENV_DPDK_DEFAULT_MEM_CHANNEL -1 28 #define SPDK_ENV_DPDK_DEFAULT_CORE_MASK "0x1" 29 #define SPDK_ENV_DPDK_DEFAULT_BASE_VIRTADDR 0x200000000000 30 31 #define DPDK_ALLOW_PARAM "--allow" 32 #define DPDK_BLOCK_PARAM "--block" 33 #define DPDK_MAIN_CORE_PARAM "--main-lcore" 34 35 static char **g_eal_cmdline; 36 static int g_eal_cmdline_argcount; 37 static bool g_external_init = true; 38 39 static char * 40 _sprintf_alloc(const char *format, ...) 41 { 42 va_list args; 43 va_list args_copy; 44 char *buf; 45 size_t bufsize; 46 int rc; 47 48 va_start(args, format); 49 50 /* Try with a small buffer first. */ 51 bufsize = 32; 52 53 /* Limit maximum buffer size to something reasonable so we don't loop forever. */ 54 while (bufsize <= 1024 * 1024) { 55 buf = malloc(bufsize); 56 if (buf == NULL) { 57 va_end(args); 58 return NULL; 59 } 60 61 va_copy(args_copy, args); 62 rc = vsnprintf(buf, bufsize, format, args_copy); 63 va_end(args_copy); 64 65 /* 66 * If vsnprintf() returned a count within our current buffer size, we are done. 67 * The count does not include the \0 terminator, so rc == bufsize is not OK. 68 */ 69 if (rc >= 0 && (size_t)rc < bufsize) { 70 va_end(args); 71 return buf; 72 } 73 74 /* 75 * vsnprintf() should return the required space, but some libc versions do not 76 * implement this correctly, so just double the buffer size and try again. 77 * 78 * We don't need the data in buf, so rather than realloc(), use free() and malloc() 79 * again to avoid a copy. 80 */ 81 free(buf); 82 bufsize *= 2; 83 } 84 85 va_end(args); 86 return NULL; 87 } 88 89 void 90 spdk_env_opts_init(struct spdk_env_opts *opts) 91 { 92 if (!opts) { 93 return; 94 } 95 96 memset(opts, 0, sizeof(*opts)); 97 98 opts->name = SPDK_ENV_DPDK_DEFAULT_NAME; 99 opts->core_mask = SPDK_ENV_DPDK_DEFAULT_CORE_MASK; 100 opts->shm_id = SPDK_ENV_DPDK_DEFAULT_SHM_ID; 101 opts->mem_size = SPDK_ENV_DPDK_DEFAULT_MEM_SIZE; 102 opts->main_core = SPDK_ENV_DPDK_DEFAULT_MAIN_CORE; 103 opts->mem_channel = SPDK_ENV_DPDK_DEFAULT_MEM_CHANNEL; 104 opts->base_virtaddr = SPDK_ENV_DPDK_DEFAULT_BASE_VIRTADDR; 105 } 106 107 static void 108 free_args(char **args, int argcount) 109 { 110 int i; 111 112 if (args == NULL) { 113 return; 114 } 115 116 for (i = 0; i < argcount; i++) { 117 free(args[i]); 118 } 119 120 if (argcount) { 121 free(args); 122 } 123 } 124 125 static char ** 126 push_arg(char *args[], int *argcount, char *arg) 127 { 128 char **tmp; 129 130 if (arg == NULL) { 131 SPDK_ERRLOG("%s: NULL arg supplied\n", __func__); 132 free_args(args, *argcount); 133 return NULL; 134 } 135 136 tmp = realloc(args, sizeof(char *) * (*argcount + 1)); 137 if (tmp == NULL) { 138 free(arg); 139 free_args(args, *argcount); 140 return NULL; 141 } 142 143 tmp[*argcount] = arg; 144 (*argcount)++; 145 146 return tmp; 147 } 148 149 #if defined(__linux__) && defined(__x86_64__) 150 151 /* TODO: Can likely get this value from rlimits in the future */ 152 #define SPDK_IOMMU_VA_REQUIRED_WIDTH 48 153 #define VTD_CAP_MGAW_SHIFT 16 154 #define VTD_CAP_MGAW_MASK (0x3F << VTD_CAP_MGAW_SHIFT) 155 #define RD_AMD_CAP_VASIZE_SHIFT 15 156 #define RD_AMD_CAP_VASIZE_MASK (0x7F << RD_AMD_CAP_VASIZE_SHIFT) 157 158 static int 159 get_iommu_width(void) 160 { 161 int width = 0; 162 glob_t glob_results = {}; 163 164 /* Break * and / into separate strings to appease check_format.sh comment style check. */ 165 glob("/sys/devices/virtual/iommu/dmar*" "/intel-iommu/cap", 0, NULL, &glob_results); 166 glob("/sys/class/iommu/ivhd*" "/amd-iommu/cap", GLOB_APPEND, NULL, &glob_results); 167 168 for (size_t i = 0; i < glob_results.gl_pathc; i++) { 169 const char *filename = glob_results.gl_pathv[0]; 170 FILE *file = fopen(filename, "r"); 171 uint64_t cap_reg = 0; 172 173 if (file == NULL) { 174 continue; 175 } 176 177 if (fscanf(file, "%" PRIx64, &cap_reg) == 1) { 178 if (strstr(filename, "intel-iommu") != NULL) { 179 /* We have an Intel IOMMU */ 180 int mgaw = ((cap_reg & VTD_CAP_MGAW_MASK) >> VTD_CAP_MGAW_SHIFT) + 1; 181 182 if (width == 0 || (mgaw > 0 && mgaw < width)) { 183 width = mgaw; 184 } 185 } else if (strstr(filename, "amd-iommu") != NULL) { 186 /* We have an AMD IOMMU */ 187 int mgaw = ((cap_reg & RD_AMD_CAP_VASIZE_MASK) >> RD_AMD_CAP_VASIZE_SHIFT) + 1; 188 189 if (width == 0 || (mgaw > 0 && mgaw < width)) { 190 width = mgaw; 191 } 192 } 193 } 194 195 fclose(file); 196 } 197 198 globfree(&glob_results); 199 return width; 200 } 201 202 #endif 203 204 static int 205 build_eal_cmdline(const struct spdk_env_opts *opts) 206 { 207 int argcount = 0; 208 char **args; 209 bool no_huge; 210 211 args = NULL; 212 no_huge = opts->no_huge || (opts->env_context && strstr(opts->env_context, "--no-huge") != NULL); 213 214 /* set the program name */ 215 args = push_arg(args, &argcount, _sprintf_alloc("%s", opts->name)); 216 if (args == NULL) { 217 return -1; 218 } 219 220 /* disable shared configuration files when in single process mode. This allows for cleaner shutdown */ 221 if (opts->shm_id < 0) { 222 args = push_arg(args, &argcount, _sprintf_alloc("%s", "--no-shconf")); 223 if (args == NULL) { 224 return -1; 225 } 226 } 227 228 /* Either lcore_map or core_mask must be set. If both, or none specified, fail */ 229 if ((opts->core_mask == NULL) == (opts->lcore_map == NULL)) { 230 if (opts->core_mask && opts->lcore_map) { 231 fprintf(stderr, 232 "Both, lcore map and core mask are provided, while only one can be set\n"); 233 } else { 234 fprintf(stderr, "Core mask or lcore map must be specified\n"); 235 } 236 free_args(args, argcount); 237 return -1; 238 } 239 240 if (opts->lcore_map) { 241 /* If lcore list is set, generate --lcores parameter */ 242 args = push_arg(args, &argcount, _sprintf_alloc("--lcores=%s", opts->lcore_map)); 243 } else if (opts->core_mask[0] == '-') { 244 /* 245 * Set the coremask: 246 * 247 * - if it starts with '-', we presume it's literal EAL arguments such 248 * as --lcores. 249 * 250 * - if it starts with '[', we presume it's a core list to use with the 251 * -l option. 252 * 253 * - otherwise, it's a CPU mask of the form "0xff.." as expected by the 254 * -c option. 255 */ 256 args = push_arg(args, &argcount, _sprintf_alloc("%s", opts->core_mask)); 257 } else if (opts->core_mask[0] == '[') { 258 char *l_arg = _sprintf_alloc("-l %s", opts->core_mask + 1); 259 260 if (l_arg != NULL) { 261 int len = strlen(l_arg); 262 263 if (l_arg[len - 1] == ']') { 264 l_arg[len - 1] = '\0'; 265 } 266 } 267 args = push_arg(args, &argcount, l_arg); 268 } else { 269 args = push_arg(args, &argcount, _sprintf_alloc("-c %s", opts->core_mask)); 270 } 271 272 if (args == NULL) { 273 return -1; 274 } 275 276 /* set the memory channel number */ 277 if (opts->mem_channel > 0) { 278 args = push_arg(args, &argcount, _sprintf_alloc("-n %d", opts->mem_channel)); 279 if (args == NULL) { 280 return -1; 281 } 282 } 283 284 /* set the memory size */ 285 if (opts->mem_size >= 0) { 286 args = push_arg(args, &argcount, _sprintf_alloc("-m %d", opts->mem_size)); 287 if (args == NULL) { 288 return -1; 289 } 290 } 291 292 /* set no huge pages */ 293 if (opts->no_huge) { 294 mem_disable_huge_pages(); 295 } 296 297 /* set the main core */ 298 if (opts->main_core > 0) { 299 args = push_arg(args, &argcount, _sprintf_alloc("%s=%d", 300 DPDK_MAIN_CORE_PARAM, opts->main_core)); 301 if (args == NULL) { 302 return -1; 303 } 304 } 305 306 /* set no pci if enabled */ 307 if (opts->no_pci) { 308 args = push_arg(args, &argcount, _sprintf_alloc("--no-pci")); 309 if (args == NULL) { 310 return -1; 311 } 312 } 313 314 if (no_huge) { 315 if (opts->hugepage_single_segments || opts->unlink_hugepage || opts->hugedir) { 316 fprintf(stderr, "--no-huge invalid with other hugepage options\n"); 317 free_args(args, argcount); 318 return -1; 319 } 320 321 if (opts->mem_size < 0) { 322 fprintf(stderr, 323 "Disabling hugepages requires specifying how much memory " 324 "will be allocated using -s parameter\n"); 325 free_args(args, argcount); 326 return -1; 327 } 328 329 /* iova-mode=pa is incompatible with no_huge */ 330 if (opts->iova_mode && 331 (strcmp(opts->iova_mode, "pa") == 0)) { 332 fprintf(stderr, "iova-mode=pa is incompatible with specified " 333 "no-huge parameter\n"); 334 free_args(args, argcount); 335 return -1; 336 } 337 338 args = push_arg(args, &argcount, _sprintf_alloc("--no-huge")); 339 args = push_arg(args, &argcount, _sprintf_alloc("--iova-mode=va")); 340 341 } else { 342 /* create just one hugetlbfs file */ 343 if (opts->hugepage_single_segments) { 344 args = push_arg(args, &argcount, _sprintf_alloc("--single-file-segments")); 345 if (args == NULL) { 346 return -1; 347 } 348 } 349 350 /* unlink hugepages after initialization */ 351 /* Note: Automatically unlink hugepage when shm_id < 0, since it means we're not using 352 * multi-process so we don't need the hugepage links anymore. But we need to make sure 353 * we don't specify --huge-unlink implicitly if --single-file-segments was specified since 354 * DPDK doesn't support that. 355 */ 356 if (opts->unlink_hugepage || 357 (opts->shm_id < 0 && !opts->hugepage_single_segments)) { 358 args = push_arg(args, &argcount, _sprintf_alloc("--huge-unlink")); 359 if (args == NULL) { 360 return -1; 361 } 362 } 363 364 /* use a specific hugetlbfs mount */ 365 if (opts->hugedir) { 366 args = push_arg(args, &argcount, _sprintf_alloc("--huge-dir=%s", opts->hugedir)); 367 if (args == NULL) { 368 return -1; 369 } 370 } 371 } 372 373 if (opts->num_pci_addr) { 374 size_t i; 375 char bdf[32]; 376 struct spdk_pci_addr *pci_addr = 377 opts->pci_blocked ? opts->pci_blocked : opts->pci_allowed; 378 379 for (i = 0; i < opts->num_pci_addr; i++) { 380 spdk_pci_addr_fmt(bdf, 32, &pci_addr[i]); 381 args = push_arg(args, &argcount, _sprintf_alloc("%s=%s", 382 (opts->pci_blocked ? DPDK_BLOCK_PARAM : DPDK_ALLOW_PARAM), 383 bdf)); 384 if (args == NULL) { 385 return -1; 386 } 387 } 388 } 389 390 /* Disable DPDK telemetry information by default, can be modified with env_context. 391 * Prevents creation of dpdk_telemetry socket and additional pthread for it. 392 */ 393 args = push_arg(args, &argcount, _sprintf_alloc("--no-telemetry")); 394 if (args == NULL) { 395 return -1; 396 } 397 398 /* Lower default EAL loglevel to RTE_LOG_NOTICE - normal, but significant messages. 399 * This can be overridden by specifying the same option in opts->env_context 400 */ 401 args = push_arg(args, &argcount, strdup("--log-level=lib.eal:6")); 402 if (args == NULL) { 403 return -1; 404 } 405 406 /* Lower default CRYPTO loglevel to RTE_LOG_ERR to avoid a ton of init msgs. 407 * This can be overridden by specifying the same option in opts->env_context 408 */ 409 args = push_arg(args, &argcount, strdup("--log-level=lib.cryptodev:5")); 410 if (args == NULL) { 411 return -1; 412 } 413 414 /* `user1` log type is used by rte_vhost, which prints an INFO log for each received 415 * vhost user message. We don't want that. The same log type is also used by a couple 416 * of other DPDK libs, but none of which we make use right now. If necessary, this can 417 * be overridden via opts->env_context. 418 */ 419 args = push_arg(args, &argcount, strdup("--log-level=user1:6")); 420 if (args == NULL) { 421 return -1; 422 } 423 424 if (opts->env_context) { 425 char *ptr = strdup(opts->env_context); 426 char *tok = strtok(ptr, " \t"); 427 428 /* DPDK expects each argument as a separate string in the argv 429 * array, so we need to tokenize here in case the caller 430 * passed multiple arguments in the env_context string. 431 */ 432 while (tok != NULL) { 433 args = push_arg(args, &argcount, strdup(tok)); 434 tok = strtok(NULL, " \t"); 435 } 436 437 free(ptr); 438 } 439 440 #ifdef __linux__ 441 442 if (opts->iova_mode) { 443 /* iova-mode=pa is incompatible with no_huge */ 444 args = push_arg(args, &argcount, _sprintf_alloc("--iova-mode=%s", opts->iova_mode)); 445 if (args == NULL) { 446 return -1; 447 } 448 } else { 449 /* When using vfio with enable_unsafe_noiommu_mode=Y, we need iova-mode=pa, 450 * but DPDK guesses it should be iova-mode=va. Add a check and force 451 * iova-mode=pa here. */ 452 if (!no_huge && rte_vfio_noiommu_is_enabled()) { 453 args = push_arg(args, &argcount, _sprintf_alloc("--iova-mode=pa")); 454 if (args == NULL) { 455 return -1; 456 } 457 } 458 459 #if defined(__x86_64__) 460 /* DPDK by default guesses that it should be using iova-mode=va so that it can 461 * support running as an unprivileged user. However, some systems (especially 462 * virtual machines) don't have an IOMMU capable of handling the full virtual 463 * address space and DPDK doesn't currently catch that. Add a check in SPDK 464 * and force iova-mode=pa here. */ 465 if (!no_huge && get_iommu_width() < SPDK_IOMMU_VA_REQUIRED_WIDTH) { 466 args = push_arg(args, &argcount, _sprintf_alloc("--iova-mode=pa")); 467 if (args == NULL) { 468 return -1; 469 } 470 } 471 #elif defined(__PPC64__) 472 /* On Linux + PowerPC, DPDK doesn't support VA mode at all. Unfortunately, it doesn't correctly 473 * auto-detect at the moment, so we'll just force it here. */ 474 args = push_arg(args, &argcount, _sprintf_alloc("--iova-mode=pa")); 475 if (args == NULL) { 476 return -1; 477 } 478 #endif 479 } 480 481 482 /* Set the base virtual address - it must be an address that is not in the 483 * ASAN shadow region, otherwise ASAN-enabled builds will ignore the 484 * mmap hint. 485 * 486 * Ref: https://github.com/google/sanitizers/wiki/AddressSanitizerAlgorithm 487 */ 488 args = push_arg(args, &argcount, _sprintf_alloc("--base-virtaddr=0x%" PRIx64, opts->base_virtaddr)); 489 if (args == NULL) { 490 return -1; 491 } 492 493 /* --match-allocation prevents DPDK from merging or splitting system memory allocations under the hood. 494 * This is critical for RDMA when attempting to use an rte_mempool based buffer pool. If DPDK merges two 495 * physically or IOVA contiguous memory regions, then when we go to allocate a buffer pool, it can split 496 * the memory for a buffer over two allocations meaning the buffer will be split over a memory region. 497 */ 498 499 /* --no-huge is incompatible with --match-allocations 500 * Ref: https://doc.dpdk.org/guides/prog_guide/env_abstraction_layer.html#hugepage-allocation-matching 501 */ 502 if (!no_huge && 503 (!opts->env_context || strstr(opts->env_context, "--legacy-mem") == NULL)) { 504 args = push_arg(args, &argcount, _sprintf_alloc("%s", "--match-allocations")); 505 if (args == NULL) { 506 return -1; 507 } 508 } 509 510 if (opts->shm_id < 0) { 511 args = push_arg(args, &argcount, _sprintf_alloc("--file-prefix=spdk_pid%d", 512 getpid())); 513 if (args == NULL) { 514 return -1; 515 } 516 } else { 517 args = push_arg(args, &argcount, _sprintf_alloc("--file-prefix=spdk%d", 518 opts->shm_id)); 519 if (args == NULL) { 520 return -1; 521 } 522 523 /* set the process type */ 524 args = push_arg(args, &argcount, _sprintf_alloc("--proc-type=auto")); 525 if (args == NULL) { 526 return -1; 527 } 528 } 529 530 /* --vfio-vf-token used for VF initialized by vfio_pci driver. */ 531 if (opts->vf_token) { 532 args = push_arg(args, &argcount, _sprintf_alloc("--vfio-vf-token=%s", 533 opts->vf_token)); 534 if (args == NULL) { 535 return -1; 536 } 537 } 538 #endif 539 540 g_eal_cmdline = args; 541 g_eal_cmdline_argcount = argcount; 542 return argcount; 543 } 544 545 int 546 spdk_env_dpdk_post_init(bool legacy_mem) 547 { 548 int rc; 549 550 rc = pci_env_init(); 551 if (rc < 0) { 552 SPDK_ERRLOG("pci_env_init() failed\n"); 553 return rc; 554 } 555 556 rc = mem_map_init(legacy_mem); 557 if (rc < 0) { 558 SPDK_ERRLOG("Failed to allocate mem_map\n"); 559 return rc; 560 } 561 562 rc = vtophys_init(); 563 if (rc < 0) { 564 SPDK_ERRLOG("Failed to initialize vtophys\n"); 565 return rc; 566 } 567 568 return 0; 569 } 570 571 void 572 spdk_env_dpdk_post_fini(void) 573 { 574 pci_env_fini(); 575 576 free_args(g_eal_cmdline, g_eal_cmdline_argcount); 577 g_eal_cmdline = NULL; 578 g_eal_cmdline_argcount = 0; 579 } 580 581 int 582 spdk_env_init(const struct spdk_env_opts *opts) 583 { 584 char **dpdk_args = NULL; 585 char *args_print = NULL, *args_tmp = NULL; 586 OPENSSL_INIT_SETTINGS *settings; 587 int i, rc; 588 int orig_optind; 589 bool legacy_mem; 590 591 /* If SPDK env has been initialized before, then only pci env requires 592 * reinitialization. 593 */ 594 if (g_external_init == false) { 595 if (opts != NULL) { 596 fprintf(stderr, "Invalid arguments to reinitialize SPDK env\n"); 597 return -EINVAL; 598 } 599 600 printf("Starting %s / %s reinitialization...\n", SPDK_VERSION_STRING, rte_version()); 601 pci_env_reinit(); 602 603 return 0; 604 } 605 606 if (opts == NULL) { 607 fprintf(stderr, "NULL arguments to initialize DPDK\n"); 608 return -EINVAL; 609 } 610 611 settings = OPENSSL_INIT_new(); 612 if (!settings) { 613 fprintf(stderr, "Failed to create openssl settings object\n"); 614 ERR_print_errors_fp(stderr); 615 return -ENOMEM; 616 } 617 618 OPENSSL_INIT_set_config_file_flags(settings, 0); 619 rc = OPENSSL_init_ssl(OPENSSL_INIT_LOAD_CONFIG, settings); 620 if (rc != 1) { 621 fprintf(stderr, "Failed to initialize OpenSSL\n"); 622 ERR_print_errors_fp(stderr); 623 return -EINVAL; 624 } 625 OPENSSL_INIT_free(settings); 626 627 rc = build_eal_cmdline(opts); 628 if (rc < 0) { 629 SPDK_ERRLOG("Invalid arguments to initialize DPDK\n"); 630 return -EINVAL; 631 } 632 633 SPDK_PRINTF("Starting %s / %s initialization...\n", SPDK_VERSION_STRING, rte_version()); 634 635 args_print = _sprintf_alloc("[ DPDK EAL parameters: "); 636 if (args_print == NULL) { 637 return -ENOMEM; 638 } 639 for (i = 0; i < g_eal_cmdline_argcount; i++) { 640 args_tmp = args_print; 641 args_print = _sprintf_alloc("%s%s ", args_tmp, g_eal_cmdline[i]); 642 if (args_print == NULL) { 643 free(args_tmp); 644 return -ENOMEM; 645 } 646 free(args_tmp); 647 } 648 SPDK_PRINTF("%s]\n", args_print); 649 free(args_print); 650 651 /* DPDK rearranges the array we pass to it, so make a copy 652 * before passing so we can still free the individual strings 653 * correctly. 654 */ 655 dpdk_args = calloc(g_eal_cmdline_argcount, sizeof(char *)); 656 if (dpdk_args == NULL) { 657 SPDK_ERRLOG("Failed to allocate dpdk_args\n"); 658 return -ENOMEM; 659 } 660 memcpy(dpdk_args, g_eal_cmdline, sizeof(char *) * g_eal_cmdline_argcount); 661 662 fflush(stdout); 663 orig_optind = optind; 664 optind = 1; 665 rc = rte_eal_init(g_eal_cmdline_argcount, dpdk_args); 666 optind = orig_optind; 667 668 free(dpdk_args); 669 670 if (rc < 0) { 671 if (rte_errno == EALREADY) { 672 SPDK_ERRLOG("DPDK already initialized\n"); 673 } else { 674 SPDK_ERRLOG("Failed to initialize DPDK\n"); 675 } 676 return -rte_errno; 677 } 678 679 legacy_mem = false; 680 if (opts->env_context && strstr(opts->env_context, "--legacy-mem") != NULL) { 681 legacy_mem = true; 682 } 683 684 rc = spdk_env_dpdk_post_init(legacy_mem); 685 if (rc == 0) { 686 g_external_init = false; 687 } 688 689 return rc; 690 } 691 692 /* We use priority 101 which is the highest priority level available 693 * to applications (the toolchains reserve 1 to 100 for internal usage). 694 * This ensures this destructor runs last, after any other destructors 695 * that might still need the environment up and running. 696 */ 697 __attribute__((destructor(101))) static void 698 dpdk_cleanup(void) 699 { 700 /* Only call rte_eal_cleanup if the SPDK env library called rte_eal_init. */ 701 if (!g_external_init) { 702 rte_eal_cleanup(); 703 } 704 } 705 706 void 707 spdk_env_fini(void) 708 { 709 spdk_env_dpdk_post_fini(); 710 } 711 712 bool 713 spdk_env_dpdk_external_init(void) 714 { 715 return g_external_init; 716 } 717