Un sistema operativo (SO) es un software que actúa como intermediario entre el hardware de una computadora y las aplicaciones que ejecutan los usuarios. Su objetivo principal es gestionar los recursos del sistema, como procesos, memoria y almacenamiento. Voy a proponerte, en esta entrada del blog, un simulador de un sistema operativo simple.
En este proyecto, simularemos las siguientes funcionalidades básicas de un sistema operativo:
Gestión de Procesos: Los procesos son programas en ejecución. Implementaremos algoritmos de planificación como:
– First-Come, First-Served (FCFS): Los procesos se ejecutan en el orden en que llegan.
– Shortest Job First (SJF): Los procesos más cortos se ejecutan primero.
– Planificación por Prioridad: Los procesos con mayor prioridad se ejecutan antes.
Gestión de Memoria: Incluye la asignación de bloques de memoria a procesos usando los algoritmos:
– First-Fit: Asigna el primer bloque libre que sea suficientemente grande.
– Best-Fit: Asigna el bloque libre más pequeño que sea suficientemente grande.
Gestión de procesos en un simulador de un sistema operativo simple
La gestión de procesos es una de las funciones fundamentales de un sistema operativo (SO). Su propósito principal es supervisar, controlar y coordinar los procesos que se ejecutan en el sistema. Un proceso es una instancia de un programa en ejecución, y la gestión de estos asegura que los recursos del sistema se usen eficientemente y que las tareas se ejecuten correctamente.
A continuación, desglosamos los principales conceptos y funciones relacionadas con la gestión de procesos:
1. ¿Qué es un proceso?
Un proceso es un programa en ejecución, lo que significa que incluye:
Código del programa: Las instrucciones del programa.
Contador de programa (Program Counter): La dirección de la instrucción siguiente que se ejecutará.
Pila (Stack): Almacena datos como variables locales, direcciones de retorno y parámetros de función.
Datos (Data Section): Incluye las variables globales del programa.
Registros de CPU: Contienen información del estado actual del proceso, como registros intermedios.
En resumen, un proceso no es solo el código del programa, sino también su contexto de ejecución y su estado.
2. Estados de un proceso
Los procesos en un sistema operativo pasan por varios estados durante su ciclo de vida:
Nuevo (New): El proceso está siendo creado.
Listo (Ready): El proceso está preparado para ejecutarse, pero está esperando que el procesador esté disponible.
Ejecución (Running): El proceso está siendo ejecutado por la CPU.
Bloqueado (Blocked/Waiting): El proceso está esperando que ocurra un evento externo (como la finalización de una operación de E/S).
Terminado (Terminated): El proceso ha finalizado su ejecución.
El sistema operativo se encarga de mover los procesos entre estos estados según las condiciones del sistema.
3. Planificación de procesos
Dado que el procesador puede ejecutar solo un proceso a la vez (en sistemas de un solo núcleo), el sistema operativo utiliza algoritmos de planificación para decidir cuál proceso ejecutará la CPU.
Tipos de planificación
Planificación a corto plazo (Short-term Scheduling):
– Selecciona qué proceso será ejecutado por la CPU a continuación.
– Este es el nivel de planificación más frecuente y crítico.
Planificación a medio plazo (Medium-term Scheduling): Decide qué procesos deben ser suspendidos temporalmente para liberar recursos.
Planificación a largo plazo (Long-term Scheduling): Decide qué procesos deben admitirse en el sistema (entrada al estado «Listo»).
4. Algoritmos de planificación de procesos
Los algoritmos de planificación son reglas que determinan el orden de ejecución de los procesos. Algunos de los más comunes son:
a. First-Come, First-Served (FCFS)
Descripción: Los procesos se ejecutan en el orden en que llegan.
Ventajas: Simple y fácil de implementar.
Desventajas: Puede causar el problema de convoy effect, donde un proceso largo bloquea a los cortos.
b. Shortest Job First (SJF)
Descripción: Ejecuta primero el proceso con el menor tiempo de ejecución.
Ventajas: Minimiza el tiempo de espera promedio.
Desventajas: Puede ser injusto con procesos largos y requiere conocer el tiempo de ejecución de los procesos de antemano.
c. Planificación por prioridad
Descripción: Ejecuta primero los procesos con mayor prioridad.
Ventajas: Permite priorizar procesos críticos.
Desventajas: Puede causar inanición (starvation) para procesos de baja prioridad.
d. Round Robin (RR)
Descripción: Cada proceso recibe un tiempo fijo (cuánto de CPU), después de lo cual es enviado al final de la cola.
Ventajas: Justo para todos los procesos y efectivo en sistemas interactivos.
Desventajas: Puede aumentar el tiempo de espera si el quantum (tiempo asignado) es demasiado pequeño.
5. Estructuras de datos para la gestión de procesos
El sistema operativo utiliza estructuras de datos específicas para gestionar procesos:
Tabla de procesos (Process Table): Contiene información sobre todos los procesos del sistema, como:
– Identificador de proceso (PID).
– Estado del proceso.
– Contador de programa.
– Información de prioridad.
– Recursos asignados.
6. Gestión de multitarea
En un entorno multitarea, el sistema operativo alterna rápidamente entre procesos para dar la impresión de que se ejecutan simultáneamente. Esto se logra mediante:
Interrupciones: Señales que permiten al sistema operativo recuperar el control y cambiar de proceso.
Context Switching (Cambio de contexto): El sistema operativo guarda el estado del proceso actual y carga el estado de otro proceso.
7. Gestión de Deadlocks (interbloqueos)
Un interbloqueo ocurre cuando dos o más procesos quedan esperando recursos que nunca estarán disponibles porque están siendo retenidos mutuamente. El sistema operativo puede:
Prevenir el interbloqueo mediante restricciones en la asignación de recursos.
Detectar y recuperar interbloqueos cuando ocurren.
8. Importancia de la gestión de procesos
Maximiza la eficiencia: Asegura que los recursos del sistema se utilicen de manera óptima.
Garantiza la equidad: Permite que todos los procesos tengan acceso equitativo a los recursos.
Soporte para multitarea: Hace posible ejecutar múltiples aplicaciones simultáneamente.
Fiabilidad del sistema: Previene errores como el interbloqueo y asegura la estabilidad general.
Código de ejemplo en C de un simulador de un sistema operativo simple
El programa estará diseñado para ser compilado y ejecutado en Linux usando el IDE Geany.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
// Estructuras de datos para procesos y memoria
typedef struct {
int id;
char nombre[20];
int tiempo_ejecucion;
int prioridad;
} Proceso;
typedef struct {
int id;
int tamaño;
bool ocupado;
} BloqueMemoria;
// Funciones para gestionar procesos
void fcfs(Proceso procesos[], int n);
void sjf(Proceso procesos[], int n);
void prioridad(Proceso procesos[], int n);
// Funciones para gestionar memoria
void first_fit(BloqueMemoria bloques[], int n_bloques, int tamaño_proceso);
void best_fit(BloqueMemoria bloques[], int n_bloques, int tamaño_proceso);
// Función principal
int main() {
int opcion;
printf("Simulador de Sistema Operativo\n");
printf("Seleccione una opción:\n");
printf("1. Planificación de procesos\n");
printf("2. Asignación de memoria\n");
scanf("%d", &opcion);
if (opcion == 1) {
int n;
printf("Ingrese el número de procesos: ");
scanf("%d", &n);
Proceso procesos[n];
for (int i = 0; i < n; i++) {
printf("Ingrese los datos del proceso %d (Nombre, Tiempo de ejecución, Prioridad):\n", i + 1);
scanf("%s %d %d", procesos[i].nombre, &procesos[i].tiempo_ejecucion, &procesos[i].prioridad);
procesos[i].id = i + 1;
}
printf("Seleccione un algoritmo de planificación:\n");
printf("1. FCFS\n2. SJF\n3. Prioridad\n");
scanf("%d", &opcion);
switch (opcion) {
case 1:
fcfs(procesos, n);
break;
case 2:
sjf(procesos, n);
break;
case 3:
prioridad(procesos, n);
break;
default:
printf("Opción no válida.\n");
}
} else if (opcion == 2) {
int n_bloques;
printf("Ingrese el número de bloques de memoria: ");
scanf("%d", &n_bloques);
BloqueMemoria bloques[n_bloques];
for (int i = 0; i < n_bloques; i++) {
printf("Ingrese el tamaño del bloque %d: ", i + 1);
scanf("%d", &bloques[i].tamaño);
bloques[i].id = i + 1;
bloques[i].ocupado = false;
}
printf("Ingrese el tamaño del proceso a asignar: ");
int tamaño_proceso;
scanf("%d", &tamaño_proceso);
printf("Seleccione un algoritmo de asignación de memoria:\n");
printf("1. First-Fit\n2. Best-Fit\n");
scanf("%d", &opcion);
switch (opcion) {
case 1:
first_fit(bloques, n_bloques, tamaño_proceso);
break;
case 2:
best_fit(bloques, n_bloques, tamaño_proceso);
break;
default:
printf("Opción no válida.\n");
}
} else {
printf("Opción no válida.\n");
}
return 0;
}
void fcfs(Proceso procesos[], int n) {
printf("Planificación FCFS:\n");
for (int i = 0; i < n; i++) {
printf("Proceso %s (ID: %d) ejecutado. Tiempo de ejecución: %d\n",
procesos[i].nombre, procesos[i].id, procesos[i].tiempo_ejecucion);
}
}
void sjf(Proceso procesos[], int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (procesos[j].tiempo_ejecucion > procesos[j + 1].tiempo_ejecucion) {
Proceso temp = procesos[j];
procesos[j] = procesos[j + 1];
procesos[j + 1] = temp;
}
}
}
printf("Planificación SJF:\n");
for (int i = 0; i < n; i++) {
printf("Proceso %s (ID: %d) ejecutado. Tiempo de ejecución: %d\n",
procesos[i].nombre, procesos[i].id, procesos[i].tiempo_ejecucion);
}
}
void prioridad(Proceso procesos[], int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (procesos[j].prioridad > procesos[j + 1].prioridad) {
Proceso temp = procesos[j];
procesos[j] = procesos[j + 1];
procesos[j + 1] = temp;
}
}
}
printf("Planificación por Prioridad:\n");
for (int i = 0; i < n; i++) {
printf("Proceso %s (ID: %d) ejecutado. Prioridad: %d\n",
procesos[i].nombre, procesos[i].id, procesos[i].prioridad);
}
}
void first_fit(BloqueMemoria bloques[], int n_bloques, int tamaño_proceso) {
for (int i = 0; i < n_bloques; i++) {
if (!bloques[i].ocupado && bloques[i].tamaño >= tamaño_proceso) {
bloques[i].ocupado = true;
printf("Proceso asignado al bloque %d. Tamaño del bloque: %d\n",
bloques[i].id, bloques[i].tamaño);
return;
}
}
printf("No se encontró un bloque adecuado para el proceso.\n");
}
void best_fit(BloqueMemoria bloques[], int n_bloques, int tamaño_proceso) {
int best_index = -1;
for (int i = 0; i < n_bloques; i++) {
if (!bloques[i].ocupado && bloques[i].tamaño >= tamaño_proceso) {
if (best_index == -1 || bloques[i].tamaño < bloques[best_index].tamaño) {
best_index = i;
}
}
}
if (best_index != -1) {
bloques[best_index].ocupado = true;
printf("Proceso asignado al bloque %d. Tamaño del bloque: %d\n",
bloques[best_index].id, bloques[best_index].tamaño);
} else {
printf("No se encontró un bloque adecuado para el proceso.\n");
}
}
Explicación
Estructuras de datos:
Proceso
: Representa un proceso con su ID, nombre, tiempo de ejecución y prioridad.
BloqueMemoria
: Representa un bloque de memoria con su ID, tamaño y estado de ocupación.
Funciones de planificación:
fcfs
: Muestra los procesos en el orden de llegada.
sjf
: Ordena los procesos por tiempo de ejecución y los ejecuta.
prioridad
: Ordena los procesos por prioridad y los ejecuta.
Funciones de asignación de memoria:
first_fit
: Busca el primer bloque adecuado y lo asigna.
best_fit
: Busca el bloque más pequeño que pueda contener el proceso y lo asigna.
Con este programa te harás una idea de la manera básica cómo los sistemas operativos gestionan recursos, ayudando a comprender conceptos clave. Espero que este simulador de un sistema operativo simple te haya ayudado. Espero tus comentarios.