Saltar al contenido

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

Solución:

En el diagrama que publicaste, la “interrupción”, la dirección manipulada por brk y sbrk—Es la línea punteada en la parte superior del montón.

imagen simplificada del diseño de la memoria virtual

La documentación que ha leído describe esto como el final del “segmento de datos” porque en las tradicionales (bibliotecas precompartidas, premmap) Unix, el segmento de datos era continuo con el montón; antes del inicio del programa, el kernel cargaba 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 apuntaba a nada) y establecí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 ruptura y crear el montón entre la parte superior del segmento de datos y la nueva dirección de ruptura más alta, como se muestra en el diagrama, y ​​el uso posterior de malloc lo usaría para hacer el montón más grande 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 tanta RAM asignada como 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 utilizar para la pila. Si esta región existe (todos los sistemas modernos hacen esto) está permanentemente sin mapear; si cualquiera la pila o el montón intenta crecer en él, se produce un error de segmentación. Tradicionalmente, sin embargo, el núcleo no hizo ningún intento por imponer un límite; la pila podría crecer en la pila, o la pila podría crecer en la pila, y de cualquier manera, garabatearían los datos de los demás y el programa fallaría. Si tuvieras mucha suerte, se estrellaría de inmediato.

No estoy seguro de dónde proviene el número de 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:

espacio de direcciones menos simplificado

              Legend:  t: text, d: data, b: BSS

Esto no se puede escalar de manera remota, y no debe interpretarse exactamente como un sistema operativo determinado 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 pensaba, y las bibliotecas compartidas en direcciones sorprendentemente altas). Las regiones negras de este diagrama no están mapeadas (cualquier acceso provoca una falla de segmentación inmediata) y son gigantesco 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 a todo-bits-cero en lugar de ocupar espacio en el ejecutable o 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 negro, hechas con mmap en lugar de brk. (El sistema operativo intentará mantenerlos lejos del brk área para que no choquen).

Ejemplo mínimo ejecutable

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

Pide al kernel que le permita leer y escribir en una porción de memoria contigua llamada heap.

Si no lo pregunta, es posible que lo separe.

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 en sentido ascendente.

Es posible que lo anterior no llegue a una nueva página y no se produzca un error de segmentación incluso sin el brk, por lo que aquí hay una versión más agresiva que asigna 16MiB y es muy probable que se produzca un error de segmentación 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 brk:

+------+ <-- Heap Start == Heap End

Después brk(p + 2):

+------+ <-- Heap Start + 2 * sizof(int) == Heap End 
|      |
| You can now write your ints
| in this memory area.
|      |
+------+ <-- Heap Start

Después 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 única 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, de ahí la necesidad de _GNU_SOURCE para acceder al contenedor glibc.

Es probable que la eliminación se deba a la introducción mmap, que es un superconjunto que permite asignar varios rangos y más opciones de asignación.

Creo que no hay un caso válido en el que deba usar brk en lugar de malloc o mmap hoy en día.

brk vs malloc

brk es una vieja posibilidad de implementar malloc.

mmap es el mecanismo más nuevo estrictamente más poderoso 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 tu malloc se implementa con brk, No tengo idea de cómo eso no puede hacer estallar las cosas, ya que brk solo gestiona 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, ya que mmap es probable que se use para malloc.

Ver también:

  • ¿Qué tiene de inseguro / heredado brk / sbrk?
  • ¿Por qué llamar a sbrk (0) dos veces da un valor diferente?

Más información

Internamente, el kernel 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 ensamblaje x86?

Puedes usar brk y sbrk usted mismo para evitar la “sobrecarga de malloc” de la que todo el mundo siempre se queja. Pero no se puede utilizar fácilmente este método junto con malloc por lo que solo es apropiado cuando no es necesario 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 es.

Llama sbrk como llamarías malloc. Devuelve un puntero a la ruptura actual e incrementa la ruptura en esa cantidad.

void *myallocate(int n){
    return sbrk(n);
}

Si bien no puede liberar asignaciones individuales (porque no hay malloc-overhead, te recuerdo pueden gratis todo el espacio llamando brk con el valor devuelto por la primera llamada a sbrk, por lo tanto rebobinando el brk.

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 al inicio de la región.


Una cosa más …

sbrk también es útil en golf de código porque es 2 caracteres más corto que malloc.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *