Tengo una caja ARM Linux integrada con una cantidad limitada de RAM (512 MB) y sin espacio de intercambio, en la que necesito crear y luego manipular un archivo bastante grande (~ 200 MB). Cargar todo el archivo en la RAM, modificar el contenido en la RAM y luego volver a escribirlo a veces invocaría el OOM-killer, que quiero evitar.
Mi idea para evitar esto fue usar mmap() para mapear este archivo en el espacio de direcciones virtuales de mi proceso; de esa manera, las lecturas y escrituras en el área de memoria mapeada irían al sistema de archivos flash local en su lugar, y se evitaría el asesino de OOM ya que si la memoria se agotara, Linux podría simplemente vaciar parte de la memoria mmap () páginas de vuelta al disco para liberar algo de RAM. (Eso podría hacer que mi programa sea lento, pero lento está bien para este caso de uso)
Sin embargo, incluso con la llamada mmap() , sigo viendo de vez en cuando que el OOM-killer mata procesos mientras realizo la operación anterior.
Mi pregunta es, ¿era demasiado optimista acerca de cómo se comportaría Linux en presencia de un gran mmap() y RAM limitada? (es decir, mmap()-ing un archivo de 200 MB y luego leer/escribir en la memoria de mmap() todavía requiere 200 MB de RAM disponible para lograr de manera confiable?) ¿O mmap() debería ser lo suficientemente inteligente como para paginar páginas mmap'd cuando la memoria es baja, pero estoy haciendo algo mal en la forma en que la uso?
FWIW mi código para hacer el mapeo está aquí:
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)); } Puedo reescribir este código para usar simple-viejo-archivo-I/O si es necesario, pero sería bueno si mmap() pudiera hacer el trabajo (o si no, al menos me gustaría entender por qué no ).
Después de mucha más experimentación, determiné que el OOM-killer me estaba visitando no porque el sistema se hubiera quedado sin RAM, sino porque la RAM ocasionalmente se fragmentaba lo suficiente como para que el kernel no pudiera encontrar un conjunto de páginas de RAM físicamente contiguas lo suficientemente grandes. para satisfacer sus necesidades inmediatas. Cuando esto sucedía, el kernel invocaba el OOM-killer para liberar algo de RAM y evitar un pánico en el kernel, lo cual está muy bien para el kernel, pero no es tan bueno cuando elimina un proceso en el que el usuario confiaba para obtener su trabajo hecho. :/
Después de intentar y fallar en encontrar una manera de convencer a Linux de que no hiciera eso (creo que habilitar una partición de intercambio evitaría el asesino de OOM, pero hacer eso no es una opción para mí en estas máquinas en particular), se me ocurrió un truco solución alterna; Agregué un código a mi programa que verifica periódicamente la cantidad de fragmentación de la memoria informada por el kernel de Linux, y si la fragmentación de la memoria comienza a verse demasiado severa, ordena preventivamente que ocurra una desfragmentación de la memoria, para que el OOM-killer (con suerte) no se haga necesario. Si el pase de desfragmentación de la memoria no parece estar mejorando las cosas, luego de 20 intentos consecutivos, también eliminamos la memoria caché de la página de VM como una forma de liberar RAM física contigua. Todo esto es muy feo, pero no tan feo como recibir una llamada telefónica a las 3 a.m. de un usuario que quiere saber por qué su programa de servidor se bloqueó. :/
La esencia de la implementación alternativa se encuentra a continuación; tenga en cuenta que se espera que DefragTick(Milliseconds) se llame periódicamente (preferiblemente una vez por segundo).
// 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= eg: "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 (eg 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); } } } }