我问这个问题的原因是,在测试Linux软脏位的行为时,我发现如果我在不接触任何内存的情况下创建一个线程,所有页面的软脏位将被设置为1(脏).

例如,在主线程中设置为malloc(100MB),然后清除柔软的脏位,然后创建一个只会Hibernate 的线程.创建线程后,所有malloc(100MB)MB内存块的软脏位被设置为1.

下面是我正在使用的测试程序:

#include <thread>
#include <iostream>
#include <vector>
#include <cstdint>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>

#define PAGE_SIZE_4K 0x1000

int GetDirtyBit(uint64_t vaddr) {
  int fd = open("/proc/self/pagemap", O_RDONLY);
  if (fd < 0) {
    perror("Failed open pagemap");
    exit(1);
  }

  off_t offset = vaddr / 4096 * 8;
  if (lseek(fd, offset, SEEK_SET) < 0) {
    perror("Failed lseek pagemap");
    exit(1);
  }

  uint64_t pfn = 0;
  if (read(fd, &pfn, sizeof(pfn)) != sizeof(pfn)) {
    perror("Failed read pagemap");
    sleep(1000);
    exit(1);
  }
  close(fd);

  return pfn & (1UL << 55) ? 1 : 0;
}

void CleanSoftDirty() {
  int fd = open("/proc/self/clear_refs", O_RDWR);
  if (fd < 0) {
    perror("Failed open clear_refs");
    exit(1);
  }

  char cmd[] = "4";
  if (write(fd, cmd, sizeof(cmd)) != sizeof(cmd)) {
    perror("Failed write clear_refs");
    exit(1);
  }

  close(fd);
}

int demo(int argc, char *argv[]) {
  int x = 1;
  // 100 MB
  uint64_t size = 1024UL * 1024UL * 100;
  void *ptr = malloc(size);
  for (uint64_t s = 0; s < size; s += PAGE_SIZE_4K) {
    // populate pages
    memset(ptr + s, x, PAGE_SIZE_4K);
  }

  char *cptr = reinterpret_cast<char *>(ptr);
  printf("Soft dirty after malloc: %ld, (50MB offset)%ld\n",
        GetDirtyBit(reinterpret_cast<uint64_t>(cptr)),
        GetDirtyBit(reinterpret_cast<uint64_t>(cptr + 50 * 1024 * 1024)));

  printf("ALLOCATE FINISHED\n");

  std::string line;
  std::vector<std::thread> threads;
  while (true) {
    sleep(2);
    // Set soft dirty of all pages to 0.
    CleanSoftDirty();

    char *cptr = reinterpret_cast<char *>(ptr);
    printf("Soft dirty after reset: %ld, (50MB offset)%ld\n",
      GetDirtyBit(reinterpret_cast<uint64_t>(cptr)),
      GetDirtyBit(reinterpret_cast<uint64_t>(cptr + 50 * 1024 * 1024)));
    
    // Create thread.
    threads.push_back(std::thread([]() { while(true) sleep(1); }));

    sleep(2);
    
    printf("Soft dirty after create thread: %ld, (50MB offset)%ld\n",
      GetDirtyBit(reinterpret_cast<uint64_t>(cptr)),
      GetDirtyBit(reinterpret_cast<uint64_t>(cptr + 50 * 1024 * 1024)));

    // memset the first 20MB
    memset(cptr, x++, 1024UL * 1024UL * 20);
    printf("Soft dirty after memset: %ld, (50MB offset)%ld\n",
      GetDirtyBit(reinterpret_cast<uint64_t>(cptr)),
      GetDirtyBit(reinterpret_cast<uint64_t>(cptr + 50 * 1024 * 1024)));
  }

  return 0;
}

int main(int argc, char *argv[]) {
  std::string last_arg = argv[argc - 1];
  printf("PID: %d\n", getpid());

  return demo(argc, argv);
}

我打印第一页的脏部分,然后打印偏移量为50 * 1024 * 1024的页面.以下是发生的情况:

  1. malloc()之后的软脏位是1,这是预期的.
  2. 在清洗软脏之后,它们变成了0.
  3. 创建一个只是Hibernate 的线程.
  4. 判断脏位,100MB区域中的所有页面(我没有打印所有页面的脏位,但我自己进行了判断)现在将软脏位设置为1.
  5. 重新启动循环,现在行为正确,创建额外线程后,软脏位仍为0.
  6. 自从我做了memset()之后,位于偏移量0的页面的软脏比特是1,而页面50 MB的软脏比特保持为0.

以下是输出:

Soft dirty after malloc: 1, (50MB offset)1
ALLOCATE FINISHED
Soft dirty after reset: 0, (50MB offset)0
Soft dirty after create thread: 1, (50MB offset)1
Soft dirty after memset: 1, (50MB offset)1

(steps 1-4 above)
(step 5 starts below)
Soft dirty after reset: 0, (50MB offset)0
Soft dirty after create thread: 0, (50MB offset)0
Soft dirty after memset: 1, (50MB offset)0

Soft dirty after reset: 0, (50MB offset)0
Soft dirty after create thread: 0, (50MB offset)0
Soft dirty after memset: 1, (50MB offset)0

Soft dirty after reset: 0, (50MB offset)0
Soft dirty after create thread: 0, (50MB offset)0
Soft dirty after memset: 1, (50MB offset)0

我认为创建线程只会将页面标记为"共享"状态,而不是修改它们,因此软脏部分应该保持不变.显然,他们的行为是不同的.因此,我在想:创建线程是否会在所有页面上触发页面错误?因此,在处理页面错误时,操作系统会将所有页面的软脏位设置为1.

如果不是这样,为什么创建线程会使进程的所有内存页都变得"脏"呢?为什么只有第一线程创建有这样的行为?

我希望我已经很好地解释了这个问题,如果需要更多的细节,或者如果有什么不合理的地方,请告诉我.

推荐答案

所以,这是一种有趣和有趣的事情.你的具体情况,以及软性肮脏部分的行为,都是非常特殊的.没有发生页面错误,并且没有在all个内存页上设置软脏位,而是只在其中一些页上设置了软脏位(您分配到malloc页的页).

如果您的程序运行在strace以下,您将注意到一些有助于解释您所观察到的内容的事情:

[1] mmap(NULL, 104861696, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f8669b66000
    ...
[2] mmap(NULL, 8392704, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7f8669365000
[2] mprotect(0x7f8669366000, 8388608, PROT_READ|PROT_WRITE) = 0
[2] clone(child_stack=0x7f8669b64fb0, ...) = 97197
    ...

正如您在上面看到的:

  1. malloc()是相当大的,因此您将不会得到普通的堆区块,而是通过mmap系统调用保留的专用内存区.

  2. 当您创建线程时,库代码将为该线程设置一个堆栈,该堆栈的长度依次为mmapmprotect.

在Linux中,mmap的正常行为是保留内存,从创建进程时 Select 的mmap_base开始,每次减go 请求的大小(除非明确请求了特定地址,在这种情况下不考虑mmap_base).因此,点1的mmap将保留动态加载器映射的最后一个共享库的正上方的页面,而上面的点2的mmap将保留恰好在点1映射的页面之前的页面.然后,mprotect将该第二区域(除了第一个页面)标记为RW.

因为从内核的Angular 来看,这些映射是连续的,既是匿名的,也是具有相同保护(RW)的.事实上,内核将其视为单个VMA(vm_area_struct).

现在,正如我们可以阅读关于软脏部分的from the kernel documentation(请注意我用粗体突出显示的部分):

而在大多数情况下,#PF-S跟踪内存变化不仅仅是 足够了,还有一种情况,我们可能会失go 柔软的脏部分--a 任务取消映射以前映射的内存区域,然后映射新的内存区域 在完全相同的地方.当调用Unmap时,内核在内部 清除PTE值,包括软脏位.通知用户空间 关于这样的存储区更新的申请the kernel always marks new memory regions (and expanded regions) as soft dirty.

因此,您看到软脏位在清除后重新出现在初始恶意锁定的内存块上的原因是一个有趣的巧合:线程堆栈的分配导致包含它的内存区(VMA)不是那么直观的"扩展"的结果.


为了让事情更清楚,我们可以在不同的阶段通过/proc/[pid]/maps判断进程的虚拟内存布局.它将看起来像这样(取self 的机器):

  • malloc()年前:

    ...
    5653d8b82000-5653d8b83000 r--p 00005000 00:18 77464613     [your program]
    5653d8b83000-5653d8b84000 rw-p 00006000 00:18 77464613     [your program]
    5653d983f000-5653d9860000 rw-p 00000000 00:00 0            [heap]
    7f866ff6c000-7f866ff79000 r--p 00000000 00:18 77146186     [shared libraries]
    7f866ff79000-7f8670013000 r-xp 0000d000 00:18 77146186     [shared libraries]
    ...
    
  • malloc()年后:

    ...
    5653d8b82000-5653d8b83000 r--p 00005000 00:18 77464613     [your program]
    5653d8b83000-5653d8b84000 rw-p 00006000 00:18 77464613     [your program]
    5653d983f000-5653d9860000 rw-p 00000000 00:00 0            [heap]
    7f8669b66000-7f866ff6c000 rw-p 00000000 00:00 0        *** MALLOC'D MEMORY
    7f866ff6c000-7f866ff79000 r--p 00000000 00:18 77146186     [shared libraries]
    7f866ff79000-7f8670013000 r-xp 0000d000 00:18 77146186     [shared libraries]
    ...
    
  • 创建第一个线程后(请注意,VMA的起点如何从7f8669b66000更改为7f8669366000,因为它的大小增加了):

    ...
    5653d8b82000-5653d8b83000 r--p 00005000 00:18 77464613     [your program]
    5653d8b83000-5653d8b84000 rw-p 00006000 00:18 77464613     [your program]
    5653d983f000-5653d9860000 rw-p 00000000 00:00 0            [heap]
    7f8669365000-7f8669366000 ---p 00000000 00:00 0        *** GUARD PAGE
    7f8669366000-7f866ff6c000 rw-p 00000000 00:00 0        *** THREAD STACK + MALLOC'D MEMORY
    7f866ff6c000-7f866ff79000 r--p 00000000 00:18 77146186     [shared libraries]
    7f866ff79000-7f8670013000 r-xp 0000d000 00:18 77146186     [shared libraries]
    ...
    

您可以清楚地看到,在创建线程之后,内核将两个内存区域(线程堆栈+您的Malloc块)一起显示为单个VMA,假设它们是连续的、匿名的并且具有相同的保护(rw).

线程堆栈上方的保护页被视为单独的VMA(它具有不同的保护),后续线程将在其上方mmap个它们的堆栈,因此它们不会影响原始内存区的软脏位:

...
5653d8b82000-5653d8b83000 r--p 00005000 00:18 77464613     [your program]
5653d8b83000-5653d8b84000 rw-p 00006000 00:18 77464613     [your program]
5653d983f000-5653d9860000 rw-p 00000000 00:00 0            [heap]
7f8668363000-7f8668364000 ---p 00000000 00:00 0        *** GUARD PAGE
7f8668364000-7f8668b64000 rw-p 00000000 00:00 0        *** THREAD 3 STACK
7f8668b64000-7f8668b65000 ---p 00000000 00:00 0        *** GUARD PAGE
7f8668b65000-7f8669365000 rw-p 00000000 00:00 0        *** THREAD 2 STACK
7f8669365000-7f8669366000 ---p 00000000 00:00 0        *** GUARD PAGE
7f8669366000-7f866ff6c000 rw-p 00000000 00:00 0        *** THREAD 1 STACK + MALLOC'D MEMORY
7f866ff6c000-7f866ff79000 r--p 00000000 00:18 77146186     [shared libraries]
7f866ff79000-7f8670013000 r-xp 0000d000 00:18 77146186     [shared libraries]
...

这就是为什么从第二个线程开始,您看不到任何意想不到的事情发生.

Linux相关问答推荐

C++17/Linux:信号未解锁单独线程中被阻止的网络套接字调用

如何在带模式的文件频繁更改的管道中使用grep-f带模式的文件?

没有发现运行时依赖关系,但它在S的运行时路径中有吗?

Linux-如何区分目录中名称相同但扩展名不同的所有文件

C++调试器如何知道如何在源代码和可执行文件之间映射行?

Linux 上 Ada 任务优先级的语义是什么?

如何使用 gdb 调试堆栈分段错误?

使用 sed 或 awk 在 linux 中将第一行中的一个单词替换为第二行中的另一个单词

访问证书里面的图片

如何在ubuntu中通过.sh文件启动多个服务

使用 Dockerfile RUN 执行某些操作但忽略错误

函数在 shell 脚本中抛出错误语法错误:} unexpected

我在哪里放置第三方库来设置 C++ Linux 开发环境?

使用 awk 或 sed 删除特定字符

如何使用该位置的相对路径在单个位置创建多个文件夹?

为什么在 Linux 中使用 select

将 $_GET 参数传递给 cron 作业(job)

从Linux中的行尾删除空格

并行运行 shell 脚本

使用sudo apt-get install build-essentials