Against the Law: Beware the Natives
Voy a comenzar algunas entradas en el blog relativas a depuración de aplicaciones Win32, por lo que parece interesante sentar antes unas bases teóricas sobre arquitectura de memoria y procesos de Windows NT. Esto, por otra parte, es muy útil a la hora de planificar el despliegue y configuración de aplicaciones de servidor, como puede ser SQL Server, por lo que no solo recomiendo la lectura a los interesados en el desarrollo de software.
Me plantee escribir la introducción desde cero, pero me acordé que hace cosa de un año escribí un artículo similar para la revista de los clubes .NET, de modo que saltándome a la torera los derechos de autor (que para algo el artículo es mío XD) y manteniéndome fiel a mi principio de Supreme Laziness, he decidido hacer un copy&paste sin remordimientos.
Espero que os guste!
Beware the Natives!
Un vistazo a la arquitectura de memoria y procesos de Windows NT®
Los que me conocéis sabéis que siento verdadera pasión por la plataforma .NET, pasión compartida, supongo, con todos los que estáis leyendo esta revista. De hecho, mis últimos temas de conversación favoritos para esas tardes de cervezas con los colegas son el recolector de basura y los objetos lázaro, el namespace System.Transactions y las plantas rodadoras.
Sin embargo hoy me siento rebelde y provocador, y me gustaría que me acompañarais en una breve excursión. Imaginaos a vosotros mismos en un bote en un río (ejem, no seguiré por ahí, que no estamos para pagar derechos de autor a los de Liverpool). Partiremos de nuestro hábitat natural, el mundo administrado, la tierra de Heljsberg y Wiltamuth, para adentrarnos en las oscuras y desconocidas tierras nativas (nunca mejor dicho) de Cutler, el núcleo de Windows NT® (2000, 20003, XP).
Muchas veces tengo la impresión de que en nuestro entorno, entre los apasionados de .NET, existe una especie de corriente autárquica; parece que no queremos saber nada de ése mundo exterior. Parecemos creer que nos podemos olvidar de sistema operativo subyacente (ya sea Windows, GNU/Linux, etc…) y permanecer en nuestra burbuja administrada. Sin embargo, veo constantemente ejemplos en la vida real de problemas de rendimiento o fallos de aplicaciones que, o bien se derivan de un desconocimiento de lo que hay por debajo de la plataforma, o bien podrían ser detectados y depurados mucho más fácilmente de conocer estas nociones básicas. Por eso creo importante dedicar unas pocas palabras a sentar al menos un conocimiento básico sobre la arquitectura de los sistemas Windows NT®, y hoy me centrare principalmente su gestión de memoria y de procesos.
NOTA: Antes de comenzar, me gustaría aclarar que todo lo que os voy a comentar de aquí en adelante es cierto en entornos Windows de 32 bits, ya que en los 64 bits la historia cambia bastante.
Programas, Procesos, hilos, y la madre que los parió…
Como ya sabemos, las aplicaciones que ejecutamos en nuestra máquina se convierten en procesos. Aunque a primera vista parece que un proceso es lo mismo que un programa, esto no es del todo cierto; podríamos decir que un programa es una secuencia estática de instrucciones, y el proceso es una estructura dinámica, creada por el loader sistema operativo a la hora de cargar un programa en memoria para comenzar su ejecución.
Estos procesos contienen, entre otras cosas, un espacio de memoria virtual privado (del que hablaremos más adelante), el código y datos del ejecutable inicial, así como uno o más hilos de ejecución.
Estos hilos son los que el sistema operativo planifica para su ejecución; podríamos considerarlos como la unidad mínima de planificación. Así que ya sabéis, a partir de ahora si alguien os vuelve a decir que el sistema operativo está ejecutando un proceso, podéis aclararle que lo que se ejecutan son los diferentes hilos del proceso, y de paso seguro que os ganáis un Certificado de Súper Gafotas.
Os adjunto la salida generada por la herramienta gratuita PsList de SysInternals (www.sysinternals.com) sobre uno de los procesos de mi máquina (un notepad.exe en éste caso). Como podéis ver, el proceso está compuesto por cinco hilos de ejecución diferentes, de los cuales podemos ver su identificador (TID), su prioridad, número de cambios de contexto, estado y estadísticas de tiempo de ejecución.
Como ya sabéis, Notepad.exe es una aplicación nativa; os propongo como ejercicio observar cómo se comporta un PsList sobre una aplicación administrada con múltiples hilos de ejecución usando System.Threading.
Espacio de Memoria Virtual
Ahí va una afirmación sorprendente: todos los procesos que se ejecutan en nuestras máquinas Windows disponen para 2 Gb de espacio de memoria virtual para ellos solitos, que se llama espacio de memoria virtual privado. Esto es así independientemente de que nuestra máquina tenga 128 Mb o 16 Gb de RAM, y es independiente de la cantidad de memoria que tengamos establecida en los archivos de paginación. Es más, si tenemos en nuestro sistema 10 procesos, entre todos tendremos un total de 20 Gb de espacio de memoria virtual privado.
Evidentemente toda magia tiene su explicación. No hay manera en la cual podamos tener persistidos en memoria datos que ocupen más que la cantidad total de memoria física disponible (RAM + Archivos de Paginación), por tanto el sistema operativo dispone de mecanismos que mapean las direcciones del espacio de memoria virtual con ubicaciones físicas de memoria, dándose la posibilidad de que la misma dirección de memoria dentro del espacio de direcciones virtual en dos procesos diferentes apunte a dos posiciones de memoria física diferentes.
Cada proceso, por tanto, tiene 2 Gb de memoria virtual donde ubicará sus recursos, que principalmente serán los siguientes:
- Imagen del código de la aplicación
- Imagen de todos los módulos (.dlls) empleados por la aplicación
- Uno o más heaps
- Una pila (stack) por cada hilo de ejecución (otra pila se crea automáticamente dentro del área de memoria destinada al Kernel, que comentaremos después)
- En caso de ser una aplicación .NET, un heap adicional llamado Managed Heap, que es donde se realizan todas las reservas de memoria y por tanto, el ámbito donde opera el recolector de basura.
La figura que se muestra a continuación representa el espacio de memoria privado de una hipotética aplicación administrada compuesta de un ejecutable (demo1.exe), con dos librerías enlazadas (lib1.dll y lib2.dll), dos hilos de ejecución y un heap administrado.
Este heap administrado se comporta de manera diferente si estamos en un entorno de servidor o en un Workstation. En el primer caso, el managed heap se compone de segmentos de 64 Mb, con un managed heap en cada procesador del sistema. En el caso de tratase de una estación de trabajo, se trata de segmentos de 16 Mb. ¿Cómo podemos determinar que versión del recolector de basura estamos empleando? Para ello solo debemos comprobar si estamos empleando la mscorwks.dll (Workstation) o la mscorsrv.dll (Servidor).
Como apreciamos en la figura, los diferentes hilos de un mismo proceso comparten el mismo espacio de memoria virtual privada, ya que sus diferentes pilas están en el mismo área de 2 Gb. De éste modo los diferentes hilos pueden acceder a la misma memoria. Esto no ocurre a la hora de comunicarse con otros procesos, para lo cual hay que emplear otros mecanismos de los que hablaremos en otra entrega de ésta serie.
A todo esto hay que sumarle un área inmutable (común para todos los procesos) de otros 2 Gb dentro del espacio de memoria virtual que almacena estructuras internas del sistema operativo, como las Page Table Entries, handles y demás. Es la zona de memoria para el Kernel, y se puede ver en la siguiente figura.
La figura sirve de sumario de la estructura de procesos y memoria en un entorno Windows en su configuración por defecto. Y digo en su configuración por defecto porque este balance “2 Gb para proceso de usuario / 2 Gb para Kernel” puede ser alterado mediante los siguientes parámetros suministrados al kernel en el fichero boot.ini:
Mediante este switch indicamos al sistema operativo que queremos asignar más memoria a las aplicaciones y menos al sistema operativo, de modo que el particionado se hará dejando 1 Gb para el Kernel y 3 Gb para el modo usuario.
Para que nuestras aplicaciones nativas puedan beneficiarse de éste incremento en su espacio de direcciones virtual, deberemos compilarlas con la opción /LargeAdressAware. En el caso de nuestras aplicaciones .NET, si bien el CLR respeta este flag, el compilador de C# no tiene opción para establecerlo, por lo que debemos editar la cabecera PE del ensamblado usando editbin:
“editbin /largeadressaware <myapp.exe>”
Usando el switch /3Gb en conjunción con /UserVA=xxxx podemos hacer una ajuste fino de la cantidad de memoria que queremos asignar al proceso de usuario, estableciendo un valor en megas entre 2000 y 3000. Esta opción solo está disponible en Windows 2003 Server y Windows XP.
¿Y por qué me cuentas todo eso?
Un ejemplo sencillo, pero que me encanta exponer cuando comento estos temas, es un servidor de FTP. Cuando propongo el diseño de un servidor de FTP, la gente se suele sentir atraída por la idea de usar un hilo de escucha de peticiones, más otros n hilos, uno por cada cliente que se conecta al servidor de FTP. El hilo principal haría un spawn de un nuevo hilo de ejecución con cada conexión entrante, y cuando ésta conexión se cerrara, se haría lo propio con el hilo principal.
Suena bastante lógico, pero si pensamos un poco en ello, y a la vista de lo que hemos comentado antes, podemos ver que ésta solución no escala precisamente bien. El tamaño por defecto para cada pila es de 512 Kb, y considerando que necesitamos dos pilas por cada hilo, la aplicación consumiría 1 Mb por cada conexión solo para sus pilas.
¿Quiere esto decir que no debemos emplear múltiples hilos a la hora de diseñar nuestras aplicaciones? Evidentemente no es así, pero hay que tener un cuidado especial a la hora de diseñar nuestras aplicaciones multi-hilo para asegurarnos que escalen correctamente. Para mejorar la escalabilidad se recomienda emplear un Pool de hilos, como el ThreadPool de System.Threading o el QueueUserWorkItem de la API de Windows 2000, y que por cuestiones de espacio y ámbito no trataré en éste artículo.
En la práctica también podemos ver ejemplos de problemas de fragmentación de memoria (muy típicos en despliegues de IIS), bloqueos completos de un servidor por agotamiento de PTEs, y un largo etcétera de problemas que un conocimiento superficial del sistema operativo subyacente nos puede ayudar a detectar y tratar rápidamente.
Despedida
Hasta aquí hemos llegado en esta breve introducción a la arquitectura de procesos y memoria de los sistemas Windows NT®. En un futuro artículo exploraremos ese espacio entre el código administrado y el código nativo, chapotearemos en ese rio entre P/Invokes y Marhallings (y de paso, haremos nuestros pinitos con WinDbg) para comunicar una aplicación .NET con una aplicación Win32 existente. Si os resulta atractivo, ¡ya sabéis donde podréis encontrarlo!
Referencias Bibliográficas:
- Microsoft Windows Internals, 4th Ed. [ M. Russinovich / D. Solomon], Microsoft Press, ISBN: 0-7356-1917-4
Rock Tip:
En el caso de hoy, y debido a mi infracción flagrante de los derechos de autor de mi artículo en student.NET, he decidido hacer alusión no ya a una canción, sino a un disco; se trata de 'Against the Law', de los heavys cristianos Stryper. Es una banda curiosa, famosos por la increible voz del guitarrista/vocalista Michael Sweet, por los peinados cardados que caracterizaban a estas bandas en los años 80 y, como no, por esas mallas y toreras amarillas y negras que se atrevían a lucir en sus conciertos!! Algún día conseguiré yo unas iguales! :)
Os voy a dejar una enlace a uno de sus temas más clásicos, 'Calling on You'. Yo adoro a este grupo, pero pinchad bajo vuestra propia responsabilidad :)