Hay un mapeo de memoria privado anónimo especial designado (tradicionalmente ubicado justo más allá de los datos/bss, pero el Linux moderno en realidad ajustará la ubicación con ASLR). En principio, no es mejor que cualquier otro mapeo que puedas crear con mmap
, pero Linux tiene algunas optimizaciones que hacen posible expandir el final de este mapeo (usando el brk
syscall) hacia arriba con un costo de bloqueo reducido en relación con lo que mmap
o mremap
incurriría. Esto lo hace atractivo para malloc
implementaciones para usar al implementar el montón principal.
Puedes usar brk
y sbrk
usted mismo para evitar los "gastos generales de malloc" de los que todos siempre se quejan. Pero no puede usar fácilmente este método junto con malloc
así que solo es apropiado cuando no tienes que free
cualquier cosa. Porque no puedes. Además, debe evitar cualquier llamada a la biblioteca que pueda usar malloc
internamente. Es decir. strlen
probablemente sea seguro, pero fopen
probablemente no lo sea.
Llama al sbrk
tal como llamarías a malloc
. Devuelve un puntero a la pausa actual e incrementa la pausa en esa cantidad.
void *myallocate(int n){
return sbrk(n);
}
Si bien no puede liberar asignaciones individuales (porque no hay gastos generales de malloc , recuerda), tú puedes liberar todo el espacio llamando al brk
con el valor devuelto por la primera llamada a sbrk
, rebobinando así el freno .
void *memorypool;
void initmemorypool(void){
memorypool = sbrk(0);
}
void resetmemorypool(void){
brk(memorypool);
}
Incluso podría apilar estas regiones, descartando la región más reciente rebobinando la ruptura hasta el inicio de la región.
Una cosa más...
sbrk
también es útil en Code Golf porque tiene 2 caracteres menos que malloc
.
Ejemplo ejecutable mínimo
¿Qué hace la llamada al sistema brk( )?
Pide al núcleo que le permita leer y escribir en una parte contigua de la memoria llamada montón.
Si no pregunta, podría cometer una falta de privacidad.
Sin brk
:
#define _GNU_SOURCE
#include <unistd.h>
int main(void) {
/* Get the first address beyond the end of the heap. */
void *b = sbrk(0);
int *p = (int *)b;
/* May segfault because it is outside of the heap. */
*p = 1;
return 0;
}
Con brk
:
#define _GNU_SOURCE
#include <assert.h>
#include <unistd.h>
int main(void) {
void *b = sbrk(0);
int *p = (int *)b;
/* Move it 2 ints forward */
brk(p + 2);
/* Use the ints. */
*p = 1;
*(p + 1) = 2;
assert(*p == 1);
assert(*(p + 1) == 2);
/* Deallocate back. */
brk(b);
return 0;
}
GitHub ascendente.
Es posible que lo anterior no llegue a una nueva página y no se produzca un error de segmento incluso sin el brk
, así que aquí hay una versión más agresiva que asigna 16MiB y es muy probable que falle en el segmento sin el brk
:
#define _GNU_SOURCE
#include <assert.h>
#include <unistd.h>
int main(void) {
void *b;
char *p, *end;
b = sbrk(0);
p = (char *)b;
end = p + 0x1000000;
brk(end);
while (p < end) {
*(p++) = 1;
}
brk(b);
return 0;
}
Probado en Ubuntu 18.04.
Visualización del espacio de direcciones virtuales
Antes de brk
:
+------+ <-- Heap Start == Heap End
Después de brk(p + 2)
:
+------+ <-- Heap Start + 2 * sizof(int) == Heap End
| |
| You can now write your ints
| in this memory area.
| |
+------+ <-- Heap Start
Después de brk(b)
:
+------+ <-- Heap Start == Heap End
Para comprender mejor los espacios de direcciones, debe familiarizarse con la paginación:¿Cómo funciona la paginación x86?.
¿Por qué necesitamos ambos brk
y sbrk
?
brk
por supuesto, podría implementarse con sbrk
+ cálculos de compensación, ambos existen solo por conveniencia.
En el backend, el kernel de Linux v5.0 tiene una sola llamada al sistema brk
que se usa para implementar ambos:https://github.com/torvalds/linux/blob/v5.0/arch/x86/entry/syscalls/syscall_64.tbl#L23
12 common brk __x64_sys_brk
Es brk
POSIX?
brk
solía ser POSIX, pero se eliminó en POSIX 2001, por lo tanto, la necesidad de _GNU_SOURCE
para acceder al envoltorio glibc.
La eliminación probablemente se deba a la introducción mmap
, que es un superconjunto que permite asignar múltiples rangos y más opciones de asignación.
Creo que no hay ningún caso válido en el que debas usar brk
en lugar de malloc
o mmap
hoy en día.
brk
contra malloc
brk
es una vieja posibilidad de implementar malloc
.
mmap
es el mecanismo más nuevo estrictamente más potente que probablemente todos los sistemas POSIX utilizan actualmente para implementar malloc
. Aquí hay un mínimo ejecutable mmap
ejemplo de asignación de memoria.
¿Puedo mezclar brk
y malloc?
Si su malloc
se implementa con brk
, no tengo idea de cómo es posible que eso no haga explotar las cosas, ya que brk
solo administra un único rango de memoria.
Sin embargo, no pude encontrar nada al respecto en los documentos de glibc, por ejemplo:
- https://www.gnu.org/software/libc/manual/html_mono/libc.html#Resizing-the-Data-Segment
Es probable que las cosas funcionen allí, supongo, desde mmap
es probable que se use para malloc
.
Véase también:
- ¿Qué es inseguro/heredado sobre brk/sbrk?
- ¿Por qué llamar a sbrk(0) dos veces da un valor diferente?
Más información
Internamente, el núcleo decide si el proceso puede tener tanta memoria y asigna páginas de memoria para ese uso.
Esto explica cómo se compara la pila con el montón:¿Cuál es la función de las instrucciones push/pop utilizadas en los registros en el ensamblado x86?
En el diagrama que publicaste, la "ruptura":la dirección manipulada por brk
y sbrk
—es la línea de puntos en la parte superior del montón.
La documentación que ha leído describe esto como el final del "segmento de datos" porque en las bibliotecas tradicionales (pre-shared-libraries, pre-mmap
) Unix, el segmento de datos era continuo con el montón; antes del inicio del programa, el kernel cargaría los bloques de "texto" y "datos" en la RAM comenzando en la dirección cero (en realidad, un poco por encima de la dirección cero, de modo que el puntero NULL realmente no apuntara a nada) y establecería la dirección de interrupción en el final del segmento de datos. La primera llamada a malloc
entonces usaría sbrk
para mover la separación y crear el montón en el medio la parte superior del segmento de datos y la nueva dirección de corte superior, como se muestra en el diagrama, y el uso posterior de malloc
lo usaría para agrandar el montón según sea necesario.
Mientras tanto, la pila comienza en la parte superior de la memoria y crece hacia abajo. La pila no necesita llamadas explícitas al sistema para hacerla más grande; o comienza con la mayor cantidad de RAM asignada que pueda tener (este era el enfoque tradicional) o hay una región de direcciones reservadas debajo de la pila, a la que el kernel asigna RAM automáticamente cuando nota un intento de escribir allí (este es el enfoque moderno). De cualquier manera, puede haber o no una región de "protección" en la parte inferior del espacio de direcciones que se puede usar para la pila. Si esta región existe (todos los sistemas modernos hacen esto) no está mapeada permanentemente; si cualquiera la pila o el montón intentan crecer en él, se produce un error de segmentación. Sin embargo, tradicionalmente, el kernel no intentaba imponer un límite; la pila podría crecer hasta convertirse en un montón, o el montón podría crecer hasta convertirse en una pila, y de cualquier manera, escribirían sobre los datos del otro y el programa colapsaría. Si tuviera mucha suerte, se bloquearía de inmediato.
No estoy seguro de dónde viene el número 512 GB en este diagrama. Implica un espacio de direcciones virtuales de 64 bits, que es inconsistente con el mapa de memoria muy simple que tiene allí. Un espacio de direcciones real de 64 bits se parece más a esto:
Legend: t: text, d: data, b: BSS
Esto no está remotamente a escala, y no debe interpretarse exactamente como un sistema operativo dado hace las cosas (después de dibujarlo, descubrí que Linux en realidad coloca el ejecutable mucho más cerca de la dirección cero de lo que pensé, y las bibliotecas compartidas en direcciones sorprendentemente altas). Las regiones negras de este diagrama no están mapeadas (cualquier acceso genera una falla de segmento inmediata) y son gigantescas en relación con las áreas grises. Las regiones de color gris claro son el programa y sus bibliotecas compartidas (puede haber docenas de bibliotecas compartidas); cada uno tiene un independiente segmento de texto y datos (y segmento "bss", que también contiene datos globales pero se inicializa en todos los bits cero en lugar de ocupar espacio en el ejecutable o la biblioteca en el disco). El montón ya no es necesariamente continuo con el segmento de datos del ejecutable; lo dibujé de esa manera, pero parece que Linux, al menos, no hace eso. La pila ya no está vinculada a la parte superior del espacio de direcciones virtuales, y la distancia entre el montón y la pila es tan enorme que no tiene que preocuparse por cruzarla.
La ruptura sigue siendo el límite superior del montón. Sin embargo, lo que no mostré es que podría haber docenas de asignaciones independientes de memoria en algún lugar, hechas con mmap
en lugar de brk
. (El sistema operativo intentará mantenerlos alejados del brk
para que no choquen).