Práctica de programación POSIX

Anuncio
Práctica3: Programación POSIX
Programación y Administración de Sistemas
2011-2012
Javier Sánchez Monedero
[email protected]
2o curso de Grado en Ingeniería Informática
Departamento de Informática y Análisis Numérico
Escuela Politécnica Superior
Universidad de Córdoba
26 de octubre de 2012
Resumen
Entre otras cosas, el estándar POSIX define una serie de funciones (comportamiento,
parámetros y valores devueltos) que deben proporcionar todos los sistemas operativos
que quieran satisfacer el estándar. De esta forma, si una aplicación hace un buen uso de
estas funciones deberá compilar y ejecutarse sin problemas en cualquier sistema operativo
POSIX. En esta práctica vamos a conocer qué es el estándar POSIX, y una implementación de la biblioteca C de POSIX, la biblioteca libre GNU C Library o glibc. En Moodle se
adjunta el fichero pas-practica3.tgz que contiene código de ejemplo de las funciones que iremos viendo en clase.
Índice
1. Introducción a POSIX
2
2. Objetivos
4
3. Entrega de prácticas
4
4. Documentación de POSIX y las bibliotecas
4
5. Procesado de línea de comandos tipo POSIX
5.1. Introducción y documentación . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.2. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
4
5
6. Variables de entorno
6.1. Introducción y documentación . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.2. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5
5
6
1
2
Programación y Administración de Sistemas– 2011-2012
7. Obtención de información de un usuario/a
7.1. Introducción y documentación . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.2. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
6
6
8. Ejercicio resumen 1 (0.5 punto)
7
9. Creación de procesos Fork y Exec
9.1. Introducción y documentación . . . . . . . . . . . . . . . . . . . . . . . . . . .
9.2. Ejercicios y ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
7
8
10. Señales entre procesos
10.1. Introducción y documentación . . . . . . . . . . . . . . . . . . . . . . . . . . .
10.2. Ejercicios y ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11
11
11
11. sockets POSIX
11.1. Introducción y conceptos básicos . . . . . . .
11.2. Algoritmo básico Cliente-Servidor . . . . . .
11.3. Pasos de programación C en el lado Servidor
11.4. Pasos de programación C en el lado Cliente .
.
.
.
.
12
12
13
14
14
12. Todo junto: un servidor web básico en C
12.1. Introducción y código . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12.2. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
15
15
16
13. Ejercicio resumen 2 (1 punto)
17
14. Trabajo futuro
18
Referencias
18
1.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Introducción a POSIX
POSIX es el acrónimo de Portable Operating System Interface; la X
viene de UNIX como seña de identidad de la API (Application Programming Interface, interfaz de programación de aplicaciones). Son una
familia de estándares de llamadas al sistema operativo definidos
por el IEEE (Institute of Electrical and Electronics Engineers, Instituto
de Ingenieros Eléctricos y Electrónicos) y especificados formalmente
en el IEEE 1003. Persiguen generalizar las interfaces de los sistemas
operativos para que una misma aplicación pueda ejecutarse en distintas plataformas. Estos estándares surgieron de un proyecto de
normalización de las API y describen un conjunto de interfaces de Figura 1: Mascota del
aplicación adaptables a una gran variedad de implementaciones de proyecto GNU (http:
sistemas operativos [1]. La última versión de la especificación POSIX es
del año 2008, se cono//www.gnu.org/)
ce por “POSIX.1-2008”, “IEEE Std 1003.1-2008” y por “The Open Group Technical Standard
Base Specifications, Issue 7”[2].
1. Introducción a POSIX
3
Figura 2: Dennis MacAlistair Ritchie. Colaboró en el diseño y desarrollo de los sistemas
operativos Multics y Unix, así como el desarrollo de varios lenguajes de programación como
el C, tema sobre el cual escribió un célebre clásico de las ciencias de la computación junto a
Brian Wilson Kernighan: El lenguaje de programación C [4].
GNU C Library, comúnmente conocida como glibc es la biblioteca estándar de lenguaje
C de GNU. Se distribuye bajo los términos de la licencia GNU LGPL. Esta biblioteca sigue
todos los estándares más relevantes como ISO C99 y POSIX.1-2008 [3]. gnulib, también
conocida como “Biblioteca de portabilidad de GNU” es una colección de subrutinas diseñadas para usarse en distintos sistemas operativos y arquitecturas. El objetivo de esta segunda
biblioteca es facilitar el desarrollo multi-plataforma de aplicaciones de software libre.
En los sistemas en los que se usan, estas
bibliotecas de C proporcionan y definen las
llamadas al sistema y otras funciones básicas, y son utilizadas por casi todos los programas. Sobre todo, es muy usada en los sistemas GNU y en el kernel de Linux. Cuando hablamos de Linux como sistema operativo completo debemos referirnos a él como
Figura 3: GNU + Linux = GNU/Linux
“GNU/Linux” para reconocer que el sistema lo compone tanto el núcleo Linux como las bibliotecas de C y otras herramientas de
GNU que hacen posible Linux1 .
glibc y gnulib son muy portables y soportan gran cantidad de plataformas de hardware [5]. En los sistemas Linux se instalan normalmente con el nombre de libc6 y gnulib
respectivamente. No debe confundirse con GLib2 , otra biblioteca que proporciona estructuras de datos avanzadas como árboles, listas enlazadas, tablas hash, etc. y un entorno de
orientación a objetos en C.
En estas prácticas utilizaremos la biblioteca glibc como implementación de programa1
2
http://es.wikipedia.org/wiki/Controversia_por_la_denominaci%C3%B3n_GNU/Linux
http://library.gnome.org/devel/glib/
4
Programación y Administración de Sistemas– 2011-2012
ción del API POSIX. Algunas distribuciones de GNU/Linux, como Debian o Ubuntu utilizan
una variante de la glibc llamada eglibc3 , pero a efectos de programación no debería haber diferencias.
2.
Objetivos
Conocer algunas rutinas POSIX y su implementación glibc.
Aprender a utilizar bibliotecas externas en nuestros programas.
Aprender cómo funcionan internamente algunas partes de GNU/Linux.
Mejorar la programación viendo ejemplos hechos por los desarrolladores de las bibliotecas.
Aprender a comunicar aplicaciones en red y conocer brevemente la arquitectura ClienteServidor.
3.
Entrega de prácticas
Se pedirá la entrega y defensa de algunos ejercicios resúmenes que integren varios de los
conceptos y funciones estudiadas en clase. Las prácticas deben realizarse a nivel individual
y se utilizarán mecanismos para comprobar que se han realizado y entendido.
4.
Documentación de POSIX y las bibliotecas
Documentación POSIX.1-2008: Especificación del estándar POSIX. Dependiendo de la parte que documente es más o menos pedagógica4 .
Documentación GNU C Library: Esta documentación incluye muchos de los conceptos que
ya habéis trabajado en asignaturas de introducción a la programación o de sistemas
operativos. Es una guía completa de programación en el lenguaje C, pero sobre todo incluye muchas funciones que son esenciales para programar, útiles para ahorrar
tiempo trabajando o para garantizar la portabilidad del código entre sistemas POSIX5 .
5.
Procesado de línea de comandos tipo POSIX
5.1.
Introducción y documentación
Los parámetros que procesa un programa en sistemas POSIX deben seguir un estándar
de formato y respuesta esperada6 . Un resumen de lo definido en el estándar es lo siguiente:
3
http://www.eglibc.org/home
http://pubs.opengroup.org/onlinepubs/9699919799/
5
http://www.gnu.org/software/libc/manual/
6
12.1
Utility
Argument
Syntax,
http://pubs.opengroup.org/onlinepubs/9699919799/
basedefs/V1_chap12.html
4
6. Variables de entorno
5
Una opción es un guión ’-’ seguido de un carácter alfanumérico, por ejemplo, -o.
Una opción requiere un parámetro que debe aparecer inmediatamente después de la
opción, por ejemplo, para -o lo siguiente: -o parámetro o -oparámetro.
Las opciones que no requieren parámetros pueden agruparse detrás de un guión, por
ejemplo, -lst es equivalente a -t -l -s.
Las opciones puede aparecer en cualquier orden, así -lst es equivalente a -tls.
Las opciones pueden aparecer muchas veces.
El parámetro -- indica el fin de las opciones en cualquier caso.
La opción - a menudo se indica para representar alguna entrada estándar.
La función getopt()7 del estándar ayuda a desarrollar el manejo de las opciones siguiendo las directrices POSIX.1-2008. getopt() está implementada en libc89 . Puedes ver
código de ejemplo comentado en el fichero ejemplo-getopt.c. El fichero ejemplo-getoptlong.c
contiene un ejemplo de procesado de órdenes al estilo de GNU (por ejemplo, --help y -h
como órdenes compatibles). Este segundo ejemplo no lo veremos en clase.
5.2.
Ejercicios
Descarga el código de ejemplo de getopt() de la documentación de glibc10 . Lee el
código, compílalo, ejecútalo para comprobar que admite las opciones de parámetros POSIX.
Trata de entender el código y añade más opciones (por ejemplo una sin parámetros y otra con
parámetros). Debes consultar la sección Using the getopt function de la documentación11
para saber cómo funciona getopt, qué valores espera y qué comportamiento tiene.
6.
Variables de entorno
6.1.
Introducción y documentación
Una variable de entorno es un objeto designado para contener información usada por
una o más aplicaciones. La variables de entorno se asociación a toda la máquina, pero también a usuarios individuales. Si utilizas bash, puedes consultar las variables de entorno de
tu sesión con el comando env. También puedes consultar o modificar el valor de una variable de forma individual:
$ env
$ ...
$ echo $LANG
es_ES.UTF-8
$ export LANG=en_GB.UTF-8
7
http://pubs.opengroup.org/onlinepubs/9699919799/functions/getopt.html
http://www.gnu.org/software/libc/manual/html_node/Getopt.html
9
Notas sobre portabilidad en http://www.gnu.org/software/gnulib/manual/gnulib.html#
getopt
10
http://www.gnu.org/software/libc/manual/html_node/Example-of-Getopt.html
11
http://www.gnu.org/software/libc/manual/html_node/Using-Getopt.html
8
6
Programación y Administración de Sistemas– 2011-2012
6.2.
Ejercicios
Utilizando la función getenv()12 , haz un programa que dependiendo del idioma de la
sesión de usuario, imprima un mensaje con su carpeta personal en castellano o en inglés.
Puedes ver código de ejemplo en el fichero ejemplo-getenv.c.
7.
7.1.
Obtención de información de un usuario/a
Introducción y documentación
En los sistemas operativos la base de datos de usuarios puede ser local y/o remota. Por
ejemplo, en GNU/Linux puedes ver los usuarios y grupos locales en los ficheros /etc/passwd
y /etc/group. Si los usuarios no son locales normalmente se encuentran en una máquina
remota a la que se accede por un protocolo específico. Algunos ejemplos son el servicio de
información de red (NIS, Network Information Service) o el protocolo ligero de acceso a directorios (LDAP, Lightweight Directory Access Protocol). En la actualizad NIS apenas se usa y
LDAP es el estándar para autenticar usuarios tanto en sistemas Unix o GNU/Linux como
en sistemas Windows.
En el caso de GNU/Linux, la autenticación de usuarios la realizan los módulos de autenticación PAM (Pluggable Authentication Module). PAM es un mecanismo de autenticación
flexible que permite abstraer las aplicaciones del proceso de identificación. La búsqueda de
su información asociada la realiza el servicio NSS (Name Service Switch), que provee una interfaz para configurar y acceder a diferentes bases de datos de cuentas de usuarios y claves
como /etc/passwd, /etc/group, /etc/hosts, LDAP, etc.
POSIX presenta una interfaz para el acceso a la información de usuarios que abstrae al
programador de dónde se encuentran los usuarios (en bases de datos locales y/o remotas,
con distintos formatos, etc.). Por ejemplo, la llamada getpwuid() devuelve una estructura
con información de un usuario. El sistema POSIX se encarga de intercambiar información
con NSS para conseguir la información del usuario. NSS leerá ficheros en el disco duro o
realizará consultas a través de la red para conseguir esa información.
7.2.
Ejercicios
Puedes ver las funciones y estructuras de acceso a la información de usuarios/as y grupos en los siguientes ficheros de cabecera: /usr/include/pwd.h /usr/include/grp.h.
La función getpwnam()13 permite obtener información de un usuario/a. Mira el programa
de ejemplo de getpwnam() y amplíalo para utilizar getgrgid()14 para obtener el nombre
del grupo del usuario a través del identificador del grupo. Puedes ver código de ejemplo en
el fichero ejemplo-infousuario.c.
12
http://pubs.opengroup.org/onlinepubs/009604599/functions/getenv.html
http://pubs.opengroup.org/onlinepubs/009604599/functions/getpwnam.html
http://www.gnu.org/software/libc/manual/html_node/User-Database.html
14
http://www.gnu.org/software/libc/manual/html_node/Group-Database.html
13
9. Creación de procesos Fork y Exec
8.
7
Ejercicio resumen 1 (0.5 punto)
Realizar un programa que obtenga e imprima la información de un usuario (todos los
campos de la estructura passwd) pasado por el parámetro -u. Si además se le pasa la opción
-g deberá buscar e imprimir la información del grupo del usuario (número ID del grupo y
nombre).
Si se le pasa la opción -e imprimirá el mensaje en castellano, y la opción -s hace que lo
imprima en inglés. Si no se le pasa ni -e ni -s se mirará la variable de entorno LANG para
mostrar la información. Por ejemplo, las siguientes llamadas serían válidas:
#
#
$
#
#
$
#
#
$
Obtener la información de un usuario usando el idioma
configurado en LANG
infousuario -u i02samoj
Obtener la información de un usuario forzando el
idioma en castellano
infousuario -u i02samoj -e
Obtener la información de un usuario añadiendo la
información de su grupo principal e imprimiendo todo en inglés
infousuario -u i02samoj -s -g
El control de errores debe ser el siguiente:
Asegurar que se pasa el nombre del usuario (tienes un ejemplo de cómo controlar que
se pasa el parámetro de una opción en el ejemplo-getopt.c).
Las opciones -e y -s no pueden activarse a la vez.
Sugerencia: empezar por el código de ejemplo ejemplo-getopt.c. Primero debe procesarse la entrada de comandos para asegurarse que no falta ninguna opción esencial (por
ejemplo el nombre de usuario). Después debe ir la lógica del programa dependiendo de las
opciones que se hayan reconocido. Se pueden hacer funciones para simplificar la función
main. Por ejemplo, una función para imprimir la información del usuario en castellano/inglés a la que se le pase la estructura struct passwd y una opción. Una sugerencia para el
esquema general puede ser:
1. Procesar opciones de entrada.
2. Usar la variable de entorno LANG si las “flags” de las opciones -e o -s están a cero.
3. Llamar a una función que imprima la información de un usuario en inglés o castellano
con las opciones (login, grupo si/no, idioma).
9.
9.1.
Creación de procesos Fork y Exec
Introducción y documentación
En general, en sistemas operativos y lenguajes de programación, se llama bifurcación o
fork a la creación de un subproceso copia del proceso que llama a la función. El subproceso
creado, o “proceso hijo”, proviene del proceso originario, o “proceso padre”. Los procesos
8
Programación y Administración de Sistemas– 2011-2012
pid=fork()
case 0:
// hijo
default:
// padre
Figura 4: Esquema de llamadas y procesos generados por fork() en el ejemplo.
resultantes son idénticos, salvo que tienen distinto número de proceso (PID)15 . En UNIX
otra forma de crear subprocesos es utilizar tuberías o pipes, las cuáles son esenciales para la
comunicación inter-procesos, por ejemplo:
$ find . -name "*.c" -print | wc -l
En C se crea un subproceso con llamando a la función fork()16 . Tienes un pequeño
manual y mucho código de ejemplo en [6]. Puedes ver un código con muchos comentarios
en la entrada de Wikipedia17 . El nuevo proceso hereda varias propiedades del proceso padre
(variables de entorno, descriptores de ficheros, etc. ). En esta asignatura no entraremos en
qué sucede internamente a nivel del sistema operativo. Después de una llamada exitosa a
fork habrá dos copias del código original ejecutándose a la vez. En el proceso original, el
valor devuelto de fork será el identificador del proceso hijo. En cambio, en el proceso hijo
el valor devuelto por fork será 0.
9.2.
Ejercicios y ejemplos
El listado siguiente (ejemplo-fork.c) es un ejemplo de uso de fork que controla qué
tipo de proceso es el que ejecuta el código utilizando el valor devuelto por la función, así como otras funciones POSIX para obtener información de los procesos (puedes ver un esquema
de las subprocesos creados en la Figura 4):
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
pid_t rf;
rf = fork();
switch (rf)
15
http://www.gnu.org/software/libc/manual/html_node/Processes.html
http://pubs.opengroup.org/onlinepubs/9699919799/
17
http://es.wikipedia.org/wiki/Bifurcaci%C3%B3n_%28sistema_operativo%29
16
9. Creación de procesos Fork y Exec
9
{
case -1:
printf ("No he podido crear el proceso hijo \n");
exit(-1);
case 0:
printf ("Soy el hijo, mi PID es %d y mi PPID es %d \n",
getpid(), getppid());
sleep (5);
break;
default:
printf ("Soy el padre, mi PID es %d y el PID de mi hijo es
%d \n", getpid(), rf);
sleep (10);
}
printf ("Final de ejecución de %d \n", getpid());
exit (0);
}
Un ejemplo de llamada a este código sería:
$ ./ejemplo-fork
Soy el padre, mi PID es 23455 y el PID de mi hijo es 23456
Soy el hijo, mi PID es 23456 y mi PPID es 23455
Final de ejecución de 23456
Final de ejecución de 23455
En este ejemplo el proceso padre no queda bloqueado esperando al hijo, prueba a poner
un valor menor de espera (sleep) para el padre que para el hijo:
$ ./ejemplo-fork
Soy el padre, mi PID es 23437 y el PID de mi hijo es 23438
Soy el hijo, mi PID es 23438 y mi PPID es 23437
Final de ejecución de 23437
$ Final de ejecución de 23438
Si quisiéramos que el proceso padre esperase a que el proceso (o los procesos) hijos terminasen, debemos utilizar la función wait18 . El valor devuelto por la función wait es el
PID del proceso hijo que se está esperando que termine, si se le ha pasado un PID como
argumento. Si sólo se le pasa &status, wait() espera a que terminen todos los procesos
hijos y devuelve el PID del último proceso en terminar. Un ejemplo:
#include <unistd.h>
#include <sys/wait.h>
int main(){
pid_t pid;
int status, died;
18
www.gnu.org/software/libc/manual/html_node/Process-Completion.html
10
Programación y Administración de Sistemas– 2011-2012
switch(pid=fork()){
case -1: printf("can’t fork\n");
exit(-1);
case 0 : sleep(2); // código que ejecuta el hijo
exit(3);
default: died= wait(&status); // código que ejecuta el padre
}
}
En ocasiones puede interesar ejecutar un programa distinto, no diferentes partes de él, y
se quiere iniciar este segundo proceso diferente desde el programa principal. La familia de
funciones exec() permiten iniciar un programa dentro de otro programa. En lugar de crear
una copia del proceso, como fork(), exec() provoca el reemplazo total del programa
que llama a la función por el programa llamado. Por ese motivo se suele utilizar exec()
junto con fork(), de forma que sea un proceso hijo el que cree el nuevo proceso para que
el proceso padre no sea destruido. Podemos ver este mecanismo en el siguiente código de
ejemplo:
#include
#include
#include
#include
<unistd.h>
<sys/wait.h>
<stdio.h>
<stdlib.h>
int main()
{
pid_t pid;
int status, died;
char *args[] = {"/bin/ls", "-r", "-t", "-l", (char *) 0 };
switch(pid=fork())
{
case -1:
printf("can’t fork\n");
exit(-1);
case 0 :
printf("child\n"); // código que ejecuta el hijo
execv("/bin/ls", args);
default:
died= wait(&status); // código que ejecuta el padre
}
return(0);
}
11
10. Señales entre procesos
10.
10.1.
Señales entre procesos
Introducción y documentación
Las señales entre programas son interrupciones software que se generan para informar
a un proceso de la ocurrencia de un evento. Otras formas alternativas o complementarias de
comunicación entre procesos son las tuberías POSIX (funciones popen y relacionadas) y las
las colas de mensajes POSIX o POSIX message queues (funciones mq_open, mq_send. . . ). Los
programas pueden diseñarse para capturar una o varias señales proporcionando una función que las maneje. Este tipo de funciones se llaman técnicamente callbacks o retrollamadas.
Una callback es una referencia a un trozo de código ejecutable, normalmente una función,
que se pasa como parámetro a otro código. Esto permite, por ejemplo, que una capa de bajo
nivel del software llame a la subrutina o función definida en una capa superior (ver Figura
5, fuente Wikipedia19 ).
Application program
Main program
Callback function
calls
calls
Library function
Software library
Figura 5: Esquema del funcionamiento de las callbacks o retrollamadas.
Por ejemplo, cuando se apaga GNU/Linux, se envía la señal SIGTERM a todos los procesos, así los procesos pueden capturar esta señal y terminar de forma adecuada, por ejemplo
liberando recursos, cerrando ficheros abiertos, etc. La función signal20 permite asociar una
determinada función (a través de un puntero a función) a una señal identificada por un entero (SIGTERM, SIGKILL, etc.).
#include <signal.h>
sighandler_t signal(int signum, sighandler_t handler);
...
10.2.
Ejercicios y ejemplos
El código de ejemplo ejemplo-signals.c21 contiene ejemplos captura de señales POSIX enviadas a un programa. Recuerda que la función signal() no llama a ninguna función,
19
http://en.wikipedia.org/wiki/Callback_%28computer_science%29
http://pubs.opengroup.org/onlinepubs/9699919799/functions/signal.html
21
Fuente http://www.amparo.net/ce155/signals-ex.html
20
12
Programación y Administración de Sistemas– 2011-2012
lo que hace es asociar una función del programador a eventos que se generan en el sistema,
esto es, pasar un puntero a una función.
$ ./ejemplo-signal
Cannot handle SIGKILL!
I caught the SIGHUP signal!
I caught the SIGTERM signal!
Terminado (killed)
# En otra terminal: obtener el PID del proceso
$ ps -e|grep signal
31188 pts/0
00:00:05 ejemplo-signal
$ kill -SIGHUP 31188
$ killall ejemplo-signal # mandamos señal SIGTERM
$ killall ejemplo-signal -9 # Forzamos parar el programa con
SIGKILL
11.
11.1.
sockets POSIX
Introducción y conceptos básicos
En esta sección haremos una pequeña introducción a los sockets POSIX, una herramienta que permite desarrollar aplicaciones que se comuniquen entre sí a través de una red
utilizando diferentes protocolos o facilitando el desarrollo de otros protocolos sobre TCP/IP.
Como esta no es una asignatura específica de redes, aprenderemos algunos conceptos y algoritmos básicos para la comunicación pero sin entrar en detalle del funcionamiento y de
todas las opciones disponibles. Si tienes duda sobre alguna función, hay un buen resumen
en castellano con los pasos y funciones para utilizar sockets en la web chuidiang.com [7],
nosotros usaremos algunas descripciones de este manual por ser muy sencillas y claras. A
continuación daremos algunas definiciones.
Protocolo: En una red de ordenadores hay varios ordenadores que están conectados entre si
por un medio físico (cable, ondas inalámbricas, etc.) a través del cuál puede transmitirse información. Para que exista esta comunicación debe haber un “idioma” o protocolo
común entre los equipos. Hay muchísimos protocolos de comunicación, entre los cuales el más extendido es el TCP/IP. El más extendido porque es el que se utiliza en
Internet.
socket: Un socket no es más que un “canal de comunicación” entre dos programas que
corren sobre ordenadores distintos o incluso en el mismo ordenador. Desde el punto
de vista de programación, un socket no es más que un “fichero” que se abre de una
manera especial. Una vez abierto se pueden escribir y leer datos de él con las habituales
funciones de read() y write() del lenguaje C. Existen básicamente dos tipos de
“canales de comunicación” o sockets, los orientados a conexión (por ejemplo TCP)
y los no orientados a conexión (por ejemplo UDP). Nosotros veremos un ejemplo de
funcionamiento de sockets orientados a la conexión.
arquitectura cliente-servidor: La arquitectura cliente-servidor es un modelo de aplicación
distribuida en el que las tareas se reparten entre los proveedores de recursos o servicios,
13
11. sockets POSIX
llamados servidores, y los demandantes, llamados clientes. Por ejemplo, la World Wide
Web (WWW) se implementa con este modelo. El servidor es el programa que permanece
pasivo a la espera de que alguien solicite conexión con él, normalmente, para pedirle
algún dato. Cliente es el programa que solicita la conexión para, normalmente, pedir
datos al servidor.
11.2.
Algoritmo básico Cliente-Servidor
La Figura 6 muestra el diagrama de flujo general de la comunicación Cliente-Servidor
utilizando sockets.
Diagrama de flujo
de un socket TCP
implementando el modelo
Cliente-Servidor
Servidor
socket()
bind()
Cliente
listen()
socket()
accept()
connect()
send()
recv()
closeSocket()
El cliente envía datos y
el servidor los recive
El servidor envía datos
y el cliente los recive
El cliente manda un
mensaje para finalizar
la conexión
recv()
send()
recv()
closeSocket()
Figura 6: Diagrama de flujo de la implementación del modelo Cliente-Servidor orientado a
la conexión con sockets POSIX.
14
Programación y Administración de Sistemas– 2011-2012
11.3.
Pasos de programación C en el lado Servidor
Con C en Unix/Linux, los pasos que debe seguir un programa servidor son los siguientes
[7]:
Apertura de un socket, mediante la función socket(). Esta función devuelve un descriptor de fichero normal, como puede devolverlo open() o fopen(). La función
socket() no hace absolutamente nada respecto a iniciar la comunicación, salvo devolvernos y preparar un descriptor de fichero que el sistema posteriormente asociará
a una conexión en red.
Avisar al sistema operativo de que hemos abierto un socket y queremos que asocie
nuestro programa a dicho socket. Se consigue mediante la función bind(). El sistema todavía no atenderá a las conexiones de clientes, simplemente anota que cuando
empiece a hacerlo, tendrá que avisarnos a nosotros. Es en esta llamada cuando se debe
indicar el número de servicio (o puerto) al que se quiere atender.
Avisar al sistema de que comience a atender dicha conexión de red. Se consigue mediante la función listen(). A partir de este momento el sistema operativo anotará
la conexión de cualquier cliente para pasárnosla cuando se lo pidamos. Si llegan clientes más rápido de lo que somos capaces de atenderlos, el sistema operativo hace una
“cola” con ellos y nos los irá pasando según vayamos pidiéndolo.
Pedir y aceptar las conexiones de clientes al sistema operativo. Para ello hacemos una
llamada a la función accept(). Esta función le indica al sistema operativo que nos dé
al siguiente cliente de la cola. Si no hay clientes se quedará bloqueada hasta que algún
cliente se conecte.
Escribir y recibir datos del cliente, por medio de las funciones write() y read(), que
son exactamente las mismas que usamos para escribir o leer de un fichero. Obviamente,
tanto cliente como servidor deben saber qué datos esperan recibir, qué datos deben
enviar y en qué formato, por ejemplo el protocolo HTTP y el formato HTML o XHTML.
Cierre de la comunicación y del socket, por medio de la función close(), que es la
misma que sirve para cerrar un fichero.
11.4.
Pasos de programación C en el lado Cliente
Nosotros no veremos la parte del cliente, ya que cualquier navegador web será el cliente
de nuestro servidor. Los pasos que debe seguir un programa cliente son los siguientes [7]:
Apertura de un socket, como el servidor, por medio de la función socket().
Solicitar conexión con el servidor por medio de la función connect(). Dicha función
quedará bloqueada hasta que el servidor acepte nuestra conexión o bien si no hay
servidor en el sitio indicado, saldrá dando un error. En esta llamada se debe facilitar la
dirección IP del servidor y el número de servicio que se desea.
Escribir y recibir datos del servidor por medio de las funciones write() y read().
Cerrar la comunicación por medio de close().
12. Todo junto: un servidor web básico en C
15
Figura 7: Ejemplo de ejecución del servidor web con la llamada ejemplo-webserver -p
8080 -r htdocs
12.
12.1.
Todo junto: un servidor web básico en C
Introducción y código
En esta sección vamos a ver un ejemplo que combina muchas de las funciones POSIX
que hemos visto en las prácticas. Esta sección la trabajaremos directamente sobre el código
de ejemplo ejemplo-webserver.c. Este ejemplo es un código ampliado descargado de
internet con algunas funciones auxiliares que se han añadido para facilitar los ejercicios.
Pregunta todas las dudas que tengas durante la explicación del código. El objetivo es tratar
de reconocer los pasos del modelo Cliente-Servidor de la Figura 6 a grandes rasgos y sin
perderse en los detalles, ya que no se pedirá ninguna modificación de las partes de código
relativas a la comunicación.. La Figura 8 muestra el funcionamiento general del código del
servidor web combinando el esquema de la Figura 6 con fork() para poder atender varias
conexiones de manera simultánea.
16
Programación y Administración de Sistemas– 2011-2012
Servidor
Diagrama de flujo
del ejemplo de
servidor web básico
El socket creado por el proceso
principal debería ser cerrado
al finalizar el programa,
por ejemplo capturando las
señales SIGTERM, SIGINT...
socket()
startServer()
bind()
listen()
Cliente
La llamada a accept() crea un nuevo
socket para atender la conexión
que deberá ser cerrado al finalizar
el intercambio de datos.
socket()
accept()
connect()
while(1) {
...
fork()
...
}
fork()
GET /hola.html HTTP/1.1
recv()
send()
respond()
HTTP/1.0 200 OK
....
Datos
send()
recv()
closeSocket()
El cliente manda un
mensaje para finalizar
la conexión
recv()
closeSocket()
Figura 8: Diagrama de flujo de la implementación del modelo Cliente-Servidor orientado a
la conexión con sockets POSIX.
12.2.
Ejercicios
Arranca y para el servidor siguiendo la ayuda de la línea de comandos. Prueba a conectarte a él a través del navegador y prueba a cambiar las opciones. Si no sabes HTML, mira
los ficheros de ejemplo del directorio htdocs del paquete de prácticas.
Para arrancar el servidor, que escuche en el puerto 8080 y que utilice el directorio htdocs
como raíz para servir ficheros web:
$ ./ejemplo-webserver -p 8080 -r htdocs
ahora visita la dirección http://localhost:8080/ y comprueba que puedes navegar por
las páginas alojadas en el servidor. En el navegador deberías obtener un resultado similar al
de la Figura 7. Puedes probar a ejecutar el servidor en ts.uco.es, en este caso la dirección
17
13. Ejercicio resumen 2 (1 punto)
sería http://ts.uco.es:8080/. Si esto lo hacéis varias personas, tal vez tengáis que
cambiar el puerto porque sólo una aplicación puede escuchar en un puerto, este mensaje
de error os indicaría que ya hay una aplicación asociada al puerto que pretendéis usar:
$ ./ejemplo-webserver -p 8080 -r htdocs
$ socket() or bind(): Address already in use
Este mensaje de error puede obtenerse aún cuando haya finalizado la ejecución de programa que escucha el puerto. Aunque el documento es antiguo y la explicación tal vez no
sea precisa, puedes leer una explicación de por qué pasa esto en [8]
13.
Ejercicio resumen 2 (1 punto)
Para este ejercicio partiremos del código ejemplo-webserver.c. El Makefile del paquete de prácticas ya incluye una regla para compilar el fichero webserverex.c. Ejercicios:
1. La función webServerLog() implementa un pequeño sistema de registro o log (no
óptimo, por supuesto). Esta función es similar a printf(). Utilizándola se pueden
registrar mensajes de inicio o parada del servidor o los accesos al servidor en el fichero
access.log y los errores en el fichero error.log. Por ejemplo, puedes hacer las
siguientes llamadas:
webServerLog(WS_LOG_ACCESS, \
"Servidor iniciado en el puerto %s", PORT);
webServerLog(WS_LOG_ERROR, "Error en la conexión");
No necesitas reemplazar todos los mensajes, pero si los más significativos: cuando no
se puede iniciar el servidor por estar el puerto ocupado, el inicio o parada del servidor
y las peticiones HTTP que llegan al servidor.
2. Modifica el código del servidor web para que tenga una nueva opción -f. Cuando se
pase esta opción, el servidor deberá incluir una foto del Fary en todas las páginas web
que sirva. Una imagen se incluye en HTML con el código
<img src="fichero.jpg" alt="Texto alternativo"> insertado entre las etiquetas <BODY> y </BODY>, que es donde va el contenido en si de una página HTML.
Figura 9: El Fary
18
Programación y Administración de Sistemas– 2011-2012
Ten en cuenta que el navegador hará una petición por cada fichero .html, y otra(s) por
cada imagen (u otros datos) contenidos en la web. Así, si una web tiene una imagen,
el navegador pedirá primero el fichero .html con un GET /hola.html HTTP/1.1 y
cuando lo abra y detecte la etiqueta <img ...> hará otra petición GET /fichero.jpg
HTTP/1.1 para que el servidor le envíe el fichero de la imagen22 . Ayúdate y modifica
si lo necesitas las funciones auxiliares strcasestr()23 y copyfile(). Acuérdate de
actualizar la ayuda del programa.
3. Captura algunas llamadas al sistema (SIGTERM, SIGINT y SIGHUP) para gestionar el
fin del programa adecuadamente. Puedes asociar estas tres señales con la misma función de parar el servidor. Se deberían finalizar y cerrar adecuadamente las conexiones
y ficheros abiertos. Opcionalmente puedes hacer un script de bash que arranque y pare
el servidor ejecutando el servidor y enviándole la señal SIGTERM respectivamente. Por
ejemplo, start-server.sh y stop-server.sh.
4. Captura la URL solicitada por el cliente. Si se pide la URL http://servidor:puerto/
infousuario/i02samoj el servidor debe generar una web con la información del
usuario. Con el código del ejercicio resumen 1 y usando un fichero temporal no debería ser muy difícil.
Para el segundo ejercicio, una llamada similar a
$ ./ejemplo-webserver -p 8080 -r htdocs/ -f
debería producir un resultado similar en el navegador al de la Figura 10.
14.
Trabajo futuro
Algunas ideas que no ha dado tiempo a hacer:
Utilizar el patrón de diseño singleton para guardar la configuración del programa.
Controlar la destrucción de subprocesos del servidor pasado un tiempo.
Comunicación inter-procesos con diferentes alternativas (tuberías, POSIX MQ, etc.).
Capturar cómo ha muerto un proceso WEXITSTATUS, WTERMSIG...
Hacer que el servidor sirva código generado con PHP (por ejemplo recogiendo la salida
de php ejemplo.php)
22
La petición de estos ficheros auxiliares se hace al servidor que sirve la web, pero también se puede hacer a
otro si el fichero se encuentra en otro sitio web. Esto ocurre por ejemplo cuando se inserta un vídeo de youtube o
similares en una página.
23
En el código del servidor web se proporciona la función strcasestr(cadena, subcadena) para facilitar uno de los ejercicios. Esta función busca una subcadena dentro de una cadena. Devuelve un 0 si no encuentra
la subcadena o la posición de la subcadena si la encuentra, es decir, un valor mayor que cero.
14. Trabajo futuro
19
Figura 10: Ejemplo del resultado de la ejecución del servidor web para implementar la opción -f.
20
Programación y Administración de Sistemas– 2011-2012
Referencias
[1] Wikipedia. Posix – wikipedia, la enciclopedia libre, 2012. [Internet; descargado 12abril-2012]. Available from: http://es.wikipedia.org/w/index.php?title=
POSIX&oldid=53746603.
[2] The IEEE and The Open Group. Posix.1-2008 – the open group base specifications issue 7,
2008. Available from: http://pubs.opengroup.org/onlinepubs/9699919799/.
[3] Proyecto GNU. Gnu c library, 2012.
software/libc/libc.html.
Available from: http://www.gnu.org/
[4] Brian W. Kernighan, Dennis Ritchie, and Dennis M. Ritchie. C Programming Language
(2nd Edition). Pearson Educación, 2 edition, 1991.
[5] Wikipedia. Glibc – wikipedia, la enciclopedia libre, 2012. [Internet; descargado 12abril-2012]. Available from: http://es.wikipedia.org/w/index.php?title=
Glibc&oldid=53229698.
[6] Tim Love. Fork and exec, 2008. Available from: http://www-h.eng.cam.ac.uk/
help/tpl/unix/fork.html.
[7] chuidiang.com. Programación de sockets en c de unix/linux, 2007. Available from:
http://www.chuidiang.com/clinux/sockets/sockets_simp.php.
[8] Andrew Gierth Vic Metcalfe and other contributers. Programming UNIX Sockets in
C - Frequently Asked Questions. 4.2 Why don’t my sockets close?, 1996. Available from: http://www.softlab.ntua.gr/facilities/documentation/unix/
unix-socket-faq/unix-socket-faq-4.html#ss4.2.
[9] Wikipedia. Dennis ritchie – wikipedia, la enciclopedia libre, 2012. Available from: http:
//es.wikipedia.org/wiki/Dennis_Ritchie.
Descargar