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

184

De acuerdo con el manual de programadores de Linux:

brk () y sbrk () cambian la ubicación de la interrupción del programa, que define el final del segmento de datos del proceso.

¿Qué significa el segmento de datos aquí? ¿Es solo el segmento de datos o datos, BSS y montón combinados?

De acuerdo con wiki:

A veces, las áreas de datos, BSS y montón se denominan colectivamente "segmento de datos".

No veo ninguna razón para cambiar el tamaño de solo el segmento de datos. Si se trata de datos, BSS y almacenamiento colectivo, entonces tiene sentido ya que el almacenamiento dinámico tendrá más espacio.

Lo que me lleva a mi segunda pregunta. En todos los artículos que leí hasta ahora, el autor dice que el montón crece hacia arriba y la pila crece hacia abajo. Pero lo que no explican es qué sucede cuando el montón ocupa todo el espacio entre el montón y la pila.

ingrese la descripción de la imagen aquí

nik
fuente
1
Entonces, ¿qué haces cuando estás fuera del espacio? cambia a HDD. Cuando ha utilizado el espacio, lo libera para otro tipo de información.
Igoris Azanovas
29
@Igoris: Confunde la memoria física (que puede cambiar al disco según sea necesario, usando la memoria virtual) y el espacio de direcciones . Cuando complete su espacio de direcciones, ninguna cantidad de intercambio le devolverá esas direcciones en el medio.
Daniel Pryden
77
Solo como recordatorio, la brk()llamada al sistema es más útil en lenguaje ensamblador que en C. En C, malloc()debe usarse en lugar de brk()para cualquier propósito de asignación de datos, pero esto no invalida la pregunta propuesta de ninguna manera.
alecov
2
@Brian: El montón es una estructura de datos compleja para manejar regiones de diferentes tamaños y alineaciones, grupo libre, etc. Las pilas de hilos son siempre secuencias contiguas (en el espacio de direcciones virtuales) de páginas completas. En la mayoría de los sistemas operativos, hay un asignador de páginas subyacente en las pilas, el montón y los archivos mapeados en memoria.
Ben Voigt el
2
@Brian: ¿Quién dijo que hay alguna "Pila" siendo manipulada por brk()y sbrk()? Las asignaciones son administradas por el asignador de página, en un nivel mucho más bajo.
Ben Voigt

Respuestas:

233

En el diagrama que publicó, el "corte", la dirección manipulada por brky, sbrkes la línea punteada en la parte superior del montón.

imagen simplificada de diseño de memoria virtual

La documentación que ha leído describe esto como el final del "segmento de datos" porque en las bibliotecas tradicionales (precompartidas, pre mmap) Unix, el segmento de datos era continuo con el montón; antes del inicio del programa, el núcleo 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 apuntaba a nada) y establecería la dirección de corte en El final del segmento de datos. La primera llamada a mallocluego se usaría sbrkpara mover la división y crear el montón entre la parte superior del segmento de datos y la nueva dirección de corte más alta, como se muestra en el diagrama, y ​​el uso posterior de la mallocusaría para aumentar 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 del sistema para agrandarla; o comienza con la cantidad de RAM asignada como siempre (este era el enfoque tradicional) o hay una región de direcciones reservadas debajo de la pila, a la que el núcleo asigna automáticamente RAM cuando nota un intento de escribir allí (Este es el enfoque moderno). De cualquier manera, puede haber o no una región de "guardia" en la parte inferior del espacio de direcciones que se puede usar para apilar. Si esta región existe (todos los sistemas modernos hacen esto) está permanentemente sin asignar; si cualquierala pila o el montón intenta crecer, obtienes un error de segmentación. Sin embargo, tradicionalmente, el núcleo no hizo ningún intento de imponer un límite; la pila podría crecer en el montón, o el montón podría crecer en la pila, y de cualquier manera garabatearían sobre los datos del otro y el programa se bloquearía. Si tuviera mucha suerte, se estrellaría de inmediato.

No estoy seguro de dónde proviene el número 512GB 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 forma remota, y no debe interpretarse como exactamente cómo funciona un sistema operativo determinado (después de dibujarlo descubrí que Linux realmente pone 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 asignadas (cualquier acceso causa una falla predeterminada 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 independientesegmento 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 grande 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 del negro, hechas con en mmaplugar de brk. (El sistema operativo intentará mantenerlos alejados del brkárea para que no choquen).

zwol
fuente
77
+1 para una explicación detallada. ¿Sabes si malloctodavía se basa brko si se está utilizando mmappara poder "devolver" bloques de memoria separados?
Anders Abel
18
Depende de la implementación específica, pero IIUC muchos de los actuales mallocusan el brkárea para asignaciones pequeñas y los individuales mmappara asignaciones grandes (por ejemplo,> 128K). Vea, por ejemplo, la discusión de MMAP_THRESHOLD en la página de manual de Linux malloc(3).
zwol
1
De hecho, una buena explicación. Pero como dijiste, Stack ya no se encuentra en la parte superior del espacio de direcciones virtuales. ¿Es esto cierto solo para el espacio de direcciones de 64 bits o es cierto incluso para el espacio de direcciones de 32 bits? Y si la pila se encuentra en la parte superior del espacio de direcciones, ¿dónde ocurren los mapas de memoria anónimos? Está en la parte superior del espacio de direcciones virtuales justo antes de la pila.
Nik
3
@Nikhil: es complicado. La mayoría de los sistemas de 32 bits colocan la pila en la parte superior del espacio de direcciones en modo de usuario , que a menudo es solo el 2 o 3G inferior del espacio de direcciones completo (el espacio restante está reservado para el núcleo). Actualmente no puedo pensar en uno que no, pero no los conozco todos. La mayoría de las CPU de 64 bits no le permiten utilizar todo el espacio de 64 bits; los 10 a 16 bits altos de la dirección deben ser todo cero o todo uno. La pila generalmente se coloca cerca de la parte superior de las direcciones bajas utilizables. No puedo darte una regla mmap; Es extremadamente dependiente del sistema operativo.
zwol
3
@RiccardoBestetti Se desperdicia espacio de direcciones , pero eso es inofensivo: un espacio de direcciones virtual de 64 bits es tan grande que si se quema un gigabyte cada segundo , aún le tomaría 500 años quedarse sin espacio . [1] La mayoría de los procesadores ni siquiera permiten el uso de más de 2 ^ 48 a 2 ^ 53 bits de dirección virtual (la única excepción que conozco es POWER4 en modo tabla de páginas hash). No desperdicia RAM física; Las direcciones no utilizadas no se asignan a la RAM.
zwol
26

Ejemplo ejecutable mínimo

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

Pide al núcleo que le permita leer y escribir en un fragmento contiguo de memoria llamado montón.

Si no pregunta, podría dejarlo por defecto.

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 aguas arriba .

Es posible que lo anterior no llegue a una nueva página y no se convierta en segfault incluso sin el brk, por lo que aquí hay una versión más agresiva que asigna 16MiB y es muy probable que segfault 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 virtual del espacio de direcciones

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 brky sbrk?

brkpor supuesto, podría implementarse con sbrkcálculos de + desplazamiento, ambos existen solo por conveniencia.

En el backend, el kernel de Linux v5.0 tiene una sola llamada al sistema brkque 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 brkPOSIX?

brksolía ser POSIX, pero se eliminó en POSIX 2001, por lo tanto, la necesidad de _GNU_SOURCEacceder al contenedor glibc.

La eliminación probablemente se deba a la introducción mmap, que es un superconjunto que permite asignar rangos múltiples y más opciones de asignación.

Creo que no hay un caso válido en el que deba usar en brklugar de malloco en la mmapactualidad.

brk vs malloc

brkEs una antigua posibilidad de implementación malloc.

mmapes el mecanismo más estrictamente más potente que probablemente todos los sistemas POSIX usan actualmente para implementar malloc. Aquí hay un ejemplo mínimo de mmapasignación de memoria ejecutable .

¿Puedo mezclar brky malloc?

Si mallocse implementa con brk, no tengo idea de cómo eso posiblemente no puede hacer estallar las cosas, ya que brksolo administra un único rango de memoria.

Sin embargo, no pude encontrar nada al respecto en los documentos de glibc, por ejemplo:

Las cosas probablemente funcionarán allí, supongo, ya mmapque probablemente se usen para malloc.

Ver también:

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 la pila se compara con el montón: ¿Cuál es la función de las instrucciones push / pop utilizadas en los registros en el ensamblaje x86?

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fuente
44
Dado que pes un puntero para escribir int, ¿no debería haber sido brk(p + 2);?
Johan Boulé
Pequeña nota: La expresión en el bucle for de la versión agresiva probablemente debería ser*(p + i) = 1;
lima.sierra
Por cierto, ¿por qué necesitamos usar un en brk(p + 2)lugar de simplemente aumentarlo sbrk(2)? ¿Es realmente necesario brk?
Yi Lin Liu
1
@YiLinLiu Creo que son solo dos interfaces C muy similares para un backend de núcleo único ( brksyscall). brkes un poco más conveniente restaurar la pila previamente asignada.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
1
@CiroSantilli 新疆 改造 中心 996ICU 六四 事件 Considerando que el tamaño de int es de 4 bytes y el tamaño de un int * como 4 bytes (en una máquina de 32 bits), me preguntaba si no debería incrementarse en solo 4 bytes (en lugar de 8 - (2 * sizeof int)). ¿No debería apuntar al siguiente almacenamiento dinámico disponible, que estará a 4 bytes de distancia (no a 8)? Corrígeme si me falta algo aquí.
Saket Sharad
10

Puede usar brky sbrkusted mismo para evitar la "sobrecarga de malloc" de la que todos siempre se están quejando. Pero no puede usar fácilmente este método en conjunción con, mallocpor lo que solo es apropiado cuando no tiene que hacer freenada. Porque no puedes Además, debe evitar cualquier llamada a la biblioteca que pueda usar mallocinternamente. Es decir. strlenes probablemente seguro, pero fopenprobablemente no lo es.

Llama sbrkigual que llamarías malloc. Devuelve un puntero al descanso actual e incrementa el descanso en esa cantidad.

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

Si bien no puede liberar asignaciones individuales (porque no hay sobrecarga de malloc , recuerde), puede liberar todo el espacio llamando brkcon el valor devuelto por la primera llamada a sbrk, rebobinando así 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 el salto al inicio de la región.


Una cosa más ...

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

luser droog
fuente
77
-1 porque: malloc/ freeciertamente puede (y lo hace) devolver la memoria al sistema operativo. Es posible que no siempre lo hagan cuando lo desee, pero eso se debe a que la heurística se ajusta de manera imperfecta para su caso de uso. Más importante aún, no es seguro llamar sbrkcon un argumento distinto de cero en cualquier programa que pueda llamar malloc, y casi todas las funciones de la biblioteca C pueden llamar mallocinternamente. Los únicos que definitivamente no serán las funciones de seguridad de señal asíncrona .
zwol
Y por "no es seguro", me refiero a "su programa se bloqueará".
zwol
He editado para eliminar el alarde de memoria que regresa , y mencioné el peligro de las funciones de la biblioteca que usan internamente malloc.
luser droog
1
Si desea hacer una asignación de memoria sofisticada, basarla sobre Malloc o sobre mmap. No toques brk y sbrk, son reliquias del pasado que hacen más daño que bien (¡incluso las páginas del manual te dicen que te mantengas alejado de ellas!)
Eloff
3
Esto es tonto. Si desea evitar la sobrecarga de malloc para muchas asignaciones pequeñas, haga una asignación grande (con malloc o mmap, no sbrk) y repartirlo usted mismo. Si mantiene los nodos de su árbol binario en una matriz, puede usar índices 8b o 16b en lugar de punteros 64b. Esto funciona muy bien cuando no tiene que eliminar ningún nodo hasta que esté listo para eliminar todos los nodos. (por ejemplo, construya un diccionario ordenado sobre la marcha). El uso sbrkpara esto solo es útil para el golf de código, porque el uso manual mmap(MAP_ANONYMOUS)es mejor en todos los sentidos, excepto en el tamaño del código fuente.
Peter Cordes
3

Hay un mapeo de memoria privada anónimo designado especial (tradicionalmente ubicado más allá de los datos / bss, pero el Linux moderno realmente ajustará la ubicación con ASLR). En principio, no es mejor que cualquier otra asignación se puede crear con el que es mmap, pero Linux tiene algunas optimizaciones que hacen que sea posible ampliar el final de este mapeo (usando la brkllamada al sistema) hacia arriba con la reducción de costos de cierre en relación con lo que mmapo mremapincurriría. Esto lo hace atractivo para las mallocimplementaciones cuando se implementa el montón principal.

R .. GitHub DEJA DE AYUDAR AL HIELO
fuente
Que quería decir es posible ampliar el final de este mapeo hacia arriba, ¿verdad?
zwol
Si, arreglado. ¡Lo siento por eso!
R .. GitHub DEJA DE AYUDAR AL HIELO
0

Puedo responder tu segunda pregunta. Malloc fallará y devolverá un puntero nulo. Es por eso que siempre busca un puntero nulo cuando asigna memoria de forma dinámica.

Brian Gordon
fuente
entonces, ¿de qué sirve brk y sbrk?
nik
3
@NikhilRathod: malloc()usará brk()y / o sbrk()debajo del capó, y usted también puede hacerlo, si desea implementar su propia versión personalizada de malloc().
Daniel Pryden
@Daniel Pryden: cómo pueden trabajar brk y sbrk en el montón cuando está entre la pila y el segmento de datos como se muestra en el diagrama anterior. para que esto funcione, el montón debería estar al final. Estoy en lo cierto?
nik
2
@Brian: Daniel dijo que el sistema operativo administra el segmento de la pila , no el puntero de la pila ... cosas muy diferentes. El punto es que no hay sbrk / brk syscall para el segmento de pila: Linux asigna automáticamente páginas al intentar escribir al final del segmento de pila.
Jim Balter
1
Y Brian, solo respondiste la mitad de la mitad de la pregunta. La otra mitad es lo que sucede si intentas empujar a la pila cuando no hay espacio disponible ... obtienes una falla de segmentación.
Jim Balter
0

El montón se coloca en último lugar en el segmento de datos del programa. brk()se usa para cambiar (expandir) el tamaño del montón. Cuando el montón no puede crecer más, cualquier mallocllamada fallará.

Anders Abel
fuente
Entonces estás diciendo que todos los diagramas en Internet, como el de mi pregunta, están equivocados. Si es posible, ¿puede indicarme un diagrama correcto?
nik
2
@Nikkhil Tenga en cuenta que la parte superior de ese diagrama es el final de la memoria. La parte superior de la pila se mueve hacia abajo en el diagrama a medida que la pila crece. La parte superior del montón se mueve hacia arriba en el diagrama a medida que se expande.
Brian Gordon
0

El segmento de datos es la porción de memoria que contiene todos sus datos estáticos, leídos desde el ejecutable en el lanzamiento y generalmente llenos de cero.

monchalve
fuente
También contiene datos estáticos no inicializados (no presentes en el ejecutable) que pueden ser basura.
luser droog
El .bsssistema operativo inicializa los datos estáticos no inicializados ( ) a todos-bits-cero antes del inicio del programa; esto está realmente garantizado por el estándar C. Supongo que algunos sistemas integrados pueden no molestar (nunca he visto uno, pero no trabajo tan integrado)
zwol
@zwol: Linux tiene una opción de tiempo de compilación para no poner a cero las páginas devueltas mmap, pero supongo .bssque todavía se pondría a cero. El espacio BSS es probablemente la forma más compacta de expresar el hecho de que un programa quiere algunas matrices zerod.
Peter Cordes
1
@PeterCordes Lo que dice el estándar C es que las variables globales declaradas sin inicializador se tratan como si se inicializaran a cero. Por lo tanto, la implementación de CA que coloca tales variables .bssy no es cero no .bsssería conforme. Pero nada obliga a una implementación de C a usar .bssen absoluto o incluso tener tal cosa.
zwol
@PeterCordes Además, la línea entre la "implementación C" y el programa puede ser muy difusa, por ejemplo, generalmente hay un pequeño fragmento de código de la implementación, estáticamente vinculado a cada ejecutable, que se ejecuta antes main; ese código podría poner a cero el .bssárea en lugar de hacer que el núcleo lo haga, y eso aún se conformaría.
zwol
0

malloc usa la llamada al sistema brk para asignar memoria.

incluir

int main(void){

char *a = malloc(10); 
return 0;
}

ejecuta este sencillo programa con strace, llamará al sistema brk.

skanzariya
fuente