返回

mmap 共享内存指定 NUMA 节点:libnuma 和 memfd 方案

Linux

为 mmap 共享内存指定 NUMA 节点

使用 mmap 创建共享内存,系统会依据默认内存策略进行物理页面的分配。如果需要在特定的 NUMA 节点上分配这些共享内存页,直接使用 mbindset_mempolicy 可能会失败。原因在于这些函数通常作用于进程或线程的内存策略,而不是对已经映射的共享内存片段生效。本文探讨其他解决此问题的途径。

问题分析

mmap 提供了一种将文件或者匿名内存区域映射到进程地址空间的方式,可以创建共享内存。 内存分配的行为受操作系统的 NUMA(Non-Uniform Memory Access)策略控制,默认情况下操作系统倾向于在进程运行的 CPU 所在节点上分配内存。 这在很多情况下是合适的,能提升程序性能。但有时候,我们可能需要在指定的 NUMA 节点分配共享内存。直接尝试修改进程的内存策略未必能解决问题,因为mmap 创建的共享映射会在第一次访问页面时才会触发实际物理内存的分配。 如果该线程没有直接写入,则实际的内存位置会延迟确定,进程级别的绑定策略通常对此无能为力。

解决方案

以下提供了两种能够为 mmap 创建的共享内存指定 NUMA 节点的方案。

方案一:libnuma 库绑定内存

libnuma 是一个专门处理 NUMA 相关操作的库,它提供细粒度的内存控制功能。我们可以使用 numa_allocate_onnode 在特定 NUMA 节点分配内存,然后再将这部分内存 mmap 到进程空间。这样做本质上是先分配指定 NUMA 节点的内存,再做内存映射,使得物理内存从开始就在期望的节点。

操作步骤:

  1. 安装 libnuma

    sudo apt-get install libnuma-dev  # Debian/Ubuntu
    sudo yum install numactl-devel    # Red Hat/CentOS
    
  2. 编写代码:

    #define _GNU_SOURCE
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/mman.h>
    #include <numa.h>
    #include <errno.h>
    #include <unistd.h>
    
    #define MEM_SIZE 1024*1024*64 // 64MB
    
    int main() {
       int node = 1; // 目标 NUMA 节点 ID
       void *numa_mem;
    
       // 1.在指定的NUMA节点上分配内存
       numa_mem = numa_alloc_onnode(MEM_SIZE, node);
    
       if (!numa_mem) {
          perror("numa_alloc_onnode failed");
          exit(1);
        }
    
      // 2.创建一个临时文件,映射此内存区
       char tmp_file_name[L_tmpnam];
       tmpnam(tmp_file_name); //Create tmp filename, thread-safe
       int fd = open(tmp_file_name, O_CREAT | O_RDWR, 0666);
      if (fd < 0) {
           perror("open file failed");
          numa_free(numa_mem, MEM_SIZE);
           exit(1);
        }
    
        //3.写入空数据,为接下来的mmap铺路
        if (write(fd,numa_mem, MEM_SIZE)!=MEM_SIZE) {
            perror("write file error");
            numa_free(numa_mem, MEM_SIZE);
            close(fd);
            remove(tmp_file_name);
             exit(1);
         }
    
    
       // 4. 使用 mmap 进行映射
        void* mmap_addr = mmap(NULL,MEM_SIZE, PROT_READ| PROT_WRITE, MAP_SHARED,fd,0 );
       if(mmap_addr == MAP_FAILED){
           perror("mmap failed");
          numa_free(numa_mem, MEM_SIZE);
          close(fd);
          remove(tmp_file_name);
          exit(1);
    
        }
    
    
       //测试 mmap是否真的访问指定numa节点的物理内存,尝试访问一下。
       memset(mmap_addr,'A',1000); //访问mmap,迫使linux在对应的numa分配物理页
        printf("mmap'ed to : %p\n", mmap_addr);
    
    
        printf("please check numactl --show node_memory output,it must indicate current shared mem alloc at target numa node.");
        numa_free(numa_mem, MEM_SIZE);
        munmap(mmap_addr, MEM_SIZE);
       close(fd);
        remove(tmp_file_name);
    
        return 0;
     }
    

    编译:gcc -o numa_mmap_example numa_mmap_example.c -lnuma

    执行:./numa_mmap_example

  3. 代码说明
    这段代码首先使用numa_alloc_onnode 在指定 NUMA 节点上分配一段内存区域,此后在系统/临时文件写入数据,然后将此数据通过 mmap 的方式映射到用户态内存。 这种先分配好内存区域再进行映射的方式可以有效将目标物理内存固定到指定NUMA节点上。 请使用numactl等工具来观察进程运行过程中的内存状态变化,以验证这种内存布局策略的正确性。代码使用了一个临时文件,以符合 mmap 对于 MAP_SHARED 的文件需求。 注意 numa_alloc_onnode 分配的内存最后要使用numa_free 来释放掉。

原理: numa_allocate_onnode 明确地指定在特定 NUMA 节点上分配物理内存页。 这段内存在写入后被mmap共享,所以最终访问这段 mmap 共享内存将落在指定的 NUMA 节点上。

方案二:memfd_creatembind 配合使用

另一种方案使用memfd_create 创建匿名文件,结合mbind进行内存绑定,从而指定 mmap 映射后的内存物理地址。此方法避免了显式操作 libnuma

操作步骤:

  1. 编译运行:
   #define _GNU_SOURCE
    #include <stdio.h>
   #include <stdlib.h>
    #include <string.h>
    #include <sys/mman.h>
   #include <sys/syscall.h>
   #include <unistd.h>
  #include <linux/memfd.h>
    #include <errno.h>

   #define MEM_SIZE 1024*1024*64 // 64MB

   int main() {
        int node = 1; // 目标 NUMA 节点 ID

        int memfd = syscall(SYS_memfd_create, "shared_mem", 0);
        if (memfd == -1) {
            perror("memfd_create failed");
           return 1;
       }


        //分配虚拟内存空间到fd
        if (ftruncate(memfd, MEM_SIZE) == -1) {
            perror("ftruncate failed");
            close(memfd);
           return 1;
        }

        void *mmap_addr = mmap(NULL, MEM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, memfd, 0);
         if (mmap_addr == MAP_FAILED) {
             perror("mmap failed");
             close(memfd);
            return 1;

         }
       //NUMA Binding。这里才是绑定的关键!
       cpu_set_t cpuset;
        CPU_ZERO(&cpuset);
       CPU_SET(node,&cpuset); //这里不是指设置cpu affinity,而是numa node
       if (syscall(SYS_mbind,(long)mmap_addr, MEM_SIZE, MPOL_BIND,(long)cpuset.__bits, sizeof(cpu_set_t), MPOL_MF_STRICT)!=0){
            perror("mbind error");
          munmap(mmap_addr, MEM_SIZE);
             close(memfd);
          return 1;
         }


      memset(mmap_addr, 'B', 1000);
     printf("mmap'ed to : %p\n", mmap_addr);


         printf("please check numactl --show node_memory output,it must indicate current shared mem alloc at target numa node.\n");
        munmap(mmap_addr, MEM_SIZE);
       close(memfd);


       return 0;
    }
编译: `gcc -o memfd_mmap_example memfd_mmap_example.c`

执行:./memfd_mmap_example

  1. 代码说明

    memfd_create 创建了一个匿名内存文件符。 然后通过 mmap 进行内存映射, 最后通过mbind 调用强制绑定该内存到目标 NUMA 节点上。 该方案巧妙的利用mbind作用于mmap 后的内存空间。 该方案省去了和系统文件打交道的方式,更为简洁。 MPOL_MF_STRICT 标记强制绑定的意思,如果绑定的目标NUMA不存在将直接失败。 同样要使用 numactl等工具来确认运行状态。

原理: memfd_create 提供匿名共享内存空间,mbind 强制内存绑定到目标 NUMA 节点。

注意事项

  • NUMA 节点 ID 从 0 开始,注意查找对应的系统节点配置。
  • mbind 绑定可能失败,尤其是使用 MPOL_MF_STRICT 时,如果指定的 NUMA 节点不存在,绑定操作会出错,代码中需要捕获和处理错误信息。
  • 选择合适的方案取决于具体的应用场景,使用 libnuma 方案比较灵活,memfd_creatembind 的结合可能更加方便,根据实际需求做出权衡。

通过上述方案,可以更有效地控制 mmap 创建的共享内存在 NUMA 环境下的物理分配。 保证内存和计算资源之间的最佳搭配, 获得更高的应用程序性能。