我们可以在不访问其中的值的情况下更改数据块的虚拟内存地址吗?

问题描述

那么,在像 C++ 这样的任何高级语言中,我们都可以使用指针和引用来检查变量的地址,但我们真的可以改变它吗?例如让我们说 int A 的地址为 1000h,int B 的地址为 1004h,以下是内存中的表示:-

1000h 1004h
A B
100 104

我可以交换他们的地址吗? (看起来像这样)。

1004h 1000h
A B
100 104

这甚至可能吗?

注意:请将 1000h 视为虚拟地址,将 100 视为实际地址,您可以使用任何您想要的编程语言。

如果我弄错了,请告诉我。

解决方法

不,您不能更改对象的地址。每个对象在其整个生命周期内都具有相同的存储区域

,

您无法更改 &a,但可以通过使用虚拟内存来更改 a 的内容。但不是只是 int a;无需复制实际内存内容,您只能更改整个页面大小的块。

可以潜在地用虚拟内存技巧交换这两个数组,作为执行 std::swap_range(a,a+1024,b) 的替代方法,这可能会也可能不会更快。

alignas(4096) int32_t a[1024];     // assuming 4k page size and
alignas(4096) int32_t b[1024];     // CHAR_BITS=8 so sizeof(int32_t) = 4

对于更大的数组,可能只会更快,因为复制是 O(N),而操作页表具有很大的固定成本(系统调用、跨内核的 TLB 击落)但每页触摸的成本很小,例如 8 / 4096实际操作的数据量。 (例如,在 x86-64 上,每 4096 字节数据有 8 字节的页表条目。)或更少的大/大页。


在每个实际系统上,页面大小(远)大于 4 个字节,因此在您的示例中,这两个对象位于同一个虚拟页面中。 4 字节的页面大小是完全不切实际的,页表占用的空间与实际数据的空间一样多,并且需要比缓存大的 TLB。 (对于每 48-2 = 46 位虚拟页码,对于您想要覆盖的每 4 个字节的地址空间,一个 ~40 位的物理地址。具有访问、脏和 R/W/X 权限。)>

常见的页面大小范围从 4kiB (x86-64) 到 16k 或 64k,其中 4k 太小了(需要太多 TLB 条目来覆盖现代软件经常使用的大型工作集)。一些系统使用 page-directory entry(在基数树中更高)作为一个连续的大页面来支持大页面/大页面,例如x86-64 的 2M / 1G 大/hugepages。


理论上可以要求操作系统将您的虚拟地址空间以不同的方式重新映射到物理内存中的相同数据上,例如交换两个完整虚拟页面的内容,只需更新它们的页表条目 (PTE) 以交换物理地址。 (并使当前和所有其他核心上的 TLB 条目无效:TLB 击落。)


Linux 没有 AFAIK 的 API 来请求两个虚拟页面的映射交换,但它有 mremap(2)。 (mremap 是特定于 Linux 的。其他操作系统可能有类似的东西。ISO C++ 不需要虚拟内存,因此没有任何可移植地操作它的函数)。

通过三个 mremap(MREMAP_FIXED) 调用和一个临时虚拟页面(您没有使用或您知道未分配),您可以执行 tmp=a / {{ 1}} / a=b 交换,其中 b=tmpa 是整个(范围)页面的内容。

b

您可以使用 #define _GNU_SOURCE #include <sys/mman.h> // swap contents of pa[0..size] with pa[0..size] // effectively mmap(tmp,MAP_FIXED) then munmap(tmp,size) // size must be a multiple of system page size,and pointers must be page-aligned void swap_page_contents(void *pa,void *pb,void *tmp,size_t size) { // need to force moving,otherwise kernel will leave it in place because we aren't growing. void *ret = mremap(pa,size,MREMAP_MAYMOVE|MREMAP_FIXED,tmp); assert(ret == tmp); // t2 != MAP_FAILED ret = mremap(pb,pa); assert(ret != MAP_FAILED); ret = mremap(tmp,pb); assert(ret != MAP_FAILED); } 分配 tmp。延迟分配意味着永远不会分配物理页面来支持该映射,Linux 会将其放在虚拟地址空间中未使用的某个位置。这个交换最终取消了它的映射,所以也许我应该把它放在这个函数中。但是,如果您可以确定自上次交换以来您的进程没有映射任何新内存,您可以重用相同的 mmap(MAP_PRIVATE|MAP_ANONYMOUS)。它不需要被映射,你只需要知道它没有用于任何其他

如果您传递错误的参数(不是页面对齐或重叠),这可能会因 EINVAL 而失败。所以也许让它返回一个错误而不是断言,尽管如果 tmp 没有对齐,那么它会在已经将 b 移动到 a 后失败。

这也不是原子或线程安全的tmp 暂时未映射,暂时我们有 papa 都指向原始内容pbpb 并没有真正的帮助;它仅适用于 MAP_PRIVATE|MAP_ANONYMOUS 映射(例如,像 MREMAP_DONTUNMAP 会分配,但当然,如果您向下舍入到页面的开头并交换其元数据,您可能会破坏 malloc 的簿记。)此外,DONTUNMAP 使旧映射读取为零,尽管手册页说您可以使用 malloc 安装处理程序以执行其他操作(例如协助垃圾收集)。

显然,您可以通过 userfaultfd(2) 来获取它为数据创建另一个虚拟映射,但前提是原始映射是 old_size=0 映射。所以你不能这样做让内核为任意映射选择一个未使用的 MAP_SHARED 页面范围,只能共享(可能是文件支持的)映射。


Linux 也有 remap_file_pages(2),它可以在 tmpfs 文件支持的 tmp 中复制页面映射,尽管该系统调用已被弃用,并且显然总是使用“较慢的内核模拟”而不是它的任何内容以前做。无论如何,我认为它仍然无法交换,只能在更大的映射中为文件的一部分创建第二个映射。

,

一般来说,没有。正在运行的进程中的变量没有名称。编译源代码时,编译器会识别变量的声明并计划它们在内存中的分配。然后每个变量都被分配其地址(如果变量是静态的,则可以是常量,即在整个流程执行期间分配一次,或者如果变量是自动的,则是相对的,即在某个代码块的每个条目上创建并销毁退出时)。然后这些地址被内置到使用变量的指令代码中。

因此,一旦程序构建完成,名称 AB 就不再存在。当您运行您的程序时,它在内部使用例如地址 0x1004 来访问一个四字节的数据,但它不知道任何 int B

尽管没有意义,但交换内存地址是不可能的。地址或多或少是一个存储单元的序号。你不能改变细胞的顺序,就像你不能把街上的第三个房子变成第一个。当然,您可以(理论上)交换建筑物,就像您可以交换内存单元的内容一样,但是单元的序号将保持不变,类似于建筑物(或其地块)的序号。