GNU/Linux >> Tutoriales Linux >  >> Linux

¿Qué hace la llamada al sistema brk()?

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).


Linux
  1. ¿Cuál es el nivel de ejecución actual del sistema Linux?

  2. ¿Qué hace el permiso de ejecución?

  3. ¿Qué significa el 'rc' en '.bashrc', etc.?

  4. ¿Qué hizo la llamada del sistema tuxcall?

  5. ¿Qué significa la capacidad ep?

¿Qué significa en la salida de Ps?

El Punto De Uniq -u Y Que Hace??

¿Qué hace el atajo Ctrl-alt-+?

¿Cuál es la diferencia entre la llamada al sistema y la llamada a la biblioteca?

¿Qué hace exactamente init?

¿Qué significa el sufijo .d en Linux?