Ir al contenido

6. Interfaz de programación de aplicaciones

Cómo accede un programa a los servicios del sistema operativo a través de distintas capas: llamadas al sistema, librería del sistema y librería estándar.

23 min de lectura

Un sistema operativo proporciona un entorno controlado para la ejecución de programas. Como hemos comentado en capítulos, 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 —acrónimo del inglés Application Programming Interface.

Existen muchas API diferentes, según el sistema operativo al que nos refiramos. Algunas de las disponibles para los desarrolladores de aplicaciones para sistemas de propósito general son Windows API y POSIX.

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 las 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.

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.

Sus orígenes se remontan a los años 80, cuando la industria estaba dominada por los sistemas UNIX y sus diferentes variantes. 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. Para resolver este problema, el IEEE desarrolló el estándar POSIX, que define una API común para todos los sistemas tipo UNIX modernos —como es el caso de GNU/Linux—. Así que la práctica totalidad de estos sistemas son compatibles 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 UNIX 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 se las denomina feature_test_macros.

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.

softstack.c
#define _POSIX_C_SOURCE 200809L
#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);
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;
}

Este mecanismo tiene la doble ventaja de que:

  1. 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.

  2. 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 feature_test_macros, 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 feature_test_macros.

  • _XOPEN_SOURCE. Definiendo esta macro se indica la especificación de la Guía de portabilidad X/Open que debe activarse para el programa. X/Open es un consorcio de fabricantes de sistemas operativos que desarrolló un estándar de portabilidad para sistemas UNIX. Según el valor se activarán ciertas especificaciones POSIX junto con algunas extensiones adicionales del ecosistema UNIX que no están presentes en el estándar POSIX.

  • _BSD_SOURCE. Activa funcionalidades específicas de los sistemas UNIX BSD.

  • _DEFAULT_SOURCE. Activa las especificaciones y extensiones por defecto en sistemas GNU, por si se diera el caso de que han sido desactivadas 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.

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 (ver el apartado «Protección de la memoria»). Eso significa que para que un proceso pueda invocar los servicios que necesita hace falta un procedimiento diferente, denominado llamada 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 y en x86-64 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. Mientras que en MIPS la misma llamada al sistema se identifica con el número 4005. Este número se debe guardar en el registro v0 en MIPS o eax / rax en x86, antes de invocar la excepción correspondiente.

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.

Este cambio de modo de ejecución, junto con el trabajo adicional que conlleva —guardar el contexto del proceso, saltar a la rutina del núcleo correspondiente y, al terminar, restaurar el contexto—, hace que una llamada al sistema sea considerablemente más lenta que una llamada a una función convencional1, aunque desde el código del programa ambas puedan invocarse de forma parecida.

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 de una llamada al sistema, pero 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. Como hemos comentado en capítulos anteriores, una de las funciones del sistema operativo es la supervisión de los procesos. Si el sistema operativo no comprobara la validez de los parámetros, un proceso podría corromper la memoria del núcleo del sistema y provocar que el sistema operativo dejara de funcionar correctamente.

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, de forma que algunos sistemas operativos limitan el número de parámetros que pueden ser pasados en cada llamada al sistema o usan otro método cuando se superan los registros disponibles.

Es utilizado, por ejemplo, en Linux para MIPS (ver el siguiente ejemplo) y en la mayoría de sistemas operativos para x86-64, ya que son arquitecturas con un gran número de registros de propósito general. En Linux para x86-32 también se utiliza este método, pero hay que tener en cuenta que es una regla de diseño de Linux que las llamadas al sistema no pueden tener más de 6 argumentos. Si un programador del núcleo quiere añadir una llamada al sistema con más de 6 parámetros, está obligado a definir una struct con todos los parámetros y pasar un puntero a esa struct como primer parámetro de la llamada al sistema.

Ejemplo Llamada al sistema en Linux MIPS

Sección titulada « Llamada al sistema en Linux MIPS»

Veamos cómo invocar directamente la llamada al sistema write() en Linux para MIPS. En syscall.s se puede observar un ejemplo completo similar, pero para Linux x86-64.

La llamada al sistema write() sirve para escribir datos en un archivo, por lo 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.

Para poder invocar esta llamada al sistema en Linux para MIPS:

  • Se utiliza la instrucción syscall, que genera una excepción y hace que el sistema operativo tome el control de la CPU para realizar la tarea solicitada.

  • Para indicar al sistema operativo qué llamada al sistema se quiere invocar, se carga el identificador de la llamada en el registro v0. Para write() ese identificador es 4004, según vemos en unistd.h en el código fuente de Linux.

  • Los tres argumentos de la llamada al sistema se cargan en los registros a0, a1 y a2, según el orden en que se indican en la documentación de la llamada al sistema.

lw $a0, FILEDES
la $a1, BUFFER
lw $a2, SIZE
li $v0, 4004
syscall # v0 = valor de retorno: número de bytes escritos

Al volver de la llamada al sistema, el registro v0 contiene el valor de retorno de la llamada, que en el caso de write() es el número de bytes escritos en el archivo, o un código de error indicando el tipo de error.

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 preestablecido 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 ya que la tabla puede ser tan grande como se necesite.

Este método era utilizado por Microsoft Windows 2000 y anteriores para cualquier llamada al sistema. También en versiones antiguas de Linux para x86-32, cuando el número de parámetros de la llamada es superior a 5.

Ejemplo Llamada al sistema en Linux x86-32

Sección titulada « Llamada al sistema en Linux x86-32»

Veamos cómo se invocaba directamente la llamada al sistema mmap() en las primeras versiones de Linux para x86-32. Esta llamada al sistema mmap() sirve para mapear un archivo o dispositivo en la memoria del proceso, por lo que necesita seis argumentos: ADDR, LEN, PROT, FLAGS, FD y OFFSET. En aquel momento, la convención de llamada al sistema de Linux para x86-32 solo reservaba registros de la CPU para 5 argumentos. Como mmap() necesita 6, se optó por copiar los seis parámetros, en el orden indicado antes, en una tabla en memoria y pasar únicamente la dirección de dicha tabla en el registro ebx.

Para poder invocar esta llamada al sistema:

  • Se copian los seis argumentos, en orden, en una tabla (ARGS) reservada en la memoria del proceso.

  • Se carga la dirección de la tabla en el registro ebx.

  • Se carga el identificador de la llamada al sistema en el registro eax. Para mmap() en x86-32 ese identificador es 90.

  • Se utiliza la instrucción int 0x80, que genera una excepción y hace que el sistema operativo tome el control de la CPU para realizar la tarea solicitada. En versiones posteriores de Linux para x86-32, esta instrucción fue sustituida por mecanismos de entrada al núcleo más rápidos, incorporados posteriormente a la arquitectura x86.

ARGS: .long ADDR
.long LEN
.long PROT
.long FLAGS
.long FD
.long OFFSET
lea ebx, ARGS
mov eax, 90
int 0x80 ; eax = valor de retorno

Al volver de la llamada al sistema, el registro eax contiene el valor de retorno, que en el caso de mmap() es la dirección de memoria donde se ha realizado el mapeo, o un código de error indicando el tipo de error.

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 los argumentos de las 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 ya que se está usando la memoria del proceso.

Es utilizado, por ejemplo, en sistemas UNIX BSD y en Windows XP y posteriores para x86-32. También es utilizado por las versiones modernas de Windows para x86-64, aunque en este caso solo para las llamadas al sistema que requieren más de 4 parámetros, ya que los 4 primeros se pasan en registros de la CPU y los restantes en la pila del proceso.

Ejemplo Llamada al sistema en FreeBSD x86-32

Sección titulada « Llamada al sistema en FreeBSD x86-32»

Veamos cómo invocar directamente la llamada al sistema write() en FreeBSD para x86-32.

La llamada al sistema write() necesita los mismos tres argumentos que en el ejemplo anterior en Linux para MIPS: SIZE, BUFFER y FILEDES. Para poder invocar esta llamada al sistema en FreeBSD para x86-32:

  • Se insertan los tres argumentos en la pila, en el orden en que se indican en la documentación de la llamada al sistema.

  • Se carga el identificador de la llamada al sistema en el registro eax. Para write() ese identificador es 4.

  • A diferencia de Linux, la rutina de FreeBSD que atiende la excepción generada por la instrucción int 0x80 espera encontrar en la cima de la pila un valor adicional del que no hace uso —normalmente la dirección de retorno de una llamada a función, ya que la convención que sigue FreeBSD imita a la de una llamada en C—. Por eso, en lugar de ejecutar int 0x80 directamente, se invoca una rutina auxiliar mediante la instrucción call, que se encarga de colocar esa dirección de retorno en la pila automáticamente.

push SIZE
push BUFFER
push FILEDES
mov eax, 4
call invocar_syscall
add esp, 12
...
invocar_syscall:
int 0x80 ; eax = valor de retorno
ret

Al volver de la llamada al sistema, el registro eax contiene el valor de retorno, que en el caso de write() es el número de bytes escritos en el archivo, o un código de error indicando el tipo de error.

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 (ver el ejemplo anterior) 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. Por tanto, 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 una colección de clases o funciones que ofrecen los servicios del sistema operativo a los programas, apoyándose en las llamadas al sistema.

    Algunas funciones de la librería del sistema son traducciones literales de llamadas al sistema —por ejemplo, write() o close()— mientras que otras pueden ser más complejas, hacer más trabajo o mostrar conceptos más abstractos que los usados por el sistema operativo en sus 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.

  • Es parte del sistema operativo, por lo que se distribuye con él.

  • 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, a diferencia de las llamadas al sistema, 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. El resto de lenguajes pueden usarla si tienen un mecanismo para invocar funciones en C, que es algo muy común en la mayoría de los lenguajes.

Lenguajes distintos de C y C++ pueden tener más 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. Además, 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, muchos lenguajes de programación soportan el concepto de flujo de datos —o streams—, que es una abstracción de los archivos y dispositivos de E/S del sistema operativo. O en los lenguajes con orientación a objetos, puede ser conveniente que los servicios del sistema operativo se presenten como objetos y métodos, en lugar de funciones y estructuras.

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.

La librería de tiempo de ejecución —o runtime library— es el código que acompaña al lenguaje de programación para proporcionar el entorno de ejecución necesario para que los programas puedan ejecutarse.

En lenguajes compilados como C y C++, esta librería es relativamente pequeña. Se invoca en el punto de entrada del proceso, antes de que se ejecute la función main(), y se encarga de tareas como: inicializar variables globales y estáticas, configurar los flujos de E/S estándar —para poder leer del teclado y escribir en la pantalla—, inicializar las estructuras de datos necesarias para la gestión de la memoria dinámica o leer y preparar los argumentos de la línea de comandos —que posteriormente se pasarán a la función main(), donde empieza la ejecución del código del programa—. Igualmente, al terminar la ejecución del programa, se encarga de liberar los recursos utilizados, de invocar las funciones registradas con atexit() y de terminar el proceso usando el código de salida que haya devuelto la función main(). En el caso de C++, también se encarga de invocar los destructores de los objetos globales y estáticos al terminar y de gestionar las excepciones que se produzcan durante la ejecución del programa.

En otros lenguajes de programación, la librería de tiempo de ejecución puede ser mucho más compleja. Por ejemplo, en lenguajes con gestión automática de memoria, la librería de tiempo de ejecución incluye el código del recolector de basura o algún otro mecanismo de gestión de memoria. En lenguajes que usan una máquina virtual, como Java o C#, el entorno de tiempo de ejecución es aún más complejo, incluyendo el intérprete o máquina virtual encargado de ejecutar el código del programa.

Un ejemplo del papel de las librerías estándar lo podemos encontrar en el acceso a los archivos.

Las llamadas al sistema y la librería del sistema de los sistemas operativos ofrecen funciones básicas para manipular archivos. Estos se abren indicando su ruta y, al hacerlo, el sistema operativo devuelve un identificador del archivo abierto (ver la función open()). Este identificador se puede usar para leer o escribir en bytes el contenido del archivo.

Sin embargo en C, C++ y otros lenguajes, todo lo que son flujos de datos se generalizan en el concepto de flujo o stream (ver <stdio.h> e std::iostream). En él se incluye tanto la entrada de teclado y la salida por pantalla, como la impresión de documentos, las conexiones de red —potencialmente— y el acceso a archivos y a dispositivos. Los flujos pueden ser de texto o binarios, lo que implica algunas transformaciones en los datos. Además van ligados al concepto del buffering, es decir, que los bytes o caracteres escritos en el flujo no se «envían» inmediatamente, sino que se acumulan en la memoria para ser enviados en bloque.

Todas estas características adicionales las implementa la librería estándar. Pero por debajo, al final, los datos tienen que ser escritos en un archivo, una impresora o el monitor, recursos que gestiona el sistema operativo. Por lo tanto, las librerías estándar necesitan hacer uso de la librería del sistema para comunicarse con el sistema operativo.

Algo que suele ocurrir al crear mayores abstracciones es que se pierde control y características específicas. Por ejemplo, la llamada al sistema open() con la que se pueden crear archivos permite asignar permisos o crear archivos temporales. Sin embargo, con las interfaces de streams de C y C++ no se puede hacer eso, ya que los permisos y la temporalidad del almacenamiento son propiedades de los archivos que no son comunes a todas fuentes de flujos de datos.

Así que en ocasiones puede ser que nos resulte más útil llamar a las funciones de la librería del sistema, que usar las facilidades de la librería estándar. Sin embargo, debemos valorar que así perdemos portabilidad, ya que ahora nuestro programa ya no podrá usarse solo con que haya un compilador o intérprete de nuestro lenguaje, sino que además tendrá que ser en sistemas operativos con una librería del sistema compatible.

En la siguiente figura se ilustra el papel de todos los elementos comentados, con el ejemplo de programas en C y Python, ejecutados en Microsoft Windows, que invocan las funciones fopen() y open() de la librería estándar de estos lenguajes, respectivamente.

Elementos de la interfaz de programación de aplicaciones en Microsoft Windows.

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.

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 anterior—. Sin embargo, en el programa en Python no tenemos esa facilidad —al menos directamente—.

Como comentamos anteriormente, 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,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile
);
  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. Son los mismos que se pueden ver en el Explorador de Windows: oculto, solo lectura, comprimido, etc.
  7. Archivo abierto del que copiar los atributos en este, en caso de crearlo.

que fopen() no posee:

FILE* fopen(
const char *path,
const char *mode
);
  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 Windows, mientras que fopen() viene con la librería estándar de cualquier compilador de C.

En la siguiente figura 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 siguiente— si necesitan alguna característica adicional de la función open().

Elementos de la interfaz de programación de aplicaciones en GNU/Linux.

La única diferencia es que en la figura anterior 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.

Lenguaje C

atexit«función»atexit

Registra una función para que se invoque automáticamente cuando el programa termine.

int atexit(void (*function)(void));
fopen«función»fopen

Abre un archivo y devuelve un puntero de flujo (FILE*).

FILE* fopen(const char* pathname, const char* mode);
main«función»main

Punto de entrada de un programa C. Recibe los argumentos de la línea de comandos.

int main(int argc, char* argv[]);

Lenguaje C++

std::iostream«clase»std::iostream

Clase base para flujos de entrada/salida bidireccionales.

explicit iostream(std::streambuf* sb);
std::ofstream
std::ofstream::open«método»std::ofstream::open

Abre un archivo para escritura en un objeto ofstream.

void open(const char* filename,
std::ios_base::openmode mode = std::ios_base::out);

POSIX

close«función»close

Cierra un descriptor de archivo.

int close(int fd);
feature_test_macros«macro»feature_test_macros

Macros para controlar qué funcionalidades POSIX están disponibles al compilar.

#define _POSIX_C_SOURCE 200809L
#define _XOPEN_SOURCE 700
#define _GNU_SOURCE
mkstemp«función»mkstemp

Crea un archivo temporal único y lo abre.

int mkstemp(char* template);
mmap«función»mmap

Proyecta un archivo o dispositivo en la memoria del proceso.

void* mmap(void* addr, size_t length, int prot, int flags, int fd,
off_t offset);
open«función»open

Abre o crea un archivo y devuelve un descriptor de archivo.

int open(const char* pathname, int flags);
int open(const char* pathname, int flags, mode_t mode);
write«función»write

Escribe datos en un descriptor de archivo.

ssize_t write(int fd, const void* buf, size_t count);

Python

open«función»open

Abre un archivo y devuelve un objeto flujo (file object).

open(file, mode="r", encoding=None)

Windows API

CreateFile«función»CreateFile

Crea o abre un archivo, dispositivo, directorio o tubería.

HANDLE CreateFile(LPCTSTR lpFileName,
DWORD dwDesiredAccess,
DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes,
HANDLE hTemplateFile);
  1. Abhinav Upadhyay, «What Makes System Calls Expensive», Coding Confessions, 16 de septiembre de 2025.