如何使用mmap()映射大文件而不冒着OOM killer的风险?

19

我有一个嵌入式ARM Linux盒子,内存很小(512MB)且没有交换空间,我需要在上面创建并操作一个相当大的文件(~200MB)。把整个文件加载到内存中,在内存中修改内容,然后再写回去,有时会触发OOM杀手,这是我想要避免的。

我的解决方法是使用mmap()将该文件映射到进程的虚拟地址空间中;这样,对映射内存区域的读取和写入将转到本地闪存文件系统,并且如果内存不足,Linux可以将一些的内存页刷新回磁盘以释放一些RAM,从而避免OOM-killer。 (这可能会使我的程序变慢,但对于这种情况慢是可以接受的)

然而,即使使用mmap()调用,我仍然偶尔看到进程被OOM-killer杀死,而执行以上操作。

我的问题是,我是否过于乐观地认为Linux在存在大量和受限RAM的情况下会表现得如何? (即是否一个200MB的文件,然后读/写到是不是足够聪明,以在内存不足时换出页面,但我在使用它时做错了什么?

对于我来说,做映射的代码在这里:

void FixedSizeDataBuffer :: TryMapToFile(const std::string & filePath, bool createIfNotPresent, bool autoDelete)
{
   const int fd = open(filePath.c_str(), (createIfNotPresent?(O_CREAT|O_EXCL|O_RDWR):O_RDONLY)|O_CLOEXEC, S_IRUSR|(createIfNotPresent?S_IWUSR:0));
   if (fd >= 0)
   {
      if ((autoDelete == false)||(unlink(filePath.c_str()) == 0))  // so the file will automatically go away when we're done with it, even if we crash
      {
         const int fallocRet = createIfNotPresent ? posix_fallocate(fd, 0, _numBytes) : 0;
         if (fallocRet == 0)
         {
            void * mappedArea = mmap(NULL, _numBytes, PROT_READ|(createIfNotPresent?PROT_WRITE:0), MAP_SHARED, fd, 0);
            if (mappedArea)
            {
               printf("FixedSizeDataBuffer %p: Using backing-store file [%s] for %zu bytes of data\n", this, filePath.c_str(), _numBytes);
               _buffer         = (uint8_t *) mappedArea;
               _isMappedToFile = true;
            }
            else printf("FixedSizeDataBuffer %p: Unable to mmap backing-store file [%s] to %zu bytes (%s)\n", this, filePath.c_str(), _numBytes, strerror(errno));
         }
         else printf("FixedSizeDataBuffer %p: Unable to pad backing-store file [%s] out to %zu bytes (%s)\n", this, filePath.c_str(), _numBytes, strerror(fallocRet));
      }
      else printf("FixedSizeDataBuffer %p: Unable to unlink backing-store file [%s] (%s)\n", this, filePath.c_str(), strerror(errno));

      close(fd); // no need to hold this anymore AFAIK, the memory-mapping itself will keep the backing store around
   }
   else printf("FixedSizeDataBuffer %p: Unable to create backing-store file [%s] (%s)\n", this, filePath.c_str(), strerror(errno));
}

如果必须的话,我可以将这段代码重写为普通的文件I/O,但如果mmap()能胜任这个任务(或者如果不能胜任,我至少想知道为什么不能),那就太好了。


2
建议使用madvise(MADV_DONTNEED)来释放不再需要的映射文件范围,并且对文件进行“窗口化”处理。否则,mmap()将会在内存中保留数据。 - JATothrim
1个回答

9
经过进一步试验,我发现 OOM-killer 造访我并不是因为系统内存用尽了,而是因为内存偶尔会变得足够碎片化,使得内核无法找到足够大的一组物理连续的内存页来满足其即时需求。当这种情况发生时,内核将调用 OOM-killer 释放一些 RAM 以避免内核恐慌,对于内核来说一切都很好,但当它杀死用户依赖于完成工作的进程时就不那么好了。:/

在尝试并未成功地寻找说服 Linux 不要这样做的方法后(我认为启用交换分区可以避免 OOM-killer 的出现,但在这些特定机器上这样做不是一个选项),我想出了一个 hack 解决方案;我向我的程序添加了一些代码,定期检查 Linux 内核报告的内存碎片化程度,如果内存碎片化开始看起来太严重,则预先订购进行一次内存碎片整理,以便 OOM-killer (希望如此)不会成为必要的选项。如果内存碎片整理似乎没有改善情况,那么在连续 20 次尝试之后,我们还会删除 VM 页面缓存以释放连续的物理 RAM。这一切都非常丑陋,但没有从用户那里在凌晨 3 点接到电话,询问他们的服务器程序为什么崩溃。:/

解决方案的要点如下;请注意,DefragTick(Milliseconds) 应定期调用(最好每秒钟一次)。

 // Returns how safe we are from the fragmentation-based-OOM-killer visits.
 // Returns -1 if we can't read the data for some reason.
 static int GetFragmentationSafetyLevel()
 {
    int ret = -1;
    FILE * fpIn = fopen("/sys/kernel/debug/extfrag/extfrag_index", "r");
    if (fpIn)
    {
       char buf[512];
       while(fgets(buf, sizeof(buf), fpIn))
       {  
          const char * dma = (strncmp(buf, "Node 0, zone", 12) == 0) ? strstr(buf+12, "DMA") : NULL;
          if (dma)
          {  
             // dma= e.g.:  "DMA -1.000 -1.000 -1.000 -1.000 0.852 0.926 0.963 0.982 0.991 0.996 0.998 0.999 1.000 1.000"
             const char * s = dma+4;  // skip past "DMA ";
             ret = 0; // ret now becomes a count of "safe values in a row"; a safe value is any number less than 0.500, per me
             while((s)&&((*s == '-')||(*s == '.')||(isdigit(*s))))
             {  
                const float fVal = atof(s);
                if (fVal < 0.500f)
                {  
                   ret++;
                   
                   // Advance (s) to the next number in the list
                   const char * space = strchr(s, ' ');   // to the next space
                   s = space ? (space+1) : NULL;
                }
                else break;  // oops, a dangerous value!  Run away!
             }
          }
       }
       fclose(fpIn);
    }
    return ret;
 }

 // should be called periodically (e.g. once per second)
 void DefragTick(Milliseconds current_time_in_milliseconds)
 {
     if ((current_time_in_milliseconds-m_last_fragmentation_check_time) >= Milliseconds(1000))
     {
        m_last_fragmentation_check_time = current_time_in_milliseconds;

        const int fragmentationSafetyLevel = GetFragmentationSafetyLevel();
        if (fragmentationSafetyLevel < 9)
        {
           m_defrag_pending = true;  // trouble seems to start at level 8
           m_fragged_count++;        // note that we still seem fragmented
        }
        else m_fragged_count = 0;    // we're in the clear!

        if ((m_defrag_pending)&&((current_time_in_milliseconds-m_last_defrag_time) >= Milliseconds(5000)))
        {
           if (m_fragged_count >= 20)
           {
              // FogBugz #17882
              FILE * fpOut = fopen("/proc/sys/vm/drop_caches", "w");
              if (fpOut)
              {
                 const char * warningText = "Persistent Memory fragmentation detected -- dropping filesystem PageCache to improve defragmentation.";
                 printf("%s (fragged count is %i)\n", warningText, m_fragged_count);
                 fprintf(fpOut, "3");
                 fclose(fpOut);

                 m_fragged_count = 0;
              }
              else
              {
                 const char * errorText = "Couldn't open /proc/sys/vm/drop_caches to drop filesystem PageCache!";
                 printf("%s\n", errorText);
              }
           }

           FILE * fpOut = fopen("/proc/sys/vm/compact_memory", "w");
           if (fpOut)
           {
              const char * warningText = "Memory fragmentation detected -- ordering a defragmentation to avoid the OOM-killer.";
              printf("%s (fragged count is %i)\n", warningText, m_fragged_count);
              fprintf(fpOut, "1");
              fclose(fpOut);

              m_defrag_pending   = false;
              m_last_defrag_time = current_time_in_milliseconds;
           }
           else
           {
              const char * errorText = "Couldn't open /proc/sys/vm/compact_memory to trigger a memory-defragmentation!";
              printf("%s\n", errorText);
           }
        }
     }
 }

3
请注意,有些情况下添加少量的交换空间(即使在RAM中使用zram)可能会有所帮助,因为它允许内核也使用通过交换重新定位页面的代码路径来操作RAM。执行echo 7 > /proc/sys/vm/zone_reclaim_mode,并增加watermark_scale_factorextfrag_threshold可能也会有所帮助。 - Mikko Rantalainen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接