UP | HOME

Relación con el hardware

Índice

1 Introducción

Todos los sitemas de cómputo están compuestos por al menos una unidad de proceso junto con dispositivos que permiten ingresar datos (teclado, mouse, micrófono, etc.) y otros que permiten obtener resultados (pantalla, impresora, parlantes, etc.). Como se vio anteriormente, una de las funciones del sistema operativo es la de abstraer el hardware de la computadora y presentar al usuario una versión unificada y simplificada de los dispositivos. En este capítulo se verá la relación que mantiene el sistema operativo con el hardware, las funciones que cumplen y algunas abstracciones comunes utilizadas en sistemas operativos modernos.

2 Unidad de Procesamiento

La unidad de procesamiento es la parte fundamental de todo sistema de cómputo. Es la encargada de ejecutar tanto los programas del usuario como el sistema operativo en sí mismo. La funciones del sistema operativo respecto a la unidad de procesamiento son:

Inicialización
Luego de ser cargado el sistema operativo debe realizar varias tareas de inicialización como habilitar las interrupciones de hardware y software (excepciones y trampas), configurar el sistema de memoria virtual (paginación, segmentación), etc.
Atender las interrupciones y excepciones
Como se verá más adelante, la unidad de procesamiento puede encontrar una situación que no puede resolver por sí misma (una instrucción o dirección inválida, una división por cero, etc.) ante lo cual le pasa el control al sistema operativo para que éste trate o resuelva la situación.
Multiplexación
En un sistema multiproceso, el sistema operativo es el encargado de administrar la unidad de procesamiento dando la ilusión a los procesos que están ejecutando de forma exclusiva.

2.1 Jerarquía de almacenamiento

Las computadoras que siguen la arquitectura von Neumann, esto es, prácticamente la totalidad hoy en día1 podrían resumir su operación general a alimentar a una unidad de proceso (CPU) con los datos e instrucciones almacenados en memoria, que pueden incluir llamadas a servicio (y respuestas a eventos) originados en medios externos.

Una computadora von Neumann significa básicamente que es una computadora de programa almacenado en la memoria primaria — esto es, se usa el mismo almacenamiento para el programa que está siendo ejecutado y para sus datos, sirviéndose de un registro especial para indicar al CPU cuál es la dirección en memoria de la siguiente instrucción a ejecutar.

La arquitectura von Neumann fue planteada, obviamente, sin considerar la posterior diferencia entre la velocidad que adquiriría el CPU y la memoria. En 1977, John Backus presentó al recibir el premio Turing un artículo describiendo el cuello de botella de von Neumann. Los procesadores son cada vez más rápidos (se logró un aumento de 1000 veces tanto entre 1975 y 2000 tan sólo en el reloj del sistema), pero la memoria aumentó su velocidad a un ritmo mucho menor — aproximadamente un factor de 50 para la tecnología en un nivel costo-beneficio suficiente para usarse como memoria primaria.

./img/dot/jerarquia_memoria.png

Jerarquía de memoria entre diversos medios de almacenamiento.

Una respuesta parcial a este problema es la creación de una jerarquía de almacenamiento, yendo de una pequeña área de memoria mucho más cara hasta un gran espacio de memoria muy económica. En particular, la relación entre las capas superiores está administrada por hardware especializado de modo que su existencia resulta transparente al programador.

Velocidad y gestor de los principales niveles de memoria. (Silberschatz, Galvin, Gagne; p.28)
Nivel1234
NombreRegistrosCacheMemoria princ.Disco
Tamaño<1KB<16MB<64GB>100GB
TecnologíaMultipuerto, CMOSSRAM CMOSCMOS DRAMMagnética
Acceso (ns)0.25-0.50.5-2580-2505,000,000
Transf (MB/s)20,000-100,0005,000-10,0001,000-5,00020-150
AdministraCompiladorHardwareSist. Op.Sist. op.
Respaldado enCacheMemoria princ.DiscoCD o cinta

Ahora bien, si bien la relación entre estos medios de almacenamiento puede parecer natural, para una computadora tiene una realidad completamente distinta: los registros son parte integral del procesador, y la memoria está a sólo un paso de distancia (el procesador puede referirse a ella directamente, de forma transparente, indicando la dirección desde un programa). El caché no existe para efectos prácticos: el procesador no hace referencia directa a él, sino que es manejado por los controladores de acceso a memoria.

Como se verá, el sistema operativo es el encargado de mantener todas estas jerarquías de memoria consistentes y de realizar las transferencias entre unas y otras.

2.1.1 Registros

La memoria más rápida de la computadora son los registros, ubicados dentro de cada uno de los núcleos de cada uno de los CPU. Las arquitecturas tipo RISC (Reduced Instruction Set Computer) sólo contemplan la ejecución de instrucciones entre registros (excepto, claro, las de carga y almacenamiento a memoria primaria).

Los primeros CPU trabajaban con pocos registros, muchos de ellos de propósito específico — trabajaban más bien con una lógica de registro acumulador. Por ejemplo, el MOS 6502 (en el cual se basaron las principales computadoras de 8 bits) tenía un acumulador de 8 bits (A), dos registros índice de 8 bits (X e Y), un registro de estado del procesador de 8 bits (P), un apuntador al stack de 8 bits (S), y un apuntador al programa de 16 bits (PC). El otro gran procesador de su era, el Zilog Z80, tenía 14 registros (3 de 8 bits y el resto de 16), pero sólo uno era un acumulador de propósito general.

El procesador Intel 8088, en el cual se basó la primer generación de la arquitectura PC, ofrecía cuatro registros de uso casi general. En los ochenta comenzaron a producirse los primeros procesadores tipo RISC, muchos de los cuales ofrecían 32 registros, todos ellos de propósito general.

./img/registros_8086.png

Ejemplo de registros: Intel 8086/8088 (Imagen de la Wikipedia: Intel 8086 y 8088)

El compilador 2 busca realizar muchas operaciones que deben ocurrir reiteradamente, donde la rapidez es fundamental, con sus operadores cargados en los registros. El estado del CPU en un momento dado está determinado por el contenido de los registros. El contenido de la memoria, obviamente, debe estar sincronizado con lo que ocurre dentro de éste — pero el estado actual del CPU, lo que está haciendo, las indicaciones respecto a las operaciones recién realizadas que se deben entregar al programa en ejecución están todos representados en los registros. Se debe mantener esto en mente cuando posteriormente se habla de todas las situaciones en que el flujo de ejecución debe ser quitado de un proceso y entregado a otro.

La relación de la computadora y del sistema operativo con la memoria principal será abordada en el capítulo MEM.

2.2 Interrupciones y excepciones

La ejecución de los procesos podría seguir siempre linealmente, atendiendo a las instrucciones de los programas tal como fueron escritas, pero en el modelo de uso de cómputo actual, eso no serviría de mucho: para que un proceso acepte interacción, su ejecución debe poder responder a los eventos que ocurran alrededor del sistema. Y los eventos son manejados a través de las interrupciones y excepciones (o trampas).

Cuando ocurre algún evento que requiera la atención del sistema operativo, el hardware encargado de procesarlo escribe directamente a una ubicación predeterminada de memoria la naturaleza de la solicitud (el vector de interrupción) y, levantando una solicitud de interrupción, detiene el proceso que estaba siendo ejecutado. El sistema operativo entonces ejecuta su rutina de manejo de interrupciones (típicamente comienza grabando el estado de los registros del CPU y otra información relativa al estado del proceso desplazado) y posteriormente la atiende.

Las interrupciones pueden organizarse por prioridades, de modo que una interrupción de menor jerarquía no interrumpa a una más importante — dado que las interrupciones muchas veces indican que hay datos disponibles en algún buffer, el no atenderlas a tiempo podría llevar a la pérdida de datos.

Hay un número limitado de interrupciones definidas para cada arquitectura, mucho más limitado que el número de dispositivos que tiene un equipo de cómputo actual. Las interrupciones son, por tanto, generadas por el controlador del canal en que son producidas. Si bien esto resuelve la escasez de interrupciones, dificulta su priorización — con canales de uso tan variado como el USB3, una interrupción puede indicar que hay desde un teclazo para ser leído hasta un paquete de red esperando a ser procesado — y si bien demorar la atención al primero no llevaría a pérdida notable de información, no ateneder el paquete de red sí.

El sistema operativo puede elegir ignorar (enmascarar) a ciertas interrupciones — pero hay interrupciones que son no enmascarables.

Se hace la distinción entre interrupciones y excepciones según su origen: una interrupción es generada por causas externas al sistema (un dispositivo requiere atención), mientras que una excepción es una evento generado por un proceso (una condición en el proceso que requiere la intervención del sistema operativo). Si bien hay distinciones sutiles entre interrupciones, trampas y excepciones, al nivel de discusión que se abordará basta con esta distinción.

Los eventos pueden ser, como ya se mencionó, indicadores de que hay algún dispositivo requiriendo atención, pero pueden también provenir del mismo sistema, como una alarma o temporizador (que se emplea para obligar a todo programa a entregar el control en un sistema multitareas) o indicando una condición de error (por ejemplo, una división sobre cero o un error leyendo de disco).

Las funciones del sistema operativo respecto a las interrupciones son:

Administrar el hardware manejador de interrupciones

Esto incluye el enmascarado y desenmascarado de las interrupciones, configurar y asignar interrupciones a cada dispositivo, notificar al manejador cuando la interrupción ya ha sido atendida, etc.

Abstraer las interrupciones
El sistema operativo oculta a los programas de usuario la existencia de interrupciones de hardware ya que éstas son dependientes de la arquitectura del procesador. En cambio el sistema operativo lo comunica de una forma unificada a través de distintos mecanismos, por ejemplo mensajes o señales o detiendo el proceso que espera la acción relacionada con una interrupción y continuando su ejecución cuando ésta ocurre.
Punto de entrada al sistema operativo
Como se verá más adelante en la sección HWSYSCALLS, muchos procesadores y sistemas operativos utilizan las interrupciones como medio por el cual un proceso de usuario realiza una llamada al sistema. Por ejemplo, en Linux para arquitecturas x86 el programa de usuario genera la interrupción 0x80 para iniciar una llamada al sistema. En arquitecturas más recientes como x8664, MIPS y ARM esto ha sido reemplazado por una instrucción especial syscall.
Atender excepciones y fallas
Como se discutió antes, durante la ejecución de un programa pueden ocurrir situaciones anómalas, como por ejemplo, una división sobre cero. Desde el punto de vista del CPU, esto es similar a una interrupción de hardware y debe ser tratada por el sistema operativo. Dependiendo de la causa de la excepción, el sistema operativo tomará acción para resolver en lo posible esta situación. En muchos casos las excepciones resultan en una señal enviada al proceso, y este último es el encargado de tratar la excepción. En otros casos la falla o excepción son irrecuperables (una instrucción inválida o un error de bus) ante la cual el sistema operativo terminará el proceso que la generó. En el capítulo MEM se cubre con mucho mayor detalle un tipo de excepción muy importante que debe tratar el sistema operativo: el fallo de paginación.

3 Terminales

Las terminales son dispositivos electrónicos utilizados para ingresar datos y emitir resultados dentro de un sistema de cómputo. Las primeras terminales utilizaban tarjetas perforadas e impresiones en papel. Debido a su limitada velocidad e imposibilidad de "editar" el papel ya impreso, este tipo de terminales fue cediendo terreno ante la aparición sobre principios de los setenta de las terminales de texto con pantalla de video y teclado.

Conceptualmente una terminal de texto es un dispositivo mediante el cual la computadora recibe y envía un flujo de caracteres desde y hacia el usuario respectivamente. Las operaciones más complejas, como edición, borrado y movimiento, en general son tratadas con secuencias de escape, esto es, una serie de caracteres simples que tomados en conjunto representan una acción a realizar en la terminal.

Durante la década de los setenta también se desarrollaron terminales gráficas las cuales podían representar imágenes junto con texto. Con la inclusión del ratón o "mouse" estas terminales dieron lugar a lo que hoy se conoce como Interfaz Gráfica de Usuario (Graphical User Interface o GUI) y a los sistemas de ventana.

En los sistemas operativos modernos es común referirse al emulador de terminal, un programa especializado ya sea para tener múltiples instancias de una terminal o para ejectuar una terminal de texto dentro de una interfaz gráfica. Estos programas se denominan de esta forma dado que sólo replican el comportamiento de las terminales (que eran originalmente equipos independientes), siendo únicamente un programa que recibe la entrada del usuario a través del teclado enviándola al sistema operativo como un flujo de datos, y recibe otro flujo de datos del sistema operativo, presentándolo de forma adecuada al usuario.

4 Dispositivos de almacenamiento

El almacenamiento en memoria primaria es volátil, esto es, se pierde al interrumpirse el suministro eléctrico. Esto no era muy importante en la época definitoria de los conceptos que se presentan en esta sección, dado que el tiempo total de vida de un conjunto de datos en almacenamiento bajo el control del procesador iba únicamente desde la entrada y hasta el fin de la ejecución del trabajo del usuario. Pero desde la década de los sesenta se popularizó la posibilidad de almacenar en la computadora información a largo plazo y con expectativas razonables de permanencia.

De las muchas tecnologías de almacenamiento, la que ha dominado fuertemente durante los últimos 40 años ha sido la de los discos magnéticos4. El acceso a disco (miles de veces más lento que el acceso a memoria) no es realizado directamente por el procesador, sino que requiere de la comunicación con controladores externos, con lógica propia, que podrían ser vistos como computadoras independientes de propósito limitado.

El procesador no puede referirse directamente más información que la que forma parte del almacenamiento primario — esto es, de la memoria RAM. En las secciones HWinterrupciones (Interrupciones y excepciones) y HWdma (Acceso directo a memoria), se explica cómo es que se efectúan dichas referencias.

Los dispositivos de almacenamiento (discos, memorias flash, cintas) pueden ser vistos como una región donde la computadora lee y escribe una serie de bytes que preservarán su valor incluso luego de apagada la computadora.

A nivel de hardware el sistema operativo no accede al dispositivo de almacenamiento byte por byte, sino que éstos se agrupan en bloques de tamaño fijo. El manejo de estos bloques (adminstración de bloques libres, lectura y escritura) es una tarea fundamental del sistema operativo, que asimismo se encarga de presentar abstracciones como la de archivos y directorios al usuario. Esto se verá en el capítulo DIR.

5 Relojes y temporizadores

Todas las computadoras incluyen uno o más relojes y temporizadores que son utilizados para funciones varias como mantener la hora del sistema actualizada, implementar alarmas tanto para los programas de usuario como para el sistema operativo, ejecutar tareas de mantenimiento periódicas, cumplir con requisitos temporales de aplicaciones de tiempo real, etc.

Mantener el tiempo correctamente dentro del sistema operativo es algo crucial. Permite establecer un orden cronológico entre los eventos que ocurren dentro del sistema, por ejemplo la creación de un archivo y de otro o el tiempo consumido en la ejecución de un proceso.

Por otro lado si el sistema operativo utiliza una política de planificación de procesos preventiva (capítulo PLAN), como la Ronda (Round Robin), éste debe interrumpir al proceso en ejecución luego de cierta cantidad de unidades de tiempo. Esto se implementa haciendo que el temporizador de la computadora genere interrupciones periódicamente, lo cual luego invocará al planificador de procesos.

6 Canales y puentes

Los distintos componentes de un sistema de cómputo se comunican a través de los diferentes canales (generalmente se hace referencia a ellos por su nombre en inglés: buses). Al nivel más básico, los canales son líneas de comunicación entre el procesador y los demás componentes del chipset5, a los cuales a su vez se conectan los diferentes dispositivos del sistema — desde aquellos que requieren mayor velocidad, como la misma memoria, hasta los puertos más sencillos.

Un chipset provee distintos buses, con un agrupamiento lógico según la velocidad requerida por sus componentes y otras características que determinan su topología.

./img/northbridge_southbridge.png

Diagrama de la comunicación entre componentes de un sistema de cómputo basado en puente norte y puente sur. Imagen de la Wikipedia: Puente Norte

Hoy en día, el acomodo más frecuente6 de estos buses es a través de una separación en dos chips: el puente norte (Northbridge), conectado directamente al CPU, encargado de gestionar los buses de más alta velocidad y que, además, son fundamentales para el más básico inicio de la operación del sistema: la memoria y el reloj. La comunicación con algunas tarjetas de video se incorpora al puente norte a través del canal dedicado AGP (Advanced Graphics Port, Puerto Gráfico Avanzado).

Al puente norte se conecta el puente sur (Southbridge), que controla el resto de los dispositivos del sistema — normalmente se ven aquí las interfaces de almacenamiento (SCSI, SATA, IDE), de expansión interna (PCI, PCIe) y de expansión externa (USB, Firewire, puertos heredados seriales y paralelos).

6.1 Contención

Una de las principales razones de la existencia de tantos canales (buses) distintos en un mismo sistema es a la frecuencia acorde a los dispositivos para los cuales está diseñado: la cantidad de datos que tiene que viajar entre el procesador y la memoria a lo largo de la operación del sistema es muy superior que la que tiene que transferirse desde los discos, y a su vez, esta es mucho mayor que la que enviarse a la impresora, o la que se recibe del teclado. Claro está, los demás dispositivos podrían incrementar su frecuencia para participar en un canal más rápido, aunque su costo se incrementaría, dado que harían falta componentes capaces de sostener un reloj varios órdenes de magnitud más rápido.

Pero incluso obviando la diferencia económica: cuando el sistema requiere transferir datos de o hacia varios dispositivos de la misma categoría, es frecuente que ocurra contención: puede saturarse el ancho de banda máximo que alcanza uno de los canales y, aún si los dispositivos tienen información lista, tendrán que esperar a que los demás dispositivos desocupen el canal.

./img/dot/chipset_857.png

Esquema simplificado del chipset Intel 875 (para el procesador Pentium 4) ilustrando la velocidad de cada uno de los canales

En la figura HWchipset857 se puede ver el diseño general del chipset Intel 875, introducido en el 2003, incluyendo el ancho de banda de cada uno de los canales del sistema. Hay que recordar que hay canales como el USB que permiten la conexión de múltiples dispositivos, los cuales deberán compartir el ancho de banda total permitido por el canal: en la figura se presentan dos discos duros sobre el canal SATA y dos unidades ópticas en el ATA paralelo; el canal USB permite el uso de un máximo de 127 unidades por canal, por lo cual la contención puede ser muy alta.

6.2 Acceso directo a memoria (DMA)

La operación de dispositivos de entrada/salida puede ser altamente ineficiente. Cuando un proceso está en una sección limitada por entrada-salida (esto es, donde la actividad principal es la transferencia de información entre la memoria principal y cualquier otra área del sistema), si el procesador tiene que encargarse de la transferencia de toda la información7, se crearía un cuello de botella por la cantidad y frecuencia de interrupciones. Hoy en día, para evitar que el sistema se demore cada vez que hay una transferencia grande de datos, todas las computadoras implementan controladores de acceso directo a memoria (DMA) en uno o más de sus subsistemas.

El DMA se emplea principalmente al tratar con dispositivos con un gran ancho de banda, como unidades de disco, subsistemas multimedia, tarjetas de red, e incluso para transferir información entre niveles del caché.

Las transferencias DMA se hacen en bloques preestablecidos; en vez de que el procesador reciba una interrupción cada vez que hay una palabra lista para ser almacenada en la memoria, el procesador indica al controlador DMA la dirección física base de memoria en la cual operará, la cantidad de datos a transferir, el sentido en que se efectuará la operación (del dispositivo a memoria o de memoria al dispositivo), y el puerto del dispositivo en cuestión; el controlador DMA efectuará la transferencia solicitada, y sólo una vez terminada ésta (o en caso de encontrar algún error en el proceso) lanzará una interrupción al sistema; el procesador queda libre para realizar otras tareas, sin más limitante que la posible contención que tendrá que enfrentar en el bus de acceso a la memoria.

6.2.1 Coherencia de cache

Cuando se realiza una transferencia DMA de un dispositivo a la memoria, puede haber páginas de la memoria en cuestión que estén en alguno de los niveles de la memoria caché; dado que el caché está uno o más niveles por encima de la memoria principal, es posible que la información haya ya cambiado pero el caché retenga la información anterior.

Los sistemas de caché coherente implementan mecanismos en hardware que notifican a los controladores de caché que las páginas que alojan están sucias y deben ser vueltas a cargar para ser empleadas, los sistemas no coherentes requieren que el subsistema de memoria del sistema operativo haga esta operación.

Los procesadores actuales implementan normalmente varios niveles de caché, estando algunos dentro del mismo CPU, por lo que típicamente se encuentran sistemas híbridos, en los que los cachés de nivel 2 son coherentes, pero los de nivel 1 no, y deben ser manejados por software.

7 Interfaz del Sistema Operativo: llamadas al sistema

De forma análoga a las interrupciones, se puede hablar de las llamadas al sistema. El sistema operativo protege a un proceso de otro, y previene que un proceso ejecutándose en espacio no privilegiado tenga acceso directo a los dispositivos. Cuando un proceso requiere de alguna acción privilegiada, acede a ellas realizando una llamada al sistema. Las llamadas al sistema pueden agruparse, a grandes rasgos, en:

Control de procesos
Crear o finalizar un proceso, obtener atributos del proceso, esperar la finalización de un proceso o cierto tiempo, asignar o liberar memoria, etc.
Manipulación de archivos
Crear, borrar o renombrar un archivo; abrir o cerrar un archivo existente; modificar sus metadatos; leer o escribir de un descriptor de archivo abierto, etc.
Manipulación de dispositivos
Solicitar o liberar un dispositivo; leer, escribir o reposicionarlo, y otras varias. Muchas de estas llamadas son análogas a las de manipulación de archivos, y varios sistemas operativos las ofrecen como una sola.
Mantenimiento de la información
Obtener o modificar la hora del sistema; pedir detalles acerca de procesos o archivos, etc.
Comunicaciones
Establecer una comunicación con determinado proceso (local o remoto), aceptar una solicitud de comunicación de otro proceso, intercambiar información sobre un canal establecido.
Protección
Consultar o modificar la información relativa al acceso de objetos en el disco, otros procesos, o la misma sesión de usuario.

Cada sistema operativo expone una serie de llamadas al sistema. Estas son, a su vez, expuestas al programador a través de las interfaces de aplicación al programador (API), que se alínean de forma cercana (pero no exacta). Del mismo modo que cada sistema operativo ofrece un conjunto de llamadas al sistema distinto, cada implementación de un lenguaje de programación puede ofrecer un API ligeramente distinto de otros.

./img/dot/llamada_al_sistema.png

Transición del flujo entre espacio usuario y espacio núcleo en una llamada al sistema

7.1 Llamadas al sistema, arquitecturas y APIs

Cada familia de sistemas operativos provee distintas llamadas al sistema, y sus lenguajes/bibliotecas implementan distintos APIs. Esto es el que distingue principalmente a uno de otro. Por ejemplo, los sistemas Windows 95 en adelante implementan Win32, Win16 (compatibilidad con Windows previos) y MSDOS; MacOS implementa Cocoa (aplicaciones MacOS X) y Carbon (compatibilidad con aplicaciones de MacOS previos), y Linux y los *BSDs, POSIX (el estándar que define a Unix). El caso de MacOS X es interesante, porque también implementa POSIX, ofreciendo la semántica de dos sistemas muy distintos entre sí.

Los lenguajes basados en máquinas virtuales abstractas, como Java o la familia .NET (ver sección VIRTarqinexistentes), exponen un API con mucha mayor distancia respecto al sistema operativo; la máquina virtual se presenta como un pseudo-sistema operativo intermedio que se ejecuta dentro del real, y esta distinción se hace especialmente notoria cuando se busca conocer los detalles del sistema operativo.

7.1.1 Depuración por trazas (trace)

La mayor parte de los sistemas operativos ofrecen programas que, para fines de depuración, envuelven al API del sistema y permiten ver la traza de las llamadas al sistema que va realizando un proceso. Algunos ejemplos de estas herramientas son strace en Linux, truss en la mayor parte de los Unixes históricos o ktrace y kdump en los *BSD. A partir de Solaris 10 (2005), Sun incluye una herramienta mucho más profunda y programable para esta tarea llamada dtrace, que al paso del tiempo ha sido /portada/8 a otros Unixes (*BSD, MacOS).

La salida de una traza brinda amplio detalle acerca de la actividad realizada por un proceso, y permite comprender a grandes rasgos su interacción con el sistema. El nivel de información que da es, sin embargo, a veces demasiado — eso se puede ver si se considera la siguiente traza, ante uno de los comandos más sencillos: pwd (obtener el directorio actual)

$ strace pwd
execve("/bin/pwd", ["pwd"], [/* 43 vars */]) = 0
brk(0)                                  = 0x8414000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb773d000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY)      = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=78233, ...}) = 0
mmap2(NULL, 78233, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7729000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0po\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1351816, ...}) = 0
mmap2(NULL, 1366328, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb75db000
mprotect(0xb7722000, 4096, PROT_NONE)   = 0
mmap2(0xb7723000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x147) = 0xb7723000
mmap2(0xb7726000, 10552, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb7726000
close(3)                                = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb75da000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb75da8d0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0xb7723000, 8192, PROT_READ)   = 0
mprotect(0xb775c000, 4096, PROT_READ)   = 0
munmap(0xb7729000, 78233)               = 0
brk(0)                                  = 0x8414000
brk(0x8435000)                          = 0x8435000
open("/usr/lib/locale/locale-archive", O_RDONLY|O_LARGEFILE) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=1534672, ...}) = 0
mmap2(NULL, 1534672, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7463000
close(3)                                = 0
getcwd("/home/gwolf/vcs/sistemas_operativos", 4096) = 36
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 1), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb773c000
write(1, "/home/gwolf/vcs/sistemas_operati"..., 36/home/gwolf/vcs/sistemas_operativos
) = 36
close(1)                                = 0
munmap(0xb773c000, 4096)                = 0
close(2)                                = 0
exit_group(0)                           = ?

8 Abstracciones comunes

Como se mencionó antes una de las funciones del sistema operativo es la de abstraer el hardware de la computador. Cuál es esa abstracción que ve efectivamente el usuario varía de un sistema operativo a otro. Se verán en esta sección algunas abstracciones utilizadas en varios sistemas dejando las correspondientes a sistemas de archivos para el capítulo FS.

8.1 Sistemas tipo Windows

Los sistemas del tipo Windows presentan una abstracción diversa para cada uno de los componentes de la computadora.

Por ejemplo los volúmenes de almacentamiento secundario (discos rígidos, discos compactos, memorias flash, etc.) son relacionados con una letra cada uno, así (en general) C: es el volumen o partición del disco principal, A: y B: se utilizan para discos extraibles.

Una desventaja de esta abstracción es que no queda claro cuáles unidades pertenecen al mismo disco físico y cuáles no.

A los puertos de entrada/salida más utilizados también se les asignan nombres alfanuméricos, por ejemplo el primer puerto paralelo se denomina LPT1 y el segundo puerto serie COM2.

8.2 Sistemas tipo Unix

Unix introdujo el concepto de que todo es un archivo: en el sistema Unix original, todos los dispositivos podían ser controlados a través de un archivo especial que, en vez de almacenar información, apunta a estructuras en el sistema que controlan a cada dispositivo. Este concepto sobrevive en los sistemas derivados de Unix al día de hoy, aunque varias clases de dispositivo rompen esta lógica. El sistema operativo Plan9 de Bell Labs mantiene y amplía este concepto e introduce los espacios de nombres mutables, que presenta con interfaz de archivo prácticamente cualquier objeto empleado por el sistema.

Las principales estructuras relacionadas de este tipo que existen en un sistema tipo Unix son:

Dispositivos de caracteres
Son aquellos en los cuales la información es leída o escrita de a un caracter a la vez y se presentan como streams (flujos) de información, ya sea entrante, saliente o mixta. Algunos pueden permitir operaciones adicionales (por ejemplo, rebobinado), pero la manipulación de la información se efectúa de forma secuencial.

Ejemplos: impresora, unidad de cinta, modem.

Dispositivos de bloques
Presentan una interfaz de acceso aleatorio y entregan o reciben la información en bloques de tamaño predeterminado.

El ejemplo más claro de este tipo de dispositivos es una unidad de disco o una de sus particiones.

Archivos especiales
Los sistemas Unix actuales incluyen también un gran número de archivos especiales, por medio de los cuales el usuario puede monitorear el estado del sistema (memoria libre, número de procesos, consumo de procesador, etc.), e incluso modificar la configuración del sistema operativo a través de un archivo; por ejemplo, en un sistema Linux, escribir el valor "100" al archivo "/proc/sys/vm/swappiness" hará que el sistema envíe a espacio de intercambio una mayor cantidad de programas de lo que normalmente haría.

9 Cuando dos cabezas piensan mejor que una

9.1 Multiprocesamiento

El multiprocesamiento es todo entorno donde hay más de un procesador (CPU). En un entorno multiprocesado, el conjunto de procesadores se vuelve un recurso más a gestionar por el sistema operativo — y el que haya concurrencia real tiene un fuerte impacto en su diseño.

Si bien en el día a día se usan de forma intercambiable9, es importante enfatizar en la diferencia fundamental entre el multiprocesamiento, que se abordará en esta sección, y la multiprogramación, de la cual se hablará en la sección INTROmultiprogramados (Sistemas multiprogramados). Un sistema multiprogramado da la ilusión de que está ejecutando varios procesos al mismo tiempo, pero en realidad está alternando entre los diversos procesos que compiten por su atención. Un sistema multiprocesador tiene la capacidad de estar atendiendo simultáneamente a diversos procesos.

./img/ditaa/multiproceso_y_multiprogramacion.png

Esquema de la ejecución de tres procesos en un sistema secuencial, multiprogramado, multiprocesado, e híbrido

En la figura HWmultiprocesoymultiprogramacion, el primer diagrama ilustra una ejecución estrictamente secuencial: cada uno de los procesos que demandan atención del sistema es ejecutado hasta que termina; el segundo muestra cómo se comportaría un sistema multiprogramado, alternando entre los tres procesos, de modo que el usuario vea que los tres avanzan de forma simultánea; el tercero corresponde a un sistema de multitarea pura: cada proceso es ejecutado por un procesador distinto, y avanzan en su ejecución de forma simultánea. El cuarto caso, un esquema híbrido, presenta cómo reaccionaría un equipo con capacidad de atender a dos procesos al mismo tiempo, pero con tres procesos solicitando ser atendidos. Este último esquema es el que más comunmente se encuentra en equipos de uso general hoy en día.

Probablemente el tema que se aborda más recurrentemente a lo largo de este texto será precisamente la complejidad que proviene de la multiprogramación; se la desarrollará particularmente en los capítulos PROC y PLAN. Valga la nota en este momento únicamente para aclarar la diferencia entre los dos conceptos.

El multiprocesamiento se emplea ampliamente desde los sesenta en los entornos de cómputo de alto rendimiento, pero por muchos años se vio como el área de especialización de muy pocos — las computadoras con más de un procesador eran prohibitivamente caras, y para muchos sistemas, ignorar el problema resultaba una opción válida. Muchos sistemas operativos ni siquiera detectaban la existencia de procesadores adicionales, y en presencia de éstos, ejecutaban en uno sólo.

./img/moore_orig.png

La Ley de Moore, en su artículo publicado en 1965, prediciendo la miniaturización por diez años

Esto cambió hacia el 2005. Tras más de 40 años de cumplirse, el modelo conocido como la Ley de Moore, enunciando que cada dos años la densidad de transistores por circuito integrado se duplicaría, llevaba a velocidades de CPU que, en el ámbito comercial, excedían los 3GHz, lo cual presentaba ya problemas serios de calentamiento. Además, el diferencial de velocidad con el acceso a memoria era cada vez más alto. Esto motivó a que las principales compañías productoras de los CPU cambiaran de estrategia, introduciendo chips que son, para propósitos prácticos, paquetes con 2 o más procesadores dentro.

./img/gnuplot/ley_de_moore.png

La Ley de Moore se sostiene al día de hoy: conteo de transistores por procesador de 1971 al 2012

Con este cambio, el reloj de los procesadores se ha mantenido casi sin cambios, cerca de 1GHz, pero el rendimiento de los equipos sigue aumentando. Sin embargo, los programadores de sistemas operativos y programas de aplicación ya no pueden ignorar esta complejidad adicional.

Se denomina multiprocesamiento simétrico (típicamente abreviado SMP) a la situación en la que todos los procesadores del sistema son iguales y pueden realizar en el mismo tiempo las mismas operaciones. Todos los procesadores del sistema tienen acceso a la misma memoria (aunque cada uno puede tener su propio caché, lo cual obliga a mantener en mente los puntos relacionados con la coherencia de caché abordados en la sección anterior).

Existe también el multiprocesamiento asimétrico; dependiendo de la implementación, la asimetría puede residir en diferentes puntos. Puede ir desde que los procesadores tengan una arquitectura distinta (típicamente dedicada a una tarea específica), en cuyo caso pueden verse como coprocesadores o procesadores coadyuvantes, casi computadoras independientes contribuyendo sus resultados a un mismo cómputo. Hoy en día, este sería el caso de las tarjetas gráficas 3D, que son computadoras completas con su propia memoria y responsabilidades muy distintas del sistema central.

Es posible tener diferentes procesadores con la misma arquitectura pero funcionando a diferente frecuencia. Esto conlleva una fuerte complejidad adicional, y no se utiliza hoy en día.

Por último, existen los diseños de Acceso No-Uniforme a Memoria (Non-Uniform Memory Access, NUMA). En este esquema, cada procesador tiene afinidad con bancos específicos de memoria — para evitar que los diferentes procesadores estén esperando al mismo tiempo al bus compartido de memoria, cada uno tiene acceso exclusivo a su área. Los sistemas NUMA pueden ubicarse como en un punto intermedio entre el procesamiento simétrico y el cómputo distribuído, y puede ser visto como un cómputo distribuído fuertemente acoplado.

9.2 Cómputo distribuído

Se denomina cómputo distribuído a un proceso de cómputo realizado entre computadoras independientes, o, más formalmente, entre procesadores que no comparten memoria (almacenamiento primario). Puede verse que un equipo de diseño NUMA está a medio camino entre una computadora multiprocesada y el cómputo distribuído.

Hay diferentes modelos para implementar el cómputo distribuído, siempre basados en la transmisión de datos sobre una red. Estos son principalmente:

Cúmulos (clusters)
Computadoras conectadas por una red local (de alta velocidad), ejecutando cada una su propia instancia de sistema operativo. Pueden estar orientadas al alto rendimiento, alta disponibilidad o al balanceo de cargas (o a una combinación de estas). Típicamente son equipos homogéneos, y dedicados a la tarea en cuestión.
Mallas (Grids)
Computadoras distribuídas geográficamente y conectadas a través de una red de comunicaciones. Las computadoras participantes pueden ser heterogéneas (en capacidades y hasta en arquitectura); la comunicación tiene que adecuarse a enlaces de mucha menor velocidad que en el caso de un cluster, e incluso presentar la elasticidad para permitir las conexiones y desconexiones de nodos en el transcurso del cómputo.
Cómputo en la nube
Un caso específico de cómputo distribuído con partición de recursos (al estilo del modelo cliente-servidor); este modelo de servicio está fuertemente orientado a la tercerización de servicios específicos. A diferencia del modelo cliente-servidor tradicional, en un entorno de cómputo en la nube lo más común es que tanto el cliente como el servidor sean procesos que van integrando la información, posiblemente por muchos pasos, y que sólo eventualmente llegarán a un usuario final. La implementación de cada uno de los servicios empleados deja de ser relevante, para volverse un servicio opaco. Algunos conceptos relacionados son:
Servicios Web
Mecanismo de descripción de funcionalidad, así como de solicitud y recepción de resultados, basado en el estándar HTTP y contenido XML.
Software como servicio
El proveedor ofrece una aplicación completa y cerrada sobre la red, exponiendo únicamente su interfaz (API) de consultas.
Plataforma como servicio
El proveedor ofrece la abstracción de un entorno específico de desarrollo de modo que un equipo de programadores pueda desplegar una aplicación desarrollada sobre dicha plataforma tecnológica. Puede ser visto como un conjunto de piezas de infraestructura sobre un servidor administrado centralmente.
Infraestructura como servicio
El proveedor ofrece computadoras completas (en hardware real o máquinas virtuales); la principal ventaja de esta modalidad es que los usuarios, si bien retienen la capacidad plena de administración sobre sus granjas, tienen mucho mayor flexibilidad para aumentar o reducir el consumo de recursos (y por tanto, el pago) según la demanda que alcancen.

El tema del cómputo en la nube va muy de la mano de la virtualización, que se abordará en el apéndice VIRT.

9.3 Amdahl y Gustafson: ¿qué esperar del paralelismo?

Al programar una aplicación de forma que aproveche al paralelismo (esto es, diseñarla para que realice en distintos procesadores o nodos sus porciones paralelizables) ¿cuál es el incremento al rendimiento que se puede esperar?

En 1967, Gene Amdahl presentó un artículo10 en que indica los límites máximos en que resultará la programación multiprocesada ante determinado programa: parte de la observación de que aproximadamente 40% del tiempo de ejecución de un programa se dedicaba a administración y mantenimiento de los datos, esto es, a tareas secuenciales.

Si únicamente el 60% del tiempo de procesamiento es susceptible, pues, de ser paralelizado, el rendimiento general del sistema no se incrementará en una proporción directa con el número de procesadores, sino que debe sumársele la porción estrictamente secuencial. Puesto en términos más formales: la ganancia en la velocidad de ejecución de un programa al ejecutarse en un entorno paralelo estará limitado por el tiempo requerido por su fracción secuencial. Esto significa que, si $T(1)$ representa al tiempo de ejecución del programa con un sólo procesador y $T(P)$ al tiempo de ejecución con $P$ procesadores, y si $t_s$ es el tiempo requerido para ejecutar la porción secuencial del programa, y $t_p(P)$ el tiempo que requiere la ejecución de la porción paralelizable, repartida entre $P$ procesadores, se puede hablar de una ganancia $g$ en términos de:

$$g = \frac{T(1)}{T(P)} = \frac{t_s + t_p(1)}{t_s + \frac{t_p(1)}{P}}$$

./img/dot/amdahl_1.png

Procesadores: 1, t=500

./img/dot/amdahl_2.png

Procesadores: 2, t=400, ganancia: 1.25x

./img/dot/amdahl_4.png

Procesadores: 4, t=350, ganancia: 1.4x

Ley de Amdahl: ejecución de un programa con 500 unidades de tiempo total de trabajo con uno, dos y cuatro procesadores.

Esta observación, conocida como la Ley de Amdahl, llevó a que por varias décadas el cómputo paralelo fuera relegado al cómputo de propósito específico, para necesidades muy focalizadas en soluciones altamente paralelizables, como el cómputo científico.

En términos del ejemplo presentado en la figura HWamdahl, se ve un programa que, ejecutado secuencialmente, resulta en $T=500$. Este programa está dividido en tres secciones secuenciales, de $t=100$ cada una, y dos secciones paralelizables, totalizando $t=100$ cada una, como se puede ver al representar una ejecución estrictamente secuencial (HWamdahl1).

Al agregar un procesador adicional (HWamdahl2), se obtiene una ganancia de 1.25x — la ejecución se completa en $T=400$ (con $g=1.25$). Las secciones paralelizables sólo toman un tiempo externo de 50 cada una, dado que la carga fue repartida entre dos unidades de ejecución. Al ejecutar con cuatro procesadores (HWamdahl4), si bien se sigue notando mejoría, esta apenas lleva a $T=350$, con $g=1.4$.

Si el código fuera infinitamente paralelizable, y se ejecutase este programa en una computadora con un número infinito de procesadores, este programa no podría ejecutarse en menos de $T=300$, lo cual presentaría una ganancia de apenas $g=1.66$. Esto es, al agregar procesadores adicionales, rápidamente se llegaría a un crecimiento asintótico — el comportamiento descrito por la Ley de Amdahl es frecuentemente visto como una demostración de que el desarrollo de sistemas masivamente paralelos presenta rendimientos decrecientes.

./img/gnuplot/amdahl.png

Ganancia máxima al paralelizar un programa, según la Ley de Amdahl

Si bien el ejemplo que se acaba de presentar resulta poco optimizado, con sólo un 40% de código paralelizable, se puede ver en la gráfica HWamdahlporcentajes que el panorama no cambia tan fuertemente con cargas típicas. Un programa relativamente bien optimizado, con 80% de ejecución paralela, presenta un crecimiento atractivo en la región de hasta 20 procesadores, y se estaciona apenas arriba de una ganancia de 4.5 a partir de los 40.11 Incluso el hipotético 95% llega a un tope en su crecimiento, imposibilitado de alcanzar una ganancia superior a 20.

Dado que el factor económico resulta muy importante para construir computadoras masivamente paralelas,12 y que se ve claramente que el poder adicional que da cada procesador es cada vez menor, la Ley de Amdahl resultó (como ya se mencionó) en varias décadas de mucho mayor atención a la miniaturización y aumento de reloj, y no al multiprocesamiento.

Fue hasta 1988 que John Gustafson publicó una observación a esta ley13 que, si bien no la invalida, permite verla bajo una luz completamente diferente y mucho más optimista. Gustafson publicó este artículo corto tras haber obtenido ganancias superiores a 1020 en una supercomputadora con 1024 procesadores — un incremento casi perfectamente lineal al número de procesadores. Sí, respondiendo a una carga altamente optimizada, pero no por eso menos digna de análisis.

El argumento de Gustafson es que al aumentar el número de procesadores, típicamente se verá una modificación al problema mismo. Citando de su artículo (traducción propia),

(…)Asumen implícitamente que el tiempo que se ejecuta en paralelo es independiente del número de procesadores, lo cual virtualmente nunca ocurre de este modo. Uno no toma un problema de tamaño fijo para ejecutarlo en varios procesadores como no sea para hacer un ejercicio académico; en la práctica, el tamaño del problema crece con el número de procesadores. Al obtener procesadores más poderosos, el problema generalmente se expande para aprovechar las facilidades disponibles. Los usuarios tienen control sobre cosas como la resolución de la malla, el número de pasos, la complejidad de los operadores y otros parámetros que usualmente se ajustan para permitir que el programa se ejecute en el tiempo deseado. Por tanto, podría ser más realista que el tiempo de ejecución, no el tamaño del problema, es constante.

Lo que escribe Gustafson se traduce a que es posible obtener la eficiencia deseada de cualquier cantidad de procesadores aumentando suficientemente el tamaño del problema. Al enfrentarse explícitamente con el bloqueo mental contra el paralelismo masivo que nació de esta lectura errónea de lo comentado por Amdahl, su artículo sencillo y de apenas más de una cuartilla de extensión cambió la percepción acerca de la utilidad misma del paralelismo masivo.

10 Otros recursos

Pies de página:

1 Algunos argumentarán que muchas de las computadoras en uso hoy en día siguen la arquitectura Harvard modificada, dado que empleando distintos bancos de memoria caché, un procesador puede tanto referirse a la siguiente instrucción como iniciar una transferencia de memoria primaria. Esta distinción no tiene mayor relevancia para este tema, la referencia se incluye únicamente por no llevar a confusión.

2 A veces asistido por instrucciones explíticas por parte del programador, pero muchas veces como resultado del análisis del código.

3 Algunas arquitecturas, particularmente de sistemas embebidos y por un criterio altamente económico, están estructuradas íntegramente alrededor de un bus USB.

4 Se verán en la sección FSFISestadosolido detalles acerca de las tecnologías de almacenamiento en estado sólido, que pueden poner fin a esta larga dominación.

5 Los chips que forman parte de un equipo, casi siempre provistos por el mismo fabricante que el procesador mismo.

6 La separación aquí descrita ha sido característica de las computadoras x86 de los últimos 20 años, aunque la tendencia apunta a que se abandone paulatinamente para dar paso a procesadores que integren en un sólo paquete todos estos componentes. Sin embargo, el acomodo funcional electrónico, al menos hasta el momento, sigue basado en estos puntos.

7 Este modo de operación es también conocido como entrada/salida programada.

8 Se denomina portar el hacer las adecuaciones necesarias para que una herramienta diseñada para determinado entorno pueda emplearse en otros distintos.

9 O poco más que eso, al grado de que rara vez se emplea el término multiprogramación, mucho más acorde a los equipos que se emplean día a día.

10 Validity of the Single Processor Approach to Achieving Large Scale Computing Capabilities, Amdahl, 1967

11 De un máximo de 5 al que puede alcanzar con un número infinito de procesadores

12 Dado que los componentes más caros son necesariamente los procesadores

13 Reevaluating Amdahl's Law, John L. Gustafson, Communications of the ACM, vol. 31, mayo de 1988