Ir al contenido

Manual de programación de OS/2

De Wikilibros, la colección de libros de texto de contenido libre.

El autor, Sergio Costas, ha permitido que este documento se distribuya bajo la licencia GFDL para poder ser publicado en Wikilibros.

Tabla de Contenidos

[editar]
  1. Condiciones de distribución
  2. Créditos
  3. Introducción
  4. Historia de los sistemas operativos
  5. Historia de OS/2
  6. Llamada al API de OS/2
    1. Llamada al API de OS/2/Estructura de OS/2 (Sistema de ficheros y Subsistemas)
  7. Implementación del sistema de ficheros de OS/2
  8. El sistema de archivos de OS/2
    1. El sistema de archivos de OS/2/Apertura y cierre de ficheros
    2. El sistema de archivos de OS/2/Lectura y escritura de datos
    3. El sistema de archivos de OS/2/Compartición de ficheros
    4. El sistema de archivos de OS/2/Gestión de dispositivos
    5. El sistema de archivos de OS/2/Funciones del DOS
    6. El sistema de archivos de OS/2/Busqueda de ficheros
    7. El sistema de archivos de OS/2/Redirección de entradas y salidas
    8. El sistema de archivos de OS/2/Variables de entorno
    9. El sistema de archivos de OS/2/Otras funciones DOS
  9. Los subsistemas en OS/2
    1. Los subsistemas en OS/2/El subsistema de vídeo
      1. Los subsistemas en OS/2/El subsistema de vídeo/Funciones VIO
      2. Los subsistemas en OS/2/El subsistema de vídeo/Funciones VIO/Salida por TTY virtual
      3. Los subsistemas en OS/2/El subsistema de vídeo/Funciones VIO/Salida de cadenas de caracteres
      4. Los subsistemas en OS/2/El subsistema de vídeo/Funciones VIO/Funciones de Scroll
      5. Los subsistemas en OS/2/El subsistema de vídeo/Funciones VIO/Definición y movimiento del cursor
      6. Los subsistemas en OS/2/El subsistema de vídeo/Funciones VIO/Acceso directo al buffer real de vídeo
    2. Los subsistemas en OS/2/El subsistema de teclado
      1. Los subsistemas en OS/2/El subsistema de teclado/Funciones KBD
      2. Los subsistemas en OS/2/El subsistema de teclado/Modos de teclado
        1. Los subsistemas en OS/2/El subsistema de teclado/Modos de teclado/ASCII
        2. Los subsistemas en OS/2/El subsistema de teclado/Modos de teclado/BINARIO
        3. Los subsistemas en OS/2/El subsistema de teclado/Modos de teclado/ECO
    3. Los subsistemas en OS/2/El subsistema de ratón
    4. Los subsistemas en OS/2/Acceso a las funciones de 16 bits
      1. Los subsistemas en OS/2/Acceso a las funciones de 16 bits/El THUNKING
        1. Los subsistemas en OS/2/Acceso a las funciones de 16 bits/Gestión de memoria en el modo protegido del 286
        2. Los subsistemas en OS/2/Acceso a las funciones de 16 bits/Gestión de memoria en el modo protegido del 386
  10. Multitarea
    1. Multitarea/Concepto de thread
    2. Multitarea/Concepto de proceso
      1. Multitarea/Concepto de proceso/Prioridades de ejecución
    3. Multitarea/Concepto de sesión
    4. Multitarea/Estructura del OS/2
      1. Multitarea/Estructura del OS/2/(Selector de Programas, Sesiones, Procesos y Threads)
  11. Gestión de la memoria
    1. Gestión de la memoria/Subasignación de memoria
  12. Sincronización y comunicación entre procesos
    1. Sincronización y comunicación entre procesos/Concepto de semáforo
    2. Sincronización y comunicación entre procesos/Temporizadores en OS/2
    3. Sincronización y comunicación entre procesos/Comunicación entre procesos
      1. /Sincronización y comunicación entre procesos/Comunicación entre procesos/Memoria compartida y semáforos
      2. /Sincronización y comunicación entre procesos/Comunicación entre procesos/Cauces
      3. /Sincronización y comunicación entre procesos/Comunicación entre procesos/Colas
  13. Apéndice A
  14. Apéndice B

Condiciones de distribución

[editar]

El presente manual se distribuye bajo una licencia dual GFDL 1.2 / Creative Commons Reconocimiento-CompartirIgual 2.5.

Bajo estas licencias, usted es libre de:

  • copiar, distribuir y comunicar públicamente la obra
  • hacer obras derivadas
  • hacer un uso comercial de esta obra

Bajo las condiciones siguientes:

Reconocimiento.

  • Debe reconocer los créditos de la obra de la manera especificada por el autor o el licenciador.
  • Compartir bajo la misma licencia. Si altera o transforma esta obra, o genera una obra derivada, sólo puede distribuir la obra generada bajo una licencia idéntica a ésta.

Al reutilizar o distribuir la obra, tiene que dejar bien claro los términos de la licencia de esta obra, y puede redistribuirla bajo cualquiera de las dos licencias.

Alguna de estas condiciones puede no aplicarse si se obtiene el permiso del titular de los derechos de autor

Los derechos derivados de usos legítimos u otras limitaciones reconocidas por ley no se ven afectados por lo anterior.

Créditos

[editar]

Este manual ha sido realizado por Sergio Costas Rodríguez

La versión original de documento se encuentra en rastersoft.com.

Introducción

[editar]

En este documento voy a intentar explicar el funcionamiento y el API de OS/2. Con esto intento hacer un pequeño manual de programación de este Sistema Operativo, algo que se echa en falta al hojear las listas de libros de casi cualquier librería, por muy especializada que esté en Informática. Pretende ser también el complemento del compilador EMX/GCC, un excelente compilador de C totalmente freeware, disponible para OS/2.

Debo advertir que el API no se encuentra descrito en su totalidad. Por ejemplo, faltan algunas llamadas puntuales muy poco usadas, como por ejemplo el acceso al hardware mediante IOCTL. Tampoco viene descrito cómo trabajar con el Presentation Manager, sino que se 'limita' a la multitarea, gestión de memoria, comunicación interproceso y acceso a pantalla y teclado en sesiones de modo texto.

¿Por qué el formato HTML?

Las razones de haber usado el formato HTML en la confección de este documento en vez de usar el formato INF de OS/2 han sido tres:

  • facilidad de edición de las fuentes
  • portabilidad del texto y posibilidad de lectura en el browser preferido por el usuario (el propio OS/2 incluye uno de serie, en el Bonus Pack)
  • no tengo ningún editor de INFs

Historia de los sistemas operativos

[editar]

Cuando aparecieron los primeros ordenadores, la programación de estos era hecha íntegramente en código máquina, lo cual resultaba una tarea extremadamente pesada: cada vez que se escribía un nuevo programa, además de escribir el algoritmo adecuado era preciso añadir todo el código necesario para que el ordenador pudiese leer datos desde una cinta perforada, imprimir en un teletipo, etc. Dado que, en general, todas estas rutinas eran exactamente iguales para todos los programas que se hacían, pronto los programadores de aquellas máquinas aprendieron a organizarlas en bibliotecas de rutinas. Cada vez que había que escribir un nuevo programa, solo tenían que ir a su libreta y copiar las rutinas de Entrada/Salida que necesitaban, lo cual les simplificaba un poco el trabajo. Otro sistema era el que la propia empresa que fabricaba el ordenador incluyese un paquete de fichas perforadas con dichas rutinas, de modo que el programador solo tenía que coger las que le interesasen y añadirlas estratégicamente en su paquete.

El siguiente paso fue generalizar este conjunto de rutinas. La idea era incluir juntas todas las rutinas necesarias para acceder al hardware, y hacerlas accesibles a cualquier programador en forma de llamadas a subrutina. De este modo, cada vez que se hacía un programa no era preciso incluir en él todas esas rutinas. Había nacido el Sistema Operativo.

Los primeros 'Sistemas Operativos' (si es que podían recibir ese nombre) no eran más que un conjunto de subrutinas que ayudaban al programador, ofreciéndole servicios básicos como lectura de caracteres desde un teletipo, escritura en tambor, disco, etc.

Sin embargo, pronto se vio la necesidad de un nuevo enfoque. Los ordenadores eran caros, y su mantenimiento también, por lo que solo algunas grandes empresas y universidades podían disponer de ellos. Para amortizarlos, se alquilaba tiempo de proceso, de modo que se podía ir allí con un programa, ejecutarlo, y pagar por el tiempo que le llevase hacerlo al ordenador.

El primer problema que había era que se tardaba mucho en cargar cada programa. Cuando el ordenador terminaba de ejecutar uno, el operador tenía que insertar el siguiente, lo cual era una tarea bastante pesada y larga. Durante el tiempo que se hacía esto, el ordenador estaba totalmente inactivo, se estaba desperdiciando tiempo. Fue entonces cuando surgieron los sistemas de proceso por lotes (batch).

En los sistemas de proceso por lotes, los programas se almacenan en una cinta, todos seguidos. Cada vez que el ordenador terminaba de ejecutar un programa, leía el siguiente de dicha cinta. Si llegaba alguien con un programa, este se añadía a continuación del último que hubiese, mientras el ordenador iba ejecutando el que ya tenía en memoria. El coste de mantenimiento del nuevo sistema era ligeramente superior, pues había que añadir al sistema el lector de cintas en donde se escribían los programas; pero como podía ejecutar muchos más programas en el mismo tiempo (porque no se perdía parte de este en cargar los programas), el precio de alquiler bajó drásticamente, permitiendo que más gente tuviese acceso a los ordenadores.

Es en los sistemas de proceso por lotes donde empieza a aparecer realmente lo que hoy conocemos como Sistema Operativo: un programa principal que es el que va cargando los distintos programas desde la cinta, les cede el control de la CPU pero ofreciéndoles una serie de servicios de Entrada/Salida, y cuando terminan, recupera el control del ordenador.

Pronto los programadores se dieron cuenta de que había aún más tiempos muertos de los que parecía: si un ordenador tenía que leer algo de una cinta o una tarjeta con datos, como el sistema era mecánico tardaba mucho tiempo en encontrarlo (mucho tiempo de cara al ordenador, claro. Una décima de segundo es algo muy relativo). Así mismo, se empezaban a usar terminales para dar entrada de datos en tiempo real al ordenador, y el tiempo que éste esperaba a que el usuario pulsase cada tecla también era una eternidad. Entonces se empezó a pensar en los Sistemas Operativos multitarea.

La idea consistía en tener varios programas simultáneamente en memoria, si bien, como el ordenador es único, solo uno estaría activo en cada momento. Cuando dicho programa hace una operación de Entrada/Salida en la que se pierde tiempo de CPU (como leer una tarjeta, por ejemplo), mientras la mecánica realiza la operación de búsqueda y lectura, el ordenador conmuta al siguiente programa que tiene en memoria, el cual continuará ejecutándose hasta que haga una operación de Entrada/Salida. Cuando se llega al último, se vuelve a empezar. Para ese momento, la operación de Entrada/Salida pedida por el primer programa ya estará lista, con lo que podrá seguir su ejecución. De esta forma, hemos aprovechado un tiempo que antes perdíamos.

Finalmente, llegamos a los sistemas distribuidos. Generalmente, lo que tenemos es una red de ordenadores (por ejemplo, en una universidad es normal tener una gran cantidad de PC's) conectados entre sí. Si nos fijamos, normalmente solo unos cuantos están en uso en cada momento, y el resto simplemente se dedican a ejecutar un bonito salvapantallas, o peor aún, a realizar la estética función de caro pisapapeles. En otras palabras, estamos desperdiciando una vez más tiempo de proceso. Para solucionarlo, se han ideado los Sistemas Operativos distribuidos. Estos sistemas operativos son el siguiente paso en la evolución de los Sistemas Operativos multitarea.

La idea consiste en que en los Sistemas Operativos multitarea los programas suelen estar divididos en varios Threads o hilos de ejecución. Cada uno de estos Threads se puede ver como un programa absolutamente independiente de los demás, si bien, trabajando todos en conjunto forman el programa completo. Dado que cada Thread es independiente de los demás, nada impediría, en principio, que se ejecutase cada uno en un procesador diferente. Esa es la base de los Sistemas Operativos distribuidos: cada nuevo Thread es ejecutado en una máquina distinta, de modo que la potencia de cálculo se halla distribuida por toda la red. De esta forma, los equipos que estaban inactivos son aprovechados al máximo. Por desgracia, esta idea resulta extremadamente compleja de implementar, por lo que esta clase de Sistemas Operativos se encuentran todavía poco extendidos.

Historia de OS/2

[editar]

OS/2 son las siglas de "Sistema operativo de segunda generación". La idea de OS/2 surgió entre IBM y Microsoft a mediados de los 80, en un intento de hacer un sucesor de MS-DOS, el cual ya empezaba a acusar el paso del tiempo y resultaba claramente desaprovechador de los recursos de las máquinas de la época (basadas en el Intel 286).

OS/2 1.0 salió en abril de 1987 y era un sistema operativo de 16 bits, pues estaba pensado para trabajar sobre el microprocesador 286. Sin embargo, aprovechaba plenamente el modo protegido de este ordenador, haciendo uso de sus capacidades para protección de memoria, gestión de multitarea, etc. El resultado fue un S.O. estable, rápido y muy potente.

OS/2 ya tenía incorporada desde esa primera versión la multitarea real. Se podían ejecutar varias sesiones simultáneamente, en cada una de ellas se podían tener múltiples programas, y cada uno de ellos podía tener múltiples threads en ejecución. Se trataba de una multitarea jerárquica, con cuatro niveles de prioridad: Crítico (útil para programas que requieran atención casi constante por parte de la CPU, como un módem), Primer plano (correspondiente al programa que tiene acceso a la pantalla, teclado y ratón), Medio (programas lanzados por el usuario que se ejecutan en BackGround) y Desocupado (tareas de poca importancia o lentas, como el Spooler de impresión). Dentro de cada nivel (a excepción del de Primer plano), existen 32 niveles de prioridad, los cuales son asignados dinámicamente a cada programa por el S.O. en función del porcentaje de uso de la CPU, de los puertos de E/S, etc.

OS/2, además, permitía memoria virtual, con lo que se podían ejecutar programas más largos que lo que la memoria física instalada permitiría en principio (los requerimientos de aquella versión eran un 286 con 2 megas de memoria). Por otro lado, incluía la característica de compartición de código: al cargar dos veces un mismo programa, el código de este no se duplicaba en memoria, sino que el mismo código era ejecutado por dos Threads diferentes. Esto permitía ahorrar mucha memoria.

Esta versión de OS/2 era íntegramente en modo texto. Si bien el Sistema Operativo daba la posibilidad de usar los modos gráficos de la tarjeta del ordenador, no incluía ningún API que ayudase en ello, recayendo todo el trabajo de diseño de rutinas de puntos, líneas, etc en el programador de la aplicación. Esto no era realmente tan problemático, pues era lo que se hacía (y se hace) en el mundo del MS-DOS. Sin embargo, se echaba en falta un entorno gráfico como Windows.

En la versión 1.1, aparecida en octubre de 1988, llegó por fin el Presentation Manager, un gestor de modo gráfico, junto con la primera versión de Work Place Shell. Ambos formaban un entorno gráfico muy parecido al aún no comercializado Windows 3.0. También hizo su aparición el formato de ficheros HPFS (High Performance File System). Este sistema de ficheros complementaba al clásico FAT, que era el usado por MS-DOS y por OS/2 1.0; sin embargo, ofrecía una gran cantidad de ventajas, tales como

  • Menor fragmentación de ficheros: HPFS busca primero una zona en donde el archivo entre completo, con lo que la fragmentación de ficheros es prácticamente inexistente. De hecho, IBM recomienda desfragmentar los discos duros una vez al año, y solo a los paranoicos.
  • Mayor capacidad: HPFS admite discos duros de hasta 512 gigabytes de capacidad, manteniendo el tamaño del cluster (unidad mínima de información almacenable) en 512 bytes o un sector. En FAT, el tamaño mínimo de cluster para un disco duro es 2048 bytes, y para discos mayores aumenta (un disco duro de 1 giga tiene un tamaño de cluster de 32Ks).
  • Soporte para nombres largos: permite nombres de hasta 256 caracteres.
  • Mayor seguridad: si al grabar en un sector se detecta un error, se marca automáticamente como defectuoso y se graba en otra parte.
  • Mayor velocidad en el acceso, gracias a la estructura jerárquica de directorios, que optimiza el acceso a disco.

El gran problema de OS/2 es que seguía siendo un S.O. de 16 bits, con lo que no aprovechaba plenamente las capacidades de los 386 de la época, que empezaron a extenderse con más velocidad de la esperada. Según una revista del sector, Microsoft sugirió hacer una versión de 32 bits (que obligaría a ejecutarla en ordenadores 386 o superiores), pero IBM insistió en perfeccionar la de 16 bits. Sobre quien dijo cada cosa realmente solo se puede especular. Lo único que se sabe a ciencia cierta es que la versión de OS/2 de 32 bits presentada por MicroSoft en 1990 era casi igual que la versión 1.3, con la única diferencia de que el kernel era de 32 bits. IBM, por su parte, quería un escritorio orientado a objetos, y no el clásico shell de OS/2 1.x (el cual MicroSoft copiaría para su Windows 3.0). Puestas así las cosas, finalmente se rompió el acuerdo entre ambos.

OS/2 2.0, la primera versión de OS/2 de 32 bits, iba a salir inicialmente a finales de 1990; pero al no contar con la ayuda de Microsoft, IBM no fue capaz de sacarlo hasta 1992, dándole a Windows 3.0 el tiempo suficiente para asentarse en el mercado.

OS/2 2.0 tenía todas las ventajas de los anteriores OS/2, unido al nuevo núcleo de 32 bits. No se trataba, por tanto, de un retoque de la versión de 16 bits, sino un sistema operativo prácticamente nuevo que aprovechaba al máximo las capacidades del modo protegido del microprocesador 386. Sin embargo, iba más allá que Windows, pues al contrario que éste, ofrecía compatibilidad garantizada con todas las aplicaciones de 16 bits anteriores, gracias a la inclusión del API original de 16 bits junto con el nuevo de 32, y además sin perdida de prestaciones. Así mismo, ofrecía también compatibilidad con Windows 2.x y 3.0, junto con una compatibilidad con MS-DOS muy mejorada, gracias al modo V86 que incorporan los micros 386 y del que carecía el 286: en OS/2 1.x la compatibilidad DOS era muy limitada, quedando reducida a una sola tarea y realizando un cambio entre modo real y modo protegido del microprocesador, además de consumir de manera permanente 640 Ks de memoria. Aparte, la emulación no era todo lo buena que cabía esperar. Todos estos problemas desaparecieron en la versión 2.0, pudiendo tener varias sesiones DOS totalmente independientes entre sí, con una compatibilidad cercana al 100% y beneficiándose de las capacidades de Crash Protection del OS/2, que impiden que un programa pueda colapsar el sistema entero.

Por otro lado, el Work Place Shell (el shell de trabajo gráfico, de ahora en adelante WPS) fue muy mejorado, resultando un shell totalmente orientado a objetos, con acceso directo a los ficheros, carpetas dentro de carpetas, ficheros sombra (conocidos como alias en los sistemas UNIX) y un escritorio de verdad. A su lado, el shell de Windows 3.0 quedaba a la altura del betún.

IBM consiguió vender OS/2 2.0 en grandes cantidades; sin embargo, no consiguió su autentico despegue, en parte por culpa de la falta de apoyo por parte de las empresas de software. El API del Presentation Manager, aunque similar al de Windows, tenía muchas diferencias, con lo que las empresas tuvieron que elegir entre uno u otro, ante la imposibilidad de muchas de ellas de dividir su talento entre ambos sistemas.

A principios de 1994 aparece OS/2 Warp, nombre comercial de la versión 3.0 de OS/2. En ella surgen nuevos elementos: un kit completo de multimedia (mejora del que traía la versión 2.1) y el Bonus Pak, un kit de aplicaciones que permite ponerse a trabajar con el ordenador nada más instalar el Sistema Operativo, pues contiene elementos como un Kit de conexión a Internet completo, el paquete integrado IBM Works (formado por un procesador de textos, hoja de cálculo, base de datos y graficos de empresa, junto con el PIM, que añade más funcionalidades aprovechando las capacidades drag&drop del WPShell), soft de terminal, soft de captura y tratamiento de vídeo, etc. Así mismo, la cantidad de hardware soportado fue ampliado de manera considerable, soportando casi cualquier dispositivo existente en el mercado: CD-Roms, impresoras, tarjetas de sonido, soporte PCMCIA, tarjetas de vídeo, tarjetas de captura de vídeo, tarjetas SCSI, etc. Los requisitos mínimos de esta versión seguían siendo un 386sx a 16MHz con 4 megas de RAM, los mismos que Windows 3.11, y podía ejecutar programas DOS, OS/2 16bits, OS/2 32 bits, Windows 2.x y Windows 3.x (incluía además el API Win32s, con lo que se podían ejecutar incluso programas Windows de 32bits).

IBM se metió en una campaña publicitaria a nivel mundial para promocionar esta nueva versión, la cual, sin embargo, no dio los resultados esperados por ser demasiado light (todos recordaremos aquel anuncio de las monjitas en el convento, cuando una le dice a otra que ya tiene el nuevo OS/2 Warp, contando las nuevas características de este sistema operativo). A pesar de eso, OS/2 es ampliamente utilizado en múltiples empresas, bancos sobre todo, en donde su estabilidad es la mayor garantía (los propios cajeros automáticos funcionaban inicialmente con OS/2 1.0, si bien actualmente usan OS/2 Warp).

Poco después sale al mercado una revisión de Warp, denominada Warp Connect, la cual añade un kit completo de conexión a redes, soportando prácticamente cualquier estándar de red, incluyendo Novell Netware, TCP/IP, etc. junto con soporte para SLIP y PPP.

En Noviembre de 1996 se hizo la presentación de Merlín, nombre clave de OS/2 4.0, y que, en contra de lo que mucha gente piensa, no tiene nada que ver con el mítico mago de la corte del rey Arturo, sino con un pájaro parecido a un águila (siguiendo la nueva filosofía de IBM de nombrar sus creaciones con nombres de aves). Merlín trae todo lo que ofrecía OS/2 3.0, pero lo amplia con un conjunto extra de características, como son:

  • Un soporte todavía mayor de hardware.
  • Mayor simplicidad de instalación.
  • Librerías OpenDoc (compatibles con OLE 2.0, pero más potentes).
  • Librerías OpenGL, que permiten aprovechar las capacidades 3D de las tarjetas que soporten este estándar.
  • API de desarrollo Open32, que permiten recompilar con suma facilidad las aplicaciones escritas para Windows'95 y Windows NT, de forma que aprovechen al máximo los recursos de OS/2.
  • Un Bonus Pack ampliado, incluyendo una nueva versión del IBMWorks basada en OpenDoc, y las utilidades LotusNotes.
  • Un Kernel aún más optimizado.
  • Escritorio mejorado, ofreciendo una orientación a objeto aún mayor.
  • Un extenso soporte de conectividad, superior a la versión Connect de Warp 3.0, lo que le convierte en el cliente de red universal, pudiendo conectarse a casi cualquier servidor (no solo Warp Server, sino Windows NT server, Novell, etc).
  • HPFS mejorado: mayor capacidad por disco y seguridad.
  • Sesiones DOS reales (el micro se conmuta realmente a modo real, y todo el contenido de la RAM se guarda en disco, quedando el Sistema Operativo y el resto de las utilidades congelados, pudiendo rearrancar en cualquier momento. Es útil para juegos o programas de DOS muy exigentes, que se niegan a funcionar en una sesión DOS virtual).
  • La característica estrella de cara al marketing: el VoiceType. Se trata de un software reconocedor de voz, capaz de funcionar con cualquier tarjeta de sonido, y que permite al usuario trabajar exclusivamente mediante el dictado de comandos. Este sistema, al contrario que otros disponibles hasta el momento, realmente reconoce el habla de forma continua, de modo que no solo se puede usar para navegar por el escritorio y controlar programas, sino que sirve perfectamente para dictar cualquier tipo de texto, como artículos, cartas, etc. sin tocar una sola tecla. Se trata, por tanto, de un avance de los que serán, sin duda, los sistemas operativos del futuro.

Llamada al API de OS/2

[editar]

OS/2 es un S.O. pensado para poder ser programado tanto desde lenguajes de alto nivel (C, Pascal, Basic) como de bajo nivel (Assembler). Para facilitar la llamada a funciones desde los primeros, todos los parámetros se pasan por medio de la pila de máquina, y solo devuelve un código de error en el registro AX. Para facilitar las llamadas en el segundo, éstas no se implementan como saltos a supervisor, sino como FARCALLs, de modo que para un programador de código máquina no habrá diferencia entre ejecutar una llamada a una función del sistema o llamar a una función propia.

Hay tres tipos de datos que se pueden pasar a una función de OS/2: un byte (8 bits), una doble palabra (32 bits) o un puntero (32 bits), y retorna un valor de 16 bits en el acumulador (AX). Sin embargo, para facilitar la lectura de los programas, en la librería se han definido diversos tipos que ayudan en la programación, como por ejemplo, todas las estructuras de datos necesarias en muchos de los programas. Estos tipos están definidos EN MAYÚSCULAS, y serán usados en la definición de cada llamada en este curso. En el caso de llamadas al API de 32 bits (llamadas DOS y WIN), no hay peligro en cambiar el tipo predefinido por un INT, CHAR... pero en las llamadas al API de 16 bits puede ser problemático, pues se puede devolver un entero de 16 bits, de 32, etc. En estos casos (que, por fortuna, son muy pocos) conviene utilizar los tipos definidos en la librería, escribiéndolos, por tanto, en MAYÚSCULAS.

Todas las definiciones de las funciones y estructuras están definidas en el fichero OS2.H. Sin embargo, no es suficiente con añadir un #include <os2.h> al principio del programa, sino que debemos definir primero qué partes queremos que se incluyan, mediante una serie de sentencias #define. Existen varias importantes, pero en general, suele bastar con añadir un #define "INCL_BASE", la cual se encarga de definir automáticamente INCL_DOS (llamadas DOSxxx) INCL_DOSERRORS (códigos de error) e INCL_SUB (llamadas a subsistemas), la cual simplemente define INCL_KBD, INCL_VIO e INCL_MOU. Con esto es suficiente para realizar casi cualquier programa de OS/2.

Dado que solo hay un código que OS/2 devuelve, y éste está reservado para el código de error, cuando OS/2 tiene que entregar algún dato, necesita que le demos como parámetro un puntero a una zona de memoria donde pueda dejarlo. Trabajando con C, basta con que le demos un puntero a una variable del tipo adecuado. Así, si OS/2 tiene que devolvernos, por ejemplo, el número de bytes que se han leído de un fichero, debemos darle entre los parámetros de la función un puntero a una variable donde podrá almacenar ese resultado.

En la descripción de las funciones, cuando una variable es de tipo puntero lleva antepuesta una p, además de estar indicado en la descripción de los parámetros. Por supuesto, es posible definir la variable como tal en vez de como un puntero, y usar el símbolo & al pasar el parámetro a la función.

Estructura de OS/2 (Sistema de ficheros y Subsistemas)

[editar]

Vamos a empezar por ver como acceder al disco, pantalla y teclado en OS/2. Si bien es cierto que todos los lenguajes de programación ofrecen instrucciones estándar (y sobre todo, portables) para acceder a estos dispositivos, el uso de las funciones específicas que ofrece OS/2 nos permite conseguir mejores rendimientos. Un ejemplo es la lectura de datos desde un fichero. Si usamos las funciones estándar de C, por ejemplo, leeremos datos de un en un byte; dado que, al compilar, estamos haciendo una llamada al sistema en sí, en principio sería lo mismo usar estas funciones que las de OS/2; sin embargo, OS/2 permite leer múltiples bytes de una sola vez. La ventaja es que para cada lectura hay que llamar al núcleo de OS/2, lo cual consume mucho tiempo. Si tenemos que hacer 100 llamadas estándar para leer 100 bytes, será un proceso bastante lento. Pero si usamos una llamada de OS/2 para leer los 100 bytes de una sola vez, al haber una sola transición programa-núcleo-programa, la lectura será notablemente más rápida. Es imposible acelerar esa transición, pues no es problema de OS/2, sino de la propia arquitectura Intel.

Implementación del sistema de ficheros de OS/2

[editar]
Archivo:ESQOS2.png
En este gráfico vemos como está construido el acceso a los dispositivos en OS/2.

En el esquema, vemos que la aplicación se comunica con el Sistema de Ficheros de OS/2, que forma parte del núcleo, y éste determina a quien tiene que enviar o de quien debe leer los datos. Si el programa quiere acceder a la pantalla, el Sistema de Ficheros envía los caracteres al Subsistema de Vídeo (VIO), que es la parte encargada de controlar la totalidad de accesos a la pantalla. Lo mismo si se trata del teclado, el ratón o un fichero de disco.

La razón de usar el Sistema de Ficheros para acceder tanto a los archivos de disco como a los dispositivos, es que permite que el programa no se tenga que preocupar de la estructura de estos: cualquier entrada o salida será tratada como un conjunto de caracteres en serie. Esta independencia de estructura es cómoda para algunas aplicaciones, pero en otras puede ser mejor poder acceder directamente a cada dispositivo, normalmente por razones de velocidad. Esa es la razón de que OS/2 permita a las aplicaciones acceder directamente a los Subsistemas de Vídeo (VIO), teclado (KBD) y ratón (MOU). A través de estos, podemos por ejemplo hacer funciones de Scroll en la pantalla, conocer la posición del ratón, etc.

Podemos comparar los Subsistemas de OS/2 con la BIOS del ordenador, si bien estos son más completos que aquella en muchas funciones, y sobre todo mucho más rápidos.

El sistema de archivos de OS/2

[editar]

La parte básica de la conexión entre el hardware del ordenador y el programador se encuentra en el Sistema de Archivos. Es él quien se encarga de darnos acceso a los ficheros del disco, al teclado y a la pantalla en modo texto. La ventaja que tiene esto es que nos abstrae totalmente de las características del hardware, presentándonos todo como una ristra de caracteres.

El acceso a un fichero sigue una serie de pasos absolutamente necesarios: primero es preciso abrir el archivo, de modo que el programa obtenga control sobre él; una vez hecho esto, podemos realizar cualquier operación de lectura y/o escritura sobre él, y finalizar cerrando el archivo, de modo que lo liberamos y puede ser accedido por otro programa.

Apertura y cierre de ficheros

[editar]

La primera operación necesaria antes de poder acceder a un archivo es identificarlo mediante una llamada al Sistema Operativo. A esta operación se le denomina apertura del archivo. Le enviamos como parámetros el nombre del archivo a abrir y una serie de opciones, y nos devolverá un identificador de archivo (en adelante se denominará Handle) que usaremos para referirnos a él cuando queramos leer o escribir. El identificador es simplemente un número entero. Existe una única excepción en este caso: los archivos estándar no es necesario que sean abiertos, pues ya tienen asignado un identificador (STDIN=0, STDOUT=1, STDERR=2).

Las opciones de apertura especifican la forma en que el programa va a acceder a dicho fichero. Por ejemplo, puede querer acceder solo en modo lectura, o bien puede especificar que si dicho fichero no existe, devuelva un error, o bien que lo cree. O bien, si existe, puede pedir que lo sobreescriba. Etc.

El cierre del fichero se hace al final del acceso, cuando ya no vamos a leer o escribir nada más en él. Esto permite su liberación, de modo que cualquier otro programa puede acceder a él con total libertad.

DosOpen
DosClose

Lectura y escritura de datos

[editar]

Una vez que hemos abierto un fichero, podemos acceder a él. Los ficheros aparecen como un flujo de caracteres en serie, y el carácter actual está referenciado por un puntero que se incrementa automáticamente después de cada operación. Esto significa que cuando hacemos una lectura de un fichero, recibiremos el carácter siguiente al último leído. Y cada vez que escribamos un carácter, lo haremos a continuación del último escrito. Existe sin embargo la posibilidad de cambiar la posición de dicho puntero, de modo que podemos leer y escribir de forma aleatoria. Esta opción, sin embargo, solo funciona con ficheros de disco, y no con dispositivos como el teclado, pantalla, etc.

DosRead
DosWrite

Compartición de ficheros

[editar]

Dado que OS/2 es un Sistema Operativo multitarea, puede ocurrir que dos (o más) programas intenten acceder a la vez a un mismo fichero de disco. Para evitar interferencias, OS/2 ofrece una serie de opciones de compartición de archivos, que se especifican en el momento de abrirlos. Estas son:

Modos de acceso al fichero

  • READ_ONLY: solo se va a leer del fichero.
  • WRITE_ONLY: solo se va a escribir en el fichero.
  • READ_WRITE: se va a leer y escribir (es el valor por omisión).

Modos de compartición

  • DENY_ALL: OS/2 rechaza cualquier petición posterior de abrir ese archivo.
  • DENY_READ: OS/2 rechaza cualquier petición de abrir ese archivo para lectura.
  • DENY_WRITE: OS/2 rechaza cualquier petición de abrir ese archivo para escritura.
  • DENY_NONE: OS/2 permite cualquier operación con ese archivo.

Los modos de compartición permiten elegir la forma en que otros programas acceden al fichero que tenemos abierto. Por ejemplo, si tenemos un programa que lee del fichero prueba.txt y lo envía a la impresora, lo lógico es que lo abra como READ_ONLY, pues no va a escribir nada en él. Por otro lado, no puede permitir que otro programa modifique el fichero, pero no le importa que pueda leerlo, por lo que como modo de acceso asigna READ_ONLY. De esta forma, si otro programa intenta acceder al mismo fichero, OS/2 solo le dará acceso si intenta acceder en modo READ_ONLY; si intenta abrirlo como WRITE_ONLY o READ_WRITE, OS/2 le devolverá un error.

Gestión de dispositivos

[editar]

A través del sistema de ficheros podemos acceder también a los dispositivos físicos que forman el ordenador. Existen normalmente una serie de archivos que identifican a cada periférico y que nos permiten enviar y recibir datos hacia y desde ellos.

Los nombres de los archivos de que disponemos son:

  • COM1-COM4: maneja los puertos serie del ordenador.
  • CLOCK$: el reloj del sistema.
  • CON: gestiona la consola (en lectura es el teclado, en escritura, la pantalla).
  • SCREEN$: maneja la pantalla.
  • KBD$: maneja el teclado.
  • LPT1-LPT3: maneja los puertos paralelo (normalmente la impresora).
  • NUL: dispositivo nulo. Todo lo que se envíe a él simplemente es ignorado.
  • POINTER$: gestiona el dispositivo de puntero (el ratón).

Los dispositivos cuyo nombre acaba en $ no son accesibles directamente por los programas, y es preciso usar funciones específicas para acceder a ellos. Esto da una mayor riqueza y control sobre ellos.

Funciones del DOS

[editar]

OS/2 ofrece una gran colección de llamadas para gestionar ficheros en el sistema de archivos. Estas llamadas permiten copiar un fichero a otro, renombrarlo, borrarlo, cambiar de directorio, etc.

DosCopy
DosMove
DosDelete
DosForceDelete
DosCreateDir
DosDeleteDir
DosQueryCurrentDir
DosQueryCurrentDisk
DosSetCurrentDir
DosSetDefaultDisk

Busqueda de ficheros

[editar]

En muchas ocasiones un programa necesita conocer todos los ficheros que coinciden con un patrón determinado. Es el caso, por ejemplo, de querer mostrar una lista de ficheros que acaben en AL, o que tengan la letra F en el tercer lugar, etc. Para ello, OS/2 facilita una serie de funciones que permiten realizar esto. En ellas se debe especificar un nombre de fichero, sabiendo que:

  • El símbolo ? sustituye a cualquier carácter situado en esa posición. Así, la búsqueda de program?.exe devolverá los nombres de los ficheros que empiecen por program, a continuación tengan un carácter cualquiera, y por último, terminen con .exe, como por ejemplo, programa.exe, o programz.exe; pero no devolverá programad.exe ni programa.
  • El símbolo * sustituye a cualquier cadena situada desde esa posición en adelante. Así, la búsqueda de prog* devolverá cualquier fichero que empiece por prog, tenga lo que tenga después. Por ejemplo, devolverá programa, progs...
DosFindFirst
DosFindNext
DosFindClose

Redirección de entradas y salidas

[editar]

Además de estos ficheros de dispositivo, existen también tres 'archivos' especiales: la entrada estándar (STDIN), la salida estándar (STDOUT), y la salida de error estándar (STDERR). Por defecto, STDIN está asociada al teclado, y STDOUT y STDERR a la pantalla; sin embargo, pueden ser redireccionadas desde la línea de comandos a un fichero o incluso a otro programa por medio de cauces o Pipes. Por ejemplo, si tenemos un programa UPCASE.EXE que lee de la entrada estándar un carácter, lo convierte en mayúsculas y lo escribe en la salida estándar, y hacemos desde la línea de comandos un

C:\>UPCASE.EXE <entrada.txt >salida.txt

no se quedará esperando a que pulsemos una tecla, ni mostrará en pantalla lo que haya convertido. Lo que hará será leer caracteres del fichero entrada.txt, y todo lo que salga lo escribirá en el fichero salida.txt. Hemos redireccionado la entrada al fichero entrada.txt y la salida al fichero salida.txt. También podemos direccionar la salida de un programa a la entrada de otro programa distinto, con el carácter |. Así, si hiciésemos

C:\>UPCASE.EXE <entrada.txt |MORE.EXE

nos saldría el texto contenido en entrada.txt por pantalla, y cada vez que se llene ésta, esperará a que pulsemos una tecla. La salida de UPCASE.EXE ha sido tomada como entrada de MORE.EXE.

Para poder hacer esto en nuestros programas, simplemente necesitamos leer las órdenes a través de la entrada estándar (indicando 0 como handle en DosRead) y enviar los resultados y errores por la salida estándar y la salida de error estándar (indicando respectivamente 1 y 2 como handle en DosWrite).

Variables de entorno

[editar]

Para permitir la colocación de programas en múltiples directorios y simplificar algunas opciones de configuración, OS/2 facilita las variables de entorno. Se trata de una serie de variables que se definen bien en la línea de comandos, bien en el CONFIG.SYS, por medio de la sentencia SET. Por ejemplo, la línea

SET mi_variable=C:\OS2UTIL\MIPROGRAMA

asigna a la variable de entorno mi_variable el valor C:\OS2UTIL\MIPROGRAMA. De este modo, el usuario puede especificar el directorio en donde estarán diversas partes de un programa, o bien diversas opciones para éste. Por ejemplo, el compilador EMX requiere de algunas variables de este tipo para saber donde están las librerías, documentación, etc. El propio OS/2 hace uso de esta facilidad para implementar variables como el PATH de datos, etc.

DosScanEnv

Otras funciones DOS

[editar]

Otras funciones pertenecientes a este grupo son las siguientes:

DosResetBuffer
DosShutdown
DosBeep 

Los subsistemas en OS/2

[editar]

En principio, el uso del sistema de ficheros para Entrada/Salida con edl teclado y la pantalla debería ser suficiente, pero por desgracia no es así. Su uso es relativamente lento, por lo que solo parece útil utilizarlo cuando necesitamos poder redireccionar la entrada o la salida del programa, o cuando la velocidad no es importante.

Para mejorar y simplificar el acceso a los tres dispositivos principales de entrada de datos, OS/2 permite el acceso directo a los subsistemas de Vídeo, Teclado y Ratón, de modo que se puede mejorar notablemente la velocidad de los programas. El acceso a estas funciones va desde el mismo procedimiento que en el acceso a través del sistema de archivos (una ristra de caracteres) hasta el acceso directo al hardware del sistema. Entre ambos extremos hay un amplio abanico de posibilidades. De entre todas ellas, se deberá escoger la que mejor se adapte a las necesidades de velocidad y flexibilidad del programa.

Estrictamente hablando, siempre usamos los subsistemas. El sistema de ficheros, cuando sabe que una salida es para la pantalla, envía los datos al subsistema de vídeo (VIO). Sin embargo, antes tiene que comprobar para quien es la salida, con lo que pierde algo de rendimiento. Así mismo, la posibilidad de redireccionar entradas y salidas es otra opción que ralentiza el sistema. En muchos casos, la pérdida de velocidad es totalmente inapreciable, pero en algunos programas (juegos, aplicaciones a pantalla completa,...) el uso del sistema de ficheros puede ser totalmente contraproducente para el rendimiento. Usando el subsistema directamente nos ahorramos pasos intermedios, a costa de perder la posibilidad de redirección y de entrada/salida generalizada para ficheros y dispositivos.

En OS/2 disponemos de los siguientes subsistemas:

  • Subsistema de Vídeo
  • Subsistema de Teclado
  • Subsistema de Puntero (Ratón)


El subsistema de vídeo

[editar]

El subsistema de vídeo (VIO) es el encargado de gestionar la comunicación entre los programas y la pantalla. Es, sin duda, el subsistema más complejo de los tres, y el que ofrece, por tanto, mayores posibilidades.

Dado que puede haber varios programas ejecutándose a la vez en el sistema, pero solo uno puede acceder a la vez a la pantalla (normalmente el programa que se encuentra en primer plano o foreground), es necesario virtualizar ésta por medio de un buffer de pantalla propio de cada programa: el LVB (Logic Video Buffer, buffer de vídeo virtual). Cuando una aplicación quiere escribir en pantalla y se encuentra en segundo plano (background), su salida se escribe en dicho LVB. En el momento en que el usuario conmuta dicho programa a primer plano, el LVB se copia tal cual en la memoria de pantalla, y el resto de las escrituras van a ésta directamente. Si se vuelve a conmutar dicho programa a segundo plano, OS/2 copia lo que hubiese en pantalla en ese momento al LVB. De este modo, el programa nunca sabe ni le preocupa cuando está en primer o en segundo plano.

Funciones VIO

[editar]

Salida por TTY virtual

[editar]

En un extremo se encuentra el primer servicio que ofrece el subsistema VIO, que es el de salida TTY. Este servicio es casi idéntico a la salida de caracteres por medio del sistema de archivos. De hecho, éste, cuando comprueba que lo que el programa envía va dirigido a la pantalla, usa este servicio para realizar la función.

¿Cual es la diferencia entre uno y otro, entonces? Las diferencias son dos: la primera es que el uso del subsistema VIO es una opción más rápida que el sistema de ficheros; la segunda es que si usamos el subsistema, no podremos redireccionar la salida a un fichero, o a otro dispositivo de salida. Siempre irá a la pantalla.

El servicio TTY admite los caracteres de control estándar del ASCII, y también puede soportar ANSI, si éste es activado mediante la llamada correspondiente.

VioWrtTTY
VioGetAnsi
VioSetAnsi
VioGetMode
VioSetMode
VioGetState
VioSetState

Salida de cadenas de caracteres

[editar]

Los siguientes servicios se encargan del tratamiento de la pantalla a más bajo nivel. Con ellos podemos imprimir largas cadenas de caracteres con atributos y leer los caracteres que hay en determinadas posiciones de la pantalla. También podemos repetir un carácter o una pareja carácter-atributo un número determinado de veces.

Los atributos son bytes que definen el color de tinta y de fondo para cada carácter, así como otras características como el parpadeo. Están compuestos por un byte, el cual se divide en dos nibbles (grupos de 4 bits). El nibble de menor peso determina el color de la tinta del carácter, y el de mayor peso el color de fondo y, según se encuentre activo o no, el parpadeo del carácter. La distribución es como sigue:

Parpadeo activado Bit 	Significado
7 	Parpadeo del carácter
6 	Rojo del fondo
5 	Verde del fondo
4 	Azul del fondo
3 	Intensidad de la tinta
2 	Rojo de la tinta
1 	Verde de la tinta
0 	Azul de la tinta
Intensidad activada Bit 	Significado
7 	Intensidad del fondo
6 	Rojo del fondo
5 	Verde del fondo
4 	Azul del fondo
3 	Intensidad de la tinta
2 	Rojo de la tinta
1 	Verde de la tinta
0 	Azul de la tinta

Los bits de color activan directamente cada una de las componentes del monitor, de modo que éstas se suman directamente, dando lugar a los siguientes colores:

0 	Negro 	1 	Azul 	2 	Verde 	3 	Celeste
4 	Rojo 	5 	Magenta 	6 	Amarillo 	7 	Blanco

El bit de intensidad se limita a hacer estos colores más o menos brillantes.

Estos servicios orientados a carácter siguen siendo independientes del hardware utilizado de modo que no es necesario saber como se trabaja a nivel físico con ellos. Por otra parte, el propio OS/2 optimiza las transferencias para cada uno de ellos, de modo que se consigue la mayor velocidad posible, y se eliminan ciertos problemas inherentes a algunos sistemas gráficos (por ejemplo, en las tarjetas CGA sincroniza automáticamente la escritura con el retrazado vertical, de modo que se evita la aparición de nieve en la pantalla).

Tanto cuando hacemos una lectura como una escritura, si excedemos el fin de una línea se seguirá leyendo en la siguiente, y si llegamos al final de la pantalla no se seguirá leyendo ni imprimiendo.

VioWrtCellStr
VioWrtCharStr
VioWrtCharStrAtt
VioWrtNAttr
VioWrtNCell
VioWrtNChar
VioReadCellStr
VioReadCharStr

Funciones de Scroll

[editar]

El subsistema VIO ofrece, además, la posibilidad de realizar scroll de ventanas en modo texto. Con este conjunto de funciones, podemos desplazar parte o toda la pantalla en cualquiera de las cuatro direcciones posibles. La razón de incluirlas es que resulta mucho más rápido que hacer una rutina que lea cada posición del buffer de vi-deo y la reescriba en el lugar adecuado, aparte de que se trata de una función muy común en casi cualquier programa.

VioScrollDn
VioScrollLf
VioScrollRt
VioScrollUp

Definición y movimiento del cursor

[editar]

El cursor (el cuadradito parpadeante) es totalmente definible por el usuario en las sesiones de texto y gráficos de OS/2. Podemos definir tanto su tamaño como su posición dentro del carácter.

Al contrario que en MS-DOS, cuando escribimos en la pantalla de OS/2 la posición del cursor no se cambia. Esto se hace así para ganar tiempo. Normalmente el cursor solo se usa cuando hay que introducir datos por teclado, y el resto de las veces se suele hacer desaparecer de la pantalla. Esta es la razón de que halla un conjunto de funciones para situar el cursor. De esta manera se gana en velocidad.

VioGetCurPos
VioSetCurPos
VioGetCurType
VioSetCurType

Acceso al LVB

Cuando se necesite alta velocidad, se puede pedir acceso directo al buffer virtual de vi-deo asociado con la aplicación. Al hacerlo, OS/2 devuelve un puntero a la zona de memoria en donde está situado, con lo que podremos escribir en él como si se tratase de la pantalla física. Una vez que hemos terminado, debemos enviar una orden de retrazado, que hará que OS/2 copie el LVB a la pantalla física (siempre que la aplicación se encuentre en primer plano). Esto significa que los cambios que hagamos en el LVB no son visibles hasta que nosotros queramos.

Usar esta opción implica que perdemos el aislamiento entre el hardware y nosotros: dado que el LVB no es más que una copia del buffer real de pantalla, es necesario que nuestro programa conozca la geometría y la forma de almacenamiento de los datos en ésta.

VioGetBuf
VioShowBuf

Acceso directo al buffer real de vídeo

[editar]

En el extremo opuesto se encuentran las funciones de acceso directo al vi-deo. Con ellas, OS/2 da acceso directo a la memoria de pantalla. Sin embargo, esto que en el DOS de siempre es la opción más normal y común, puede resultar catastrófico en un Sistema Operativo multitarea como OS/2; no se hundiría el suelo bajo nuestros pies, pero la pantalla podría ser alterada en un momento poco oportuno... si OS/2 no tomase las debidas precauciones.

Para que un programa pueda acceder directamente a la memoria real de vi-deo, es absolutamente necesario que se encuentre en primer plano. Esto es así porque si escribiese algo en pantalla cuando estuviese en background, machacaría la imagen de la aplicación que se encontrase en ese momento en primer plano.

Lo primero que hay que hacer es pedir la dirección física de la memoria de vídeo. OS/2 devuelve un selector (o varios) a dicha zona de memoria (ver modos de direccionamiento del 286). Estos selectores deben ser convertidos a punteros antes de poder trabajar con ellos. Pueden ser varios pues cada selector no puede apuntar a un bloque de memoria mayor de 64Ks, el cual es también, casualmente, el tamaño de cada bloque de memoria de las tarjetas de vídeo actuales. Esto ayuda a simplificar el acceso, pues en modos como 640x480x16colores no necesitaremos cambiar de bancos; OS/2 nos devuelve un selector que apunta a cada uno de ellos, con lo que solo tenemos que acceder normalmente como si fuese memoria lineal, y el Sistema Operativo conmutará de uno a otro automáticamente.

El hecho de obtener un selector no significa que dispongamos de acceso todavía a la pantalla. De hecho, si intentásemos escribir o leer algo en ese momento y la aplicación no se encontrase en primer plano, OS/2 la cerraría inmediatamente, dando un Fallo de Protección General (el cual ya sabemos que no es tan fatal como el de Windows, pues en OS/2 solo afecta a la aplicación que lo ha provocado, dejando intacto al Sistema Operativo y al resto de los programas).

Cada vez que queramos acceder a la memoria física de vi-deo, debemos bloquear el acceso al buffer. Si el programa estaba en primer plano, OS/2 devolverá un valor afirmativo al retornar de la llamada, y bloqueará el selector de programas. Esto significa que el usuario no podrá conmutar la sesión actual a segundo plano hasta que ésta termine el acceso. Sin embargo, el resto de las aplicaciones siguen funcionando en segundo plano, sin verse afectadas por este hecho. Por supuesto, esto puede ser peligroso, y OS/2 toma algunas precauciones: si el sistema está bloqueado y el usuario hace una conmutación de tarea, si el programa no desbloquea el conmutador antes de un cierto tiempo definido por el sistema, queda congelado y se realiza la conmutación. Por eso es recomendable desbloquear cada x tiempo el selector de programas y volverlo a bloquear.

Si por el contrario el programa se encontraba en segundo plano, hay dos opciones: OS/2 puede retornar un código de error al programa, con lo que este sabrá que no tiene acceso al buffer y puede seguir trabajando en otra cosa, o bien OS/2 congelará al programa hasta que el usuario lo pase a primer plano, momento en que lo despertará indicándole que tiene acceso al buffer real.

Una vez que OS/2 ha devuelto un resultado afirmativo, el programa tiene acceso total a la memoria de vi-deo. Cuando haya terminado, tiene que proceder a desbloquear la pantalla, de modo que OS/2 pueda desbloquear el selector de programas y devolver el sistema a la normalidad.

VioGetPhysBuf
VioScrLock
VioScrUnLock

Existe una dificultad adicional a la hora de trabajar con acceso directo a la pantalla. Se trata de que OS/2, al conmutar de una tarea a otra, solo guarda el contenido de la pantalla si ésta se encontraba en modo texto (esto se cumple para OS/2 1.x. En Warp 4, sin embargo, SI conserva el contenido de la pantalla, pero no se si se cumple también para OS/2 2.x o 3.x). Si estábamos trabajando en modo gráfico, el contenido se perderá. Para evitarlo, OS/2 facilita la posibilidad de crear un thread (este concepto será explicado más adelante, cuando veamos la multitarea a fondo) que será activado cuando el programa vaya a cambiar de primer a segundo plano, y viceversa. Esto es así para permitir que un programa pueda almacenar el contenido de la pantalla en modo gráfico cuando no tiene bloqueado el acceso a la memoria de vi-deo. Es el thread SavRedrawWait.

Para implementarlo, es necesario crear un nuevo thread en el que se ejecute la llamada VioSavRedrawWait. Esta llamada bloqueará el thread hasta que el usuario pulse CTRL+ESC, momento en que OS/2, antes de conmutar de tarea, despertará a dicho thread indicándole que debe almacenar el contenido de la pantalla. Cuando el thread termine, debe volver a ejecutar la llamada, con lo que OS/2 sabrá que ha finalizado. El thread se quedará dormido de nuevo, y solo será despertado cuando el usuario vuelva a conmutar a primer plano el programa. Entonces OS/2 le indicará que debe repintar la pantalla.

VioSavRedrawWait

La inclusión de este sistema de acceso puede parecer innecesaria, a la vista de la potencia del acceso al LVB; la razón de haberla implementado fue que, cuando salió OS/2, no llevaba todavía el Presentation Manager, el gestor de ventanas, sino que era un Sistema Operativo en modo texto, por lo que se incluyó este sistema para poder acceder en modo gráfico a la pantalla, dado que VIO no ofrece ninguna facilidad como el trazado de puntos o líneas. Actualmente, al disponer de un completo (y complejo) gestor de ventanas, este método puede parecer inútil, sin embargo, para juegos puede ser muy útil, pues permite acceder a pantalla completa en modos como 320x200 en 256 colores, lo que permite una alta velocidad de refresco, así como una gran facilidad de manejo. Hay que señalar que el acceso directo a la memoria de vídeo solo se puede hacer estando en una sesión de pantalla completa; no funcionará en una sesión de ventana.

El subsistema de teclado

[editar]

El subsistema de teclado es el encargado de gestionar el acceso de los programas a este periférico. Al igual que el subsistema de vídeo, no es necesario usarlo para escribir programas sencillos de cara al interface de usuario, pues se puede acceder fácilmente a él a través del sistema de archivos. Es más, el uso del subsistema de teclado no ofrece casi ninguna ventaja en lo que a velocidad se refiere. Por tanto ¿para qué usarlo? Simplemente porque el acceso a través del sistema de archivos solo nos permite detectar códigos ASCII, pero no la pulsación de teclas de función, o, por ejemplo, si tenemos pulsada la tecla ALT, o CTRL, etc. Para este tipo de funciones necesitamos usar los servicios del subsistema de teclado.

Funciones KBD

[editar]

Acceso a nivel de cadenas de caracteres

La primera posibilidad que ofrece el subsistema de teclado es leer caracteres, lo cual se puede hacer de dos formas: por cadenas, o carácter a carácter.

La lectura por cadenas es exactamente igual que el uso de los servicios del sistema de archivos. De hecho, es el servicio que emplea la función DosRead cuando tiene que leer del teclado. Sin embargo, no necesita un indicativo de dispositivo abierto, ni participa en el redireccionamiento de E/S. Además, funciona de modo diferente según el modo de teclado en que se encuentre: ASCII, BINARIO o ECO.

Con este sistema, se leen caracteres hasta que el buffer definido esté lleno o hasta que se pulse el carácter de retorno, que por defecto es el retorno de carro (tecla enter).

KbdStringIn
KbdGetStatus
KbdSetStatus

La lectura por caracteres es un acceso más a bajo nivel. En este caso podemos saber exactamente qué tecla está pulsada y cual no, y saber si además se encuentran apretadas teclas como Ctrl, Alt, AltGr, etc. Además, si el informe de cambio está activo, se notificará no solo cuando se pulse una tecla, sino también cuando se suelte.

KbdCharIn
KbdPeek

Por último, tenemos una función que nos permite vaciar el buffer de teclado, de modo que todas las pulsaciones hechas hasta el momento que no han sido atendidas serán borradas. KbdFlushBuffer

Modos de teclado

[editar]

Los modos de teclado se definen como:

ASCII

[editar]

En este modo, que es el modo por defecto, el teclado es sensible al retorno de carro. Esto significa que cuando el usuario teclee datos, la llamada no retornará hasta que éste pulse ENTER. Siempre se debe leer de forma síncrona, por lo que la opción NO ESPERA de KbdStringIn no está soportada. El campo de longitud se utiliza para determinar qué cantidad del buffer de entrada es afectado por las funciones de edición de línea (como F3=recupera la ultima cadena, F1=recupera carácter, etc). Si se pone a 0, la edición está desactivada. En este modo, los códigos de caracteres de dos bytes (DBCS) se retornan de forma completa.

BINARIO

[editar]

En este modo, solo se retorna cuando el buffer se llena por completo, y el retorno de carro no tiene ningún significado especial.

ECO

[editar]

Si el modo ECO está activo, todo lo que se pulse aparecerá en pantalla automáticamente; en caso contrario, no lo hará. El modo ECO solo puede estar fijado con el modo ASCII, nunca con el BINARIO.

El subsistema de ratón

[editar]

El subsistema de ratón controla todo lo referente a este dispositivo.

Lo primero que hay que hacer para trabajar con el ratón es abrir el dispositivo. Esta acción crea una cola de eventos para el ratón e inicializa todos los parámetros de la sesión a un valor conocido.

MouOpen
MouClose

La cola de eventos del ratón almacena todos los sucesos ocurridos con éste dispositivo, de modo que puedan ser leídos por el programa en el momento adecuado. Un evento es, por ejemplo, mover el ratón, pulsar un botón, soltarlo... Existe la posibilidad de filtrar determinados eventos, de modo que al leer la cola, el resultado será exactamente igual que si no se hubiesen producido.

Cuando se lee la cola, se puede especificar además si la función retornará aún en el caso de que ésta esté vacía, o bien si debe esperar a que haya algún evento en ella. Además de esto, podemos conocer en cualquier momento la posición actual del ratón, así como cambiarla por otra. Estas opciones deben usarse sólo con fines de actualizar alguna variable del programa; si se pretende que sea el propio programa el que pinte el cursor, es mejor leer las coordenadas por medio de la cola de eventos.

MouReadEventQue
MouGetEventMask
MouSetEventMask
MouFlushQue
MouGetNumQueEl
MouGetPtrPos
MouSetPtrPos

El controlador de ratón puede encargarse él mismo de pintar el cursor en pantalla (solo en sesiones de texto), o bien relegar dicha acción al programa. Así mismo, puede devolver las coordenadas bien en coordenadas de pantalla, bien en Mickeys, que son unidades naturales del ratón.

Por otro lado, es posible definir un área de la pantalla que será especifica de la aplicación, de modo que el controlador no pintará el cursor cuando éste se encuentre dentro de aquella. Por defecto, este área ocupa toda la pantalla.

MouDrawPtr
MouRemovePtr
MouGetDevStatus
MouSetDevStatus

Por último, es posible obtener información sobre las características de nuestro ratón.

MouGetNumButtons
MouGetNumMickeys 

Acceso a las funciones de 16 bits

[editar]

El THUNKING

[editar]

Como se comentó en la historia de OS/2, cuando éste fue diseñado era un Sistema Operativo de 16 bits, pensado para funcionar en máquinas equipadas con un 286. Cuando se realizó la primera versión de 32 bits, IBM decidió (de forma bastante desafortunada), excluir algunas llamadas de la antigua API, por considerarlas obsoletas. Este conjunto esta formado principalmente por todas las llamadas a los subsistemas. Dicen las malas lenguas que IBM decidió que el futuro eran los programas preparados para el Presentation Manager, por lo que no tenía sentido pasar el trabajo de incluirlas.

Sin embargo, IBM tenía claro que quería conservar la compatibilidad con todos los programas escritos para OS/2 1.x, por lo que tuvo que incluir un API de 16 bits, que aceptaba los parámetros en el formato del modo protegido del 286, y otra API de 32 bits, que trabajaba con el formato del modo protegido de los 386 o superiores. Esto significa que, en principio, no podemos llamar a estas funciones desde nuestros programas de 32 bits, pues la forma en que se pasan los punteros, por ejemplo, es distinta.

Ahora bien; dado que el API 16 y el API 32 son casi iguales, repetir todo el código de una para la otra suponía ocupar una gran cantidad de memoria y de espacio en disco, por lo que se realizaron las partes comunes dentro del API 32, y cada vez que un programa de 16 bits quería acceder a ellas, se realizaba un proceso de recorte (THUNKING), de modo que los datos eran convertidos al formato adecuado. Si dicho programa quería acceder a una función no perteneciente al API 32 (una llamada a un subsistema, por ejemplo), podía hacerlo directamente, pues al pertenecer exclusivamente al API 16, estaba escrita para trabajar únicamente con el formato del 286.

Lo interesante de esto es que nosotros podemos aprovechar estas rutinas de thunking para convertir nuestros parámetros al formato de 16 bits, y así llamar al API de los subsistemas. Para facilitarlo aún más, Robert E. Canup ha escrito una DLL, así como ficheros LIB y H para trabajar desde C, que hacen ver al compilador que son llamadas de 32 bits, y no de 16. Esto nos permite trabajar con estas funciones con total comodidad. El fichero lo tienes disponible simplemente pulsando aquí. Este fichero no lo necesitarás si usas el compilador EMX, el cual realiza automáticamente el proceso de Thunking.

Sin embargo, ni esta librería ni el EMX realizan el Thunking de forma completa. Cuando se trata de enviar un puntero entre los parámetros de la función, lo hacen bien, pero no si la rutina devuelve un puntero.

En el caso de que se nos devuelva un selector, o un selector y un offset, debemos usar una función que nos convierta estos datos en un puntero.

MAKEP

Por otro lado, en algunas funciones tenemos que pasar un puntero dentro de una estructura de datos (por ejemplo, cuando queremos cambiar los colores de la paleta con VioSetState). En estos casos tenemos que poder convertir dichos punteros al formato de selector:offset. El caso contrario no es preocupante, pues OS/2 puede trabajar directamente con punteros del tipo selector:offset.

_emx_32to16

Gestión de memoria en el modo protegido del 286

[editar]

El microprocesador 286 incluye dos modos de funcionamiento: el modo real, en el que se comporta exactamente igual que un 8086, y el modo protegido, en el que surgen capacidades nuevas, como memoria virtual, protección de segmentos, etc.

La forma de acceder a la memoria en el modo protegido, que es en el que trabaja OS/2 1.x, es mediante una combinación de dos valores de 16 bits cada uno: un selector y un offset o desplazamiento. El valor del selector se almacena en un registro del microprocesador, y se usa como índice para acceder a una tabla de selectores. Conviene señalar que esta tabla se almacena en la memoria RAM del sistema; esto explica que el hecho de cargar un nuevo valor en el registro de selector consuma mucho tiempo, pues hay que hacer dos lecturas a RAM (cada entrada de esta tabla ocupa 4 bytes), aparte de numerosas comprobaciones. En esta tabla se definen los distintos segmentos accesibles por el programa, y entre estos datos se incluye la posición base de cada uno. El offset indica el valor a sumar a dicha dirección base para obtener la dirección del dato.

Dado que la dirección base almacenada en la tabla de selectores viene definida con 24 bits, podemos situar cada segmento en cualquier punto de la memoria física (el 286 tiene un bus de direcciones de 24 líneas, luego puede direccionar hasta 16 megas), y recorrerlo con el valor de offset. Dado que este valor es de 16 bits, el tamaño máximo de cada segmento es de 64 kilobytes.

En este gráfico se ve el proceso seguido para calcular una dirección en base al valor del selector y del offset.

INSERTAR GRÁFICO

Vemos que este sistema nos permite cambiar de sitio cada uno de los segmentos sin que el programa falle. Esto es porque el programa se refiere a cada segmento a través de un selector, y no por medio de su dirección base, como ocurre en el modo real. Si para compactar la memoria, el OS/2 necesita mover el segmento 3 a otra zona, cambiará también la dirección base en la tabla de selectores. Como el programa se refiere a esa zona como el 'Selector 3', siempre accederá de forma correcta.

También vemos que el tamaño de cada segmento es variable, pudiendo medir desde 1 byte hasta 64 Kbytes. Si el segmento mide menos que el máximo y se intenta leer o escribir más allá de sus límites, se produce un error, una excepción, que hace que OS/2 intercepte el programa y lo termine inmediatamente. Lo mismo si se carga un valor no válido en el registro de selector.

Este esquema permite además implementar un sistema de memoria virtual. Entre los muchos datos que se almacenan en la tabla de selectores, uno de ellos es si ese segmento está presente o no en la memoria física. Si el programa carga en el registro de selector un valor correspondiente a un segmento no presente, se produce una excepción, la cual salta al gestor de memoria virtual que lo que hará será pasar a disco un segmento poco usado, marcándolo como no presente, cargar en esa zona el segmento reclamado por el programa, marcarlo como presente y anotar su dirección base en la tabla de selectores, y devolver el control a la aplicación. De este modo se puede trabajar con más memoria que la que tiene físicamente la máquina.

Este esquema tiene un pequeño problema: dado que los segmentos pueden tener un tamaño variable, puede ser necesario liberar varios pequeños para hacer sitio a uno grande. Además, la memoria puede fragmentarse con facilidad, lo que obliga a reorganizarla para juntar pequeños huecos libres en uno solo grande. Todo esto consume tiempo extra durante el intercambio a disco. Pensando en esto, en el 386 se mejoró el modo protegido, de modo que se facilita la gestión de la memoria virtual.

Gestión de memoria en el modo protegido del 386

[editar]

Cuando se creó el microprocesador 386, al ser un micro de 32 bits se varió la estructura del modo protegido para darle una mayor potencia, aparte de permitirle direccionar los 4GBytes que permite su bus de direcciones de 32 bits.

En principio, el modo protegido del 386 funciona igual que el del 286: tenemos un registro de selector de 16 bits y uno de offset, esta vez de 32 bits, y en la tabla de selectores disponemos esta vez de 32 bits para indicar la posición base de cada segmento. Sin embargo, disponemos solamente de 20 bits para indicar la longitud de cada segmento. Esto nos la limitaría, en principio, a una longitud máxima de 1 mega por segmento. Sin embargo, los diseñadores de Intel reservaron un bit de granularidad entre los muchos reservados del registro de estado. Este bit permite conseguir segmentos de hasta 4 GBytes con solo 20 bits. El truco consiste en que, si el bit está a cero, la unidad de longitud será 1 byte, con lo que solo podremos tener segmentos de hasta 1 mega. En este modo se es compatible con el 286. Pero si este bit se pone a 1, la unidad será 4 KBytes. Esto significa que un segmento puede tener una longitud múltiplo de esta: 4, 8, 12, 16, 20, 24, 28, 32, etc KBytes. En total, podemos disponer de hasta 64 TeraBytes de memoria para cada programa.

Sin embargo, cuando usamos memoria virtual, este método tiene un inconveniente que ya comentamos: dado que los segmentos pueden tener un tamaño variable, puede ser necesario liberar varios pequeños para hacer sitio a uno grande. Además, la memoria puede fragmentarse con facilidad, lo que obliga a reorganizarla para juntar pequeños huecos libres en uno solo grande. Todo esto consume tiempo extra durante el intercambio a disco. Para evitarlo, se incluyó el modo paginado. Si este modo está activo, las direcciones que salen de la suma del selector y del offset son pasadas por un esquema como el siguiente:

INSERTAR GRÁFICO

Aquí vemos que el sistema se ha complicado mucho más. Mediante este, podemos dividir la memoria en bloques de 4KBytes, e intercambiar solo aquellos que nos interese. De esta forma, si tenemos que cargar un segmento de 24 KBytes, solo liberaremos 24 KBytes de otro segmento, y no un segmento completo. Esto acelera las transferencias, elimina la fragmentación (los bloques de 4KBytes que forman un segmento no tienen por qué ser consecutivas) y evita el crecimiento incontrolado del fichero de Swap. Sin embargo, hace el sistema algo más lento, pues tiene que hacer más accesos a la RAM (todas estas tablas se sitúan en la RAM normal). Para evitarlo, se incluyeron una serie de Buffers que retienen las paginas de dirección y de tabla mas usadas.

En OS/2 2.0 y posteriores se define un único segmento para cada programa, de 4 GBytes de longitud (si bien la cantidad de memoria disponible para uso propio es de 'solo' 512 MBytes) y se activa el modo paginado. De esta forma el acceso es más rápido, pues no es necesario cargar múltiples valores en el registro de selector, y conservamos la capacidad de memoria virtual. Esta es la razón de que, cada vez que se hace una reserva de memoria (con DosAllocMem o similar) la cantidad se redondea al múltiplo de 4KBytes superior.

Multitarea

[editar]

Concepto de thread

[editar]

Un thread (que de una forma un poco 'basta' se puede traducir como hilo) es la unidad básica de ejecución de OS/2. Cualquier programa que se ejecute consta de, al menos, un thread.

Un thread se puede considerar como la agrupación de un trozo de programa junto con el conjunto de registros del procesador que utiliza y una pila de máquina. El conjunto de los registros y de la pila de cada thread se denomina contexto. Como sabemos, en un Sistema Operativo multitarea, la CPU se reparte entre cada programa a ejecutar. Para ser más precisos, el S.O. reparte la CPU entre todos los threads a ejecutar en cada momento (pues un programa puede contener varios threads), simplemente adueñándose de esta y saltando al siguiente. Sin embargo, esta conmutación no se puede hacer de cualquier manera. Cada vez que el S.O. se adueña de la CPU para cedersela a otro thread, los registros y la pila (o sea, el contexto del hilo) contienen unos valores determinados. Por eso, el S.O. guarda todos esos datos en cada cambio, de modo que al volver a conmutar al thread inicial, pueda restaurar el contexto inicial. No olvidemos que OS/2 es un S.O. con multitarea preemptiva, lo que significa que la CPU puede ser arrebatada en cualquier momento. Esto significa que un thread no puede saber cuando se le va a arrebatar la CPU, por lo que no puede guardar los registros ni la pila de forma 'voluntaria'.

Dado que la forma en que un programa funciona depende principalmente del contexto, dos threads distintos pueden compartir el código de ejecución. Esto significa que si queremos dos threads que hagan las mismas operaciones sobre dos grupos de datos distintos, no necesitamos duplicar el código en memoria. Dado que conmutamos el contexto cada vez, aunque el código sea el mismo, los resultados no lo son, pues los registros y la pila son diferentes. Sin embargo, esto tiene un pequeño problema: las zonas de datos son comunes para todos los threads de un mismo proceso (esto ocurre en cualquier S.O., no solo en OS/2). Eso implica que, en estos casos, es necesario que cada thread cree su propia zona de datos, esto es, usar memoria de asignación dinámica (en C se usa MALLOC para crear una zona de memoria dinámica, si bien OS/2 también ofrece servicios de este tipo más potentes).

Por otro lado, debemos recordar que cada thread se ejecuta de forma absolutamente independiente. De hecho, cada uno trabaja como si tuviese un microprocesador para el solo. Esto significa que si tenemos una zona de datos compartida entre varios threads de modo que puedan intercambiar información entre ellos, es necesario usar algún sistema de sincronización para evitar que uno de ellos acceda a un grupo de datos que pueden estar a medio actualizar por otro thread. Estos servicios se verán más adelante.

Un thread puede crear otro thread usando una llamada de OS/2. Dado que no existe relación del tipo padre-hijo entre threads, los nuevos alcanzan los mismos privilegios que sus hermanos.

DosCreateThread

Por último, un thread acaba cuando vuelve al repartidor de threads. Trabajando en código máquina, esto implica un FAR RET; trabajando en C, significa ejecutar una instrucción EXIT o bien llegar al fin del procedimiento MAIN. Cuando acaban todos los threads de un proceso, OS/2 lo mata, liberando la memoria que ocupaba y los distintos recursos, como ficheros de disco, etc. Existe otra forma de terminar un programa en OS/2, es mediante una llamada a DosExit. Esta llamada finaliza el thread actual.

DosExit

Un thread tiene control sobre otros threads, siempre que pertenezcan al mismo PROCESO, pudiendo terminarlos si es preciso. También puede sincronizarse con ellos si es preciso, esperando a que alguno en concreto termine su ejecución, o bien dormirlo hasta que le interese ponerlo en ejecución de nuevo.

DosKillThread
DosWaitThread
DosSuspendThread
DosResumeThread

Concepto de proceso

[editar]

Un proceso no es más que un conjunto de threads que ejecutan el mismo código, junto con las zonas de memoria asociadas a ellos y los ficheros que tienen abiertos.

Un programa consta, al menos, de un proceso, y un proceso, al menos, de un thread. Cuando un programa tiene varios procesos, lo normal es que cada uno ejecute un código distinto, los cuales se encuentran en ficheros ejecutables separados. Dos procesos solo pueden compartir una zona de memoria si esta es definida expresamente como tal. Así mismo, es en este caso cuando los sistemas de sincronización a la hora de compartir memoria (de los que hablaremos más adelante) se vuelven especialmente necesarios e importantes.

DosExecPgm
DosKillProcess

Los threads que componen un proceso tienen un cierto control sobre los demás, pudiendo bloquearse entre sí de forma temporal si alguno lo necesita, entre otras posibilidades.

DosEnterCritSec
DosExitCritSec

Cada vez que un thread se adueña de un recurso del sistema (un fichero o un bloque de memoria), este pasa a formar parte del proceso al que pertenece dicho thread, y accesible a todos los threads de éste, pero los threads del resto de los procesos no tienen por qué poder acceder a estos. Por supuesto, si un proceso acaba o bien si es abortado (bien voluntariamente por el usuario, bien por un error o trap), OS/2 libera automáticamente todos los recursos ocupados por éste. Sin embargo, esto no significa que no se deban liberar estos antes de finalizar los threads que componen el proceso. Es buena práctica hacerlo, aparte de que el hecho de cerrar los ficheros permite que se actualicen los datos que se han grabado en ellos.

Si se crean dos procesos a partir del mismo código, OS/2 también procede a realizar compartición de código, con el consiguiente ahorro de memoria. Sin embargo, este hecho no implica que los recursos de cada proceso sea compartido con el resto.

En el caso de los procesos, sí existe una relación padre-hijo: si un proceso crea otro, el primero se denomina padre, y el segundo, hijo. Los hijos de un proceso, así como los hijos de los hijos, etc, se llaman descendientes.

El proceso padre mantiene un control sobre toda su descendencia, de forma que puede matar a cualquier proceso perteneciente a ésta.

Cuando se crea un proceso hijo, este hereda los recursos del padre, a menos que estos fuesen adquiridos por el padre sin derecho a herencia. Esto es: si el padre ha abierto con derecho a herencia un fichero, sus hijos podrán acceder a él; en caso contrario, no podrán (a menos que lo vuelvan a abrir ellos mismos, y los privilegios de acceso lo permitan).

Por último, se puede escoger si un proceso hijo determinado se ejecuta de forma síncrona o asíncrona. En el primer caso, el proceso padre se detiene hasta que el proceso hijo ha terminado; en el segundo, ambos procesos siguen ejecutándose de forma independiente.

Prioridades de ejecución

[editar]

En un S.O. multitarea, todos los procesos compiten por tener el máximo tiempo de CPU. Sin embargo, es obvio que no todos tienen la misma importancia. Por ejemplo, un programa que controle un MODEM tiene que tener prioridad sobre otro que controle una impresora, por ejemplo. Por esto existen las prioridades.

En OS/2 existen cuatro niveles de prioridad, en orden de mayor a menor:

  • Crítico.
  • Primer plano.
  • Medio.
  • Desocupado (Idle).

En el nivel crítico se ejecutan aplicaciones que necesiten una atención inmediata por parte del procesador, como sistemas de comunicaciones o en tiempo real (sonido, video,...). Dentro de este nivel existen 32 subniveles distintos de prioridad. El subnivel en el que se sitúa el programa es escogido por él mismo. Un ejemplo es el selector de programas del OS/2, el cual corre en el subnivel más bajo (0) del nivel crítico.

Para evitar una sobrecarga del sistema, es conveniente que los programas que se sitúen en nivel crítico consuman poco tiempo de CPU.

En el nivel de primer plano se ejecuta la aplicación que se encuentra en primer plano en ese momento. Solo tiene un subnivel de prioridad.

En el nivel medio se ejecutan los programas normales, como las aplicaciones, juegos, etc. Tiene 32 subniveles distintos de prioridad. El subnivel en que se sitúa es escogido por OS/2, y varia de forma dinámica en función de los accesos de E/S y del tiempo de CPU que consuma el programa, de forma que los más voraces tendrán menor prioridad que los menos.

Por último, en el nivel desocupado (o Idle Time) se sitúan aquellos programas que no necesitan una atención excesiva de la CPU. Es lo opuesto al nivel crítico. Tiene 32 subniveles de prioridad. El subnivel en el que se sitúa el programa es escogido por él mismo. Un ejemplo es el Spooler de Impresora, o bien un salvapantallas.

DosSetPriority

Vemos que un detalle importante a tener en cuenta en el diseño de un programa es el consumo de CPU que hace. Pero ¿como podemos hacer que nuestro programa consuma poca CPU? La respuesta es: no realizar nunca una espera activa.

Una espera activa es cuando entramos en un bucle que se limita a comprobar el estado de una variable, y solo salimos de él cuando cambia a un valor determinado. Un ejemplo es cuando queremos esperar a que el usuario pulse una tecla. Normalmente hacemos un bucle que se queda leyendo el teclado constantemente hasta que el usuario realice la pulsación. El problema es que estamos desperdiciando un tiempo que se podría aprovechar para ejecutar otras cosas. Para evitarlo, OS/2 nos suele dar la posibilidad de esperar por nosotros. Por ejemplo, cuando vimos el subsistema de teclado, podíamos pedirle a OS/2 que retornase de la llamada tanto si había una tecla pulsada como si no, o bien que no devolviese el control hasta que se pulsase. Pues bien, si le pedimos que haga la segunda opción, estaremos ahorrando ciclos de CPU. Esto es así porque OS/2 sabe que ese proceso no está listo para correr, por lo que repartirá el tiempo de CPU únicamente entre el resto de los procesos, y no volverá a dedicarle atención hasta que el usuario pulse una tecla, momento en que volverá a darle ciclos de CPU. Vemos que, de esta forma, OS/2 ahorra el tiempo que consumiríamos si nos dedicásemos a hacer una espera activa, con lo que si ejecutamos varios programas a la vez tardarán menos que la suma del tiempo que tardarían en ejecutarse uno a uno, precisamente porque aprovechamos estos tiempos muertos. Por supuesto, no solo se ahorran estos, sino también los necesarios para mover los cabezales de los discos, escribir caracteres en la pantalla, etc.

Si hay dos procesos listos para correr, siempre obtendrá el procesador aquel que tenga mayor prioridad. Puede parecer que si un proceso siempre tiene cosas que hacer y está situado en un nivel alto, siempre se quedará con la CPU, dejando al resto de las tareas bloqueadas. Esta es la razón de que la prioridad de los procesos en el nivel medio sea dinámica: de esta forma, los procesos que consumen mucha CPU ven rebajada su prioridad, de modo que todos se ejecutan por igual. Por otro lado, existe un tiempo máximo durante el cual un proceso puede estar sin recibir ciclos de CPU. Transcurrido este, OS/2 le cambiará momentáneamente la prioridad, de modo que recibirá un intervalo de tiempo, volviendo a su prioridad original después. Esto permite que los programas de prioridad desocupada obtengan siempre ciclos de CPU, aunque haya un proceso de nivel medio que consuma mucha (por ejemplo, un RayTracer).


Concepto de sesión

[editar]

Hasta ahora, cada vez que nos referíamos a un conjunto de Procesos, usábamos el término programa ; si bien algunas veces el uso fue correcto, en otras no fue así, y deberíamos haber usado el término sesión.

Una sesión es un conjunto de procesos, junto con una pantalla, teclado y ratón virtuales. Cada vez que abrimos un programa en modo texto, OS/2 crea para él una sesión, asignándole un LVB en donde escribir los datos de salida, y una cola de teclado y de ratón, en donde se almacenarán las pulsaciones y los movimientos hasta que pueda procesarlos. Si esa sesión se encuentra en primer plano, su VDU, cola de teclado y cola de ratón estarán conectadas directamente a la pantalla física, al teclado físico y al ratón físico, así hasta que el usuario conmute a otra sesión usando el selector de programas, el cual es una parte especial de OS/2 dedicada precisamente a gestionar las sesiones.

Todos los procesos pertenecientes a una sesión acceden al mismo LVB y a la misma cola de teclado y ratón. Esto no es un problema, sino un efecto buscado, pues si es necesario, es posible desde una sesión crear otras nuevas y arrancar en ellas procesos, de modo que no interfieran con el actual. De aquí surge que también existe una jerarquía de sesiones, y ciertas implicaciones entre ellas. Por ejemplo, una sesión padre puede ser notificada de que ha terminado una sesión hijo. También una sesión padre puede traer a primer plano a cualquiera de sus hijos (siempre y cuando ella esté en primer plano), o vincularse a ellos, de modo que cuando uno sea traído a primer plano, el padre también lo haga (y viceversa). Por último, un padre puede parar una sesión hijo, pero solo a ella, y no a sus 'nietos', o a ella misma. Sin embargo, cuando se para una sesión hijo, se paran también todas sus sesiones descendientes.

DosStartSession
DosStopSession
DosSelectSession

A nivel interno, los conceptos de Thread y Proceso están implementados en el núcleo, mientras que el de Sesión lo está en los subsistemas (los cuales no pertenecen al núcleo), pues son ellos los que deciden cuando un conjunto de procesos puede acceder o no a la pantalla física, o al teclado. Por otra parte, el selector de programas es el encargado de gestionar cuando se conmuta de uno a otro.

Normalmente, cada vez que arrancamos un programa, se crea una sesión para él; sin embargo, en algunos casos podemos desear que no se cree una pantalla virtual o una cola de entrada de teclado, por ejemplo si el programa no tiene que interactuar con el usuario. En estos casos, se puede arrancar en una sesión 'muda', a la cual no se podrá conmutar mediante el selector de programas, y que finalizará cuando acabe dicho proceso/procesos. Para arrancar un programa así desde la línea de comandos de OS/2, se usa la orden DETACH nombre de programa.

Estructura del OS/2

[editar]

(Selector de Programas, Sesiones, Procesos y Threads)

[editar]

La estructura de OS/2 a nivel de multitarea se centra en los tres conceptos dados anteriormente: Threads y Procesos, gestionados por el núcleo, y Sesiones, gestionadas por los subsistemas y por el Selector de programas.

El selector de programas es la parte de OS/2 que se encarga de conmutar la pantalla, teclado y ratón físicos hacia los buffers lógicos de cada sesión. Para ello, incluye un API propio, que permite a una sesión hacer pasar a primer plano a cualquier sesión hija.

Surje la cuestión de como se arranca la primera sesión, la que nos permitirá arrancar nuevos programas, etc. Para ello, OS/2 incluye una línea en el CONFIG.SYS que le indica un proceso que debe arrancar al principio de todo. Este proceso será el Shell del sistema.

El Shell del sistema, en principio, puede ser cualquier programa, de modo que al arrancar OS/2, se arrancará éste también. Sin embargo, para que sea útil, tiene que permitir arrancar nuevas sesiones desde él y pasar el control a éstas.

En OS/2 1.0, el Shell del sistema era un simple menú, que contenía los programas básicos (como una línea de comandos). Pulsando sobre estos, se arrancaba una nueva sesión con éste, y se le cedía el control. En las versiones posteriores, como Shell se pone el Presentation Manager. Este es el gestor de ventanas. Sin embargo, el PM no incluye ningún Shell, por lo que surge una nueva línea en el CONFIG.SYS, que especifica qué Shell debe arrancar el PM. Este Shell, por defecto, es el Work Place Shell, o WPS, que es el que nos da la orientación a objetos del escritorio de OS/2; sin embargo, puede ser cambiado por otro cualquiera. Por ejemplo, poniendo como Shell el CMD.EXE de OS/2, tendremos una situación parecida a UNIX, en la que arrancamos siempre desde una línea de comandos.

El selector de programas incorpora dos Hot Keys o Teclas Calientes: Ctrl+Esc y Alt+Esc. La primera pasa la sesión actual a segundo plano y vuelve a poner el Shell en primer plano. Esto nos permite arrancar nuevas sesiones sin cerrar la actual. La segunda permite conmutar de una sesión a otra de forma cíclica, pero sin pasar por la del Shell.

OS/2 es muy atento con el usuario, y por eso tiene especial cuidado con el Shell. Dado que se trata del principal nexo de unión entre ambos, si se produce un error y el Shell se cierra, OS/2 abre uno nuevo inmediatamente, sin afectar para nada al resto de las aplicaciones que estaban corriendo. De esta forma el usuario nunca se queda sin control de la máquina. No olvidemos que el Shell es un programa más, que no corre en el núcleo, y que por tanto puede contener errores.

De todo esto deducimos varias cosas

  • El Shell del sistema es un programa más, que se ejecuta en una sesión independiente, por lo que nada impide que escribamos el nuestro propio.
  • Un Shell nunca debe arrancar ningún programa en su misma sesión, pues eso implicaría que el pulsar Ctrl+Esc no nos devolvería al Shell, sino al programa que estamos ejecutando en su sesión.
  • Dado que el Shell es el padre de todas las sesiones que se arrancan, puede conmutar a cualquiera de ellas sin usar ningún truco especial, simplemente usando el API del selector de programas. Por esto mismo, tiene acceso a todas las sesiones de la lista de tareas de OS/2, y también puede matar a cualquiera de ellas.

Gestión de la memoria

[editar]

OS/2, como cualquier Sistema Operativo multitarea, mantiene un estricto control sobre la memoria usada por cada programa. Esto es así porque un programa que sobreescribiese la memoria de otro lo haría fallar con casi total seguridad. OS/2 hace uso del modo protegido de los microprocesadores 386 y superiores para asegurar que cada programa tenga acceso solo a sus bloques de memoria, y no pueda machacar el contendido de otra zona. A la vez, se encarga de intercambiar zonas de memoria entre la RAM y el disco duro, implementando así un sistema de memoria virtual eficiente, con lo que puede ejecutar programas más largos que los que la memoria física del ordenador permitiría.

Las funciones de asignación y desasignación dinámica de memoria son extremadamente útiles en programas en los que no sepamos la cantidad de esta que vamos a necesitar. Un ejemplo puede ser una zona de memoria para descomprimir imágenes en un visualizador: dado que cada una puede medir cualquier longitud, reservar una cantidad excesiva de forma estática hace al programa lento, y reservar poca puede impedir visualizar imágenes grandes. La solución es comprobar el tamaño de la imagen y reservar una zona de memoria dinámica acorde con este, liberándola al terminar.

DosAllocMem
DosFreeMem

Otra función es la de crear zonas de memoria compartida. Se trata de bloques de memoria a los que pueden acceder dos procesos distintos de forma simultánea, y se usan para intercambiar grandes cantidades de información de forma rápida, y para compartirla.

Existen dos formas de compartir un bloque de memoria: una es mediante un nombre que refiera a ese bloque; otra es mediante un puntero que señale a su inicio.

DosAllocSharedMem

En el primer caso, se debe especificar un nombre para el bloque que cumpla el convenio de nombres de ficheros de OS/2, y además debe empezar por \SHAREMEM\. Por ejemplo, un nombre válido sería \SHAREMEM\BLOQUE.DAT.

DosGetNamedSharedMem

Si no se especifíca un nombre, la memoria solo podrá ser compartida mediante el intercambio de un puntero que señale al inicio del bloque. Si ambos procesos conocen este valor, podrán acceder a dicho bloque pidiéndolo o asignándolo. Para comunicarlo, es necesario usar la comunicación entre procesos, que veremos más adelante.

DosGetSharedMem
DosGiveSharedMem

Subasignación de memoria

[editar]

La asignación de memoria es una parte muy importante en cualquier sistema operativo. Da a los programas la capacidad de adaptarse dinámicamente a las necesidades de cada momento. Sin embargo, hay un coste relativamente elevado en la asignación de memoria dinámica. En cada petición es preciso buscar una zona libre, cambiar los descriptores de segmento, y cargar los registros de nuevo. Todo esto lleva un tiempo muy elevado, debido a la propia arquitectura del microprocesador. Lo mismo ocurre al liberar una zona. De aquí se saca una conclusión clara: utiliza toda la memoria que necesites, pero evita asignar y desasignar constantemente bloques. La solución consiste en asignar un bloque de tamaño moderado y dentro de él subasignar bloques de tamaño más pequeño, aumentando el tamaño del bloque físico sólo cuando se necesita más espacio.

Para ayudar en esto, OS/2 ofrece un grupo completo de subasignación de memoria.

DosSubAllocMem
DosSubFreeMem
DosSubSetMem
DosSubUnsetMem

La primera acción consiste en reservar un bloque de memoria con DosAllocMem. Este bloque de memoria es necesario prepararlo para subasignación con DosSubSetMem. Una vez hecho esto, podemos asignar y desasignar pequeños bloques con DosSubAllocMem y DosSubFreeMem como si usásemos las llamadas de siempre, pero con la diferencia de que será notablemente más rápido. Sin embargo, no debemos olvidar que se trata de un solo bloque de memoria subdividido, por lo que aunque tengamos subasignado un bloque, podemos salirnos de él por accidente sin que el sistema dé un error (siempre, claro está, que no nos salgamos de la memoria asignada).

Debemos ver las funciones de subasignación de memoria simplemente como una ayuda en la gestión de ésta. Para cualquier aplicación es exactamente lo mismo dividir nosotros mismos un bloque de memoria de forma lógica que usar estas funciones. La ventaja consiste en que nos ahorramos programar un grupo de funciones que compruebe cuanto espacio nos queda libre y en donde y actualice las estructuras. Se trata de una ganancia en comodidad. Una aplicación clara de estas funciones se verá cuando expliquemos las colas de mensajes, pues en ellas se hace uso intensivo de pequeños bloques de memoria que se crean y liberan constantemente.

Sincronización y comunicación entre procesos

[editar]

Al ser OS/2 un Sistema Operativo multitarea, sus programas se componen de múltiples partes denominadas threads, las cuales se ejecutan de forma paralela. Debido a esto, cuando dos o más threads intentan acceder a la vez a un mismo recurso (por ejemplo, una zona de memoria compartida), el resultado puede ser , en el mejor de los casos, impredecible. Por eso surgen los sistemas de sincronización entre procesos. Estos permiten establecer un sincronismo entre dos o mas threads y procesos de una forma consistente y, sobre todo, fiable y predecible. En OS/2, estos sistemas están formados por los semáforos.

Por otro lado, dos procesos pueden necesitar intercambiar información entre ellos. En un primer momento, la memoria compartida puede parecer la panacea, pero en muchas aplicaciones no son el método más eficiente. Por eso surgen otros: los cauces y las colas de mensajes.

Concepto de semáforo

[editar]

Un semáforo es una estructura diseñada para sincronizar dos o más threads o procesos, de modo que su ejecución se realice de forma ordenada y sin conflictos entre ellos.

El por qué no se pueden usar directamente otras estructuras mas clásicas, como por ejemplo usar una variable común para decidir si se puede o no acceder a un recurso, se debe a que estamos en un sistema multitarea: hacer esto implicaría realizar una espera activa (un bucle, comprobando constantemente si la variable está o no a 0, y así saber si podemos seguir ejecutando o no). Por otro lado, puede ocurrir algo mucho peor: supongamos que un proceso comprueba la variable, y ve que el recurso está libre, por lo que procedería a cambiar dicha variable de valor y seguir. Pues bien, si justo después de la comprobación pero antes de que cambie el valor se conmuta de tarea (puede pasar, pues el sistema operativo puede hacerlo en cualquier momento), y el nuevo proceso comprueba la variable, como todavía no se ha actualizado, creerá que el recurso está libre, e intentará tomarlo, haciendo que ambos programas fallen. Lo peor del caso es que se tratará de un error aleatorio: unas veces fallará (cuando se produzca cambio de tarea en ese punto) y otras no.

Para evitarlo, se idearon los semáforos. Un semáforo básico es una estructura formada por una posición de memoria y dos instrucciones, una para reservarlo y otra para liberarlo. A esto se le puede añadir una cola de threads para recordar el orden en que se hicieron las peticiones.

Se empieza por inicializar la posición de memoria a 1 (o al valor correspondiente si ese recurso concreto admite más de un acceso simultáneo). Esto se hace en el inicio del programa principal.

A continuación, cada vez que un thread o un proceso quiera acceder a dicho recurso (por ejemplo, un fichero), hará primero una petición con la primera de las llamadas disponibles. Cuando el S.O. ejecuta esa llamada, comprueba el valor que hay en la posición de memoria del semáforo, y si es distinta de cero, se limita a restarle 1 y devolver el control al programa; sin embargo, si ya es cero, duerme al proceso que hizo la petición y lo mete en la cola de procesos, en espera de que el semáforo se ponga a un valor distinto de cero.

Por último, cuando el proceso ha terminado el acceso al recurso, usa la segunda llamada para liberar el semáforo. Cuando el S.O. la ejecuta, comprueba si la cola del semáforo está vacía, en cuyo caso se limita a incrementar el valor del semáforo, mientras que si tiene algún proceso, lo despierta, de modo que vuelve a recibir ciclos de CPU y sigue su ejecución. Si había varios procesos en espera, se irán poniendo en marcha uno tras otro a medida que el anterior va liberando el semáforo. Cuando termina el último, el semáforo se vuelve a poner a 1. Se trata, por tanto, del mismo proceso que seguiríamos con la variable, pero con la ventaja de que es un mecanismo estándar para todos los procesos, y como es una operación atómica (esto es, que durante su ejecución no se admiten cambios de tarea), no surje el problema de que una conmutación pueda producir errores aleatorios.

Vemos que la primera vez que un proceso usa el semáforo, este tiene valor 1, por lo que pasa a cero y el proceso puede acceder al recurso. Si durante ese tiempo otro proceso quiere acceder también, al usar el semáforo, este tiene valor cero, por lo que el S.O. deja de darle ciclos de CPU. Cuando el primer proceso ha terminado, libera el recurso, con lo que el S.O. puede comprobar que el segundo proceso está esperando, por lo que le vuelve a dar ciclos. En este punto, el proceso sigue como si nunca hubiese sido detenido. Este tipo de semáforos son los de Exclusión mutua, o Mutex.

DosCreateMutexSem
DosOpenMutexSem
DosCloseMutexSem
DosQueryMutexSem
DosReleaseMutexSem
DosRequestMutexSem

Otra utilización de los semáforos es cuando uno o más procesos tienen que esperar a que otro halla terminado una tarea. Para ello, el primer proceso borra el semáforo y con una primitiva adecuada se pone a esperar a que el semáforo se active (posted). Mientras, el segundo proceso va trabajando, y cuando termina lo que tiene que hacer, activa el semáforo, con lo que el primer proceso vuelve a ponerse en marcha, sin haber desperdiciado ciclos de CPU. Son semáforos evento. Vemos que puede haber varios procesos esperando por el mismo semáforo, y el thread que lo activa no tiene por qué saber cuantos son. Cobran su importancia cuando el evento no es producido por otro thread, sino por otras funciones del S.O., como las de sincronización, que veremos más adelante.

DosCreateEventSem
DosOpenEventSem
DosCloseEventSem
DosPostEventSem
DosQueryEventSem
DosResetEventSem
DosWaitEventSem

Los semáforos se identifican con un nombre, el cual tiene la forma \SEM32\un_nombre. Un_nombre es el identificador del semáforo. Existe la posibilidad de crear un semáforo sin nombre, en cuyo caso se puede especificar si es un semáforo compartido o no (por otros procesos).

Temporizadores en OS/2

[editar]

En muchos casos, un programa necesita perder tiempo, esto es, esperar unos segundos sin hacer nada, o similar. Un sistema muy usado en programas DOS consiste en un bucle de espera, que tarde justo el tiempo que nos interesa. Sin embargo, este sistema es bastante imperfecto, pues si el programa funciona bien en un ordenador concreto, en otro que sea más rápido o más lento no lo hará. La segunda solución que 'encontraron' los programadores fue usar el timer del ordenador, un integrado que puede ser programado para que provoque interrupciones cada n milisegundos. Como ese chip es el mismo en todos los PCs, sin importar su velocidad, permite que los programas funcionen igual en todos.

En OS/2, sin embargo, no se puede permitir esto, pues puede haber múltiples programas que necesiten el temporizador a la vez. Para solucionarlo, surgen los temporizadores.

Antes de nada, conviene señalar que los temporizadores de OS/2 (así como los de cualquier otro S.O. multitarea) no son todo lo precisos que sería deseable. Esto se debe en primer lugar a que, aunque especifiquemos la temporización al milisegundo, será redondeado al ciclo superior del conmutador de tareas (en OS/2, normalmente, son 32 milisegundos). Por otro lado, OS/2 es un Sistema Operativo multitarea por prioridades, por lo que puede ocurrir que en el momento en que expire un temporizador, haya otro proceso de más prioridad esperando a ejecutarse, en cuyo caso se producirá otro retardo. Sin embargo, existen maneras de minimizar estos efectos, y OS/2 las ofrece.

Una primera clasificación de los temporizadores los divide en síncronos y asíncronos. Un temporizador síncrono se activa y no devuelve el control al thread hasta que este termine. Es el caso más básico: si queremos hacer una pausa de 1 segundo, llamamos a la función DosSleep, y el thread correspondiente se dormirá durante el tiempo fijado.

DosSleep

Sin embargo, puede no interesarnos este sistema, sino que durante ese intervalo podemos necesitar hacer alguna operación y luego dormirnos hasta terminarlo. Por ejemplo, queremos un programa que, una vez por segundo, imprima un carácter en pantalla. Una primera idea sería imprimir un carácter, dormir un segundo, imprimir otro, dormir un segundo, etc. Sin embargo, el hecho de imprimir el carácter consume algo de tiempo, luego estamos gastando algo más. Por otro lado, ese tiempo es variable, en función de la velocidad del ordenador y de la tarjeta gráfica. Por eso existen los temporizadores asíncronos.

Un temporizador asíncrono necesita de un semáforo, pues lo necesita para notificar al thread que se ha acabado el intervalo de tiempo. La idea es la siguiente: el thread inicializa el temporizador, el cual activa el semáforo y empieza a contar, pero devuelve el control al programa. Este sigue trabajando (imprimiría el carácter o lo que tuviese que hacer), y cuando termine, espera a que el semáforo se active (post) (que será hecho por el temporizador cuando expire). De este modo, no importa cuanto dure el proceso que haga el thread: siempre esperará un segundo (o el tiempo elegido, redondeado al ciclo del conmutador de tareas).

DosAsyncTimer

Como decíamos, la temporización no es precisa por estar redondeada a 32 milisegundos. Esto puede ser problemático cuando se encadenan temporizaciones. En el ejemplo anterior, si programamos un temporizador para cada segundo, la ejecución de la instrucción cada vez ya lleva algo de tiempo, y además, los redondeos se van sumando cada vez más, de modo que podemos llegar a tener un desfase entre el ritmo que debería llevar y el que lleva realmente. Para eso, existe otro temporizador asíncrono que borra un semáforo no una vez, sino constantemente, a cada paso del ciclo. Es este el que deberíamos usar en nuestro ejemplo. Este temporizador, como es cíclico (está contando constantemente intervalos del tiempo que queramos, y no una sola vez como hacía el primero) puede tener en cuenta el redondeo del conmutador de tareas, y minimizar su efecto.

DosStartTimer
DosStopTimer

Por último, tenemos las funciones de fecha y hora, las cuales permiten obtener y cambiar estos datos. Un detalle importante es que solo existe una fecha y una hora. Esto significa que si un programa los cambia, lo hace para todos, no solo para él mismo.

DosGetDateTime
DosSetDateTime

Comunicación entre los procesos

[editar]

Memoria compartida y semáforos

[editar]

El primer procedimiento para intercambiar datos consiste en combinar un bloque de memoria compartida con un semáforo que se encargue de evitar que ambos procesos accedan a la vez al bloque. Cuando uno quiere leer o grabar datos, borra el semáforo, accede, y lo libera (post). Si ya estaba ocupado el bloque, el proceso es detenido hasta que el semáforo se libera. Todo lo necesario para trabajar con esto ya se ha visto en las páginas anteriores.

Cauces

[editar]

Otra manera de intercambiar información son los cauces (pipes). En OS/2 existen dos tipos: con nombre (named pipes) y sin nombre (unnamed pipes).

Los cauces son vistos por el programador como un fichero más, en el que, o bien puede leer, o bien escribir. La cuestión es que un extremo pertenece a un thread y el otro a otro distinto, y si uno de ellos escribe en ese fichero (recordemos que el sistema de archivos de OS/2, al igual que el de cualquier otro S.O., consigue que no sepamos diferenciar cuando trabajamos con un fichero de disco, con un cauce, o con la pantalla), cuando el otro lea, encontrará los datos grabados. Podríamos definirlo como una tubería (la cual es otra de las traducciones que he encontrado en algunos libros) que conecta los dos threads, y que se maneja con las mismas ordenes que el sistema de ficheros: DosRead, DosWrite y DosClose.

Cuando creamos una unnamed pipe, el sistema nos devuelve dos handles o indicativos: uno para lectura y otro para escritura. El thread que creó el cauce debe quedarse con uno, y enviar el otro al otro thread. Para ello puede usar un pequeño bloque de memoria compartida, definiendo claramente como acceder a ella. Dado que los demás threads van a acceder solo en lectura, no haría falta incluir un semáforo.

Los cauces tienen una opción muy interesante (y útil), que consiste en la posibilidad de duplicar handles. Usando la llamada DosDupHandle, se pueden crear dos handles para un mismo cauce. Lo interesante es que se puede dar no solo el siguiente número libre, sino el que se quiera. Haciendo esto, se puede asignar a un cauce el valor 0 y a otro el 1, y redirigir así la entrada y salida estándar (STDIN y STDOUT) del programa en curso. Esto es precisamente lo que hace el interprete de comandos de OS/2 para hacer la redirección desde la línea de comandos.

DosCreatePipe
DosDupHandle

En las versiones 1.x de OS/2 solo existían estos cauces; sin embargo, su potencia quedaba ligeramente oscurecida por el hecho de tener que usar memoria compartida para pasar un handle al otro thread. Para evitarlo, se añadieron en OS/2 2.0 y superiores las named pipes, o cauces con nombre (denominación horrible a mas no poder. Espero sugerencias para una posible traducción). En estos, al crearlos se les asigna un nombre del tipo \PIPE\un_nombre. Dentro de un_nombre puede ir cualquier cadena. Ejemplos válidos son \PIPE\EJEMPLO, \PIPE\CURSO\OS2\EJEMPLO_DE_PIPE. Cuando un thread cualquiera quiere acceder a ese cauce, solo tiene que referirse a él por dicho indicativo. Esto resulta mucho más cómodo, pues basta con fijar durante la programación el nombre que se le va a dar, y no hace falta pasarlo de forma dinámica en cada ejecución. Sin embargo, además de crearlo, es necesario que el proceso servidor lo haga disponible a los clientes.

DosCreateNPipe
DosConnectNPipe
DosDisConnectNPipe

Para que un cliente acceda a una named pipe, tiene que usar DosOpen, usando como nombre de fichero el nombre de la pipe. El segundo parámetro es un puntero a una variable de tipo HPIPE, que contendrá un handle que podremos usar con DosRead, DosWrite y DosClose para manejar la pipe. El tercer parámetro contendrá un puntero a un ULONG, en el cual se almacenará la causa por la que no se ha podido abrir la pipe (es mejor usar los códigos de error que se devuelven de la forma normal en vez de éste valor). El cuarto parámetro contiene la longitud del buffer que se reservará. Es conveniente que tenga un tamaño más bien grande. El resto de parámetros son particularizaciones para las pipes de los parámetro normales de DosOpen.

Se debe tener cuidado al definir las named pipes. Hay que tener en cuenta que el servidor tiene ciertos privilegios que el cliente no tiene, como por ejemplo disponer de un semáforo que le indique cuando hay datos en el cauce. Un cliente tendría que hacer una espera activa en el caso de querer estar siempre disponible a través de ese cauce. Por otro lado, cualquier cliente se puede conectar a una pipe, por lo que el servidor no puede saber (en principio) quien le está reclamando el servicio. Al revés sí ocurre, pues todo cliente sabe quien es el servidor de un cauce determinado por el nombre de éste.

DosSetNPipeSem
DosQueryNPHState
DosSetNPHState
DosWaitNPipe
DosPeekNPipe

Un detalle importante es que las unnamed pipes son unidireccionales. Esto es, si se quiere intercambiar datos en ambos sentidos, son necesarios dos cauces, no se puede usar el mismo. El sentido de la comunicación lo decide el que cree el cauce, al enviar uno u otro handle. En el caso de named pipes, por el contrario, el creador puede determinar si la comunicación va del servidor al cliente, al revés, o en ambos sentidos.

Colas

[editar]

Los cauces no siempre son la solución ideal. En muchos casos necesitamos poder tratar elementos separados en vez de una secuencia de bytes, y además puede ser necesario que se organicen de forma distinta a la clásica FIFO (First In First Out, el primero en entrar es el primero en salir). Incluso puede ser necesario eliminar elementos antes de que los lean los destinatarios. Para esto se crearon las colas.

Una cola es similar a un cauce con nombre (named pipe), pero en vez de escribirse bytes en ella, se escriben secuencias de longitud variable que son tratadas como elementos independientes. Por ejemplo, si en un cauce introduzco la secuencia Esto es una prueba, puedo leer solo la primera letra, o las dos primeras, o unas cuantas, pero la frase será tratada como un conjunto de elementos separados. En una cola, sin embargo, sería tratada siempre como un solo elemento. Cuando el proceso lee de la cola, leerá la frase completa, y no podrá leer solo un trozo, o incluso pegarlo a otro elemento introducido después.

Por otro lado, la ordenación de los elementos en las colas es definible por el usuario. Así, puede ser FIFO (First In First Out, el primero en entrar es el primero en salir), LIFO (Last In First Out, el último en entrar es el primero en salir), o bien por prioridades. Un ejemplo del primer tipo de colas sería una cola de un cine: el primer espectador que llega será el primero en obtener la entrada, y así sucesivamente. Un ejemplo del segundo tipo de colas sería una pila de revistas. Cuando pongo una nueva, la añado en la cima, y cuando cojo una, cojo la que está arriba de todo: la última en llegar es la primera en salir. Por último, un ejemplo de una cola con prioridad podría ser una recepción oficial. No importa el orden en que lleguen los dignatarios; estos siempre saldrán en orden descendente de rangos: primero el presidente invitado, luego el del país, luego ministros, etc. Y si mientras salen, llega algún rezagado, todos los de rango inferior le cederán su sitio.

Las colas se denominan, como viene siendo habitual, con un nombre propio, que es de la forma \QUEUES\nombre_de_la_cola, con nombre_de_la_cola un nombre que la define.

Dado que las colas no trabajan con secuencias de bytes individuales sino con mensajes discretos, no usan las llamadas del sistema de archivos, sino que disponen de su propio conjunto de instrucciones para leer y escribir en ellas.

DosCreateQueue
DosOpenQueue
DosReadQueue
DosWriteQueue
DosQueryQueue
DosCloseQueue

Solo el proceso que crea la cola tiene la capacidad de eliminarla o de purgar (borrar) sus elementos, así como de leer de ella; sin embargo, cualquier otro proceso puede tener acceso a ella en el sentido de escribir. Aún más interesante resulta el hecho de que es posible saltarse el orden normal de la cola y leer un elemento situado en una posición concreta. Así mismo, es posible hacer una lectura síncrona, en la cual se espera hasta que aparezca un elemento (si no había ninguno) o bien una lectura asíncrona, usando un semáforo, en la cual se pueden seguir haciendo operaciones y resincronizar cuando se desee, de forma similar a los temporizadores. Sin embargo, hay algunas restricciones en este caso: el semáforo debe ser abierto por todos los procesos que llaman a DosWriteQueue para añadir un elemento a la cola.

También es posible buscar en la cola un elemento concreto y examinarlo, pero sin eliminarlo de la cola. Además podemos saber el número de elementos que contiene la cola.

DosPeekQueue
DosPurgeQueue

El trabajo con colas es distinto que con los cauces. En los primeros, definimos una ristra de bytes que deben ser enviados a los procesos. Aquí, sin embargo, nos limitamos a enviar un puntero, el cual será el que le llegue a los demás en el orden adecuado. Los datos a enviar son almacenados en zonas de memoria compartidas.

Para verlo más claro, veamos como haríamos para enviar varios mensajes a través de una cola:

  • En primer lugar, necesitamos definir una serie de bloques de memoria compartidos, dando acceso a ellos al proceso dueño de la cola, si es necesario.
  • En segundo lugar, almacenamos el primer mensaje en uno de los bloques, y lo escribimos en la cola dando como puntero del mensaje el que apunta a dicho bloque.
  • El proceso dueño, cuando reciba el mensaje de la cola, recibirá únicamente el puntero a éste, accediendo realmente en el bloque de memoria compartida.
  • Cuando se ha terminado con la cola, se libera ésta y los bloques de memoria compartida.

Vemos, por tanto, que la cola no transmite realmente los mensajes, sino simplemente el orden en que se debe acceder a ellos, pues estos están siempre accesibles a todos los procesos por estar en memoria compartida.

Dado que la asignación de bloques de memoria es un proceso relativamente lento, la mejor solución consiste en subasignar bloques de memoria, uno para cada mensaje, dentro de un bloque mayor de memoria compartida.

Apéndice A

[editar]
  1. Llamadas DOSxxx del sistema de ficheros
  2. Llamadas DOSxxx para multitarea
  3. Llamadas DOSxxx para gestión de sesiones
  4. Llamadas DOSxxx para gestión de memoria
  5. Llamadas DOSxxx para semáforos
  6. Llamadas DOSxxx para temporizadores
  7. Llamadas DOSxxx para cauces (pipes)
  8. Llamadas DOSxxx para colas
  9. Llamadas VIOxxx para acceso a la pantalla
  10. Llamadas KBDxxx para acceso al teclado
  11. Llamadas MOUxxx para acceso al ratón

Apéndice B

[editar]
  1. Apendice B - códigos de error de OS/2
Manual de programación de OS/2 Condiciones de distribución →
Manual de programación de OS/2