Notas libro PSP
1 - Programación multiproceso
Introducción
Un ordenador moderno suele tener varias CPUs y cada una de ellas sólo puede
realizar una tarea a la vez, y para solucionar este problema trabaja en
multiproceso o multitarea dando a cada proceso un tiempo de ejecución limitado,
controlado por prioridades entre otros factores.
Procesos y sistema operativo
Un proceso es un programa en ejecución: que consiste en un código ejecutable del
programa, los datos, la pila del programa, el contador de programa, el puntero de
pila y otros registros y toda la información necesaria para ejecutar el programa.
Cada vez que el procesador cambia de proceso debe guardar todos esos datos para
recuperarlos cuando vuelva a cargar el mismo proceso donde se quedó su ejecución.
Un programa puede descomponerse en varios procesos (programación paralela, si se
atienden en el mismo equipo con múltiples CPUs o distribuída, si te atienden por
varios equipos) que se ejecutan según la prioridad que tengan.
BCP: Bloque de Control de Proceso: almacena la información acerca de un proceso:
Incluye la información:
Identificación del proceso, estado, contador del programa, registros de CPU,
información de planificación como prioridad, información de gestión de memoria,
información de tiempo de CPU consumido, información de estado de E/S, archivos
abiertos…
En consola de Linux, con el comando ps - AF podemos listar los procesos en
ejecución con los campos: UID (usuario), PID (identificador de proceso), PPID
(identificador del proceso padre), C (porcentaje de recursos de CPU utilizado), SZ
(tamaño virtual de la imagen del proceso), RSS(tamaño de la parte residente en
memoria en kilobytes), PSR (procesador que el proceso tiene actualmente asignado),
STIME (hora de inicio del proceso), TTY (terminal asociado del que lee y al que
escribe), TIME (tiempo de ejecución utilizado por proceso desde que nació), CMD
(nombre del proceso).
También podemos ver la interfaz gráfica en Linux o monitor del sistema (por consola
se abre con sudo gnome-system-monitor)
En Windows podemos ver los procesos desde la consola con el comando tasklist, que
podemos filtrar el resultado con tasklist /svc /fi “imagename eq svchost.exe” ->
filtrará todos los procesos que estén ejecutando bajo el proceso de svchost.exe
La opción gráfica de Windows sería el administrador de tareas (CTRL + Alt + Supr)
Estados de un proceso:
Nuevo.
Listo, parado temporalmente y listo para ejecutarse cuando tenga oportunidad.
En ejecución.
Bloqueado, en espera de un evento externo como la finalización de una operación de
E/S.
Terminado.
Transiciones de un proceso:
En ejecución -> Bloqueado -> Listo -> En ejecución
En ejecución -> Listo (cuando acaba el tiempo asignado al proceso)
Control de procesos en Linux
Si queremos lanzar un proceso dentro de otro en Linux podemos usar las funciones:
system(), fork() y execl()
system está en la librería stdlib.h, funciona en cualquier sistema operativo con
compilador de C/C++ como Linux o Windows. Recibe como parámetro una cadena de
caracteres que representa la instrucción a ejecutar. Si hay algún error devuelve -
1, en caso contrario devuelve el estado devuelto por la instrucción.
(probar ejemplo en linux de página 6)
creamos un fichero con touch:
touch ejemploSystem.c
lo abrimos con nano:
nano ejemploSystem.c
Guardamos con Ctrl 0 y salimos con Ctrl X.
instalamos el paquete de C:
sudo apt install gcc
y compilamos el archivo de c:
gcc ejemploSystem.c -o ejemploSystem
y ejecutamos:
./ejemploSystem
Para no comprometer la integridad del sistema se debe evitar usar la función
system() y usar mejor execl(), que recibe el nombre del programa a ejecutar con su
ruta y sus argumentos y un puntero nulo, por ejemplo:
execl(“/bin/ls”, “ls”, “-l”, (char *)NULL);
La función execl() sustituye el proceso que hace la llamada por el que se ejecuta,
por tanto no se procesan las instrucciones posteriores a la llamada a execl() del
programa original.
Creamos un fichero de prueba ejemploExec.c con touch y lo editamos con nano:
compilamos:
gcc ejemploExec.c -o ejemploExec
y ejecutamos:
./ejemploExec
En la ejecución vemos que la última instrucción no se ejecuta porque el programa se
paró al llamar a execl().
Las funciones anteriores se utilizan para ejecutar comandos, con fork() creamos un
proceso copia del padre.
Creación y ejecución de procesos:
Para evitar lo anterior (que se sustituya un proceso por otro) se usa la función
fork(), que crea un proceso hijo, copia exacta en código y datos del proceso que ha
ejecutado la llamada (proceso padre), salvo el PID y la memoria que ocupa.
La función devuelve -1 si hay errores, 0 si no hay errores y estamos en el proceso
hijo, o el PID del hijo, si nos encontramos en el proceso padre.
Para obtener el ID de un proceso usamos los métodos:
pid_t id_pactual, id_padre;
id_pactual = getpid(void); -> devuelve el id del proceso
id_padre = getppid(void); -> devuelve el id del proceso padre
Con “ps” vemos los procesos por consola de Linux.
vamos a hacer un ejemplo del uso de fork:
creamos el fichero ejemplo1Fork.c
Con wait() hacemos que el proceso padre espere la finalización del proceso hijo
(quedando bloqueado hasta que termine el hijo)
Devuelve el identificador del proceso hijo terminado.
(ver ejemplo del abuelo -> padre -> hijo en la página 10 del libro)
La ejecución sería:
Proponer actividad 1.1 de página 11
Realiza un programa en C que cree un proceso (tendremos dos procesos uno padre y
otro hijo). El programa definirá una variable entera y le dará el valor 6. El
proceso padre incrementará dicho valor en 5 y el hijo restará 5. Se deben mostrar
los valores en pantalla. A continuación se muestra un ejemplo de la ejecución:
Valor inicial de la variable: 6
Variable en Proceso Hijo: 1
Variable en Proceso Padre: 11
*************************************************************
Podemos ver el entorno de ejecución de un proceso con el comando “set” en la
consola de windows:
O con el programa Java:
Que mostraría la siguiente información al ejecutarlo:
ProcessBuilder permite un entorno de ejecución de proceso no heredado (del proceso
padre).
Comunicación entre procesos
Existen varias formas de comunicación entre procesos (Inter-Process Communication o
IPC) en Linux: pipes, colas de mensajes, semáforos y segmentos de memoria
compartida.
Pipes sin nombre.
Un pipe es una especie de fichero de conexión entre dos procesos. Uno de los
procesos escribe en el mismo y el otro lee. Cada proceso lo usa en una única
dirección (lectura o escritura). El kernel gestiona la sincronización de datos con
los pipes. Se crean con la función:
int pipe(int fd[2]);
que recibe un solo argumento , un array de dos enteros fd[0] contiene el
descriptor para la lectura y fd[1] el de escritura. Si la función tiene éxito
devuelve 0, en caso de error -1.
Para enviar datos al pipe se usa write() y para recuperar datos read().
int read(int fd, void *buf, int count);
Leemos count bytes del fichero indicado en fd y lo guardamos en buf. La función
devuelve el número de bytes leídos.
int write(int fd, void *buf, int count);
A buf le damos el valor que queremos escribir, definimos su tamaño en count y
especificamos el fichero en el que vamos a escribir en fd.
Programa de ejemplo, utilizando un fichero de texto (texto.txt) creado
previamente, y que requerirá el uso de open() y close() para abrirlo y cerrarlo.
Open tiene un parámetro que indica si se abre para lectura (0) o para escritura(1):
Otro programa de ejemplo, ya usando el método pipe(fd):
La ejecución devuelve lo siguiente:
Como con “fork()” el proceso hijo recibe una copia de todos los descriptores de
ficheros del proceso padre, se incluye copia de los descriptores del pipe creado
(fd[0] y fd[1])
Como la comunicación sólo puede establecerse cada vez en un sentido, se debe
“bloquear” el otro sentido:
Cuando el flujo de información va de padre a hijo:
El padre debe cerrar el descriptor de lectura: close(fd[0]) y escribir en el de
escritura: write(fd[1],...)
El hijo debe cerrar el descriptor de escritura: close(fd[1]) y leer en el de
lectura: read(fd[0],..)
Cuando el flujo va de hijo a padre sería lo contrario:
El padre debe cerrar el descriptor de escritura: close(fd[1]) y leer del de
lectura…
El hijo debe cerrar el de lectura close(fd[0]) y escribir en el de escritura
write(fd[1],..)
Ejemplo de envío de padre al hijo:
Resultado de ejecución (tras compilar)
Vamos a hacer otro ejemplo con tres procesos que se comunican mediante dos pipes:
Si compilamos y ejecutamos el código:
Pipes con nombre o FIFOs (First In First Out)
Los pipes anteriores establecían un canal de comunicación entre procesos
emparentados (padre-hijo). Los FIFOS permiten comunicar procesos no emparentados.
Es como un fichero con nombre, existente en el sistema de ficheros, que pueden
abrir, leer y escribir múltiples procesos. Los datos escritos se leen como en una
cola, el primero en entrar (escribirse) es el primero que sale (se lee), y una vez
leído ya no se puede leer de nuevo.
Características de los FIFOS.
Una operación de escritura en un FIFO queda en espera hasta que el proceso
pertinente abra el fichero para iniciar la lectura.
Sólo se permite la escritura cuando un proceso va a recoger la información escrita.
Para crear un FIFO podemos escribir por consola mkfifo, o usar el método mkfifo()
en un programa C.
Formato desde la consola:
mkfifo [opciones] nombreFichero p
Ejemplo:
opciones de creación:
-m -> modo-> establece los permisos igual que chmod
- - version -> muestra en la salida información sobre la versión y luego finaliza
- - help -> muestra ayuda sobre el modo de empleo del comando
Funcionamiento del FIFO:
Ejecutamos cat FIFO1 en un terminal, se quedará esperando
Desde otro terminal ejecutamos l>FIFO1 para enviar información del directorio al
FIFO1:
y vemos en la terminal inicial el efecto:
Para crear un FIFO en C:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
mkfifo(const char *pathname, mode_t modo);
donde:
pathname -> nombre del dispositivo creado
modo -> especifica los permisos de uso y el tipo de nodo que se creará, que debe
ser uno de los siguientes:
S_IFREG o 0 para especificar un fichero normal (se creará vacío)
S_IFCHR para especificar un fichero de caracteres
S_IFBLK para especificar un fichero especial de bloques
S_IFIFO para crear un fichero FIFO
Si pathname ya existe o es un enlace simbólico, la llamada fallará devolviendo
error EEXIST
mkfifo() devuelve 0 si ha funcionado bien y -1 si ocurrió algún error.
Ejemplo de uso de FIFOS:
Creamos dos programas:
fifocrea.c -> crear un FIFO y lee la información que otro programa le pase al FIFO
fifoescribe.c -> escribe información en el FIFO
Si ejecutamos el programa de creación, éste quedará a la espera:
Cada vez que ejecutemos el otro programa (fifoescribe) en otra terminal, se
escribirá un saludo en el FIFO que leerá el programa de creación.
El código de los programas es:
Sincronización entre procesos
Para que los procesos interactúen unos con otros necesitan cierto nivel de
sincronización. Podemos utilizar señales para ello.
Una señal es como un aviso que un proceso manda a otro proceso. Utilizamos la
función signal(), con el formato:
#include<signal.h>
void (*signal(int Señal, void (*Func) (int)) (int);
Recibe dos parámetros:
Señal -> contiene el número de señal que queremos capturar, por ejemplo, SIGUSR1,
definida por el usuario para uso en aplicación, o SIGKILL, usada para terminar con
un proceso.
Func -> contiene la función a la que queremos que se llame, es el manejador de la
señal (signal handler). Usaremos dos, uno para el proceso padre (void
gestion_padre(int signal)) y otro para el hijo (void gestion_hijo(int signal)).
La función devuelve un puntero al manejador previamente instalado para esa señal,
por ejemplo:
signal(SIGUSR1, gestion_padre);
Cuando el proceso reciba una señal SIGUSR1 se realizará una llamada a la función
gestion_padre().
Para enviar una señal usaremos la función kill();
#include<signal.h>
int kill(int PID, int Señal)
Recibe dos parámetros: el PID del proceso que recibirá la señal y la señal.
Ejemplo:
kill(pid_padre, SIGUSR1); -> envía una señal SIGUSR1 al proceso padre
Si queremos que un proceso espere a una señal, usamos la función pause()
int pause(void);
La función sleep() suspende al proceso que realiza la llamada el tiempo en segundos
indicado o hasta que reciba una señal.
#include<unistd.h>
unsiged int sleep (unsigned int seconds);
Ejemplo: Vamos a hacer un programa que cree un proceso hijo y el padre le enviará
dos señales SIGUSR1. Definiremos la función manejador() para gestionar la señal,
que visualizará un mensaje cuando el proceso hijo la reciba. En el proceso hijo se
usará la llamada a signal(), que decidirá qué hacer al recibir la señal, en este
caso pintará un mensaje.
Desde el proceso padre se hacen las llamadas a kill() para enviar las señales. Con
sleep() haremos que los procesos esperen un segundo antes de continuar.
La ejecución devuelve:
Veamos otro ejemplo donde un proceso padre y otro hijo se ejecutan de forma
síncrona. Se definen dos funciones manejadoras de señales, una para el padre y otra
para el hijo. Ambos procesos quedarán ejecutándose en un bucle infinito, enviándose
mensajes entre ambos. El código sería:
Para detener el proceso podemos pulsar Ctrl + C. O con el comando ps podemos ver
los procesos activos, y podemos matarlos con el comando kill pid.
2 - Programación multihilo
Introducción
Qué son los hilos
Clases para creación de hilos
Estados de un hilo
Gestión de hilos
Gestión de prioridades
Comunicación y sincronización de hilos
3 - Programación de comunicaciones en red
Introducción
Clases Java para comunicaciones en red
Qué son los sockets
Tipos de sockets
Clases para sockets TCP
Clases para sockets UDP
Envío de objetos a través de sockets
Conexión de múltiples clientes. Hilos
4 - Generación de servicios en red
Introducción
Protocolos estándar de comunicación en red
Comunicación con un servidor FTP
Comunicación con un servidor SMTP
Programación de servidores con Java
5 - Técnicas de programación segura
Introducción
Prácticas de programación segura
Técnicas de seguridad. Visión general
Seguridad en el entorno Java
Ficheros de políticas en Java
Criptografía con Java
Comunicaciones seguras con Java. JSSE
Control de acceso con Java. JAAS