¿Qué es la "expansión automática de la pila"?

13

getrlimit (2) tiene la siguiente definición en las páginas del manual:

RLIMIT_AS El tamaño máximo de la memoria virtual del proceso (espacio de direcciones) en bytes. Este límite afecta las llamadas a brk (2), mmap (2) y mremap (2), que fallan con el error ENOMEM al exceder este límite. También la expansión automática de la pila fallará (y generará un SIGSEGV que mata el proceso si no se ha puesto a disposición una pila alternativa a través de sigaltstack (2)). Como el valor es largo, en máquinas con una longitud de 32 bits, este límite es como máximo 2 GiB o este recurso es ilimitado.

¿Qué se entiende por "expansión automática de pila" aquí? ¿La pila en un entorno Linux / UNIX crece según sea necesario? En caso afirmativo, ¿cuál es el mecanismo exacto?

alto y claro
fuente

Respuestas:

1

Sí, las pilas crecen dinámicamente. La pila está en la parte superior de la memoria creciendo hacia abajo hacia el montón.

--------------
| Stack      |
--------------
| Free memory|
--------------
| Heap       |
--------------
     .
     .

El montón crece hacia arriba (cada vez que haces malloc) y la pila crece hacia abajo a medida que se llaman nuevas funciones. El montón está presente justo encima de la sección BSS del programa. Lo que significa que el tamaño de su programa y la forma en que asigna memoria en el montón también afectan el tamaño máximo de la pila para ese proceso. Por lo general, el tamaño de la pila es ilimitado (hasta que las áreas de montón y pila se encuentran y / o sobrescriben, lo que dará un desbordamiento de pila y SIGSEGV :-)

Esto es solo para los procesos del usuario. La pila del kernel se arregla siempre (generalmente 8 KB)

Santosh
fuente
"Por lo general, el tamaño de la pila es ilimitado", no, generalmente está limitado por 8Mb ( ulimit -s).
Eddy_Em
Sí, tienes razón en la mayoría de los sistemas. Está comprobando el comando ulimit del shell, si es así, hay un límite estricto en el tamaño de la pila, que es ilimitado (ulimit -Hs). De todos modos, ese punto era enfatizar que la pila y el montón crecen en direcciones opuestas.
Santosh
Entonces, ¿en qué se diferencia la "expansión automática" de "empujar elementos a la pila"? Por su explicación, tengo la sensación de que son lo mismo. Además, sentí que los puntos de partida de la pila y el montón son mucho más de 8 MB, por lo que la pila puede crecer tanto como sea necesario (o golpea el montón). ¿Es eso cierto? En caso afirmativo, ¿cómo decidió el sistema operativo dónde colocar el montón y la pila?
loudandclear
Son iguales, pero las pilas no tienen un tamaño fijo a menos que limites el tamaño con rlimit. La pila se coloca al final del área de memoria virtual, y el almacenamiento dinámico se encuentra inmediatamente después del segmento de datos del ejecutable.
Santosh
Entiendo, gracias. Sin embargo, no creo que obtenga la parte "las pilas no tienen un tamaño fijo". Si ese es el caso, ¿para qué sirve el límite flexible de 8Mb?
loudandclear
8

El mecanismo exacto se da aquí, en Linux: al manejar un error de página en asignaciones anónimas , verifica si es una "asignación de crecimiento" que debe expandirse como una pila. Si el registro del área VM dice que debería hacerlo, entonces ajusta la dirección de inicio para expandir la pila.

Cuando se produce un error de página, dependiendo de la dirección, se puede reparar (y anular el error) mediante la expansión de la pila. Este comportamiento de "crecimiento descendente en caso de falla" para la memoria virtual puede solicitarse mediante programas de usuario arbitrarios con el MAP_GROWSDOWNindicador que se pasa a la mmapllamada al sistema.

También puede jugar con este mecanismo en un programa de usuario:

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/mman.h>

int main() {
        long page_size = sysconf(_SC_PAGE_SIZE);
        void *mem = mmap(NULL, page_size, PROT_READ|PROT_WRITE, MAP_GROWSDOWN|MAP_ANONYMOUS|MAP_PRIVATE, -1, 0);
        if (MAP_FAILED == mem) {
                perror("failed to create growsdown mapping");
                return EXIT_FAILURE;
        }

        volatile char *tos = (char *) mem + page_size;

        int i;
        for (i = 1; i < 10 * page_size; ++i)
                tos[-i] = 42;

        fprintf(stderr, "inspect mappping for originally page-sized %p in /proc... press any key to continue...\n", mem);
        (void) getchar();

        if (munmap(mem, page_size))
                perror("failed munmap");

        return EXIT_SUCCESS;
}

Cuando se le solicite, encuentre el pid del programa (vía ps) y mire /proc/$THAT_PID/mapspara ver cómo ha crecido el área original.

cdleary
fuente
¿Está bien llamar a munmap para el mem original y page_size incluso si la región de memoria ha crecido a través de MAP_GROWSDOWN? Supongo que sí, porque de lo contrario sería una API muy difíciles de usar, pero la documentación no dice nada explícitamente sobre este asunto
i.petruk
2
MAP_GROWSDOWN no ​​debe usarse, y se ha eliminado de glibc (consulte lwn.net/Articles/294001 para saber por qué).
Collin el