Table of Contents
Conceptos básicos sobre la memoria RAM / Paginación / Procesos
Memoria Virtual
En los sistemas operativos cada aplicación que quiere ejecutarse es colocada en memoria RAM (entera o parcialmente) y es entonces cuando hablamos de procesos. Estos viven en su propio espacio dentro de la memoria RAM gracias a una capa de abstracción creada por el hardware y el software del equipo (Kernel + CPU) que genera un entorno virtual para los procesos, denominado memoria virtual. La memoria virtual es por tanto una técnica de gestión de memoria, encargada de mapear direcciones físicas con direcciones virtuales, atribuyendo a cada proceso posiciones de memoria físicas según se vayan necesitando y de la forma que considere adecuada. La cantidad total de espacio de direcciones virtuales para todos los procesos en ejecución supera con creces la cantidad de memoria física.
Uno de los propósitos de la memoria virtual es el aislamiento de procesos. Un proceso en el espacio de usuario, gracias a esa capa de abstracción que la memoria virtual ofrece, únicamente accederá a memoria virtual, no a la memoria física de forma directa. Esto asegura que únicamente se acceda a las direcciones previamente asignadas (su espacio virtual), impidiendo así el acceso a la memoria de otros procesos (a no ser que sea compartida de forma explícita). La memoria virtual permite, entre otras cosas, no vincular direcciones físicas a una dirección virtual hasta ser necesario, o bien cambiar la dirección física a la que se asigna una determinada dirección virtual.
Otra ventaja es la de poder asignar direcciones a cosas que no están realmente en la memoria RAM (mmpap files), permitiendo ofrecer una dirección de memoria virtual a un archivo de forma que se pueda acceder como si fuera un búfer de memoria.
Existe también la posibilidad de que dos o más procesos accedan a la misma dirección de memoria física si estos requieren del mismo dato almacenado en memoria, por lo que la memoria virtual de varios procesos pueden mapear determinadas direcciones virtuales asignadas a la misma dirección de memoria física, a esto se le llama “Kernel SamePage Merging” (KSM).
KMS fue diseñado en principio para mejorar la virtualización, siempre buscando el equilibrio entre seguridad y capacidad de procesamiento, ya que al usar menos memoria la carga sobre el procesador se incrementa. Una implementación experimental de KSM realizada por Red Hat concluyó que 52 instancias virtuales de Windows XP con 1 GB de memoria, podría ejecutarse en un equipo host anfitrión de sólo 16 GB de RAM. En el lenguaje de programación C se encuentra la llamada al sistema fork(), la cual usa este concepto de ahorro de direcciones de memoria entre procesos que utilicen los mismos datos.
Cuando una posición de memoria utilizada por varios procesos requiere un cambio de valor de parte de uno de ellos, es cuando entra en juego el mecanismo COW (copy-on-write). Cuando un proceso modifica la memoria, el núcleo del sistema operativo interviene en la operación y crea una copia de forma que los cambios en la memoria ocupada por un proceso no son visibles por los otros. Una creencia errónea es que el escritorio KDE necesita mucha memoria RAM ya que realiza cargas de proceso individuales Qt y KDElibs, sin embargo, gracias a COW, todos los procesos utilizarán la misma memoria física exacta de las piezas de sólo lectura de dichas bibliotecas.
Para poder probar conceptos se recomiendan las herramientas ps, top, pmap y memstat. Más adelante se verá muy por encima como apoyarse en estas herramientas para auditar y consultar la memoria de los procesos.
¿Es lo mismo memoria virtual y Swap?
Pese a que mucha documentación en Internet diga lo contrario, memoria virtual y swap no son exactamente lo mismo. La memoria virtual es un esquema de direccionamiento entre memoria real física y la virtual, es una capa de abstracción para manejar la memoria que requieren los procesos, entre los que lógicamente se encuentra también la memoria swap.
Los archivos / particiones de intercambio / swap, swaping, etc son utilizados por los sistemas operativos para proporcionar más memoria física a la memoria virtual. Es decir, cuando no queda memoria RAM física que pueda ser utilizada por la memoria virtual para mapear direcciones (virtuales - físicas), se usa ese área de disco duro (intercambio de página), siendo ahí donde se relacionan los dos conceptos.
Páginas
Linux asigna memoria a los procesos dividiendo la memoria física en páginas y luego estas a su vez son vinculadas a la memoria virtual, que es la utilizada por los procesos. El proceso se realiza conjuntamente entre la CPU y la unidad de gestión de memoria o MMU.
MMU, del inglés Memory Management Unit, es un dispositivo de hardware formado por un grupo de circuitos integrados, responsable del manejo de los accesos a la memoria por parte de la Unidad de Procesamiento Central (CPU) o procesador.
Normalmente, una página representará 4 KB de memoria física, llamado macro de página. Las páginas pueden estar en diferentes estados, algunas estarán sin usar y otros se usarán para almacenar código ejecutable o datos (ej. credenciales) para un determinado programa. Hay muchos algoritmos que administran esta lista de páginas y controlan cómo se almacenan, se liberan y cargan en caché.
La memoria virtual por lo tanto está dividida en lo que se denomina marcos de página, su tamaño es asignado por la CPU (Normalmente 4Kb). Los procesos se dividen en lo que se denomina páginas. Las páginas de un proceso pueden no estar contiguamente ubicadas en memoria, y pueden intercalarse con las páginas de otros procesos. Cuando se requiere algo más de memoria o bien liberarla, el kernel del sistema operativo otorga o deja libres una o más páginas (Asignación dinámica de memoria:malloc).
En la paginación simple cada proceso tiene su propia tabla de páginas y cuando carga todas sus páginas en la memoria principal, se crea y carga una tabla de páginas.
En la paginación con memoria virtual, básicamente se utilizan los mismos elementos que en la simple, donde sólo que se necesitará un BIT en cada entrada de la tabla para indicar si la página correspondiente está presente en memoria principal o no.
Para cada página asignada el kernel mantiene un conjunto de permisos: Lectura, escritura y / o ejecución (No todas las combinaciones son posibles). Estos permisos se establecen por ejemplo mediante el uso de la llamada al sistema mprotect(). Lógicamente las páginas que no se han sido asignadas todavía , no son accesibles. Al intentar realizar una acción no permitida en una página, por ejemplo de lectura puede mostrar el famoso “fallo de segmentación”.
¿Qué es un error de página?
Se produce un error de página cuando un proceso accede a una página mapeada en el espacio de direcciones virtuales, pero esta no está cargada en la memoria física, es decir, sigue en el disco duro / SSD.
El tamaño de un programa ejecutable se puede medir en megabytes, pero no todo el código se ejecuta a la vez. Parte del código únicamente se ejecutará durante la inicialización del programa o cuando se produzca un evento especial como pulsar sobre un botón. Con el transcurso del tiempo, el kernel puede descartar algunas páginas de memoria que contienen código ejecutable que considera no necesarias, por ejemplo porque no se utilizan muy frecuentemente. Como resultado, no todo el código de máquina del programa se mantendrá en la memoria física, incluso cuando el programa esté en ejecución.
La ejecución de un programa básicamente es ir leyendo con la CPU las instrucciones del código máquina que se encuentra en la memoria. Cada instrucción se almacena en la memoria física en una dirección determinada con su correspondiente mapeo a la memoria virtual. La MMU maneja la asignación desde el espacio de direcciones físicas al espacio de direcciones virtuales. Cuando la CPU solicita un código de página al que acceder, pero la MMU contesta que no está disponible en memoria física, es cuando la CPU genera un “error” de página.
No es realmente un error, sino más bien un evento conocido en el que la CPU le dice al núcleo del sistema operativo que necesita acceso físico a un poco más de código. Linux obtendrá del disco los datos necesarios y responderá asignando más páginas al proceso, completando esas páginas con la información pertinente mediante la MMU. Acto seguido el kernel indicará a la CPU que continúe ya que la información vuelve a estar disponible. Entendiendo su funcionamiento, se puede deducir que este tipo de eventos puede tener una repercusión en el rendimiento del sistema ya que se realizan petciones a disco.
También es posible que el código o datos requeridos ya esten en la memoria, pero estos no estén asignados al proceso. En tal caso se les denomina fallas o errores menores de páginas. Por ejemplo, si un usuario ejecuta un Forefox, las páginas de memoria con el código ejecutable del navegador se pueden compartir posteriormente entre varios usuarios. Si un segundo usuario inicia el mismo navegador web, Linux no volverá a cargar todo el binario desde el disco, si no que mapeará las páginas del primer usuario y le dará acceso al segundo proceso para que acceda. En otras palabras, un error menor de página ocurre solo cuando la lista de páginas se actualiza (y la MMU se configura) sin necesidad de acceder al disco.
Algo muy similar ocurre con la memoria de datos utilizadas por los programas. Un ejecutable puede pedirle al kernel algo de memoria, digamos 10 megabytes para que pueda realizar algunas tareas. Linux en realidad no le da al proceso 8 megabytes de memoria física. En su lugar, asigna 8 megabytes de memoria virtual y marca esas páginas como “copiar al escribir”. Esto significa que en el momento de necesitarse, la página se escribe, se asigna una página física real y se asigna al proceso. Esto ocurre todo el tiempo en un sistema multiusuario y multitarea. La memoria física se usa de la manera más eficiente posible para contener las partes de la memoria que realmente se necesitan para que se ejecuten los procesos.
Conocer por proceso el número de errores de página.
Veamos algunos comandos que nos permitan conocer el número de errores de paginación, tanto mayores como menores. Esto es útil para poder identificar problemas con falta de ram, mala optimización de procesos o ficheros de configuración o selección de un mal algoritmo de paginado.
- major fault: Cuando el acceso a disco es requerido ya que la información no se encuentra ni en memoria física ni en la cache de la CPU.
- minor fault: Cuando no es necesario acceder al disco.
# ps (Lista los procesos ordenados por mayor generación de errores de página). ps -axo min_flt,maj_flt,uid,gid,cmd --sort=maj_fl # top top > f > selecciconar > nMaj nMin > Esc # sar sar -B
Las segundas ejecuciones de los programas pueden tener un número 0 de paginaciones que necesiten acceder a disco. De tener otro valor suele ser muy estable ya que la totalidad o gran parte del binario suele estar accesible en memoria.
Tipos de memoria
Los tipos de memoria vienen a ser de dos tipos, privada y compartida. Luego, cada una puede ser dividida en dos axiomas, ya que el contenido en memoria puede ser con respaldo en disco (Abrir un fichero) o bien haberse generado por el proceso, sin tener respaldo en disco (cambios realizados sobre el documento antes de ser guardado).
Memoria Privada
La memoria privada es la memoria específica para un determinado proceso y por norma es la más utilizada. Los cambios hechos en los datos la memoria privada no son visibles para otros procesos. Este tipo de memoria no impide mecanismos COW donde varios procesos (binarios y librerías compartidas) comparten la misma memoria física para almacenar los datos y ejecutarlos.
La memoria privada puede tener datos obtenidos del disco duro o bien datos que nunca han pasado por el disco (creados por el proceso).
Memoria compartida
La memoria compartida está diseñada para la comunicación entre procesos, sólo se puede crear de forma de forma explícita (llamadas al sistema mmap() y shm*).. Cuando un proceso escribe en una memoria compartida, la modificación es vista por todos los procesos que mapean la misma memoria.
La memoria compartida, al igual que la privada, puede tener datos obtenidos del disco duro o bien datos que nunca han pasado por el disco (creados por el proceso).
Memoria anónima
Este tipo de memoria se denomina anónima porque el núcleo realmente no asigna esa memoria a una dirección física en concreto antes de que necesite ser escrita por el proceso. Esto permite a un proceso reservar una gran cantidad de memoria en su espacio de direcciones de memoria virtual sin usar memoria RAM, permitiendo reservar más memoria de la que realmente disponible. Algo que se ve fácilmente con el uso del comando pmap sobre cualquier proceso. Se pueden obtener páginas de memoria anónima mediante malloc(). Lógicamente esta memoria no tiene ningún dato extraído del disco duro.
Con respaldo en disco / Swap
Cuando al kernel de Linux no le queda más memoria física, comenzará a escribir en el disco algunas de las páginas que está guardando en la memoria, y usará las páginas recién liberadas para satisfacer las fallas de página actuales.
Escribir páginas en el disco es un proceso relativamente lento (en comparación con la velocidad de la CPU y la memoria principal). El proceso de escribir páginas en el disco para liberar memoria se llama intercambio. El uso de swap por parte de los procesos en un momento puntual no tiene por qué ser un problema. Pero si hay procesos importantes donde gran parte de sus instrucciones están en disco, la perdida de rendimiento puede ser muy importante ya que el proceso requerirá de estar constantemente haciendo pequeñas lecturas y escrituras a disco para poder correr. Aunque el uso del area de intercambio repercute en la velocidad del proceso, no tiene porque afectar al rendimiento de entrada y salida de los dispositivos. De afectar es un síntoma de que tenemos que ampliar la capacidad de la RAM o bien reducir el procesamiento.
Esta memoria que es depositada en disco puede ser del tipo privado o compartida, no almacenándose de la misma forma una que otra. En sistemas Linux el área de intercambio suele ser una partición específica, en otros sistemas puede ser uno o varios archivos específicos.
Lecturas obligadas:
Fichero /proc/$PID/maps
Cada fila del fichero /proc/PID$/maps describe una región de memoria virtual contigua de un proceso o hilo.
Ejemplo de linea de /proc/$PID/maps para del proceso firefox.
559283c1d000-559283c40000 r-xp 00000000 fe:00 6035181 /usr/lib/firefox/firefox 7fbd49700000-7fbd4c800000 rw-p 00000000 00:00 0 7fbdaca00000-7fbdad600000 rw-p 00000000 00:00 0 [stack:803] 7fbdadbc4000-7fbdaddc3000 ---p 005b3000 fe:00 6163680 /usr/lib/xorg/modules/dri/i965_dri.so
- Dirección (559283c1d000-559283c40000): Direcciones de inicio y final de la región del espacio de direcciones del proceso firefox.
- Permisos (r-xp): Modo de acceso a las páginas de dicha región. Permisos pueden ser de lectura, escritura, ejecución y compartido. Si una región no se comparte, es privada, se usa la letra “p” en vez de “s”. Si el proceso intenta acceder a la memoria de una manera no permitida, se genera un fallo de segmentación. Los permisos se pueden cambiar usando la llamada al sistema “mprotect”.
- Offset (00000000) Si la región fue mapeada desde un archivo (usando mmap), este es el desplazamiento donde comienza la asignación. Si la memoria no fue asignada desde un archivo, el valor es 0.
- Dispositivo (fe:00): Si la región fue asignada desde un archivo, este es el número de dispositivo mayor y menor (en hexadecimal) donde se encuentra el archivo. Para averiguar de qué dispositivo se trata (fe:00 = 254:0) se puede utilizar la orden “dmsetup -l”, obteniendo por ejemplo el LVM correspondiente Volgroup00-lv_root (254:0).
- Inodo (6035181): Si la región fue asignada desde un archivo, este es el número de inodo de dicho archivo.
- Ruta (/usr/lib/firefox/firefox): Si la región fue mapeada desde un archivo, este es el nombre del archivo. Este campo está en blanco para regiones mapeadas anónimamente, las cuales se utilizan para cosas diversas como buffers o pilas de hilos, entre otras. También hay regiones especiales con nombres como [heap], [stack] y [vdso].
Vídeo sobre la administración de la memoria virtual
Vídeo muy didáctico y bien explicado sobre la administración de la memoria virtual en sistemas operativos: http://www.youtube.com/watch?v=sBbu1Do0uj0
Auditar la memoria (Virtual, privada y compartida) del sistema / procesos con top, ps, pmap y memstat
- top: VIRT (Virtual) / RES - % MEM (Privada / RAM física) / SHR (compartida) / USED (Privada + Swap).
- ps: VSZ (Virtual) / RSS - MEM (Privada / RAM física)
- pmap: Recomendable con la opción -x o -X o -XX (más informativo). Como se vio anteriormente es muy interesante para obtener direcciones y hacer volcados de memoria de procesos.
La memoria virtual puede mostrar tamaños muy superiores a la memoria física del sistema por los motivos previamente comentados. Por ejemplo, un proceso EleasticSearch que maneja terabytes de información, puede tener muchos ficheros mapeados y por lo tanto mostrar un uso de memoria virtual de 250Gb cuando solo se tienen 32Gb de Memoria RAM en el equipo.
Si se quiere probar el concepto, se puede utilizar por ejemplo con la opción “-cache”, cuanta más cache le demos, más partes del fichero a reproducir proyectará a memoria (mapear) y la memoria virtual del proceso será mayor.
mplayer -cache 1048576 fichero.mkv (Columna VIRT de top: 1533,7m) mplayer -cache 2097152 fichero.mkv (Columna VIRT de top: 2556,6m)
memstat: Indica el uso de memoria virtual de los procesos en ejecución y por qué elementos está formada (ficheros de disco mapeados, librería compartida, ejecutables, etc). En primer lugar, si no se filtra por PID, memstat enumera los procesos mostrando la cantidad de memora privada que utilizan y el nombre del ejecutable junto con un ID de proceso. Esa memoria privada puede coincidir con la mostrada por top o ps, pero no siempre es así.
Después de mostrar los procesos, muestra los objetos que están siendo usados como memoria virtual por los procesos y los PIDs de los procesos que la están utilizando. Se muestran dos valores, la cantidad total de memoria asignada a este objeto (coincide con la memoria virtual de top o ps) y la cantidad real compartida.
Ejemplo de salida de memstat para mostrar qué ficheros y librerías usa el proceso elasticsearch en su memoria virtual asignada.
memstat -p 3650 -w 1000 41049828k: PID 3650 (/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.65-0.b17.el6_7.x86_64/jre/bin/java) 136k( 128k): /lib64/ld-2.12.so 3650 3644k( 1576k): /lib64/libc-2.12.so 3650 2148k( 92k): /lib64/libpthread-2.12.so 3650 2072k( 20k): /lib64/libnss_dns-2.12.so 3650 2064k( 8k): /lib64/libdl-2.12.so 3650 2104k( 48k): /lib64/libnss_files-2.12.so 3650 2576k( 524k): /lib64/libm-2.12.so 3650 2136k( 88k): /lib64/libgcc_s-4.4.7-20120601.so.1 3650 2080k( 28k): /lib64/librt-2.12.so 3650 2136k( 84k): /lib64/libz.so.1.2.3 3650 2144k( 88k): /lib64/libresolv-2.12.so.#prelink#.PDzi8t 3650 1120k( 92k): /tmp/jna--1985354563/jna5272940301976045346.tmp 3650 32k( 32k): /tmp/hsperfdata_elasticsearch/3650 3650 96832k( 0k): /usr/lib/locale/locale-archive 3650 2104k( 52k): /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.65-0.b17.el6_7.x86_64/jre/lib/amd64/jli/libjli.so 3650 12k( 4k): /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.65-0.b17.el6_7.x86_64/jre/bin/java 3650 1844k( 1844k): /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.65-0.b17.el6_7.x86_64/jre/lib/rt.jar 3650 3012k( 928k): /usr/lib64/libstdc++.so.6.0.13 3650 12k( 12k): /usr/share/elasticsearch/lib/antlr-runtime-3.5.jar 3650 32k( 32k): /usr/share/elasticsearch/lib/apache-log4j-extras-1.2.17.jar 3650 4k( 4k): /usr/share/elasticsearch/lib/asm-4.1.jar 3650 8k( 8k): /usr/share/elasticsearch/lib/asm-commons-4.1.jar 3650 1096k( 1096k): /usr/share/elasticsearch/lib/elasticsearch-1.7.1.jar 3650 444k( 444k): /usr/share/elasticsearch/lib/groovy-all-2.4.4.jar 3650 16k( 16k): /usr/share/elasticsearch/lib/jna-4.1.0.jar 3650 68k( 68k): /usr/share/elasticsearch/lib/jts-1.13.jar 3650 36k( 36k): /usr/share/elasticsearch/lib/log4j-1.2.17.jar 3650 68k( 68k): /usr/share/elasticsearch/lib/lucene-analyzers-common-4.10.4.jar 3650 168k( 168k): /usr/share/elasticsearch/lib/lucene-core-4.10.4.jar 3650 8k( 8k): /usr/share/elasticsearch/lib/lucene-expressions-4.10.4.jar 3650 12k( 12k): /usr/share/elasticsearch/lib/lucene-grouping-4.10.4.jar 3650 12k( 12k): /usr/share/elasticsearch/lib/lucene-highlighter-4.10.4.jar 3650 8k( 8k): /usr/share/elasticsearch/lib/lucene-join-4.10.4.jar 3650 12k( 12k): /usr/share/elasticsearch/lib/lucene-suggest-4.10.4.jar 3650 1232k( 188k): /usr/share/elasticsearch/lib/sigar/libsigar-amd64-linux.so 3650 28k( 28k): /usr/share/elasticsearch/lib/sigar/sigar-1.6.4.jar 3650 8k( 8k): /usr/share/elasticsearch/lib/spatial4j-0.4.1.jar 3650 15432k( 15432k): /var/lib/elasticsearch/dm-log/nodes/0/indices/logstash-2015.10.14/4/index/_3ff_Lucene41_0.tim 3650 9716k( 9716k): /var/lib/elasticsearch/dm-log/nodes/0/indices/logstash-2015.10.14/4/index/_4cj_Lucene41_0.tim 3650 8228k( 8228k): /var/lib/elasticsearch/dm-log/nodes/0/indices/logstash-2015.10.14/4/index/_512_Lucene41_0.tim 3650 26972k( 26972k): /var/lib/elasticsearch/dm-log/nodes/0/indices/logstash-2015.10.14/4/index/_641_Lucene41_0.tim 3650 ... ---------------------- 124365288k (83176676k)
Obtener un volcado de la memoria RAM usada por un proceso
Obtener un volcado de la memoria RAM usada por un proceso.
Puede ser utilizado para localizar credenciales o llaves de determinados programas, por ejemplo, para obtener la credencial en memoria de un cliente OpenVPN solo se debe filtrar el volcado de la siguiente manera.
strings *.bin | grep -B2 KnOQ