如何在不冒 OOM 杀手风险的情况下 mmap() 一个大文件?
How to mmap() a large file without risking the OOM killer?
我有一个嵌入式 ARM Linux 盒子,RAM 数量有限 (512MB) 并且没有交换 space,我需要在上面创建然后操作一个相当大的文件( ~200MB)。将整个文件加载到 RAM 中,修改 RAM 中的内容,然后再次将其写回有时会调用 OOM-killer,我想避免这种情况。
我解决这个问题的想法是使用 mmap()
将此文件映射到我进程的虚拟地址 space;这样,对映射内存区域的读取和写入将转到本地闪存文件系统,并且可以避免 OOM 杀手,因为如果内存不足,Linux 可以刷新一些 mmap( ) 的内存页面返回到磁盘以释放一些 RAM。 (这可能会使我的程序变慢,但对于这个用例来说,慢是可以的)
然而,即使使用 mmap()
调用,我仍然偶尔会看到进程在执行上述操作时被 OOM-killer 杀死。
我的问题是,我是否对 Linux 在存在大型 mmap() 和有限 RAM 的情况下的表现过于乐观? (即 mmap()-ing 一个 200MB 的文件然后 reading/writing 到 mmap() 的内存仍然需要 200MB 的可用 RAM 才能可靠地完成吗?)或者 mmap() 应该足够聪明以分页 mmap' d 页面时内存不足,但我在使用它时做错了什么?
FWIW 我做映射的代码在这里:
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));
}
如果必须的话,我可以重写此代码以仅使用 plain-old-file-I/O,但如果 mmap()
可以完成这项工作(或者如果不能,我至少希望明白为什么不)。
经过进一步的实验,我确定 OOM 杀手来找我不是因为系统 运行 RAM 不足,而是因为 RAM 偶尔会变得非常碎片化以至于内核找不到一组物理上连续的 RAM 页,其大小足以满足其即时需求。发生这种情况时,内核会调用 OOM-killer 来释放一些 RAM 以避免内核恐慌,这对内核来说很好,但当它杀死用户依赖的进程时就不是那么好了。完工。 :/
在尝试并未能找到说服 Linux 不这样做的方法之后(我认为启用交换分区可以避免 OOM 杀手,但在这些特定情况下这样做对我来说不是一个选择机器),我想出了一个 hack work-around;我在我的程序中添加了一些代码,定期检查 Linux 内核报告的内存碎片数量,如果内存碎片看起来太严重,则先发制人地命令进行内存碎片整理,以便 OOM 杀手将(希望)不会变得必要。如果内存碎片整理过程似乎没有改善任何问题,那么在连续尝试 20 次之后,我们还会删除 VM 页面缓存,以此作为释放连续物理 RAM 的一种方式。这一切都非常丑陋,但还不如在凌晨 3 点接到一个想知道他们的服务器程序为什么崩溃的用户的 phone 电话那么丑陋。 :/
变通实施的要点如下;请注意,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);
}
}
}
}
我有一个嵌入式 ARM Linux 盒子,RAM 数量有限 (512MB) 并且没有交换 space,我需要在上面创建然后操作一个相当大的文件( ~200MB)。将整个文件加载到 RAM 中,修改 RAM 中的内容,然后再次将其写回有时会调用 OOM-killer,我想避免这种情况。
我解决这个问题的想法是使用 mmap()
将此文件映射到我进程的虚拟地址 space;这样,对映射内存区域的读取和写入将转到本地闪存文件系统,并且可以避免 OOM 杀手,因为如果内存不足,Linux 可以刷新一些 mmap( ) 的内存页面返回到磁盘以释放一些 RAM。 (这可能会使我的程序变慢,但对于这个用例来说,慢是可以的)
然而,即使使用 mmap()
调用,我仍然偶尔会看到进程在执行上述操作时被 OOM-killer 杀死。
我的问题是,我是否对 Linux 在存在大型 mmap() 和有限 RAM 的情况下的表现过于乐观? (即 mmap()-ing 一个 200MB 的文件然后 reading/writing 到 mmap() 的内存仍然需要 200MB 的可用 RAM 才能可靠地完成吗?)或者 mmap() 应该足够聪明以分页 mmap' d 页面时内存不足,但我在使用它时做错了什么?
FWIW 我做映射的代码在这里:
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));
}
如果必须的话,我可以重写此代码以仅使用 plain-old-file-I/O,但如果 mmap()
可以完成这项工作(或者如果不能,我至少希望明白为什么不)。
经过进一步的实验,我确定 OOM 杀手来找我不是因为系统 运行 RAM 不足,而是因为 RAM 偶尔会变得非常碎片化以至于内核找不到一组物理上连续的 RAM 页,其大小足以满足其即时需求。发生这种情况时,内核会调用 OOM-killer 来释放一些 RAM 以避免内核恐慌,这对内核来说很好,但当它杀死用户依赖的进程时就不是那么好了。完工。 :/
在尝试并未能找到说服 Linux 不这样做的方法之后(我认为启用交换分区可以避免 OOM 杀手,但在这些特定情况下这样做对我来说不是一个选择机器),我想出了一个 hack work-around;我在我的程序中添加了一些代码,定期检查 Linux 内核报告的内存碎片数量,如果内存碎片看起来太严重,则先发制人地命令进行内存碎片整理,以便 OOM 杀手将(希望)不会变得必要。如果内存碎片整理过程似乎没有改善任何问题,那么在连续尝试 20 次之后,我们还会删除 VM 页面缓存,以此作为释放连续物理 RAM 的一种方式。这一切都非常丑陋,但还不如在凌晨 3 点接到一个想知道他们的服务器程序为什么崩溃的用户的 phone 电话那么丑陋。 :/
变通实施的要点如下;请注意,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);
}
}
}
}