ERROR USUARIO NO VALIDADO

MÓDULO DE MEMORIA

INTRODUCCIÓN

En este módulo se realizan ejercicios en los que intervienen los servicios de gestión de memoria de un sistema operativo UNIX, así como las funcionalidades suministradas por dicho sistema operativo en protección de memoria.

Los mandatos nuevos utilizados en este módulo son los siguientes:

pmap size readelf

Del directorio proc se manejaran los siguientes subdirectorios:

  • /proc/<PID>/maps


Se utilizarán los siguientes programas, que deberá descargarse de la web y compilar para su ejecución:

Alineamiento.c Alineamiento_struct.c Bibhtml.c
Cada3segs.c Carga_biblioteca.c config
Consumidor.c Desconocido.c Desconocido_fich.c
Endian.c Escritor_sinfreno.c Exec_th.c
Fork.c Lector_sinfreno.c Lincon.c
Lincon_th.c MemEvol.c Navegador.c
Productor.c Proyeccion.c Regiones.c
Tamanyos.c Thread.c Trasiego.c
Usa_biblioteca.c Volcado_mem.c


Los servicios UNIX utilizados en dichos programas son los siguientes:

alarm close dlopen
dlsym dlclose execlp
fork fstat ftruncate
getpid getppid kill
mmap munmap open
pause pthread_create pthread_exit
pthread_join read sigaction
unlink wait write

Las funciones de biblioteca utilizadas en dichos programas son las siguientes:

atof atoi atol fclose
fopen fprintf free fscanf
malloc memcpy offsetof perror
printf rindex sleep sprintf
strcat strcmp strcpy strsignal
strtoul system toupper

PAGINACIÓN

En un sistema con memoria virtual existe un trasiego de páginas entre la memoria principal y la zona de intercambio del disco o swap.

Linux gestiona la memoria trabajando con unidades a las que denomina páginas, de modo que cuando un proceso solicita memoria, el kernel le asigna páginas virtuales. Todas estas páginas virtuales tendrán un soporte físico ya sea en memoria principal (RAM) o en la zona de intercambio de disco (SWAP). En principio, Linux puede distribuir tanta memoria como le soliciten los procesos, sin embargo, no toda esta memoria estará disponible en memoria RAM, sino que parte estará en SWAP. Por este motivo, existe un trasiego de información entre memoria y disco. A este trasiego de información también se le conoce con el nombre de paginación.

Este trasiego de información afecta al rendimiento del sistema, pudiendo darse el caso de que el sistema pase la mayor parte del tiempo gestionando este trasiego de información, en lugar de realizar trabajo útil. Para probar la paginación se va a someter al sistema a una fuerte carga de memoria mediante un proceso que ejecute el programa Trasiego, cuya única función es consumir dicho recurso.

Obviamente, este ejercicio no puede realizarse en una máquina multiusuario, puesto que unos usuarios afectarían a otros, por lo que planteamos su ejecución en un sistema personal con sistema operativo Windows.

Los pasos a seguir son los siguientes:

  • Abra un shell alfanumérico, es decir, una ventana cmd en Windows. Este programa puede encontrarse entre los Accesorios con el nombre de “Símbolo del sistema” o “Command Prompt” o bien en “Inicio → Ejecutar → cmd”. Si no está el programa entre los accesorios búsquelo en el directorio system32 de Windows, el programa se llama cmd.exe. Ejecute dicho programa y aparecerá una ventana shell alfanumérico.
  • Para monitorizar el sistema se utilizarán el “Administrador de tareas” y el “Monitor de rendimiento”.
    • El “Administrador de tareas” se puede abrir tecleando [Crtl+Alt+Supr] y seleccione “Administrador de tareas” y la pestaña “Rendimiento”. Aparecerá una ventana con un gráfico que muestra el uso de procesador y de memoria.
    • El “Monitor de rendimiento” se abre seleccionando: Panel de control > Sistema y seguridad > Herramientas administrativas > Monitor de rendimiento > (En panel izquierdo) Herramientas de supervisión > Monitor de rendimiento. Añada los siguientes indicadores de rendimiento: (En el panel derecho, sobre la gráfica) Botón derecho > Agregar contadores > (Pestaña 'Contadores disponibles') Recorrer la lista hasta y seleccionar 'Memoria' > Doble clic (Aparece lista de contadores de Memoria) > Seleccionar contadores “Páginas/s”, “MB disponibles” y “Escritura de páginas/s” > Agregar > Aceptar. Cambie los colores los contadores que no se distingan posicionándose en la entrada correspondiente de la columna “Color”, pulsando el botón derecho y seleccionando “Propiedades”.
  • Lance la ejecución del programa Trasiego con los parámetros adecuados.
  • Observe en las pantallas “Rendimiento” y “Administrador de tareas” cómo aumenta la memoria consumida y cómo aumenta la paginación.

Si el parámetro indicado provoca un error de ejecución, pruebe a lanzar dos veces el programa Trasiego (en dos ventanas cmd diferentes) siendo el valor proporcionado como argumento la mitad.




Abra uno o dos programas como pueden ser un editor o navegador. Observará que el sistema va lentísimo, puesto que está paginando fuertemente.


FICHERO EJECUTABLE

El fichero ejecutable tiene una cabecera y unas secciones que permiten construir los segmentos de la imagen de memoria del proceso.

El sistema operativo tiene una visión del proceso consistente en un conjunto de regiones. En sistemas con memoria virtual, las diferentes regiones que integran el proceso suelen estar separadas y tienen un tamaño de un número entero de páginas.

Las regiones más relevantes de la imagen de memoria de un proceso son:

  • Código (texto).- que contiene el código máquina del programa.
  • Datos.-
    • Con valor inicial
    • Sin valor inicial
    • Datos creados dinámicamente o Heap
  • Pila.

El modo en el que se estructuran las regiones depende del diseño del sistema operativo.

Vamos a comenzar el análisis de las regiones de memoria de un proceso a través de la información proporcionada por el directorio /proc. Para ello vamos a utilizar el programa Cada3segs.c y además vamos a compilarlo estáticamente, es decir, sin bibliotecas dinámicas. Para ello utilizaremos la opción -static del mandato gcc.

alumno@maquinaLinux:~/PracticasAnalisis/ModuloMemoria$ gcc -static Cada3Segs.c -o Estatico
alumno@maquinaLinux:~/PracticasAnalisis/ModuloMemoria$ ./Estatico &
[1] 18785

A continuación, podemos obtener el mapa de memoria de memoria del proceso, accediendo al fichero maps del directorio /proc/<pid>:

Tenga en cuenta que el <pid> no será el mismo en su caso que en este ejemplo.

alumno@maquinaLinux:~/PracticasAnalisis/ModuloMemoria$ cat /proc/18785/maps
00400000-00481000                 r-xp 00000000     00:17 4166491  /home/alumno/PracticasAnalisis/ModuloMemoria/Estatico
00680000-00681000                 rw-p 00080000     00:17 4166491  /home/alumno/PracticasAnalisis/ModuloMemoria/Estatico
00681000-00684000                 rw-p 00681000     00:00 0
06516000-06538000                 rw-p 06516000     00:00 0        [heap]
7fff9573b000-7fff95750000         rw-p 7ffffffea000 00:00 0        [stack]
ffffffffff600000-ffffffffffe00000 ---p 00000000     00:00 0        [vdso]


Como podemos comprobar, el proceso dispone de 6 regiones de memoria, todas ellas privadas, puesto que tienen activo el bit p en los permisos.

  • Segmento 1 (00400000-00481000).- Esta región tiene activos los permisos de lectura y ejecución y puesto que su contenido se obtiene del propio fichero ejecutable, podemos deducir que esta región contiene el código (texto) del proceso. Su tamaño es de 0x00481000 - 0x00400000 = 0x81000B (528384 bytes)
  • Segmento 2 (00680000-00681000).- Esta región tiene activos los permisos de lectura y escritura y puesto que su contenido se obtiene del propio fichero ejecutable, podemos deducir que esta región contiene los datos del programa, más concretamente, los datos con valor inicial. Su tamaño es de: 0x00681000 - 0x00680000 = 0x1000 B.
  • Segmento 3 (00681000-00684000).- Este tercer segmento, es propio y dependiente del sistema operativo en el que se ejecuta el proceso en cuestión. En otro sistema operativo no tiene porqué existir. En este caso se trata de una zona de 12 Kb que es residente en memoria RAM.
  • Segmento 4 (06516000-06538000).- Como su propio nombre indica, esta región corresponde al heap del proceso. Su tamaño es de 0x22000 B.
  • Segmento 5 (7fff9573b000-7fff95750000).- Como su propio nombre indica, se trata de la pila del proceso. Su tamaño es de 0x15000 B.
  • Segmento 6 (ffffffffff600000-ffffffffffe00000).- El sexto segmento corresponde a una región que se estudiará en un curso más avanzado, por lo que no se incide ahora en ella. Simplemente indicar que se trata de una región de memoria común a todos los procesos de sistema.

El directorio /proc/<pid>/ contiene un fichero en el que se muestra de manera más detallada la información sobre los segmentos de memoria de un proceso, en concreto, podríamos conocer cuanta información del segmento tienen soporte en memoria RAM.


Finalmente se termina el proceso Estatico mediante un mandato kill.

alumno@maquinaLinux:~/PracticasAnalisis/ModuloMemoria$ kill 18785

Es de destacar que el rango de direcciones mostrado en el fichero maps no es del todo correcto. Esto se puede ver claramente en el caso de los segmentos de memoria 2 y 3, ya que no es posible que una dirección de memoria (0x00681000) pertenezca a dos segmentos diferentes. Por lo que para el segmento 2 el rango real debería ser 00680000-00680FFF en lugar de 00680000-00681000.

Adicionalmente, con el mandato size, podemos obtener un resumen de las necesidades de memoria del proceso:

alumno@maquinaLinux:~/PracticasAnalisis/ModuloMemoria$ size Estatico
  text    data     bss     dec     hex filename
 524253    3360   12336  539949   83d2d Estatico

La columna text indica el tamaño (en bytes) de la región de Código (texto), la columna data el tamaño de la región de datos con valor inicial, y la columna bss el tamaño de la región de los datos sin valor inicial. Por último, las columnas dec y hex indican el tamaño total expresado tanto en formato decimal con en formato hexadecimal.

Para poder profundizar un poco más en las características de las regiones de memoria del proceso, vamos a hacer uso del mandato readelf. Como ya sabemos, un fichero ejecutable, está formado por una cabecera y un conjunto de secciones. La información contenida en la cabecera será accesible ejecutando el mandato readelf con las siguientes opciones:

alumno@maquinaLinux:~/PracticasAnalisis/ModuloMemoria$  readelf -h Estatico
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
  Class:                             ELF64
  Data:                              2s complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x400190
  Start of program headers:          64 (bytes into file)
  Start of section headers:          528648 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         5
  Size of section headers:           64 (bytes)
  Number of section headers:         26
  Section header string table index: 23

Analicemos el contenido de la cabecera. Note que algunos datos están en formato hexadecimal y otros, como por ejemplo los tamaños están expresados en formato decimal.

  • La cabecera contiene el “número mágico” magic que permite identificar al fichero como un ejecutable ELF (Executable & Linked format), en este caso de 64b.
  • El campo Data indica que la información se representa en complemento a 2 y en formato little endian.
  • El campo type identifica el fichero como ejecutable y preparado para procesadores con arquitectura x86-64 (machine).
  • El campo entrypoint se corresponde con la dirección virtual del punto de entrada del programa. Note que dicha dirección pertenece al segmento 1 visto anteriormente en el fichero maps.
  • El campo size of this header especifica el tamaño de la cabecera del fichero. En nuestro caso 64 bytes.
  • El campo Start of program headers especifica la ubicación de las cabeceras de programa dentro del fichero. En este caso, la posición es 64 bytes, es decir justo a continuación de la cabecera del fichero.
  • El campo Start of section headers especifica la ubicación de las cabeceras de sección dentro del fichero. En este caso, la posición es 528648 bytes.

Por último podemos conocer el número de cabeceras del programa y de sección junto con sus respectivos tamaños. En concreto:

  • Tamaño de cada cabecera de programa (Size of program headers): 56 B.
  • Número de cabeceras de programa (Number of program headers): 5
  • Tamaño de cada cabecera de sección (Size of section headers): 64 B.
  • Número de cabeceras de sección (Number of section headers): 26

Las cabeceras de programa ocupan, por tanto, 5•56 = 280 B. Por lo que, entre la cabecera del fichero y las cabeceras de programa se ocupa 64+280 = 344 bytes.

Mediante el mandato readelf, también podemos conocer las cabeceras de programa contenidas en el fichero, que como ya sabemos, en nuestro caso son 5.

alumno@maquinaLinux:~/PracticasAnalisis/ModuloMemoria$  readelf -l -W Estatico
Elf file type is EXEC (Executable file)
Entry point 0x400190
There are 5 program headers, starting at offset 64
 
Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  LOAD           0x000000 0x0000000000400000 0x0000000000400000 0x080143 0x080143 R E 0x200000
  LOAD           0x080148 0x0000000000680148 0x0000000000680148 0x000d28 0x003d38 RW  0x200000
  NOTE           0x000158 0x0000000000400158 0x0000000000400158 0x000020 0x000020 R   0x4
  TLS            0x080148 0x0000000000680148 0x0000000000680148 0x000020 0x000050 R   0x8
  GNU_STACK      0x000000 0x0000000000000000 0x0000000000000000 0x000000 0x000000 RW  0x8
 
 Section to Segment mapping:
  Segment Sections...
   00     .note.ABI-tag .init .text __libc_freeres_fn .fini .rodata __libc_subfreeres __libc_atexit .eh_frame .gcc_except_table
   01     .tdata .ctors .dtors .jcr .data.rel.ro .got .got.plt .data .bss __libc_freeres_ptrs
   02     .note.ABI-tag
   03     .tdata .tbss
   04

La información proporcionada en este caso es:

  • Type: Identifica el tipo de segmento. Se pueden encontrar distintos tipos de segmento, pero los más importantes son los siguientes:


Tipo nº Tipo Uso
1 LOAD Segmento “cargable” en memoria
2 DYNAMIC Especifica información para montaje dinámico
3 INTERP Nombre de ubicación de un intérprete
4 NOTE Ubicación de información adicional
6 PHDR Especifica la tabla de cabeceras de programa
7 TLS Especifica el patrón de Thread-Local Storage (permite la asociación de copias separadas de datos en tiempo de compilación con los hilos de ejecución)


  • Offset.- Indica el desplazamiento desde el comienzo del fichero hasta el primer byte en el que reside el segmento.
  • VirtAddr.- Identifica la dirección de memoria virtual en la que se localiza el primer byte del segmento.
  • PhysAddr.- Identifica la dirección física en sistemas en los es relevante.
  • FileSiz.- Indica el tamaño en el fichero del segmento.
  • MemSiz.- Indica el tamaño en memoria del segmento.
  • Flg.- Especifica los privilegios del segmento, de acuerdo al convenio típico.
  • Align.- Indica el valor al que los segmentos están alineados en memoria. Un valor de 0 indica que no es necesario alineamiento, en otro caso, deberá ser un valor entero potencia de 2.

Analicemos brevemente la primera cabecera.

  • TYPE = LOAD → Segmento cargable en memoria
  • FLG = R E → Permisos de lectura y ejecución → Se trata del segmento de código (texto)
  • VirtAddr = 0x0400000 → coincide con la información mostrada por el fichero maps (dirección inicial del segmento)
  • MemSiz = 0x0080143 → tamaño en memoria del segmento 0x0080143B (524611B)
  • Align = 0x200000 → Debería tener un alineamiento a 2MB (que se supone es el tamaño de página empleado por el sistema).

Obviamente, existe un error en lo que se refiere al cálculo del alineamiento.

De la información proporcionada por maps obtenemos un segmento que ocupa el rango de direcciones 0x0400000 - 0x0481000.

De la información contenida en la cabecera de programa, sabemos que el tamaño de dicha región es 0x0080143.

Ejecutando getconf PAGESIZE, obtenemos un tamaño de página de 4096=4KB=0x1000.

Por tanto, si asumimos un alineamiento al tamaño de una página, es decir 4KB o 0x1000, la región debería abarcar un conjunto de direcciones múltiplo de 0x1000, lo cual sudece en nuestro ejemplo.

Si como indica la cabecera del programa, tuviéramos un alineamiento a 0x200000, el rango de direcciones abarcado por la región debería ser: 0x0400000 - 0x0600000, lo cuál no es cierto.

El error, tiene lugar debido a que el compilador gcc, para la arquitectura x86_64, genera un valor P_ALIGN 0x200000 == 2MB, que en realidad se podría corresponder con el tamaño máximo de página que puede gestionar el sistema, y no con el tamaño de página utilizado en realidad.

http://sourceware.org/ml/binutils/2007-08/msg00219.html

La segunda cabecera de programa corresponde al segmento de datos, puesto que tiene permisos de lectura y escritura, es de tipo LOAD y como soporte, como vimos en maps tiene al propio fichero ejecutable. Un aspecto destacable en este caso, es que si nos fijamos en las secciones que tiene asociadas este segmento (según información proporcionada por readelf), nos encontramos con .data y .bss, lo cual quiere decir, que contendrá tanto los datos con valor inicial como los datos sin valor inicial. Por tanto el tamaño en memoria de este segmento según readelf, debería ser aproximadamente igual a la suma de las columnas data y bss obtenidas con el mandato size.

Compile el programa Cada3Segs.c sin la opción -static y conteste a las siguientes preguntas:

alumno@mnaquinaLinux:~/PracticasAnalisis/ModuloMemoria$ gcc Cada3Segs.c -o Dinamico





EL MAPA DE MEMORIA DEL PROCESO

Un proceso puede generar cualquier dirección de memoria en el intervalo que le permite el rango de representación de direcciones del procesador

En arquitecturas de 32bits el rango de direcciones que puede generar un proceso es desde la dirección 0 hasta la dirección 232-1, mientras que en arquitecturas de 64bits, un proceso podrá generar direcciones en el rango 0 a 264-1. Sin embargo, el proceso sólo tiene acceso a algunas partes de ese intervalo, que corresponden con las partes que le ha asignado el sistema operativo y que ya vimos analizando el fichero maps del directorio /proc. Asimismo, dentro de las posiciones accesibles, algunas tienen acceso de sólo lectura, mientras que otras permiten la lectura y escritura.

El programa Volcado_mem.c permite conocer si una determinada posición de memoria está asignada al proceso y, en caso afirmativo, si dicha dirección de memoria es de lectura y escritura o solo de lectura. Para ello recibe como parámetros de entrada una dirección (en hexadecimal) de inicio y un valor entero que determina el tamaño de la zona de memoria en la que se va a realizar la búsqueda.


Como es lógico, el acceso por parte de un proceso a una zona de memoria que no tiene asignada, debería provocar la muerte del proceso que realizó dicho acceso (acción por defecto). Sin embargo en el caso del programa Volcado_mem esto no sucede.



Como ya hemos comentado, cada proceso puede direccionar todo el rango de direcciones de memoria, sin embargo estas direcciones son interpretadas dentro del ámbito del propio proceso. Es decir, dos procesos que generan la misma dirección de memoria (virtual), no están accediendo a la misma posición de memoria (física).

Analicemos este hecho en tres situaciones diferentes:

Procesos independientes conectados mediante tubería

Utilizaremos en este caso, los programas Productor.c y Consumidor.c. Ambos están pensados para ejecutarse de manera conjunta conectados mediante una tubería.



Compile y ejecute ambos programas conectados mediante una tubería:

alumno@maquinaLinux:~/PracticasAnalisis/ModuloMemoria$ gcc Productor.c -o Productor
alumno@maquinaLinux:~/PracticasAnalisis/ModuloMemoria$ gcc Consumidor.c -o Consumidor
alumno@maquinaLinux:~/PracticasAnalisis/ModuloMemoria$ Productor|Consumidor


Procesos generados mediante fork

Recordemos que mediante el servicio fork, un proceso puede crear un nuevo proceso (hijo), que es un clon del original. Para analizar la gestión de los mapas de memoria cuando se hace uso del servicio fork, utilizaremos el programa Lincon.c. A continuación se reproducen ciertas partes del código de dicho programa:

LC-01.- #define MYNAME  "Lincon"
LC-02.- #define TAMBUF 65536
LC-03.- 
LC-04.- int total=0;
LC-05.- 
LC-06.- int incognita(char *arg)
LC-07.- {
LC-08.-         int fo, nbytes, i, valor=0;
LC-09.-         char buf[TAMBUF];
LC-10.- 
LC-11.-         if ((fo=open(arg, O_RDONLY))<0)
LC-12.-                 return 1;
LC-13.- 
LC-14.-         while ( (nbytes= read(fo, buf, TAMBUF)) >0)
LC-15.-                 for (i=0; i<nbytes; i++)
LC-16.-                         if (buf[i]=='\n') valor++;
LC-17.- 
LC-18.-         if (nbytes<0)
LC-19.-                 return 2;
LC-20.- 
LC-21.-         total+= valor;
LC-22.-         printf("%d\t%s\n", valor, arg);
LC-23.-         return 0;
LC-24.- }
LC-25.- 
LC-26.- int main(int argc, char *argv[])
LC-27.- {
LC-28.-         int i, estado;
LC-29.- 
LC-30.-         if (argc<2)
LC-31.-         {
LC-32.-                 fprintf(stderr, "Uso: %s fichero ...\n", MYNAME);
LC-33.-                 exit(1);
LC-34.-         }
LC-35.- 
LC-36.-         for (i=1; i<argc; i++)
LC-37.-                 if (fork()==0)
LC-38.-                 {
LC-39.-                         estado=incognita(argv[i]);
LC-40.-                         exit(estado);
LC-41.-                 }
LC-41.- 
LC-42.-         for (i=1; i<argc; i++)
LC-43.-                 wait(NULL);
LC-44.- 
LC-45.-         printf("Total:\t%d\n", total);
LC-46.-         exit(0);
LC-47.- }



Compile y ejecute el programa Lincon.c especificando como parámetro *.c . Comprobará que el resultado obtenido en la variable total no es el esperado.


Procesos ligeros

El programa Lincon_th realiza la misma labor que el programa anterior utilizando la misma estructura, pero usando threads. Compile el programa y ejecútelo especificando *.c como argumento. Recuerde que para compilar un programa que utiliza threads es necesario añadir la opción -pthread.

Comprobará que en este caso, el valor obtenido en la variable total si es el esperado.


LAS REGIONES DEL MAPA DE MEMORIA

En esta sección analizaremos las distintas regiones que aparecen en el mapa de memoria de un proceso.

Como ya hemos visto anteriormente, podemos conocer el mapa de memoria de un proceso mediante el acceso al fichero /proc/<PID>/maps que tiene asociado. Además, en Linux, disponemos del mandato pmap, que proporciona el mapa de memoria de un proceso, a partir de su pid, aunque con un formato ligeramente diferente al presentado en el fichero /proc/<PID>/maps. A continuación se muestra el resultado de invocar el mandato aplicado al pid correspondiente a un mandato cat previamente arrancado.

alumno@maquinaLinux~/PracticasAnalisis/ModuloMemoria$ cat &
[1] 20773
[1]+  Stopped                 cat
alumno@maquinaLinux~/PracticasAnalisis/ModuloMemoria$ pmap 20773
20773:   cat
0000000000400000     20K r-x--  /bin/cat
0000000000604000      8K rw---  /bin/cat
00000000020b9000    132K rw---    [ anon ]
000000381d000000    112K r-x--  /lib64/ld-2.5.so
000000381d21b000      4K r----  /lib64/ld-2.5.so
000000381d21c000      4K rw---  /lib64/ld-2.5.so
000000381d400000   1328K r-x--  /lib64/libc-2.5.so
000000381d54c000   2048K -----  /lib64/libc-2.5.so
000000381d74c000     16K r----  /lib64/libc-2.5.so
000000381d750000      4K rw---  /lib64/libc-2.5.so
000000381d751000     20K rw---    [ anon ]
00002b4d6659b000      4K rw---    [ anon ]
00002b4d665bc000      8K rw---    [ anon ]
00002b4d665be000  55132K r----  /usr/lib/locale/locale-archive
00007fff69c21000     84K rw---    [ stack ]
ffffffffff600000   8192K -----    [ anon ]
 total            67116K

Como se puede apreciar, el mandato muestra una línea por cada región que posee el proceso en su mapa de memoria, especificando las características de la misma:

  • Dirección inicial.
  • Tamaño en Kbytes.
  • Permisos, que corresponden con los tres primeros caracteres de la tercera columna.
  • Región compartida o privada, que aparece en el cuarto carácter de esa tercera columna con un ‘-‘, si es privada y una ‘s’, en caso de que sea compartida.
  • Fichero asociado o, en caso de que no lo haya, [ stack ], si se trata de la pila, o [ anon ], para cualquier otra región sin fichero asociado.

Como ya vimos anteriormente el mapa de memoria de un proceso está organizado como un conjunto de regiones de diversas propiedades. Si nos centramos únicamente en las variables que maneja un proceso durante su ejecución, nos encontraremos con que éstas estarán ubicadas en diferentes regiones cuyas características se adaptan a las necesidades intrínsecas de dichas variables.

El programa Regiones.c, nos permite observar la distribución de ciertas variables en las diferentes regiones que componen su mapa de memoria. En dicho programa se definen constante, variables globales y locales (tanto inicializadas como sin inicializar) y además debemos tener en cuenta que los parámetros de la llamada a una función también se consideran variables.

Analice el programa Regiones.c

R-01.- #define MYNAME  "Regiones"
R-02.- #include <stdio.h>
R-03.- #include <unistd.h>
R-04.- #include <stdlib.h>
R-05.- #include <errno.h>
R-06.- /* Función que imprime el mapa de memoria del proceso en ese instante */
R-07.- static void mostrar_mapa(){
R-08.-         char mandato[256];
R-09.-         int mipid;
R-10.-         printf("\n-------------------------------------------------------------------\n");
R-11.-         mipid= getpid();
R-12.-         sprintf(mandato, "pmap %d ", mipid);
R-13.-         system(mandato);
R-14.-         printf("-------------------------------------------------------------------\n");
R-15.- }
R-16.- 
R-17.- int A;
R-18.- int B=666;
R-19.- int C[4000];
R-20.- const int D=1000;
R-21.- int E[]={31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
R-22.- 
R-23.- int main(int argc, char **argv) {
R-24.-         int F;
R-25.-         int G[2500];
R-26.- 
R-27.-         /* Se muestra el mapa */
R-28.-         mostrar_mapa();
R-29.- 
R-30.-         /* Se imprime dirección de variables especificadas en enunciado */
R-31.-         printf("\n%p\tmain\n%p\tA\n%p\tB\n%p\tC\n%p\tD\n%p\tE\n%p\tF\n%p\tG\n%p\targc\n", main, &A, &B, C,&D, E, &F, G, &argc);
R-32.- 
R-33.-         return 0;
R-34.- }



Compile y ejecute el programa Regiones.c


Algunas regiones del proceso, como ocurre con la pila y el heap, tienen un tamaño dinámico, que se va ajustando según evolucionan las necesidades del programa. En ambos casos, el tamaño va aumentando de acuerdo con los requisitos del programa, pero, sin embargo, nunca disminuye aunque se reduzcan las necesidades de espacio de esas regiones durante la ejecución del programa.

Por motivos de seguridad, las direcciones de inicio de la pila y del heap varían en cada ejecución del programa, por lo que el tamaño inicial de la pila/heap puede cambiar.

Analice el programa MemEvol.c. Posteriormente compile y ejecute dicho programa. Comprobará como a lo largo de la ejecución del mismo, se crean nuevas regiones de memoria y algunas modifican su tamaño.


Como hemos indicado anteriormente, el tamaño de la pila o el heap no disminuye a pesar de liberar los recursos. Este hecho lo podemos comprobar al utilizar el free, ya que vemos como no se reduce el tamaño de la pila. Por lo tanto dicha zona de memoria sigue estando asociada al proceso.


Tenga en cuenta que mediante free se libera una zona de memoria previamente reservada. Sin embargo hemos comprobado como esta zona de memoria sigue perteneciendo al mapa de memoria del proceso. Es decir, dicha zona de memoria sigue estando disponible para el proceso, pero no tiene porque haberse eliminado la información que contiene.

Como ya hemos visto el mapa de memoria de un proceso es dinámico, pudiendo crearse y liberarse regiones durante la ejecución del mismo. Esto es especialmente claro cuando se utilizan los servicios fork y exec o cuando se crean threads.

El programa Thread.c muestra la evolución del mapa de memoria de un proceso cuando crea un thread. Recordemos que cuando se crea un thread este tiene asociada una pila. Compile y ejecute dicho programa.


Con respecto a la llamada fork, recordemos que genera un clon del proceso que la invoca, creando un mapa de memoria que es un duplicado del correspondiente al proceso padre. De esa manera, el nuevo proceso se inicia desde la misma situación que el padre, pero a partir de ese punto cada uno tiene su propia ejecución independiente.

Compile y ejecute el programa Fork.c. Comprobará que antes del fork disponemos de un único proceso y que tras el fork existen 2 procesos en ejecución pero al contrario de lo indicado anteriormente no poseen el mismo mapa de memoria.


En cuanto a la llamada al servicio exec, esta llamada provoca la destrucción del mapa de memoria del proceso que existe en el momento de su invocación y la construcción de un nuevo mapa asociado al ejecutable.

Compile y ejecute el programa Exec_th. Analice como cambia el mapa de memoria y como evoluciona el proceso.




USO DE BIBLIOTECAS

Las bibliotecas constituyen un mecanismo eficiente para compartir código y para la reutilización del mismo. En la mayoría de los sistemas operativos se ofrece a los usuarios tres modalidades a la hora de utilizar bibliotecas: con enlace estático, con enlace dinámico o con carga explicita en tiempo de ejecución.

En apartado relativo a Fichero Ejecutable ya vimos como mediante la opción -static, podemos indicar al compilador que las bibliotecas necesarias para la ejecución del programa se enlacen de manera estática. También comprobamos las diferencias en las regiones del mapa de memoria de un mismo programa cuando se compila con la opción -static y sin ella. Ahora comprobaremos las diferencias entre los 2 modos de compilación en tiempo de ejecución. Para ello utilizaremos el programa Usa_biblioteca.c. Dicho programa utiliza la librería libm para el cálculo del coseno, por lo que durante la compilación se añade la opción -lm. Compile el programa y ejecute las versiones con enlace estático y enlace dinámico.

alumno@maquinaLinux:~/PracticasAnalisis/ModuloMemoria$ gcc -Wall -o Usa_biblioteca_est Usa_biblioteca.c -static -lm
alumno@maquinaLinux:~/PracticasAnalisis/ModuloMemoria$ gcc -Wall -o Usa_biblioteca_din Usa_biblioteca.c -lm

Comprobará que en la versión dinámica el número de regiones es mayor que en la versión estática, puesto que se cargan las bibliotecas necesarias para la ejecución del programa.


Con la opción de la carga explicita en tiempo de ejecución, se carga la biblioteca necesaria en el momento en que se necesita y una vez que se ha utilizado la funcionalidad requerida, se puede liberar la biblioteca. Un ejemplo de este uso lo podemos ver en el programa Carga_biblioteca.c.


Compile y ejecute el programa Carga_biblioteca.c


La técnica de carga explicita en tiempo de ejecución además, permite aumentar la funcionalidad de un programa sin necesidad de modificarlo, recompilarlo o, incluso, pararlo o volverlo a arrancar. Dicho de manera formal, el programa puede, a posteriori, y sin modificación alguna, aprender cosas que no estaban previstas cuando se desarrolló. Un ejemplo puede ser un navegador de ficheros (o Web), en el que el tratamiento de cada fichero depende de su tipo, y donde puede requerir añadir la capacidad de procesar nuevos tipos de ficheros sin necesidad de modificar ni recompilar el navegador.

El programa Navegador.c intenta ilustrar, de forma simplificada, la estrategia que se acaba de explicar. Analice dicho programa. Comprobará que son necesarios, para su correcto funcionamiento, la existencia de archivos adicionales. En concreto, un fichero de configuración config que indica, en función del tipo de extensión a tratar, que biblioteca se debe utilizar y por tanto, será necesario disponer también de la biblioteca a utilizar.


Compile y ejecute el programa Navegador.c y los archivos necesarios para su correcto funcionamiento.


PROYECCIÓN DE ARCHIVOS

El API del sistema de memoria ofrece operaciones para proyectar archivos en memoria, lo que constituye un modo alternativo de acceso a los archivos frente al basado en operaciones de lectura y escritura. La proyección de un archivo crea una nueva región de memoria en el mapa del proceso, cuyas características dependerán de los parámetros especificados en la función de proyección. En el caso de POSIX, la función que nos permite proyectar un archivo en memoria se denomina mmap.


A la hora de proyectar un fichero se pueden controlar dos aspectos que van a definir cómo se comporta una proyección. Por un lado, se puede establecer si la proyección es de tipo privada (MAP_PRIVATE) o de tipo compartida (MAP_SHARED). Por otro lado, si está basada en un fichero o es de carácter anónimo (MAP_ANON).


Es importante saber qué tipo de proyección se debe usar dependiendo de qué tipo de circunstancias concurren en la aplicación. Una selección errónea puede causar que el programa no funcione como está previsto.

Por norma general, cuando se proyecta un archivo en memoria, se reserva un espacio que será múltiplo del tamaño de página utilizado. Si el tamaño del fichero no es múltiplo de este tamaño de página, el espacio sobrante de la última página ocupada por el fichero será anulado (rellenado con 0s).

Compile y ejecute el programa Proyeccion, que permite que el usuario especifique en los argumentos del programa los parámetros de una llamada mmap mostrando cómo esa llamada afecta al mapa de memoria del proceso. Pruebe con distintos argumentos.


A continuación vamos a analizar el uso de la técnica de proyección de archivos, a través del siguiente código.

D-01.- #define MYNAME  "Desconocido"
D-02.- #include <sys/types.h>
D-03.- #include <sys/stat.h>
D-04.- #include <sys/mman.h>
D-05.- #include <fcntl.h>
D-06.- #include <stdio.h>
D-07.- #include <unistd.h>
D-08.- #include <stdlib.h>
D-09.- #include <string.h>
D-00.- 
D-11.- int main(int argc, char **argv)
D-12.- {
D-13.-         int fd, tam;
D-14.-         char *org, *p, *q;
D-15.-         struct stat bstat;
D-16.- 
D-17.-         if (argc!=3) {
D-18.-                 fprintf(stderr, "Uso: %s archivo_origen archivo_destino\n", MYNAME);
D-19.-                 return(1);
D-20.-         }
D-21.- 
D-22.-         if ((fd=open(argv[1], O_RDONLY))<0) {
D-23.-                 perror(MYNAME": No puede abrirse el archivo");
D-24.-                 return(1);
D-25.-         }
D-26.- 
D-27.-         if (fstat(fd, &bstat)<0) {
D-28.-                 perror(MYNAME": Error en fstat del archivo");
D-29.-                 close(fd);
D-30-                 return(1);
D-31.-         }
D-32.- 
D-33.-         if ((org=mmap((caddr_t) 0, bstat.st_size, PROT_READ|PROT_WRITE, MAP_PRIVATE, fd, 0)) == MAP_FAILED) {
D-34.-                 perror(MYNAME": Error en la proyeccion del archivo");
D-35.-                 close(fd);
D-36.-                 return(1);
D-37.-         }
D-38.- 
D-39.-         close(fd);
D-40.- 
D-41.-         p=org;
D-42.- 
D-43.-         for ( ; p<org+bstat.st_size; p++)
D-44.-                 (*p)=toupper(*p);
D-45.- 
D-46.-         if ((fd=open(argv[2], O_CREAT|O_TRUNC|O_RDWR, 0640))<0) {
D-47.-                 perror(MYNAME": No puede crearse el archivo destino");
D-48.-                 exit(1);
D-49.-         }
D-50.- 
D-51.-         if (ftruncate(fd, bstat.st_size)<0) {
D-52.-                 perror(MYNAME": Error en ftruncate del archivo destino");
D-53.-                 close(fd);
D-54.-                 unlink (argv[2]);
D-55.-                 exit(1);
D-56.-         }
D-57.- 
D-58.-         if ((q=mmap((caddr_t) 0, bstat.st_size, PROT_WRITE,MAP_SHARED, fd, 0)) == MAP_FAILED) {
D-59.-                 perror(MYNAME": Error en la proyeccion del archivo destino");
D-60.-                 close(fd);
D-61.-                 unlink (argv[2]);
D-62.-                 exit(1);
D-63.-         }
D-64.- 
D-65.-         memcpy(q, org, bstat.st_size);
D-66.- 
D-67.-         munmap(p, bstat.st_size);
D-68.-         munmap(q, bstat.st_size);
D-69.- 
D-70.-         return(0);
D-71.- }






El uso de la técnica de la proyección de ficheros presenta numerosas ventajas sobre la utilización convencional de las llamadas al sistema de lectura y escritura. Además de la evidente disminución en el número de llamadas al sistema, lo que redunda en una mayor eficiencia, también se facilita la programación.

Para comprobar esta mayor eficiencia, se propone hacer uso del programa Times_B.c que ya se utilizó en el Módulo de Procesos y el programa Desconocido_fich.c, que realiza la misma tarea que el programa Desconocido.c pero en lugar de proyecciones en memoria utiliza llamadas convencionales de lectura y escritura sobre ficheros.

Compile el programa Desconocido_fich.c y ejecútelo a través de Times_B:

alumno@maquinaLinux:~/PracticasAnalisis/ModuloMemoria$ ../ModuloProcesos/Times_B ./Desconocido_fich origen destino

Compare los resultados obtenidos con Desconocido_fich.c y los obtenidos con Desconocido.c


SITUACIONES DE ERROR EN EL USO DE PROYECCIÓN DE ARCHIVOS

En esta sección se plantean distintas situaciones problemáticas relacionadas con el uso de la técnica de ficheros proyectados.

Como ya hemos dicho anteriormente, cuando se proyecta un archivo en memoria, se reserva tanto espacio como páginas sean necesarias para alojar el fichero. Sin embargo lo más habitual es que el tamaño del fichero no sea múltiplo del tamaño de página, por lo que lo más probable es que haya una zona de la última página asignada a la proyección que no contendrá información relativa al fichero.

Pero, ¿qué sucede cuando intentamos acceder a esa zona de memoria? ¿Y si nos salimos de la zona de memoria asignada al fichero durante su proyección?. Analizaremos ambos aspectos tanto en el caso de que se acceda a la proyección para lectura como para escritura.

El programa Lector_sinfreno.c accede en modo lectura a un fichero proyectado. Compile y ejecute dicho programa.




El programa Escritor_sinfreno.c accede en modo escritura a un fichero proyectado. Analice el código de dicho programa.


Compile y ejecute el programa.






SITUACIONES DE ERROR EN EL USO DE LA MEMORIA DINÁMICA EN C

El uso de la memoria dinámica en C es propenso a errores ya que, a diferencia de lo que ocurre con otros lenguajes de mayor nivel, el programador debe encargarse de controlar toda la evolución de los datos reservados de esta forma. Esta sección no pretende ser un catálogo exhaustivo de qué tipos de errores se pueden producir en este ámbito, sino que, simplemente, plantea un ejemplo ilustrativo.

Analice el siguiente fragmento de código.

MD-01.- char *p;
MD-02.- char *q
MD-03.- p=malloc(256);                  
MD-04.- q=malloc(256);                  
MD-05.- strcpy(p, "Hola");              
MD-06.- q=p;                            
MD-07.- strcat(q, "Adios");             
MD-08.- free(p);
MD-09.- printf("%s\n",q);




 
docencia/asignaturas/sox/prv/practicas/analisis_so6/modulo_gm.txt · Última modificación: 2019/01/16 23:39 (editor externo)
 
Recent changes RSS feed Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki