En malloc, ¿por qué usar brk? ¿Por qué no usar simplemente mmap?

11 minutos de lectura

avatar de usuario
Nate CK

Implementaciones típicas de malloc usar brk/sbrk como el medio principal para reclamar memoria del sistema operativo. Sin embargo, también utilizan mmap para obtener fragmentos para grandes asignaciones. ¿Existe un beneficio real al usar brk en vez de mmap, o es solo tradicion? ¿No funcionaría igual de bien hacerlo todo con mmap?

(Nota: yo uso sbrk y brk indistintamente aquí porque son interfaces para la misma llamada al sistema Linux, brk.)


Como referencia, aquí hay un par de documentos que describen la glibc malloc:

Manual de referencia de la biblioteca C de GNU: el asignador de GNU
https://www.gnu.org/software/libc/manual/html_node/The-GNU-Allocator.html

wiki de glibc: descripción general de Malloc
https://sourceware.org/glibc/wiki/MallocInternals

Lo que estos documentos describen es que sbrk se utiliza para reclamar un escenario principal para pequeñas asignaciones, mmap se utiliza para reclamar arenas secundarias, y mmap también se usa para reclamar espacio para objetos grandes (“mucho más grandes que una página”).

El uso del montón de aplicaciones (reclamado con sbrk) y mmap introduce cierta complejidad adicional que podría ser innecesaria:

Arena asignada: la arena principal utiliza el montón de la aplicación. Uso de otras arenas mmapmontones. Para asignar un fragmento a un montón, debe saber qué caso se aplica. Si este bit es 0, el fragmento proviene de la arena principal y del montón principal. Si este bit es 1, el fragmento proviene de mmapLa memoria y la ubicación del montón se pueden calcular a partir de la dirección del fragmento.

[Glibc malloc is derived from ptmalloc, which was derived from dlmalloc, which was started in 1987.]


los jemalloc página de manual (http://jemalloc.net/jemalloc.3.html) tiene esto que decir:

Tradicionalmente, los asignadores han utilizado sbrk(2) para obtener memoria, que es subóptima por varias razones, incluidas las condiciones de carrera, el aumento de la fragmentación y las limitaciones artificiales en la memoria máxima utilizable. Si sbrk(2) es compatible con el sistema operativo, este asignador utiliza tanto mmap(2) y sbrk(2), en ese orden de preferencia; de lo contrario solo mmap(2) se usa

Entonces, hasta aquí dicen que sbrk es subóptimo pero lo usan de todos modos, aunque ya se han tomado la molestia de escribir su código para que funcione sin él.

[Writing of jemalloc started in 2005.]

ACTUALIZACIÓN: Pensando más en esto, ese bit sobre “en orden de preferencia” me da una línea de consulta. ¿Por qué el orden de preferencia? ¿Están simplemente usando sbrk como alternativa en caso mmap no es compatible (o carece de las funciones necesarias), o es posible que el proceso entre en algún estado en el que pueda usar sbrk pero no mmap? Miraré su código y veré si puedo averiguar qué está haciendo.


Lo pregunto porque estoy implementando un sistema de recolección de basura en C, y hasta ahora no veo ninguna razón para usar nada más que mmap. Sin embargo, me pregunto si hay algo que me estoy perdiendo.

(En mi caso tengo una razón adicional para evitar brkque es que podría necesitar usar malloc en algún momento.)

  • Te refieres a usar un solo mmap asignar un grupo para miles de asignaciones más pequeñas, ¿verdad? Ni uno mmap por asignación como lo haría para los grandes

    – ese otro tipo

    19 de abril de 2019 a las 22:42


  • Hay versiones de malloc() ese uso mmap().

    – Barmar

    19 abr 2019 a las 22:44

  • Posible duplicado de ¿Qué hace la llamada al sistema brk()?, ¿Malloc() usa brk() o mmap()?, Acerca de sbrk() y malloc(), etc.

    – jww

    19 abr 2019 a las 23:36


  • @thatotherguy: he agregado algo de información en la pregunta sobre qué hacen realmente los asignadores sobre los que he estado leyendo.

    – Nate CK

    20 de abril de 2019 a las 12:32

  • Tenga en cuenta que glibc malloc lo hace usar mmap para grandes asignaciones. los jemalloc comentarios negativos sobre brk aplicar más fuertemente a su uso para todo, como las antiguas implementaciones malloc de la historia de Unix. (especialmente la fragmentación: incapacidad para devolver memoria al kernel si hay una asignación pequeña a largo plazo después de una asignación grande a corto plazo).

    – Peter Cordes

    4 de enero a las 12:07

La llamada del sistema brk() tiene la ventaja de tener un solo elemento de datos para rastrear el uso de la memoria, que felizmente también está directamente relacionado con el tamaño total del montón.

Esto ha estado exactamente de la misma forma desde el Unix V6 de 1975. Eso sí, V6 admitía un espacio de direcciones de usuario de 65.535 bytes. Por lo tanto, no se pensó mucho en administrar mucho más de 64K, ciertamente no terabytes.

Usando mmap parece razonable hasta que empiezo a preguntarme cómo podría usar la recolección de basura alterada o agregada Mapa mm pero sin que reescribiendo el algoritmo de asignación también.

¿Funcionará bien con realloc(), fork()etc.?

  • La cuestión es que los asignadores modernos han reescrito sus algoritmos de asignación ampliamente desde entonces. Uno, jemalloc, ni siquiera se escribió hasta 2005. Y los asignadores modernos sí usan mmap extensamente, por lo que parece que han descubierto cómo hacer que funcione. Sin embargo, los que he estado mirando lo mezclan con llamadas a sbrkcomo lo he descrito ahora en algunas actualizaciones de la pregunta.

    – Nate CK

    20 de abril de 2019 a las 12:50


mmap() no existía en las primeras versiones de Unix. brk() era la única forma de aumentar el tamaño del segmento de datos del proceso en ese momento. La primera versión de Unix con mmap() estaba SunOS a mediados de los 80, la primera versión de código abierto fue BSD-Reno en 1990.

Y ser útil para malloc() no desea requerir un archivo real para hacer una copia de seguridad de la memoria. En 1988 SunOS implementó /dev/zero para este propósito, y en la década de 1990 HP-UX implementó el MAP_ANONYMOUS bandera.

Ahora hay versiones de mmap() que ofrecen una variedad de métodos para asignar el montón.

  • Eso explica por qué mmap no se usó en el pasado, pero las versiones modernas hacer usarlo, así que no estoy seguro si la historia explica por qué no lo usan exclusivamente. Tal vez fueron escritos originalmente para usar brk solo y luego se agrega mmap llama más tarde como una mejora? Pero jemalloc solo se remonta a 2005 y usa ambos sbrk y mmap.

    – Nate CK

    19 de abril de 2019 a las 23:52

Vocación mmap(2) una vez por asignación de memoria no es un enfoque viable para un asignador de memoria de propósito general porque la granularidad de asignación (la unidad individual más pequeña que se puede asignar a la vez) para mmap(2) es PAGESIZE (generalmente 4096 bytes), y porque requiere una llamada al sistema lenta y complicada. La ruta rápida del asignador para asignaciones pequeñas con baja fragmentación no debería requerir llamadas al sistema.

Entonces, independientemente de la estrategia que use, aún necesita admitir múltiples de lo que glibc llama arenas de memoria, y el manual GNU menciona: “La presencia de múltiples arenas permite que varios subprocesos asignen memoria simultáneamente en arenas separadas, mejorando así el rendimiento”.


La página de manual de jemalloc (http://jemalloc.net/jemalloc.3.html) tiene esto que decir:

Tradicionalmente, los asignadores han usado sbrk(2) para obtener memoria, que es subóptima por varias razones, incluidas las condiciones de carrera, el aumento de la fragmentación y las limitaciones artificiales de la memoria máxima utilizable. Si el sistema operativo admite sbrk(2), este asignador usa tanto mmap(2) como sbrk(2), en ese orden de preferencia; de lo contrario, solo se usa mmap(2).

No veo cómo ninguno de estos se aplica al uso moderno de sbrk(2), según tengo entendido. Las condiciones de carrera se manejan mediante subprocesos primitivos. La fragmentación se maneja tal como se haría con arenas de memoria asignadas por mmap(2). La memoria máxima utilizable es irrelevante, porque mmap(2) debe usarse para cualquier asignación grande para reducir la fragmentación y liberar memoria al sistema operativo inmediatamente free(3).


El uso del montón de la aplicación (reclamado con sbrk) y mmap introduce una complejidad adicional que podría ser innecesaria:

Arena asignada: la arena principal utiliza el montón de la aplicación. Otras arenas usan montones mmap’d. Para asignar un fragmento a un montón, debe saber qué caso se aplica. Si este bit es 0, el fragmento proviene de la arena principal y del montón principal. Si este bit es 1, el fragmento proviene de la memoria mmap’d y la ubicación del montón se puede calcular a partir de la dirección del fragmento.

Así que la pregunta ahora es, si ya estamos usando mmap(2)¿por qué no asignar una arena al inicio del proceso? mmap(2) En lugar de usar sbrk(2)? Especialmente si, como se cita, es necesario rastrear qué tipo de asignación se utilizó. Hay varias razones:

  1. mmap(2) puede que no sea compatible.
  2. sbrk(2) ya está inicializado para un proceso, mientras que mmap(2) introduciría requisitos adicionales.
  3. Como wiki de glibc dice, “Si la solicitud es lo suficientemente grande, mmap() se usa para solicitar memoria directamente desde el sistema operativo […] y puede haber un límite en la cantidad de asignaciones de este tipo que puede haber a la vez. “
  4. Un mapa de memoria asignado con mmap(2) no se puede extender tan fácilmente. Linux tiene mremap(2), pero su uso limita el asignador a los núcleos que lo admiten. Preasignación de muchas páginas con PROT_NONE el acceso utiliza demasiada memoria virtual. Usando MMAP_FIXED desasigna cualquier mapeo que pueda haber estado allí antes sin previo aviso. sbrk(2) no tiene ninguno de estos problemas y está explícitamente diseñado para permitir la ampliación de su memoria de forma segura.

  • La fragmentación sería una preocupación para brk si también lo usara para asignaciones grandes: solo puede devolver memoria al kernel en orden LIFO, por lo que una asignación pequeña de larga duración después de una asignación grande de corta duración podría impedirnos devolver ese gran trozo de memoria. Claro que podemos ponerlo en la lista libre y dividirlo en bloques más pequeños para asignaciones futuras, pero si es un gigabyte de memoria privada sucia, eso no es lo que quieres. O si es grande pero no lo suficientemente grande para la próxima gran asignación, tampoco es bueno. Esos son básicamente problemas de fragmentación. (Que glibc evita w. mmap)

    – Peter Cordes

    4 de enero a las 12:12

  • Las asignaciones de memoria en el campo de descanso no se pueden ampliar si se malloc la asignación sigue a una asignación grande. Podemos extender el descanso muy bien, pero eso no ayuda a satisfacer una realloc llamada de la biblioteca. Por lo tanto, el uso de un mmap dedicado para cada asignación grande hace que sea mucho más probable que la reasignación se vuelva aún más grande sin tocar otra asignación. (Y evitar copiar es más importante cuanto mayor sea la asignación). Esa es otra buena razón para que glibc tenga una heurística para cambiar de una arena a un mmap directo.

    – Peter Cordes

    4 ene a las 12:15

La ventaja obvia es que puede aumentar la última asignación en su lugarque es algo con lo que no puedes hacer mmap(2) (mremap(2) es una extensión de Linux, no portable).

Para programas ingenuos (y no tan ingenuos) que utilizan realloc(3) p.ej. para agregar a una cadena, esto se traduce en un aumento de velocidad de 1 o 2 órdenes de magnitud 😉

No conozco los detalles en Linux específicamente, pero en FreeBSD desde hace varios años se prefiere mmap y jemalloc en la libc de FreeBSD tiene sbrk() completamente deshabilitado. brk()/sbrk() no están implementados en el kernel en los puertos más nuevos a aarch64 y risc-v.

Si entiendo correctamente la historia de jemalloc, originalmente era el nuevo asignador en la libc de FreeBSD antes de que se rompiera y se hiciera portátil. Ahora FreeBSD es un consumidor intermedio de jemalloc. Es muy posible que su preferencia por mmap() sobre sbrk() se haya originado con las características del sistema FreeBSD VM que se construyó alrededor de la implementación de la interfaz mmap.

Vale la pena señalar que en SUS y POSIX brk/sbrk están en desuso y deben considerarse no portátiles en este punto. Si está trabajando en un nuevo asignador, probablemente no quiera depender de ellos.

¿Ha sido útil esta solución?