¿Qué podría estar sucediendo si un proceso se "cancela debido a poca RAM"?
A veces se dice que Linux por defecto nunca niega las solicitudes de más memoria del código de la aplicación, por ejemplo malloc()
.1 Esto no es de hecho cierto; el valor predeterminado utiliza un heurístico por el cual
Los excesos obvios de espacio de direcciones se rechazan. Usado para un sistema típico. Asegura que una asignación seriamente grave falle mientras permite un exceso de compromiso para reducir el uso de intercambio.
De [linux_src]/Documentation/vm/overcommit-accounting
(todas las citas son del árbol 3.11). Exactamente lo que cuenta como una "asignación seriamente salvaje" no se hace explícito, por lo que tendríamos que pasar por la fuente para determinar los detalles. También podríamos usar el método experimental en la nota al pie 2 (a continuación) para tratar de obtener un reflejo de la heurística; en base a eso, mi observación empírica inicial es que, en circunstancias ideales (== el sistema está inactivo), si no lo hace ' No tiene ningún intercambio, se le permitirá asignar aproximadamente la mitad de su RAM, y si lo hace, obtendrá aproximadamente la mitad de su RAM más todo su intercambio. Eso es más o menos por proceso (pero tenga en cuenta que este límite es dinámico y está sujeto a cambios debido al estado, ver algunas observaciones en la nota 5).
La mitad de su intercambio RAM plus es explícitamente el valor predeterminado para el campo "CommitLimit" en /proc/meminfo
. Esto es lo que significa, y tenga en cuenta que en realidad no tiene nada que ver con el límite que se acaba de discutir (de [src]/Documentation/filesystems/proc.txt
):
CommitLimit: basado en la relación de sobrecompromiso ('vm.overcommit_ratio'), esta es la cantidad total de memoria disponible actualmente para ser asignada en el sistema. Este límite solo se cumple si se habilita la contabilidad estricta de sobrecompromiso (modo 2 en 'vm.overcommit_memory'). El CommitLimit se calcula con la siguiente fórmula: CommitLimit = ('vm.overcommit_ratio' * RAM física) + Swap Por ejemplo, en un sistema con 1G de RAM física y 7G de swap con un 'vm.overcommit_ratio' de 30 produciría un CommitLimit de 7.3G.
El documento de contabilidad de sobrecompromiso citado anteriormente establece que el valor predeterminado vm.overcommit_ratio
es 50. Entonces, si puede sysctl vm.overcommit_memory=2
, puede ajustar vm.covercommit_ratio (con sysctl
) y ver las consecuencias. 3 El modo predeterminado, cuando CommitLimit
no se aplica y solo "se rechazan las sobrecompresiones obvias de espacio de direcciones", es cuándo vm.overcommit_memory=0
.
Si bien la estrategia predeterminada tiene un límite heurístico por proceso que impide la "asignación seriamente salvaje", sí deja al sistema en su conjunto libre para ponerse seriamente salvaje, en cuanto a la asignación. 4 Esto significa que en algún momento puede quedarse sin memoria y tener que declararse en bancarrota en algunos procesos a través del asesino OOM .
¿Qué mata el asesino OOM? No es necesariamente el proceso que pedía memoria cuando no había ninguno, ya que ese no es necesariamente el proceso verdaderamente culpable y, lo que es más importante, no necesariamente el que sacará al sistema más rápidamente del problema en el que se encuentra.
Aquí se cita esto, que probablemente cita una fuente 2.6.x:
/*
* oom_badness - calculate a numeric value for how bad this task has been
*
* The formula used is relatively simple and documented inline in the
* function. The main rationale is that we want to select a good task
* to kill when we run out of memory.
*
* Good in this context means that:
* 1) we lose the minimum amount of work done
* 2) we recover a large amount of memory
* 3) we don't kill anything innocent of eating tons of memory
* 4) we want to kill the minimum amount of processes (one)
* 5) we try to kill the process the user expects us to kill, this
* algorithm has been meticulously tuned to meet the principle
* of least surprise ... (be careful when you change it)
*/
Lo que parece una justificación decente. Sin embargo, sin ser forense, el n. ° 5 (que es redundante del n. ° 1) parece una implementación de venta difícil, y el n. ° 3 es redundante del n. ° 2. Por lo tanto, podría tener sentido considerar esto reducido a # 2/3 y # 4.
Busqué una fuente reciente (3.11) y noté que este comentario ha cambiado mientras tanto:
/**
* oom_badness - heuristic function to determine which candidate task to kill
*
* The heuristic for determining which task to kill is made to be as simple and
* predictable as possible. The goal is to return the highest value for the
* task consuming the most memory to avoid subsequent oom failures.
*/
Esto es un poco más explícito sobre el n. ° 2: "El objetivo es [matar] la tarea que consume la mayor cantidad de memoria para evitar fallas posteriores de OOM" y, por implicación, n. ° 4 ( "queremos matar la cantidad mínima de procesos ( uno ) ) .
Si desea ver al asesino OOM en acción, vea la nota 5.
1 Un engaño que Gilles afortunadamente me libró, ver comentarios.
2 Aquí hay un bit sencillo de C que solicita trozos de memoria cada vez más grandes para determinar cuándo fallará una solicitud de más:
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#define MB 1 << 20
int main (void) {
uint64_t bytes = MB;
void *p = malloc(bytes);
while (p) {
fprintf (stderr,
"%lu kB allocated.\n",
bytes / 1024
);
free(p);
bytes += MB;
p = malloc(bytes);
}
fprintf (stderr,
"Failed at %lu kB.\n",
bytes / 1024
);
return 0;
}
Si no conoce C, puede compilar esto gcc virtlimitcheck.c -o virtlimitcheck
y luego ejecutar./virtlimitcheck
. Es completamente inofensivo, ya que el proceso no usa nada del espacio que solicita, es decir, en realidad nunca usa RAM.
En un sistema 3.11 x86_64 con sistema de 4 GB y 6 GB de intercambio, fallé a ~ 7400000 kB; el número fluctúa, por lo que quizás el estado sea un factor. Esto es por casualidad cerca de la CommitLimit
de /proc/meminfo
, pero esto a través de la modificaciónvm.overcommit_ratio
no hace ninguna diferencia. Sin embargo, en un sistema ARM 448 MB 3.6.11 de 32 bits con 64 MB de intercambio, fallo a ~ 230 MB. Esto es interesante ya que en el primer caso la cantidad es casi el doble de la cantidad de RAM, mientras que en el segundo es aproximadamente 1/4, lo que implica que la cantidad de intercambio es un factor. Esto se confirmó al desactivar el intercambio en el primer sistema, cuando el umbral de falla bajó a ~ 1.95 GB, una relación muy similar a la pequeña caja ARM.
¿Pero es esto realmente por proceso? Parece ser. El breve programa a continuación solicita un fragmento de memoria definido por el usuario y, si tiene éxito, espera a que presione la tecla de retorno; de esta manera puede probar varias instancias simultáneas:
#include <stdio.h>
#include <stdlib.h>
#define MB 1 << 20
int main (int argc, const char *argv[]) {
unsigned long int megabytes = strtoul(argv[1], NULL, 10);
void *p = malloc(megabytes * MB);
fprintf(stderr,"Allocating %lu MB...", megabytes);
if (!p) fprintf(stderr,"fail.");
else {
fprintf(stderr,"success.");
getchar();
free(p);
}
return 0;
}
Sin embargo, tenga en cuenta que no se trata estrictamente de la cantidad de RAM e intercambio independientemente del uso; consulte la nota 5 para ver las observaciones sobre los efectos del estado del sistema.
3 se CommitLimit
refiere a la cantidad de espacio de direcciones permitido para el sistema cuando vm.overcommit_memory = 2. Presumiblemente, entonces, la cantidad que puede asignar debería ser menos menos lo que ya está comprometido, que aparentemente es el Committed_AS
campo.
Un experimento potencialmente interesante que demuestra esto es agregar #include <unistd.h>
a la parte superior de virtlimitcheck.c (ver nota al pie 2), y fork()
justo antes del while()
ciclo. No se garantiza que funcione como se describe aquí sin una sincronización tediosa, pero hay una buena posibilidad de que lo haga, YMMV:
> sysctl vm.overcommit_memory=2
vm.overcommit_memory = 2
> cat /proc/meminfo | grep Commit
CommitLimit: 9231660 kB
Committed_AS: 3141440 kB
> ./virtlimitcheck 2&> tmp.txt
> cat tmp.txt | grep Failed
Failed at 3051520 kB.
Failed at 6099968 kB.
Esto tiene sentido: al mirar tmp.txt en detalle, puede ver que los procesos alternan sus asignaciones cada vez más grandes (esto es más fácil si arroja el pid en la salida) hasta que uno, evidentemente, ha reclamado lo suficiente como para que el otro falle. El ganador es libre de agarrar todo hasta CommitLimit
menos Committed_AS
.
4 Vale la pena mencionar, en este punto, si aún no comprende el direccionamiento virtual y la paginación de demanda, lo que hace posible un compromiso excesivo en primer lugar es que lo que el núcleo asigna a los procesos del usuario no es en absoluto memoria física: es espacio de direcciones virtuales . Por ejemplo, si un proceso reserva 10 MB para algo, se presenta como una secuencia de direcciones (virtuales), pero esas direcciones aún no corresponden a la memoria física. Cuando se accede a dicha dirección, esto da como resultado un error de páginay luego el núcleo intenta mapearlo en la memoria real para que pueda almacenar un valor real. Los procesos generalmente reservan mucho más espacio virtual del que realmente acceden, lo que permite que el núcleo haga el uso más eficiente de la RAM. Sin embargo, la memoria física sigue siendo un recurso finito y, cuando se ha asignado todo al espacio de direcciones virtuales, se debe eliminar parte del espacio de direcciones virtuales para liberar algo de RAM.
5 Primero una advertencia : si intenta hacer esto vm.overcommit_memory=0
, asegúrese de guardar su trabajo primero y cierre todas las aplicaciones críticas, porque el sistema se congelará durante ~ 90 segundos y algunos procesos morirán.
La idea es ejecutar una bomba de horquilla que agota el tiempo de espera después de 90 segundos, con las horquillas asignando espacio y algunas de ellas escribiendo grandes cantidades de datos en la RAM, todo el tiempo informando a stderr.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/time.h>
#include <errno.h>
#include <string.h>
/* 90 second "Verbose hungry fork bomb".
Verbose -> It jabbers.
Hungry -> It grabs address space, and it tries to eat memory.
BEWARE: ON A SYSTEM WITH 'vm.overcommit_memory=0', THIS WILL FREEZE EVERYTHING
FOR THE DURATION AND CAUSE THE OOM KILLER TO BE INVOKED. CLOSE THINGS YOU CARE
ABOUT BEFORE RUNNING THIS. */
#define STEP 1 << 30 // 1 GB
#define DURATION 90
time_t now () {
struct timeval t;
if (gettimeofday(&t, NULL) == -1) {
fprintf(stderr,"gettimeofday() fail: %s\n", strerror(errno));
return 0;
}
return t.tv_sec;
}
int main (void) {
int forks = 0;
int i;
unsigned char *p;
pid_t pid, self;
time_t check;
const time_t start = now();
if (!start) return 1;
while (1) {
// Get our pid and check the elapsed time.
self = getpid();
check = now();
if (!check || check - start > DURATION) return 0;
fprintf(stderr,"%d says %d forks\n", self, forks++);
// Fork; the child should get its correct pid.
pid = fork();
if (!pid) self = getpid();
// Allocate a big chunk of space.
p = malloc(STEP);
if (!p) {
fprintf(stderr, "%d Allocation failed!\n", self);
return 0;
}
fprintf(stderr,"%d Allocation succeeded.\n", self);
// The child will attempt to use the allocated space. Using only
// the child allows the fork bomb to proceed properly.
if (!pid) {
for (i = 0; i < STEP; i++) p[i] = i % 256;
fprintf(stderr,"%d WROTE 1 GB\n", self);
}
}
}
Compila esto gcc forkbomb.c -o forkbomb
. Primero, pruébalo sysctl vm.overcommit_memory=2
, probablemente obtendrás algo como:
6520 says 0 forks
6520 Allocation succeeded.
6520 says 1 forks
6520 Allocation succeeded.
6520 says 2 forks
6521 Allocation succeeded.
6520 Allocation succeeded.
6520 says 3 forks
6520 Allocation failed!
6522 Allocation succeeded.
En este entorno, este tipo de bomba tenedor no llega muy lejos. Tenga en cuenta que el número en "dice N forks" no es el número total de procesos, es el número de procesos en la cadena / rama que conduce a ese.
Ahora pruébalo con vm.overcommit_memory=0
. Si redirige stderr a un archivo, puede hacer un análisis crudo después, por ejemplo:
> cat tmp.txt | grep failed
4641 Allocation failed!
4646 Allocation failed!
4642 Allocation failed!
4647 Allocation failed!
4649 Allocation failed!
4644 Allocation failed!
4643 Allocation failed!
4648 Allocation failed!
4669 Allocation failed!
4696 Allocation failed!
4695 Allocation failed!
4716 Allocation failed!
4721 Allocation failed!
Solo 15 procesos no pudieron asignar 1 GB, lo que demuestra que la heurística para overcommit_memory = 0 se ve afectada por el estado. ¿Cuántos procesos hubo? Mirando al final de tmp.txt, probablemente> 100,000. Ahora, ¿cómo puede realmente usar el 1 GB?
> cat tmp.txt | grep WROTE
4646 WROTE 1 GB
4648 WROTE 1 GB
4671 WROTE 1 GB
4687 WROTE 1 GB
4694 WROTE 1 GB
4696 WROTE 1 GB
4716 WROTE 1 GB
4721 WROTE 1 GB
Ocho, lo que nuevamente tiene sentido, ya que en ese momento tenía ~ 3 GB de RAM libre y 6 GB de intercambio.
Eche un vistazo a los registros de su sistema después de hacer esto. Debería ver los puntajes de informes del asesino OOM (entre otras cosas); presumiblemente esto se relaciona con oom_badness
.