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 #ifdef __linux__ 425 426 if (opts->iova_mode) { 427 /* iova-mode=pa is incompatible with no_huge */ 428 args = push_arg(args, &argcount, _sprintf_alloc("--iova-mode=%s", opts->iova_mode)); 429 if (args == NULL) { 430 return -1; 431 } 432 } else { 433 /* When using vfio with enable_unsafe_noiommu_mode=Y, we need iova-mode=pa, 434 * but DPDK guesses it should be iova-mode=va. Add a check and force 435 * iova-mode=pa here. */ 436 if (!no_huge && rte_vfio_noiommu_is_enabled()) { 437 args = push_arg(args, &argcount, _sprintf_alloc("--iova-mode=pa")); 438 if (args == NULL) { 439 return -1; 440 } 441 } 442 443 #if defined(__x86_64__) 444 /* DPDK by default guesses that it should be using iova-mode=va so that it can 445 * support running as an unprivileged user. However, some systems (especially 446 * virtual machines) don't have an IOMMU capable of handling the full virtual 447 * address space and DPDK doesn't currently catch that. Add a check in SPDK 448 * and force iova-mode=pa here. */ 449 if (!no_huge && get_iommu_width() < SPDK_IOMMU_VA_REQUIRED_WIDTH) { 450 args = push_arg(args, &argcount, _sprintf_alloc("--iova-mode=pa")); 451 if (args == NULL) { 452 return -1; 453 } 454 } 455 #elif defined(__PPC64__) 456 /* On Linux + PowerPC, DPDK doesn't support VA mode at all. Unfortunately, it doesn't correctly 457 * auto-detect at the moment, so we'll just force it here. */ 458 args = push_arg(args, &argcount, _sprintf_alloc("--iova-mode=pa")); 459 if (args == NULL) { 460 return -1; 461 } 462 #endif 463 } 464 465 466 /* Set the base virtual address - it must be an address that is not in the 467 * ASAN shadow region, otherwise ASAN-enabled builds will ignore the 468 * mmap hint. 469 * 470 * Ref: https://github.com/google/sanitizers/wiki/AddressSanitizerAlgorithm 471 */ 472 args = push_arg(args, &argcount, _sprintf_alloc("--base-virtaddr=0x%" PRIx64, opts->base_virtaddr)); 473 if (args == NULL) { 474 return -1; 475 } 476 477 /* --match-allocation prevents DPDK from merging or splitting system memory allocations under the hood. 478 * This is critical for RDMA when attempting to use an rte_mempool based buffer pool. If DPDK merges two 479 * physically or IOVA contiguous memory regions, then when we go to allocate a buffer pool, it can split 480 * the memory for a buffer over two allocations meaning the buffer will be split over a memory region. 481 */ 482 483 /* --no-huge is incompatible with --match-allocations 484 * Ref: https://doc.dpdk.org/guides/prog_guide/env_abstraction_layer.html#hugepage-allocation-matching 485 */ 486 if (!no_huge && 487 (!opts->env_context || strstr(opts->env_context, "--legacy-mem") == NULL)) { 488 args = push_arg(args, &argcount, _sprintf_alloc("%s", "--match-allocations")); 489 if (args == NULL) { 490 return -1; 491 } 492 } 493 494 if (opts->shm_id < 0) { 495 args = push_arg(args, &argcount, _sprintf_alloc("--file-prefix=spdk_pid%d", 496 getpid())); 497 if (args == NULL) { 498 return -1; 499 } 500 } else { 501 args = push_arg(args, &argcount, _sprintf_alloc("--file-prefix=spdk%d", 502 opts->shm_id)); 503 if (args == NULL) { 504 return -1; 505 } 506 507 /* set the process type */ 508 args = push_arg(args, &argcount, _sprintf_alloc("--proc-type=auto")); 509 if (args == NULL) { 510 return -1; 511 } 512 } 513 514 /* --vfio-vf-token used for VF initialized by vfio_pci driver. */ 515 if (opts->vf_token) { 516 args = push_arg(args, &argcount, _sprintf_alloc("--vfio-vf-token=%s", 517 opts->vf_token)); 518 if (args == NULL) { 519 return -1; 520 } 521 } 522 #endif 523 524 if (opts->env_context) { 525 char *ptr = strdup(opts->env_context); 526 char *tok = strtok(ptr, " \t"); 527 528 /* DPDK expects each argument as a separate string in the argv 529 * array, so we need to tokenize here in case the caller 530 * passed multiple arguments in the env_context string. 531 */ 532 while (tok != NULL) { 533 args = push_arg(args, &argcount, strdup(tok)); 534 tok = strtok(NULL, " \t"); 535 } 536 537 free(ptr); 538 } 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 #if OPENSSL_VERSION_NUMBER >= 0x30000000 /* OPENSSL 3.0.0 */ 619 OPENSSL_INIT_set_config_file_flags(settings, 0); 620 #endif 621 rc = OPENSSL_init_ssl(OPENSSL_INIT_LOAD_CONFIG, settings); 622 if (rc != 1) { 623 fprintf(stderr, "Failed to initialize OpenSSL\n"); 624 ERR_print_errors_fp(stderr); 625 return -EINVAL; 626 } 627 OPENSSL_INIT_free(settings); 628 629 rc = build_eal_cmdline(opts); 630 if (rc < 0) { 631 SPDK_ERRLOG("Invalid arguments to initialize DPDK\n"); 632 return -EINVAL; 633 } 634 635 SPDK_PRINTF("Starting %s / %s initialization...\n", SPDK_VERSION_STRING, rte_version()); 636 637 args_print = _sprintf_alloc("[ DPDK EAL parameters: "); 638 if (args_print == NULL) { 639 return -ENOMEM; 640 } 641 for (i = 0; i < g_eal_cmdline_argcount; i++) { 642 args_tmp = args_print; 643 args_print = _sprintf_alloc("%s%s ", args_tmp, g_eal_cmdline[i]); 644 if (args_print == NULL) { 645 free(args_tmp); 646 return -ENOMEM; 647 } 648 free(args_tmp); 649 } 650 SPDK_PRINTF("%s]\n", args_print); 651 free(args_print); 652 653 /* DPDK rearranges the array we pass to it, so make a copy 654 * before passing so we can still free the individual strings 655 * correctly. 656 */ 657 dpdk_args = calloc(g_eal_cmdline_argcount, sizeof(char *)); 658 if (dpdk_args == NULL) { 659 SPDK_ERRLOG("Failed to allocate dpdk_args\n"); 660 return -ENOMEM; 661 } 662 memcpy(dpdk_args, g_eal_cmdline, sizeof(char *) * g_eal_cmdline_argcount); 663 664 fflush(stdout); 665 orig_optind = optind; 666 optind = 1; 667 rc = rte_eal_init(g_eal_cmdline_argcount, dpdk_args); 668 optind = orig_optind; 669 670 free(dpdk_args); 671 672 if (rc < 0) { 673 if (rte_errno == EALREADY) { 674 SPDK_ERRLOG("DPDK already initialized\n"); 675 } else { 676 SPDK_ERRLOG("Failed to initialize DPDK\n"); 677 } 678 return -rte_errno; 679 } 680 681 legacy_mem = false; 682 if (opts->env_context && strstr(opts->env_context, "--legacy-mem") != NULL) { 683 legacy_mem = true; 684 } 685 686 rc = spdk_env_dpdk_post_init(legacy_mem); 687 if (rc == 0) { 688 g_external_init = false; 689 } 690 691 return rc; 692 } 693 694 /* We use priority 101 which is the highest priority level available 695 * to applications (the toolchains reserve 1 to 100 for internal usage). 696 * This ensures this destructor runs last, after any other destructors 697 * that might still need the environment up and running. 698 */ 699 __attribute__((destructor(101))) static void 700 dpdk_cleanup(void) 701 { 702 /* Only call rte_eal_cleanup if the SPDK env library called rte_eal_init. */ 703 if (!g_external_init) { 704 rte_eal_cleanup(); 705 } 706 } 707 708 void 709 spdk_env_fini(void) 710 { 711 spdk_env_dpdk_post_fini(); 712 } 713 714 bool 715 spdk_env_dpdk_external_init(void) 716 { 717 return g_external_init; 718 } 719