6. Interfaz de programación de aplicaciones
Tiempo de lectura: 18 minutos |
Un sistema operativo proporciona un entorno controlado para la ejecución de programas. Dicho entorno debe proporcionar ciertos servicios que pueden ser accedidos por los programas a través de una interfaz de programación de aplicaciones o API (Application Programming Interface).
6.1. Interfaces de programación de aplicaciones
Algunas de las API disponibles para los desarrolladores de aplicaciones son Windows API y POSIX.
6.1.1. Windows API
Windows API es el nombre que recibe la interfaz de programación de aplicaciones de Microsoft Windows, con la que prácticamente tienen que interactuar todas las aplicaciones, de una forma u otra.
Antiguamente se denominaba Win32 API, pero Microsoft ha querido aglutinar bajo una misma denominación las distintas versiones de la API de Windows que han existido, como Win16 —usada en las versiones de 16 bits de Windows— o Win64 —que es la variante de Win32 adaptada a arquitecturas de 64 bits—.
Está compuesta por funciones en C almacenadas, principalmente, en las librerías de enlace dinámico (DLL): kernel32.dll
, user32.dll
y gdi32.dll
.
Aunque según se ha ido ampliando la API, se han incorporado otras librerías adicionales.
Provee un conjunto muy amplio de servicios: E/S a archivos y dispositivos, gestión de procesos, hilos y memoria, manejo de errores, registro de Windows, interfaz a dispositivos gráficos —pantallas e impresoras— gestión de ventanas, comunicaciones en red, etc.
6.1.2. POSIX
POSIX (Portable Operating System Interface for Unix) es el nombre de una familia de estándares que definen una interfaz de programación de aplicaciones para sistemas operativos. Esto permite que un mismo programa pueda ser ejecutado en distintos sistemas operativos, siempre que sean compatibles con POSIX.
El lenguaje C fue diseñado originalmente para implementar sistemas UNIX y por eso la librería estándar de C tenía mucho parecido con la librería del sistema de UNIX. Con el tiempo, al ir añadiendo más funcionalidades, la librería del sistema de los sistemas UNIX de los distintos fabricantes fue divergiendo, haciendo muy complicado desarrollar programas que usasen las características más avanzadas y que a la vez pudieran ejecutarse en varios de ellos. Por eso el IEEE desarrolló el estándar POSIX, que define una API común para todos los UNIX y sistemas estilo UNIX modernos —como es el caso de GNU/Linux—. Así que la práctica totalidad de estos sistemas son compatible POSIX.
Por su origen, la API POSIX es un superconjunto de la API de la librería estándar de C. Por eso en los sistemas POSIX, la librería estándar de C es parte de la librería del sistema, en lugar de dos librerías separadas.
Las funciones POSIX están almacenadas, principalmente, en la librería libc
.
Aunque algunas características pueden estar en otras librerías, como libm
—la librería matemática— o libpthread
—la librería de hilos—.
Los desarrolladores del sistema a veces añaden funciones no incluidas en el estándar POSIX, con el objeto de soportar algún tipo de funcionalidad avanzada del sistema. Este es el caso de las diferentes versiones de BSD y la librería del sistema del proyecto GNU —usada generalmente en los sistemas Linux— que incluye sus propias extensiones. Además, el estándar POSIX ha tenido varias revisiones desde la primera —publicada en 1988— cada una de las cuales añade características y funcionalidades adicionales.
Antes de usar extensiones y características avanzadas debemos tener presente que:
-
Un programa que solo utilice funcionalidades hasta cierta versión de la API POSIX, podrá ejecutarse en cualquier sistema operativo compatible POSIX que implemente al menos hasta esa versión del estándar.
-
Mientras que uno que utilice, por ejemplo, alguna funcionalidad adicional no POSIX de GNU/Linux o macOS, solo podrá compilarse y ejecutarse en GNU/Linux o en macOS, según el caso.
Como la compatibilidad con diferentes sistemas puede ser algo bastante complejo de gestionar para los desarrolladores, los sistemas POSIX ofrecen macros con las que controlar qué funcionalidades del sistema están disponibles para nuestro programa. A estas macros sé las denomina macros de test de características.
Por ejemplo, el siguiente programa en C —disponible en softstack.c— realiza una serie de tareas muy sencillas: crea un archivo, muestra una serie de mensajes por la salida estándar, cierra el archivo y termina.
Sin embargo, si no se define la macro _POSIX_C_SOURCE
puede que no compile, según la versión de la librería del sistema y las opciones del compilador.
El motivo es que todas las funciones utilizadas en el programa forman parte del estándar POSIX desde hace tiempo, excepto mkstemp(), que es una función introducida en el estándar POSIX.1-2008.
Por lo que si el compilador por defecto compila para una versión anterior del estándar, esta y otras funciones definidas en especificaciones posteriores no están.
#define _POSIX_C_SOURCE 200809L (1)
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
char filename[] = "/tmp/softstack-fileXXXXXX";
mkstemp(filename); (2)
puts("Antes de abrir el archivo...");
int fd = open(filename, O_RDWR | O_CREAT);
puts("Después de abrir el archivo...");
close(fd);
return EXIT_SUCCESS;
}
1 | Definir la macro _POSIX_C_SOURCE con el valor 200809L activa que las cabeceras expongan definiciones correspondientes a la especificación POSIX.1-2008. |
2 | Genera un nombre de archivo que no exista en el sistema de archivos usando la plantilla indica por filename .
Esta función pertenece al estándar POSIX.1-2008. |
Este mecanismo tiene la doble ventaja de que:
-
Al tener que incluir las macros sabemos los requisitos mínimos del sistema donde podremos compilar y ejecutar nuestro programa, aunque no estemos preocupados por cumplir con un estándar concreto.
-
Si es un requisito del proyecto que se ejecute en sistemas con un estándar particular, solo tenemos que incluir la macro correspondiente y el compilador no nos dejará usar definiciones no incluidas en la especificación indicada.
Los sistemas POSIX pueden soportar muchas otras macros de test de características, según las especificaciones y extensiones soportadas por la librería del sistema. Las de uso más común en los sistemas GNU son:
-
_POSIX_C_SOURCE
. Según el valor asignado a esta macro se establece la especificación POSIX que debe activarse para el programa. Los valores válidos se indican en la documentación de las macros de test de características. -
_XOPEN_SOURCE
. Definiendo esta macro se indica la especificación de la Guía de portabilidad X/Open que debe activarse para el programa. Según el valor se activarán ciertas especificaciones POSIX junto con algunas extensiones adicionales. -
_BSD_SOURCE
. Activa funcionalidades específicas de los sistemas BSD. -
_DEFAULT_SOURCE
. Activa las especificaciones y extensiones por defecto, por si se diera el caso de que han sido desactivas de alguna manera. Las definiciones por defecto incluyen: POSIX.1-2008, ISO C99 y algunas funcionalidades extra de sistemas UNIX BSD y System V. -
_GNU_SOURCE
. Activa_DEFAULT_SOURCE
y extensiones específicas de los sistemas GNU.
6.2. Llamadas al sistema
Para un programa, acceder a los servicios del sistema operativo no es tan sencillo como invocar una función. Para invocar una función, un programa necesita conocer la dirección en la memoria del punto de entrada de dicha función —es decir, la ubicación de su primera instrucción—. Sin embargo, el código del núcleo del sistema puede estar en cualquier ubicación de la memoria principal. Así que las direcciones de los puntos de entrada a las funciones del núcleo son desconocidas. Además, generalmente, el código y los datos del núcleo están protegidos frente a accesos indebidos (véase el Apartado 7.3). Eso significa que para que un proceso pueda invocar los servicios que necesita hace falta un procedimiento diferente, denominado llamada al sistema.
6.2.1. Invocar llamadas al sistema
Generalmente una llamada al sistema se invoca mediante una instrucción específica en lenguaje ensamblador que genera una excepción —que no es más que una interrupción lanzada por la propia CPU al detectar instrucciones especiales o un error al ejecutar una instrucción, como una división por 0 o un acceso indebido a ciertas zonas de la memoria—.
Por ejemplo, en MIPS e Intel x86 se usa la instrucción syscall
, que lanza una excepción, haciendo que la CPU salte a una rutina en el código del núcleo del sistema, deteniendo así la ejecución del proceso que la invocó.
Al realizar una llamada, es necesario que el sistema sepa qué operación le está pidiendo el proceso.
Esto se suele hacer poniendo un número identificativo de la llamada en un registro concreto de la CPU.
Por ejemplo, en Linux para x86 la llamada al sistema open() —que se utiliza para abrir archivos— se identifica con el número 2 o con el 5, según si es en 64 o en 32 bits, respectivamente.
Este número se debe guardar en el registro v0
en MIPS o eax
en x86, antes de la instrucción syscall
.
Los números utilizados para identificar cada llamada al sistema dependen del sistema operativo. Mientras que el registro donde se guarda, la instrucción utilizada y el resto de detalles sobre cómo realizar la llamada, dependen también de la arquitectura de la CPU.
6.2.2. Paso de argumentos
Obviamente una llamada al sistema suele requerir más información que la identidad de la llamada. Si, por ejemplo, se quiere abrir un archivo, al menos es necesario indicar su nombre, así como si se abre para leer o para escribir.
En concreto hay tres métodos para pasar parámetros adicionales al identificador a una llamada al sistema:
-
Mediante registros de la CPU. Consiste en cargar los parámetros de la llamada al sistema en los registros de la CPU antes de realizar la llamada al sistema. Este método es el más eficiente, pero limita el número de parámetros al número de registros disponibles.
Es utilizado, por ejemplo, en Linux para MIPS (véase el Ejemplo 6.1) y en la mayoría de sistemas operativos para x86-64.
-
Mediante tabla en memoria Consiste en copiar los parámetros de la llamada al sistema en una tabla en la memoria principal y luego guardar la dirección de dicha tabla en un registro específico de la CPU, antes de la llamada al sistema. Así no se limita el número de parámetros que pueden ser pasados en cada llamada al sistema.
Era utilizado por Microsoft Windows 2000 y anteriores. También en Linux para x86 32 bits, cuando el número de parámetros es superior a 6.
-
Mediante la pila del proceso se insertan los parámetros de la llamada al sistema en la pila del proceso —que también se suele usar para guardar variables locales y, en algunas arquitecturas, los argumentos pasados al llamar a funciones— y el sistema operativo los recupera de allí durante la llamada al sistema. Al igual que en el caso anterior, tampoco limita el número de parámetros que pueden ser pasados en cada llamada al sistema.
Es utilizado, por ejemplo, en sistemas UNIX BSD y en Windows XP y posteriores para x86 de 32 bits.
Veamos cómo invocar directamente la llamada al sistema write() en Linux para MIPS.
Esta llamada sirve para escribir datos en un archivo. Así que necesita tres argumentos:
-
SIZE: El número de bytes a escribir.
-
BUFFER: La dirección de la memoria de la que coger los bytes.
-
FILEDES: El descriptor que identifica a un archivo abierto donde se van a escribir los datos.
Al terminar devuelve el número de bytes escritos en el archivo, que puede ser inferior a SIZE
.
El identificador de la llamada al sistema es 4004, según el listado de llamadas al sistema para Linux en MIPS.
lw $a0, FILEDES (1)
la $a1, BUFFER (1)
lw $a2, SIZE (1)
li $v0, 40004 (2)
syscall (3) (4)
1 | Cargar cada uno de los 3 argumentos de la llamada al sistema en los registros a0 , a1 y a2 . |
2 | Cargar el identificador de la llamada write() en el registro v0 . |
3 | Invocar la llamada al sistema.
Aunque vemos que es una única instrucción, lo que realmente va a ocurrir es que el sistema operativo va a tomar el control de la CPU para realizar la tarea solicitada.
La siguiente instrucción no comenzará a ejecutarse hasta que el sistema operativo no lo decida, por lo que, desde el punto de vista del programa, va a ser como si syscall fuera una instrucción más lenta de lo normal. |
4 | Al ejecutar la siguiente instrucción del código del programa, el registro v0 contendrá el número de bytes escritos. |
En syscalls.s se puede ver un ejemplo completo similar, pero para Linux x86 de 64 bits.
En cualquier caso, sea cual sea el método utilizado, el sistema operativo es responsable de comprobar de manera estricta la validez de los parámetros enviados en la llamada al sistema antes de realizar cualquier operación, puesto que nunca debe confiar en que los procesos hagan su trabajo correctamente. A fin de cuentas, una de las funciones del sistema operativo es el control de dichos procesos.
6.3. Librería del sistema
Las llamadas al sistema proporcionan una interfaz con la que los procesos pueden invocar los servicios que el sistema operativo ofrece. El problema es que como se hacen mediante instrucciones en lenguaje ensamblador (véase el Ejemplo 6.1) no son demasiado cómodas de utilizar. Así que generalmente los programas no las invocan directamente. En su lugar, lo que hacen es llamar a funciones de la librería del sistema, que a su vez son las encargadas de hacer las llamadas al sistema necesarias.
Cuando hablamos anteriormente de Windows API y del estándar POSIX, hablábamos de la interfaz de la librería del sistema en esos sistemas operativos.
La librería del sistema:
-
Es parte del sistema operativo, por lo que se distribuye con él.
-
Es una colección de clases o funciones que ofrecen los servicios del sistema operativo a los programas, apoyándose en las llamadas al sistema.
-
Constituye la verdadera interfaz de programación de aplicaciones del sistema operativo. Es la forma recomendada de solicitar servicios al sistema operativo. Invocar directamente las llamadas al sistema debe ser el último recurso.
-
Sus funciones se llaman como cualquier otra. Al igual que el resto de librerías, se carga dentro de la región de memoria asignada al proceso. Por lo tanto, la invocación de las funciones de la librería del sistema se realiza como si fueran cualquier otra función del programa.
-
Es muy común que esté implementada en C, lo que permite que tanto los programas en C como en C++ la puedan utilizar directamente.
6.4. Librería estándar
Lenguajes distintos de C y C++ pueden tener difícil usar las funciones de la librería del sistema. Pero de alguna forma deben poder hacerlo, porque sus programadores necesitan acceso a los servicios que ofrece el sistema operativo.
Incluso en C y en C++ puede ser interesante tener acceso a funcionalidades adicionales a las ofrecidas por la API del sistema operativo: estructuras de datos, algoritmos de ordenamiento o búsqueda, funciones para manipular cadenas, funciones matemáticas, etc. También abstracciones de los servicios del sistema, que encajen mejor con las particularidades del lenguaje de programación en cuestión. Por ejemplo, utilizando clases y objetos en lenguajes que soportan programación orientada a objetos.
Por eso, junto a cada intérprete o compilador de cada lenguaje de programación suele ir una librería estándar que ofrece clases o funciones con las que los programas pueden acceder a los servicios del sistema operativo y realizar las tareas más comunes de forma más sencilla.
Estas librerías generalmente no forman parte del sistema operativo, sino de las herramientas de desarrollo de cada lenguaje de programación, y constituyen la interfaz de programación de aplicaciones del lenguaje al que acompañan.
La librería estándar necesita acceder a los servicios del sistema operativo para, a su vez, dar servicio a los programas que la usan. Es decir, cuando un programa invoca alguna función o método de la librería estándar que lo acompaña, es muy probable que esta necesite invocar uno o más servicios del sistema operativo para atender la petición convenientemente. Para ello la librería estándar utiliza la librería del sistema que acompaña al sistema operativo, que a su vez realiza las llamadas al sistema necesarias.
6.5. Con todas las piezas juntas
En la Figura 6.1 se ilustra el papel de todos los elementos comentados, con el ejemplo de programas en C y Python, ejecutados en Microsoft Windows, que invocan los métodos fopen() y file()
de la librería estándar de estos lenguajes, respectivamente.
En ambos casos, la librería estándar llama a la función CreateFile() de la librería del sistema de Windows, que finalmente realiza una llamada al sistema que hace que el sistema operativo tome el control, deteniendo la ejecución del proceso que la solicita. Entonces se realiza la tarea solicitada mediante el funcionamiento coordinado de los diferentes componentes del sistema (véase el Capítulo 4).
El programa en C, puede usar tanto la función fopen() de su librería estándar como llamar directamente a la función CreateFile() de la librería del sistema —marcado en rojo en la Figura 6.1—. Sin embargo, en el programa en Python no tenemos esa facilidad —al menos directamente—.
Usar directamente las funciones de la librería del sistema desde programas en C o C++ tiene la ventaja de que permite utilizar todas las características del sistema operativo. Por ejemplo, utilizar las opciones adicionales de CreateFile():
HANDLE WINAPI CreateFile(
LPCTSTR lpFileName, (1)
DWORD dwDesiredAccess, (2)
DWORD dwShareMode, (3)
LPSECURITY_ATTRIBUTES lpSecurityAttributes, (4)
DWORD dwCreationDisposition, (5)
DWORD dwFlagsAndAttributes, (6)
HANDLE hTemplateFile (7)
);
1 | Nombre del archivo. |
2 | Modo de acceso: lectura o escritura. |
3 | Modo en el que se compartirá el archivo con otros procesos que accedan al mismo tiempo. |
4 | Permisos del archivo, en caso de crearlo. |
5 | Acción en caso de que el archivo exista o no: siempre crear, solo abrir, truncar si existe, etc. |
6 | Atributos del archivo, en caso de crearlo. |
7 | Archivo abierto del que copiar los atributos para copiarlo en este, en caso de crearlo. |
que fopen() no posee:
FILE* fopen(
const char *path, (1)
const char *mode (2)
);
1 | Nombre del archivo. |
2 | Modo de acceso: lectura o escritura. |
Sin embargo, debemos tener en cuenta que se pierde portabilidad pues CreateFile() solo está disponible en Microsoft Window, mientras que fopen() viene con la librería estándar de cualquier compilador de C.
En la Figura 6.2 se puede observar un ejemplo similar en GNU/Linux —un sistema compatible POSIX— pero en esta ocasión con programas en C y C++. En este caso la llamada al sistema es open() y tanto fopen() en C como std::ofstream::open() en C++ la utilizan. Además, ambos lenguajes pueden invocar directamente la librería del sistema —marcado en rojo en la Figura 6.2— si necesitan alguna característica adicional de la función open().
La única diferencia es que en Figura 6.2 las funciones fopen() y open() están realmente en la misma librería, porque en los sistemas POSIX la librería del sistema y la librería estándar de C pueden ser la misma, dado que el estándar POSIX se diseñó como un superconjunto de la librería estándar de C.