Guía Hardware

Syscall: cómo funciona en un sistema operativo

Actualizado a: 19 de enero de 2024

Si no sabes qué son las llamadas al sistema, también conocido como syscall, aquí te explicaré todo lo que debes saber sobre ellas y su funcionamiento, además de los motivos por los que existen estos mecanismos del sistema operativo y que resultan tan interesantes para hacer posible que las aplicaciones funcionen correctamente.

¿Qué es el kernel de un sistema operativo?

Lo primero que hay que saber, para comprender mejor lo que es una llamada al sistema es lo que es un kernel del sistema operativo. El kernel es el núcleo del SO, es decir, la parte más importante, la que proporcionará los servicios básicos que necesitan las demás partes del sistema operativo, incluidas también las aplicaciones.

El kernel es la capa más baja, la que está más estrechamente ligada al hardware y a la gestión de sus recursos. Por ejemplo, puede gestionar los procesos que se cargarán en la memoria RAM, realizar swapping en la memoria virtual, controlar diversos dispositivos, hacer que se puedan crear y manipular archivos en la unidad de almacenamiento, etc.

Cuando un ordenador arranca, primero se inicia el firmware BIOS/UEFI o similar, y éste, tras unos pasos previos, comprobará en qué medio está el sistema operativo instalado para cederle el control. Una vez hecho esto, el gestor de arranque cargará el kernel del sistema operativo en la memoria RAM para que todo comience a funcionar, cediendo el control también a este kernel.

Bien, esto es lo que tienes que entender a nivel básico de lo que es un kernel.

Funciones del kernel

Ahora bien, en cuanto a las funciones del kernel del SO, debe proporcionar una serie de interfaces necesarias también para que el usuario y las aplicaciones también puedan hacer uso de estos recursos de hardware que gestiona. Resumiendo esto, las funciones esenciales que tiene un núcleo son las siguientes:

Todos los sistemas operativos disponen de un kernel o núcleo, como las distros GNU/Linux tienen el kernel Linux, Windows 11 tiene Windows NT, macOS tiene XNU, etc.

  • Carga los controladores de dispositivos o drivers, y también del firmware. Estos son necesarios para poderse comunicar y hacer uso del hardware.
  • Gestionar los procesos y subprocesos (threads o hilos) generados por los distintos programas en ejecución.
  • También se encarga de la supervisión, del control de acceso sobre las aplicaciones, con todo lo que ello implica, como la asignación de la memoria que el espacio de usuario puede usar. Y para ello el kernel trabaja en modo supervisor o modo kernel como luego veremos.
  • Maneja los conflictos y errores por la asignación de recursos de hardware.
  • Además de la CPU y la memoria, también puede gestionar y acceder a los dispositivos de E/S.
  • Y, lo más importante, tiene una interfaz que se encarga de manejar las llamadas al sistema o syscalls que se hacen.

Por supuesto, el kernel no estará solo. Suele venir acompañado de otros elementos para componer el sistema operativo completo. Y uno de ellos es una interfaz para que el usuario pueda interactuar con el ordenador. Y eso pasa por usar, por ejemplo, una CLI, o una GUI.

Controladores de dispositivos

Otra de las partes fundamentales del kernel son los controladores de dispositivos, o drivers, como se les suele conocer. Los drivers pueden estar incrustados en el propio kernel, tratarse como módulos o ser independientes. De hecho, en función de esto, se puede catalogar el tipo de kernel que es, aunque no vamos a entrar en esto.

Los drivers son una parte fundamental, ya que es un código o programa que se encargará de interactuar directamente con el hardware a bajo nivel. De esta forma, el kernel podrá comunicarse con el hardware a través del controlador. Es decir, el objetivo de un controlador no es más que agregar soporte al núcleo para un dispositivo específico, como puede ser una GPU, un teclado, una impresora, etc.

Muchos de estos controladores ya vienen con el propio sistema operativo, para que el hardware funcione de forma correcta desde el momento de la instalación del SO. Sin embargo, otros deben instalarse tras la instalación del sistema operativo, para sacar mayor partido al componente que controlan, como pueden ser los drivers para la GPU.

Por otro lado, se puede decir que existen varios tipos de drivers para dispositivos, con formas diferentes de transferir la información. Por ejemplo:

  • De caracteres: estos drivers de dispositivos pueden abrir, cerrar, leer o escribir datos para otorgar esta capacidad al espacio de usuario también.
  • De bloque: permiten el acceso a dispositivos de almacenamiento o que emplean bloques fijos de información.
  • De red: son los que gestionan paquetes de datos de red. Es decir, para que las interfaces de red se puedan comunicar.

Esto es algo que se ve bastante bien en los sistemas tipo Unix, como en Linux, ya que siguen manteniendo esta forma de separar los tipos de dispositivos.

Por otro lado, es importante diferenciar también entre los drivers que trabajan en modo kernel o privilegiado y en modo usuario o no privilegiado:

  • Modo kernel: es un controlador genérico que se carga con el sistema operativo y controla dispositivos de hardware fundamentales para el arranque, como la CPU, la placa base, memoria RAM, etc.
  • Modo usuario: son controladores que pueden controlar ad-hoc una gran cantidad de periféricos y accesorios de todo tipo, como pueden ser impresoras, ratones, teclados, sistemas de sonido, y otros dispositivos Plug and Play. Es decir, aquellos que no son imprescindibles para el arranque del ordenador.

Nuevamente, en los sistemas como Linux, esto también se puede ver muy bien con los controladores que se agregan al núcleo monolítico o los que se sacan en forma de módulos para que sean cargados de forma dinámica cuando se conecte el dispositivo o sean necesarios.

Bien, poco a poco se van entendiendo las funciones del kernel, las de los drivers, y ahora pasamos a lo que hemos venido diciendo sobre el modo kernel o privilegiado y el modo usuario, que también es importantísimo para que comprendas qué es una syscall y para qué sirve…

Modos

¿Qué es el modo kernel o privilegiado?

Los diseñadores de ordenadores, según la arquitectura, han creado una serie de capas o anillos de seguridad que son muy importantes. Cuando ablamos del modo kernel o modo privilegiado, nos estamos refiriendo al anillo central de la imagen anterior, es decir, el que está en rojo. Ese Ring 0 es el que tiene acceso total a todos los aspectos críticos, como la gestión de los recursos de hardware, el control del sistema de archivos (FS), etc. Es decir, ese nivel de privilegio sería necesario para asignarle un espacio en la memoria RAM a cualquier programa que se pretenda ejecutar, hasta crear un archivo en la unidad de almacenamiento, cualquier tarea de E/S que se haga, incluida la red, etc.

Aunque los drivers o controladores aparezcan en otros anillos menos privilegiados que el kernel, hay que decir, como he mencionado antes, que también hay algunos drivers críticos integrados en el kernel.

¿Qué es el modo de usuario?

El modo usuario es un modo no privilegiado como se puede apreciar en la imagen. Sería el anillo en verde (Ring 3), que es el que tienen todas las aplicaciones y espacio de usuario. Por tanto, al carecer de privilegios, no podría controlar esas tareas esenciales como la gestión del hardware.

Entonces te preguntarás cómo puede un videojuego hacer uso de la GPU o ejecutarse, o cómo un procesador de texto podría guardar un archivo si no tiene privilegios para poder hacer uso de estas tareas. Pues aquí está la clave de la syscall o llamada al sistema.

¿Qué es una SCI?

Como se puede ver en la imagen superior, la capa más baja que tenemos es el hardware. Sobre el hardware tenemos el kernel del sistema operativo, en este caso Linux. Y en él los controladores para poderse relacionar directamente con ese hardware, entre otros elementos esenciales.

Luego, el kernel tiene una capa «superficial», por decirlo de algún modo que se llama SCI o System Call Interface. Una interfaz de llamadas al sistema no es más que la interfaz que permite al espacio de usuario realizar llamadas que tendrá que atender el kernel para que las apps puedan, de alguna manera, aunque sea indirectamente, acceder a esos recursos que solo se pueden acceder con privilegios.

Sobre la SCI, como se aprecia en la imagen, tenemos las bibliotecas elementales del sistema operativo, que pueden variar de un SO a otro. Estas bibliotecas o APIs sirven para entregar a las aplicaciones o programas de usuario todo lo que necesiten. Aunque esto no nos interesa aquí. Lo único importante es terminar diciendo que sobre todo esto están las aplicaciones o espacio de usuario, que es ese modo no privilegiado.

¿Qué es una syscall o llamada al sistema?

Y ahora sí, llegamos a la parte realmente importante, y es qué es eso de la syscall o llamada al sistema que hemos venido mencionando. Pues bien, una llamada al sistema no es más que una función que provee la SCI para que las apps o código del espacio de usuario puedan solicitar acceso a esos casos que tienen restringidos por falta de privilegios. Es decir, es un modo de mejorar la seguridad, para que una app no pueda acceder directamente a esos recursos.

Pero claro, como estarás pensando, para que estas llamadas se realicen, habrá que hacer algo, y tendrá que suceder algo en el sistema operativo para que se atiendan. Pues bien, eso es lo que vamos a explicar en los siguientes apartados…

¿Cómo funciona la llamada al sistema?

Bien, imagina que estás usando un editor de texto. Abres el editor de texto, y al abrirlo, se está ejecutando. Es decir, el sistema operativo ha cargado el proceso en la memoria RAM que le ha asesinado y la CPU ha accedido a este proceso para ejecutar las instrucciones y datos que ha encontrado en ese proceso para que el editor de texto funcione.

Ahora, escribes algo en el editor de texto y quieres guardar el archivo. Le asignas un nombre y guardas. Se ha guardado el archivo como ejemplo.txt. Pero si el editor de texto es una app del espacio de usuario y no es privilegiada…¿cómo ha podido tener acceso al sistema de archivos de la unidad de almacenamiento para guardar el archivo de texto? Pues lo ha hecho mediante una llamada al sistema, una syscall que habrá enviado a la SCI y el kernel se habrá encargado de ejecutar un código específico para que la creación de ese archivo y el guardado sean posibles.

Todo esto es lo que voy a tratar de explicar a continuación de forma más técnica.

Tipos de syscall

No solo existe un tipo de llamada al sistema o syscall, sino que hay varios. Algunos de los principales tipos de llamadas al sistema son:

  • Control de procesos: estas son las llamadas que permiten crear, eliminar, cambiar la prioridad, poner en espera, etc., de procesos. En este caso, están estrechamente relacionadas con el scheduler o planificador del sistema operativo, y con la gestión de la memoria y el uso de CPU.
  • Gestión de archivos: en este caso son syscalls que se dedican específicamente a la manipulación de archivos, como pueden ser crear un nuevo archivo, leer, escribir, eliminarlo, etc. En este caso están estrechamente relacionadas con el control de los sistemas de archivos (FS) de las diferentes unidades de almacenamiento disponibles, tanto las internas como las externas o extraíbles.
  • Gestión de dispositivos: son las llamadas al sistema encargadas de administrar dispositivos y que permiten hacer uso de ellos, como conectar, desconectar, liberar, etc. Están estrechamente ligadas a los periféricos o dispositivos que se pueden manejar.
  • Mantenimiento de la información: es un tipo de syscall que se usa para mantener informaciones básicas sobre el sistema operativo, como puede ser la hora, fecha, etc., que son importantes para algunas aplicaciones.
  • De comunicaciones: estas llamadas al sistema, como su propio nombre indica, se encargan de las comunicaciones entre procesos. Es decir, de gestionar los mensajes o señales que se envían los procesos para transmitir información.

Una vez vistos los tipos básicos de llamadas al sistema, es momento de conocer cómo y cuáles son estas llamadas al sistema en los sistemas operativos, ya que no son iguales en todos los SSOO. Por ejemplo, aquí tienes una tabla con algunos ejemplos en Windows NT y en Linux (también válidas para Unix):

Tipos de syscallWindowsLinux
CreateProcess()fork()
Control de procesosExitProcess()exit()
WaitForSingleObject()wait()
CreateFile()open()
ReadFile()read()
Gestión de archivosWriteFile()write()
CloseHandle()close()
SetConsoleMode()ioctl()
Gestión de dispositivosReadConsole()read()
WriteConsole()write()
GetCurrentProcessID()getpid()
Mantenimiento de informaciónSetTimer()alarm()
Sleep()sleep()
CreatePipe()pipe()
ComunicaciónCreateFileMapping()shmget()
MapViewOfFile()mmap()

Como puedes comprobar, no son iguales en un sistema que en otro, aunque tengan las mismas funciones. Esto hay que tenerlo muy en cuenta cuando se programa para una plataforma o para otra, es decir, hay que conocer bien la ABI y las bibliotecas disponibles en el sistema operativo para el que estás escribiendo el código, ya que si necesitas usar alguna de estas llamadas al sistema, tendrás que conocerla y no usar la de otro SO.

Por ejemplo, imagina la llamada read() de Linux, que es de tipo de administración de archivos y sirve para leer un archivo, como puedes imaginar por su nombre. Por ejemplo, imagina un programa que lee el contenido de un archivo, como podría ser este código fuente en C:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main()
{
    FILE* ptr;
    char ch;
 
    // Abrir archivo en modo lectura
    ptr = fopen("ejemplo.txt", "r");
 
    if (NULL == ptr) {
        printf("El archivo no puede ser abierto \n");
    }
 
    printf("El contenido de este archivo es: \n");
 
    // Mostrar el contenido
    do {
        ch = fgetc(ptr);
        printf("%c", ch);
 
        // Comprobar EOF y parar
    } while (ch != EOF);
 
    // Cerrar archivo
    fclose(ptr);
    return 0;
}

En este código fuente también se hace uso de otras llamadas al sistema, como para cerrar el archivo, una close() en este caso. Pero vamos a centrarnos solo en la de lectura o read(). Pues bien, si se compila este código fuente y se ejecuta, el procedimiento que sucedería sería el siguiente:

  1. Este pequeño programa no cuenta con privilegios, se ha ejecutado en el espacio de usuario. Sin embargo, necesita abrir, leer y cerrar un archivo. Por tanto, hará una syscall o llamada al sistema que llegará a la SCI del kernel Linux en este caso.
  2. El kernel atenderá dicha llamada en el espacio privilegiado, poniendo en marcha una rutina correspondiente a la llamada solicitada, en este caso a la de lectura. Es decir, cargará en memoria el proceso que sirve para ejecutar dicha llamada, y que la CPU atenderá.
  3. Cuando se pone en marcha la rutina, se transfiere el control al kernel mediante una Trap o interrupción. Y se asignará un código único a la llamada en el registro, para poder identificarla, ya que paralelamente puede haber otras muchas llamadas siendo atendidas e incluso otras iguales (read).
  4. El kernel, a través de su control sobre el sistema de archivos, realizará la lectura del archivo solicitado.
  5. Y una vez termina la ejecución de esta rutina read(), el kernel devuelve el control al modo usuario. Y también devolverá la salida del proceso al programa que la llamó, en este caso nuestro programa de ejemplo.
  6. Al recibir la salida, el proceso del programa seguirá ejecutándose por donde lo dejó.

El resultado sería que al ejecutar nuestro programa, pese a no tener privilegios para acceder al control del sistema de archivos, podría realizar esta tarea sin problemas y obtener el contenido del archivo solicitado. En este código de ejemplo sería un archivo llamado ejemplo.txt.

La función del código fuente en lenguaje de programación C «fopen» será la encargada en este caso de desencadenar todo esto. Esta función hace uso de la biblioteca C. Como ves, por eso es importante conocer todo lo que he comentado anteriormente sobre el kernel, bibliotecas, modos, etc. De lo contrario no entenderías esto.

¿Y cómo sabe el kernel qué debe hacer? Pues para eso, en este caso Linux, tendrá una función read correspondiente. Por ejemplo, aquí tenemos el código fuente de read() en Linux, implementada en la GNU C Library en este caso:

/* Linux read syscall implementation.
   Copyright (C) 2017-2022 Free Software Foundation, Inc.
   This file is part of the GNU C Library.
   The GNU C Library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Lesser General Public
   License as published by the Free Software Foundation; either
   version 2.1 of the License, or (at your option) any later version.
   The GNU C Library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Lesser General Public License for more details.
   You should have received a copy of the GNU Lesser General Public
   License along with the GNU C Library; if not, see
   <https://www.gnu.org/licenses/>.  */

#include <unistd.h>
#include <sysdep-cancel.h>
/* Read NBYTES into BUF from FD.  Return the number read or -1.  */
ssize_t
__libc_read (int fd, void *buf, size_t nbytes)
{
  return SYSCALL_CANCEL (read, fd, buf, nbytes);
}
libc_hidden_def (__libc_read)
libc_hidden_def (__read)
weak_alias (__libc_read, __read)
libc_hidden_def (read)
weak_alias (__libc_read, read)

Hay que decir también que dependiendo de la arquitectura o ISA sobre la que se trabaje, la interrupción será tratada de una u otra forma. Por ejemplo, en ASM o ensamblador para x86, el código resultante del programa en C anterior sería:

.LC0:
        .string "r"
.LC1:
        .string "ejemplo.txt"
.LC2:
        .string "El archivo no puede ser abierto "
.LC3:
        .string "El contenido de este archivo es: "
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     esi, OFFSET FLAT:.LC0
        mov     edi, OFFSET FLAT:.LC1
        call    fopen
        mov     QWORD PTR [rbp-8], rax
        cmp     QWORD PTR [rbp-8], 0
        jne     .L2
        mov     edi, OFFSET FLAT:.LC2
        call    puts
.L2:
        mov     edi, OFFSET FLAT:.LC3
        call    puts
.L3:
        mov     rax, QWORD PTR [rbp-8]
        mov     rdi, rax
        call    fgetc
        mov     BYTE PTR [rbp-9], al
        movsx   eax, BYTE PTR [rbp-9]
        mov     edi, eax
        call    putchar
        cmp     BYTE PTR [rbp-9], -1
        jne     .L3
        mov     rax, QWORD PTR [rbp-8]
        mov     rdi, rax
        call    fclose
        mov     eax, 0
        leave
        ret

Como se puede ver, he resaltado la línea call fopen, ya que es la instrucción que se ejecutará en la CPU para realizar esa llamada al sistema. En este caso, call es una instrucción presente en la ISA x86-64 y que sirve precisamente para eso.

Llamadas más importantes

Es necesario también destacar algunas llamadas importantes que son fundamentales para la ejecución de los programas en los sistemas informáticos, y esas llamadas son:

  • wait(): esta llamada permite poner a un proceso en espera para que otro, con mayor prioridad, se pueda completar antes. Una vez se procesa, puede retornar este proceso para su ejecución. En este caso pueden pasar varias cosas:
    • Si el proceso estaba en la memoria RAM y no se ha iniciado su ejecución, pero hay suficiente espacio en la RAM para el otro proceso prioritario, se cargará el proceso prioritario también en RAM pero se pondrá antes en la cola para que la CPU lo ejecute.
    • Si tenemos el mismo escenario anterior, pero no existiese suficiente memoria RAM como para cargar el proceso prioritario, el sistema operativo hará uso de la memoria virtual, pasando el proceso en espera a la unidad de disco y el prioritario a la RAM.
    • Si e proceso que se pone en espera ha comenzado a ser procesado por la CPU, se cambiará de contexto y se almacenará lo que se conoce como PCB. Luego se ejecutará el proceso prioritario y se recuperará de la memoria este PCB con los detalles para saber por dónde iba ejecutándose el proceso anterior.
  • fork(): esta llamada al sistema crea un fork o derivado del proceso principal. Es decir, se crea una copia de sí mismo por parte de un programa, que entonces actúa como un «proceso hijo» del proceso originario, ahora llamado «padre».
  • exec(): esta llamada al sistema se usa cuando el proceso en ejecución quiere ejecutar otro archivo ejecutable diferente. El ID del proceso sigue siendo el mismo, mientras que los otros recursos utilizados por el proceso son reemplazados por el proceso recién creado.
  • kill(): a veces, mientras trabajamos, finalizar un determinado proceso. Entonces, se llama a la syscall kill(), que envía la señal de terminación al proceso. Es decir, lo mata. Esto se ve, por ejemplo, cuando se hace uso del comando kill para terminar con un programa en Linux.
  • exit(): cuando realmente se requiere que finalicemos el programa, se utiliza una llamada al sistema de salida. Los recursos que están ocupados por el proceso se liberan después de invocar la llamada al sistema de salida.

No obstante, aunque esto sería otro tema, puede suceder que un proceso no finalice adecuadamente y se quede como zombie…, sin liberar los recursos que tiene «secuestrados», pero sin necesitarlos realmente, puesto que está «muerto».

Por cierto, cuando se programa, también se pueden crear los famosos threads o hilos, es decir, subprocesos. En este caso, un proceso se puede dividir en varias tareas más elementales conocidas así. Por tanto, la CPU se puede encargar de ellas de forma independiente, como si fuesen procesos independientes. Por ejemplo, en C se puede usar la función pthread_create para crear estos subprocesos. Mientras que todos los threads de un proceso no terminan, el proceso no ha terminado.

Conclusión

¿Qué haríamos sin las llamadas al sistema, o syscalls? Las aplicaciones no podrían funcionar correctamente, y eso implicaría escribir apps en el espacio privilegiado, lo que supondría un riesgo para la seguridad muy importante.

Espero que este artículo te haya servido y ya sepas bien qué es una syscall…

Jaime Herrera

Jaime Herrera

Ingeniero Informático apasionado por el hardware y la tecnología. Llevo más de diez años dedicándome al análisis de componentes como procesadores, tarjetas gráficas y sistemas de almacenamiento. Mi objetivo es ofrecer información clara y precisa, combinando mi experiencia técnica con un enfoque práctico para ayudar a los lectores a entender mejor el mundo del hardware.

>
Guía Hardware
Logo