Programacion GNULinux
Programacion GNULinux
Programacion GNULinux
Programación de Sistemas
Pablo Garaizar Sagarminaga
[email protected]
GNU/Linux:
Programación de
Sistemas
email: [email protected]
web: http://paginaspersonales.deusto.es/garaizar
blog: http://blog.txipinet.com
1. PROGRAMACIÓN EN GNU/LINUX...........................................5
1.1 Llamadas al sistema..........................................................5
1.2 Programas, procesos, hilos................................................6
1.2.1 Estructuras de datos...............................................7
1.2.2 Estados de los procesos en Linux...........................8
1.2.3 Identificativos de proceso.......................................9
1.2.4 Planificación..........................................................11
1.3 El GCC.............................................................................12
1.3.1 Compilación básica...............................................12
1.3.2 Paso a paso...........................................................13
1.3.3 Librerías................................................................14
1.3.4 Optimizaciones.....................................................14
1.3.5 Debugging............................................................15
1.4 make world.............................................................. ........15
1.4.1 Makefile, el guión de make...................................16
1.5 Programando en C para GNU/Linux.................................20
1.5.1 Hola, mundo!........................................................20
1.5.2 Llamadas sencillas................................................21
1.5.3 Manejo de directorios...........................................32
1.5.4 Jugando con los permisos.....................................35
1.5.5 Creación y duplicación de procesos.....................38
1.5.6 Comunicación entre procesos..............................43
1.5.7 Comunicación por red...........................................62
2. LICENCIA....................................................................80
ii
Índice de figuras
iii
Índice de tablas
iv
1.Programación en
GNU/Linux
E
n este texto repasaremos conceptos de multiprogramación como las
definiciones de programa, proceso e hilos, y explicaremos el
mecanismo de llamadas al sistema que emplea Linux para poder
aceptar las peticiones desde el entorno de usuario.
5
Programación de Sistemas 6
Generalmente un proceso:
• Es la unidad de asignación de recursos: el Sistema
Operativo va asignando los recursos del sistema a cada
proceso.
• Es una unidad de despacho: un proceso es una entidad
activa que puede ser ejecutada, por lo que el Sistema
Operativo conmuta entre los diferentes procesos listos
para ser ejecutados o despachados.
txipi@neon:~$ ps xa
PID TTY STAT TIME COMMAND
1? S 0:05 init [2]
Programación de Sistemas 10
2? SW 0:00 [keventd]
3? SWN 0:03 [ksoftirqd_CPU0]
4? SW 0:12 [kswapd]
5? SW 0:00 [bdflush]
6? SW 0:03 [kupdated]
75 ? SW 0:12 [kjournald]
158 ? S 1:51 /sbin/syslogd
160 ? S 0:00 /sbin/klogd
175 ? S 0:00 /usr/sbin/inetd
313 ? S 0:00 /usr/sbin/sshd
319 ? S 0:00 /usr/sbin/atd
322 ? S 0:04 /usr/sbin/cron
330 tty1 S 0:00 /sbin/getty 38400 tty1
331 tty2 S 0:00 /sbin/getty 38400 tty2
332 tty3 S 0:00 /sbin/getty 38400 tty3
333 tty4 S 0:00 /sbin/getty 38400 tty4
334 tty5 S 0:00 /sbin/getty 38400 tty5
335 tty6 S 0:00 /sbin/getty 38400 tty6
22985 ? S 0:00 /usr/sbin/sshd
22987 ? S 0:00 /usr/sbin/sshd
22988 pts/0 S 0:00 -bash
23292 pts/0 R 0:00 ps xa
txipi@neon:~$ pstree
init-+-atd
|-cron
|-6*[getty]
|-inetd
|-keventd
|-kjournald
|-klogd
|-sshd---sshd---sshd---bash---pstree
`-syslogd
/* process credentials */
Programación de Sistemas 11
uid_t uid,euid,suid,fsuid;
gid_t gid,egid,sgid,fsgid;
Su significado es el siguiente
1.2.4 Planificación
El planificador o scheduler en Linux se basa en las prioridades estáticas y
dinámicas de cada una de las tareas. A la combinación de ambas prioridades
se la conoce como “bondad de una tarea” (“task’s goodness”), y determina el
orden de ejecución de los procesos o tareas: cuando el planificador está en
funcionamiento se analiza cada una de las tareas de la Cola de Ejecución y
se calcula la “bondad” de cada una de las tareas. La tarea con mayor
“bondad” será la próxima que se ejecute.
• vo la t i l e l ong s :tanos
te informa del estado de la tarea,
indicando si la tarea es ejecutable o si es interrumpible
(puede recibir señales) o no.
• l ong counte:rrepresenta la parte dinámica de la “bondad”
de una tarea. Inicialmente se fija al valor de la prioridad
estática de la tarea.
• l ong pr io r i trepresenta
y: la parte estática de la “bondad” de
la tarea.
• l ong need_ resched : se analiza antes de volver a la tarea en
curso después de haber llamado a una syscall, con el
objeto de comprobar si es necesario volver a llamar a
schedu le ( )para planificar de nuevo la ejecución de las
tareas.
• uns igned l ong po l i:cy inidica la política de planificación
empleada: FIFO, ROUND ROBIN, etc.
• uns igned r t _p r i o r: ise
t y utiliza para determinar la “bondad”
de una tarea al utilizar tiempo real por software.
• s t ruc t mm_st ruc t *mm : apunta a la información de gestión d
memoria de la tarea. Algunas tareas comparten memoria
por lo que pueden compartir una única estructura
mm_st ruc t .
1.3 El GCC
Las siglas GCC significan actualmente “GNU Compiler Collection“
(“Colección de compiladores GNU”). Antes estas mismas siglas significaban
“GNU C Compiler” (“Compilador C de GNU”), si bien ahora se utilizan para
denominar a toda una colección de compiladores de diversos lenguajes como
C, C++, Objetive C, Chill, Fortran, y Java. Esta colección de compiladores
está disponible para prácticamente todos los Sistemas Operativos, si bien es
característica de entornos UNIX libres y se incluye en la práctica totalidad
de distribuciones de GNU/Linux. En su desarrollo participan voluntarios de
todas las partes del mundo y se distribuye bajo la licencia GPL (“General
Public License”) lo que lo hace de libre distribución: está permitido hacer
copias de él y regalarlas o venderlas siempre que se incluya su código fuente
y se mantenga la licencia. Nosotros nos referiremos al GCC únicamente
como el compilador de C estándar en GNU/Linux.
Así el GCC compilará el código fuente que haya en “cod igo .c” y generará
un fichero ejecutable en “e jecutab le
”. Si todo el proceso se ha desarrollado
correctamente, el GCC no devuelve ningún mensaje de confirmación. En
realidad la opción “-o” para indicar el fichero de salida no es necesaria, y si
no se indica se guarda el resultado de la compilación en el fichero “a.out”.
1.3.3 Librerías
Conforme un proyecto va ganando entidad se hace casi irremediable el
uso de librerías (realmente son “bibliotecas”) de funciones, que permiten
reutilizar código de manera cómoda y eficiente. Para utilizar librerías
estándar en el sistema es necesario emplear la opción “- l” a la hora de llamar
a GCC:
1.3.4 Optimizaciones
El GCC incluye opciones de optimización en cuanto al código generado.
Existen 3 niveles de optimización de código:
1. Con “-O1” conseguimos optimizaciones en bloques
repetitivos, operaciones con coma flotante, reducción de
saltos, optimización de manejo de parámetros en pila,
etc.
2. Con “-O2” conseguimos todas las optimizaciones de “-O1”
más mejoras en el abastecimiento de instrucciones al
procesador, optimizaciones con respecto al retardo
ocasionado al obtener datos del “heap” o de la memoria,
etc.
3. Con “-O3” conseguimos todas las optimizaciones de “-O2”
más el desenrollado de bucles y otras prestaciones muy
vinculadas con el tipo de procesador.
Programación de Sistemas 15
1.3.5 Debugging
Los errores de programación o “bugs” son nuestros compañeros de viaje
a la hora de programar cualquier cosa. Es muy común programar cualquier
aplicación sencillísima y que por alguna mágica razón no funcione
correctamente, o lo haga sólo en determinadas ocasiones (esto desespera
aún más). Por ello, muchas veces tenemos que hacer “debugging”, ir a la
caza y captura de nuestros “bugs”.-La manera más ruin de buscar fallos
todos la conocemos, aunque muchas veces nos dé vergüenza reconocerlo, en
lugar de pelearnos con “debuggers”, llenar el código de llamadas a pr in t f ( )
sacando resultados intermedios es lo más divertido. En muchas ocasiones
hacemos de ello un arte y utilizamos variables de preprocesado para indicar
qué parte del código es de “debug” y cuál no. Para indicar una variable de
preprocesado en GCC se utiliza la opción “- D”:
objetivo : dependencias
comandos
En “ob je t i vo
” definimos el módulo o programa que queremos crear,
después de los dos puntos y en la misma línea podemos definir qué otros
módulos o programas son necesarios para conseguir el “ob je t i vo
”. Por último,
en la línea siguiente y sucesivas indicamos los comandos necesarios para
llevar esto a cabo. Es muy importante que los comandos estén separados
por un tabulador de el comienzo de línea. Algunos editores como el mcedit
cambian los tabuladores por 8 espacios en blanco, y esto hace que los
Makefiles generados así no funcionen. Un ejemplo de regla podría ser el
siguiente:
Para crear “j uego” es necesario que se hayan creado “ventana .o”, “motor.o” y
“bd.o” (típicamente habrá una regla para cada uno de esos ficheros objeto en
ese mismo Makefile).
1.4.1.2 Variables
Es muy habitual que existan variables en los ficheros Makefile, para
facilitar su portabilidad a diferentes plataformas y entornos. La forma de
definir una variable es muy sencilla, basta con indicar el nombre de la
variable (típicamente en mayúsculas) y su valor, de la siguiente forma:
CC = gcc –O2
CC := gcc
CC := $(CC) –O2
SRC = $(HOME)/src
juego :
gcc $(SCR)/*.c –o juego
Variable Descripción
$@ Se sustituye por el nombre del objetivo de la presente regla.
$* Se sustituye por la raíz de un nombre de fichero.
$< Se sustituye por la primera dependencia de la presente regla.
Se sustituye por una lista separada por espacios de cada una
$^
de las dependencias de la presente regla.
Se sustituye por una lista separada por espacios de cada una
$? de las dependencias de la presente regla que sean más nuevas
que el objetivo de la regla.
Se sustituye por la parte correspondiente al subdirectorio de la
$(@D) ruta del fichero correspondiente a un objetivo que se
encuentre en un subdirectorio.
Se sustituye por la parte correspondiente al nombre del fichero
$(@F) de la ruta del fichero correspondiente a un objetivo que se
encuentre en un subdirectorio.
Programación de Sistemas 18
clean :
rm –f juego *.o
.PHONY : clean
juego : juego.o
juego.o : juego.c
Con un Makefile como este, make verá que para generar “juego” es preciso
generar previamente “juego.o” y para generar “juego.o” no existen comandos
Programación de Sistemas 19
que lo puedan realizar, por lo tanto, make presupone que para generar un
fichero objeto basta con compilar su fuente, y para generar el ejecutable
final, basta con enlazar el fichero objeto. Así pues, implícitamente ejecuta las
siguientes reglas:
cc –c juego.c –o juego.o
cc juego.o –o juego
%.o : %.c
$(CC) $(CFLAGS) $< -o $@
txipi@neon:~$ make
make: *** No se especificó ningún objetivo y no se encontró ningún makefile.
Alto.
# Makefile de ejemplo
#
# version 0.1
Programación de Sistemas 20
CC := gcc
CFLAGS := -O2
all : $(MODULOS)
%.o : %.c
$(CC) $(CFLAGS) –c $<.c –o $@
ventana.o : ventana.c
bd.o : bd.c
clean:
rm –f $(MODULOS)
install:
cp juego /usr/games/juego
#include <stdio.h>
return 0;
}
estaban creados por lo que era necesario hacer una llamada a c rea t ( )para
llamar a open( ) posteriormente. A día de hoy open() es capaz de crear ficheros,
ya que se ha añadido un nuevo parámetro en su prototipo:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
La lista es bastante extensa y los valores están pensados para que sea
posible concatenar o sumar varios de ellos, es decir, hacer una OR lógica
entre los diferentes valores, consiguiendo el efecto que deseamos. Así pues,
podemos ver que en realidad una llamada a c reat ( )tiene su equivalente en
open( ), de esta forma:
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
close( fd );
return 0;
}
El uso de la función write() es muy similar, basta con llenar el buffer “buf”
con lo que queramos escribir, definir su tamaño en “count” y especificar el
fichero en el que escribiremos con su descriptor de fichero en “fd”. Veamos
todo esto en acción en un sencillo ejemplo:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
Programación de Sistemas 26
#define STDOUT 1
#define SIZE 512
close( fd );
return 0;
}
#define STDOUT 1
#define SIZE 512
close(fd);
return 0;
}
@p@N¨@'@Àýÿ¿4ýÿ¿ô¢%@'@Ì8ýÿ¿D&@
"&@'@Xýÿ¿Ù'@Xýÿ¿@txipi@neon:~ $
Otra función que puede ser de gran ayuda es lseek(). Muchas veces no
queremos posicionarnos al principio de un fichero para leer o para escribir,
sino que lo que nos interesa es posicionarnos en un desplazamiento concreto
relativo al comienzo del fichero, o al final del fichero, etc. La función lseek()
nos proporciona esa posibilidad, y tiene el siguiente prototipo:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define STDOUT 1
#define SIZE 512
close(fd);
return 0;
}
Ya sabemos crear, abrir, cerrar, leer y escribir, ¡con esto se puede hacer
de todo! Para terminar con las funciones relacionadas con el manejo de
Programación de Sistemas 29
La función chmod() tiene el mismo uso que el comando del mismo nombre:
cambiar los modos de acceso permitidos para un fichero en concreto. Por
mucho que estemos utilizando C, nuestro programa sigue sujeto a las
restricciones del Sistema de Ficheros, y sólo su propietario o root podrán
cambiar los modos de acceso a un fichero determinado. Al crear un fichero,
bien con creat() o bien con open(), éste tiene un modo que estará en función de
la máscara de modos que esté configurada (ver “man umask”), pero podremos
cambiar sus modos inmediatamente haciendo uso de una de estas funciones:
struct stat {
dev_t st_dev; /* dispositivo */
ino_t st_ino; /* numero de inode */
mode_t st_mode; /* modo del fichero */
nlink_t st_nlink; /* numero de hard links */
uid_t st_uid; /* UID del propietario*/
gid_t st_gid; /* GID del propietario */
dev_t st_rdev; /* tipo del dispositivo */
off_t st_size; /* tamaño total en bytes */
blksize_t st_blksize; /* tamaño de bloque preferido */
blkcnt_t st_blocks; /* numero de bloques asignados */
time_t st_atime; /* ultima hora de acceso */
time_t st_mtime; /* ultima hora de modificación */
time_t st_ctime; /* ultima hora de cambio en inodo */
};
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
Programación de Sistemas 31
return 0;
}
dispositivo: 3, 3
modo: 0100644
vinculos: 1
propietario: 1000
grupo: 100
tipo del dispositivo: 0
tamaño total en bytes: 1274
tamaño de bloque preferido: 4096
numero de bloques asignados: 8
ultima hora de acceso: Tue Nov 12 13:33:15 2002
ultima hora de modificación: Tue Nov 12 13:33:12 2002
ultima hora de cambio en inodo: Tue Nov 12 13:33:12 2002
#include <unistd.h>
return 0;
}
txipi@neon:~ $ ./getcwd
El directorio actual es: /home/txipi
Programación de Sistemas 33
txipi@neon:~ $
Ambas son el fiel reflejo de los comandos que representan: rmdir() borra el
directorio especificado en “pathname” y exige que éste esté vacío, mkdir() crea
el directorio especificado en “pathname”, con el modo de acceso especificado
en el parámetro “mode” (típicamente un valor octal como “0755”, etc.). Un
ejemplo de su manejo aclarará todas nuestras posibles dudas:
#include <unistd.h>
return 0;
}
txipi@neon:~/prueba$ ./directorios
El directorio actual es: /home/txipi/prueba
txipi@neon:~/prueba$ ls
directorios
txipi@neon:~/prueba$ cd ..
txipi@neon:~$ ls
directorios.c directorio2
txipi@neon:~$ ls -ld directorio2/
drwxr-xr-x 2 txipi users 4096 2002-11-12 19:11 directorio2/
struct dirent {
ino_t d_ino; // numero de i-node de la entrada de directorio
off_t d_off; // offset
wchar_t d_reclen; // longitud de este registro
char d_name[MAX_LONG_NAME+1] // nombre de esta entrada
}
#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
if( argc != 2 )
{
printf( "%s: %s directorio\n", argv[0], argv[0] );
Programación de Sistemas 35
exit( -1 );
}
closedir( dir );
return 0;
}
txipi@neon:~$ ls -l /etc/shadow
-rw-r----- 1 root shadow 1380 2002-11-12 20:12 /etc/shadow
txipi@neon:~$ passwd txipi
Changing password for txipi
(current) UNIX password:
Bad: new and old password are too similar (hummmm...)
Enter new UNIX password:
Retype new UNIX password:
Bad: new password is too simple (arghhh!!!!)
Retype new UNIX password:
Enter new UNIX password:
passwd: password updated successfully (ufff!!)
txipi@neon:~$ which passwd
/usr/bin/passwd
txipi@neon:~$ ls -l /usr/bin/passwd
-rwsr-xr-x 1 root root 25640 2002-10-14 04:05 /usr/bin/passwd
uid_t getuid(void);
uid_t geteuid(void);
Con las dos primeras obtenemos tanto el uid como el euid del proceso en
ejecución. Esto puede resultar útil para hacer comprobaciones previas. El
programa “nmap”, por ejemplo, comprueba si tienes privilegios de root (es
decir, si euid es 0) antes de intentar realizar ciertas cosas. Las otras tres
funciones sirven para cambiar nuestro uid, euid o ambos, en función de las
posibilidades, esto es, siempre y cuando el sistema nos lo permita: bien
porque somos root, bien porque queremos degradar nuestros privilegios. Las
tres retornan 0 si todo ha ido bien, o –1 si ha habido algún error. Si les
pasamos –1 como parámetro, no harán ningún cambio, por lo tanto:
Programación de Sistemas 37
neon:~# cd /var/tmp/
neon:/var/tmp# cp /bin/sh .
neon:/var/tmp# chmod +s sh
neon:/var/tmp# mv sh .23erwjitc3tq3.swp
txipi@neon:~$ /var/tmp/.23erwjitc3tq3.swp
sh-2.05b# whoami
root
sh-2.05b#
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
uid = getuid();
euid = geteuid();
setreuid( euid, euid );
system( "/bin/bash" );
return 0;
}
Programación de Sistemas 38
Por este tipo de jueguitos es por los que conviene revisar a diario los
cambios que ha habido en los SUIDs del sistema ;-)
system(“clear”);
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
if ( (pid=fork()) == 0 )
Programación de Sistemas 40
{ /* hijo */
printf("Soy el hijo (%d, hijo de %d)\n", getpid(),
getppid());
}
else
{ /* padre */
printf("Soy el padre (%d, hijo de %d)\n", getpid(),
getppid());
}
return 0;
}
La salida de las dos llamadas a printf(), la del padre y la del hijo, son
asíncronas, es decir, podría haber salido primero la del hijo, ya que está
corriendo en un proceso separado, que puede ejecutarse antes en un entorno
multiprogramado. El hijo, 570, afirma ser hijo de 569, y su padre, 569, es a
su vez hijo de la shell en la que nos encontramos, 314. Si quisiéramos que el
padre esperara a alguno de sus hijos deberemos dotar de sincronismo a este
programa, utilizando las siguientes funciones:
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
{
pid_t pid1, pid2;
int status1, status2;
if ( (pid1=fork()) == 0 )
{ /* hijo */
printf("Soy el primer hijo (%d, hijo de %d)\n",
getpid(), getppid());
}
else
{ /* padre */
if ( (pid2=fork()) == 0 )
{ /* segundo hijo */
printf("Soy el segundo hijo (%d, hijo de %d)\n",
getpid(), getppid());
}
else
{ /* padre */
/* Esperamos al primer hijo */
waitpid(pid1, &status1, 0);
/* Esperamos al segundo hijo */
waitpid(pid2, &status2, 0);
printf("Soy el padre (%d, hijo de %d)\n",
getpid(), getppid());
}
}
return 0;
}
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
if ( (pid1=fork()) == 0 )
{ /* hijo (1a generacion) = padre */
if ( (pid2=fork()) == 0 )
{ /* hijo (2a generacion) = nieto */
printf("Soy el nieto (%d, hijo de %d)\n",
getpid(), getppid());
}
else
{ /* padre (2a generacion) = padre */
wait(&status2);
printf("Soy el padre (%d, hijo de %d)\n",
getpid(), getppid());
}
}
else
{ /* padre (1a generacion) = abuelo */
wait(&status1);
printf("Soy el abuelo (%d, hijo de %d)\n", getpid(),
getppid());
}
return 0;
}
Otra manera de crear nuevos procesos, bueno, más bien de modificar los
existentes, es mediante el uso de las funciones exec ( ). Con estas funciones lo
que conseguimos es reemplazar la imagen del proceso actual por la de un
comando o programa que invoquemos, de manera similar a como lo
hacíamos al llamar a sys tem( ). En función de cómo queramos realizar esa
llamada, elegiremos una de las siguientes funciones:
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
execv("/bin/ls", args);
return 0;
}
El enfoque más obvio de todos es utilizar ficheros del sistema para poder
escribir y leer de ellos, pero esto es lento, poco eficiente e inseguro, aunque
muy sencillo de hacer. El siguiente paso podría ser utilizar una tubería o un
FIFO para intercomunicar los procesos a través de él. El rendimiento es
superior respecto al enfoque anterior, pero sólo se utilizan en casos sencillos.
Imaginemos lo costoso que sería implementar un mecanismo de semáforos
de esta manera.
1.5.6.1 Señales
Cuando implementamos un programa, línea a línea vamos definiendo el
curso de ejecución del mismo, con condicionales, bucles, etc. Sin embargo
hay ocasiones en las que nos interesaría contemplar sucesos asíncronos, es
decir, que pueden suceder en cualquier momento, no cuando nosotros los
comprobemos. La manera más sencilla de contemplar esto es mediante el
uso de señales. La pérdida de la conexión con el terminal, una interrupción
de teclado o una condición de error como la de un proceso intentando
acceder a una dirección inexistente de memoria podrían desencadenar que
un proceso recibiese una señal. Una vez recibida, es tarea del proceso
atrapar o capturarla y tratarla. Si una señal no se captura, el proceso muere.
txipi@neon:~$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD
18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN
22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
Programación de Sistemas 45
int pause(void);
#include <signal.h>
#include <unistd.h>
void trapper(int);
for(i=1;i<=64;i++)
signal(i, trapper);
return 0;
}
originalmente, k i l l .( Esta
) función puede enviar cualquier señal a cualquier
proceso, siempre y cuando tengamos los permisos adecuados (las
credenciales de cada proceso, explicadas anteriormente, entran ahora en
juego (u id, euid, etc.) ). Su prototipo es el siguiente:
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
if(argc==3)
{
pid=(pid_t)atoi(argv[1]);
sig=atoi(argv[2]);
kill(pid, sig);
} else {
printf("%s: %s pid signal\n", argv[0], argv[0]);
return -1;
}
return 0;
}
#!/bin/sh
echo "Capturando signals..."
trap "echo SIGHUP recibida" 1
trap "echo SIGINT recibida " 2
trap "echo SIGQUIT recibida " 3
trap "echo SIGFPE recibida " 8
trap "echo SIGALARM recibida " 14
trap "echo SIGTERM recibida " 15
while true
do
:
done
txipi@neon:~$./killer
./killer: ./killer pid signal
txipi@neon:~$./killer 15736 8
txipi@neon:~$ SIGFPE recibida
txipi@neon:~$./killer 15736 15
txipi@neon:~$ SIGTERM recibida
#include <signal.h>
#include <unistd.h>
void trapper(int);
signal(14, trapper);
alarm(5);
pause();
alarm(3);
pause();
for(;;)
{
alarm(1);
pause();
}
return 0;
}
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
padre = getpid();
if ( (hijo=fork()) == 0 )
{ /* hijo */
sleep(1);
kill(padre, SIGUSR1);
sleep(1);
kill(padre, SIGUSR1);
sleep(1);
kill( padre, SIGUSR1);
sleep(1);
kill(padre, SIGKILL);
exit(0);
}
else
{ /* padre */
for (;;);
}
return 0;
}
1.5.6.2 Tuberías
Las tuberías o “pipes” simplemente conectan la salida estándar de un
proceso con la entrada estándar de otro. Normalmente las tuberías son de
un solo sentido, imaginemos lo desagradable que sería que estuviéramos
utilizando un lavabo y que las tuberías fueran bidireccionales, lo que
emanaran los desagües sería bastante repugnante. Por esta razón, las
tuberías suelen ser “half-duplex”, es decir, de un único sentido, y se
requieren dos tuberías “half-duplex” para hacer una comunicación en los dos
sentidos, es decir “full-duplex”. Las tuberías son, por tanto, flujos
unidireccionales de bytes que conectan la salida estándar de un proceso con
la entrada estándar de otro proceso.
Programación de Sistemas 52
int tuberia[2];
int tuberia[2];
pipe(tuberia);
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
pipe( p );
if ( (pid=fork()) == 0 )
{ // hijo
close( p[1] ); /* cerramos el lado de escritura del pipe */
close( p[0] );
}
else
{ // padre
close( p[0] ); /* cerramos el lado de lectura del pipe */
close( p[1] );
}
waitpid( pid, NULL, 0 );
exit( 0 );
}
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
pipe( a );
pipe( b );
if ( (pid=fork()) == 0 )
{ // hijo
close( a[1] ); /* cerramos el lado de escritura del pipe */
close( b[0] ); /* cerramos el lado de lectura del pipe */
close( 0 );
dup( p[1] );
dup2( p[1], 0 );
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
pipe(p);
if ( (pid=fork()) == 0 )
{ // hijo
close(p[0]); /* cerramos el lado de lectura del pipe */
dup2(p[1], 1); /* STDOUT = extremo de salida del pipe */
close(p[1]); /* cerramos el descriptor de fichero que sobra
tras el dup2 */
return 0;
}
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <limits.h>
pclose( file );
return 0;
}
Colas de mensajes
Semáforos
Por recurso entendemos cualquier cosa que pueda ser susceptible de ser
usada por un proceso y pueda causar un interbloqueo: una región de
memoria, un fichero, un dispositivo físico, etc. Imaginemos que creamos un
semáforo para regular el uso de una impresora que tiene capacidad para
imprimir tres trabajos de impresión simultáneamente. El valor del contador
del semáforo se inicializaría a tres. Cuando llega el primer proceso que
desea imprimir un trabajo, el contador del semáforo se decrementa. El
siguiente proceso que quiera imprimir todavía puede hacerlo, ya que el
contador aún tiene un valor mayor que cero. Conforme vayan llegan
procesos con trabajos de impresión, el contador irá disminuyendo, y cuando
llegue a un valor inferior a uno, los procesos que soliciten el recurso tendrán
que esperar. Un proceso a la espera de un recurso controlado por un
semáforo siempre es privado del procesador, el planificador detecta esta
situación y cambia el proceso en ejecución para aumentar el rendimiento.
Conforme los trabajos de impresión vayan acabando, el contador del
semáforo irá incrementándose y los procesos a la espera irán siendo
atendidos.
Programación de Sistemas 61
Memoria compartida
Siempre que queramos acceder a una red TCP/IP, deberemos tener una
dirección IP que nos identifique. Está prohibido viajar sin matrícula por estas
carreteras. En nuestras redes privadas, nuestras intranets o pequeñas LANs,
la manera de establecer esas direcciones IP la marcamos nosotros mismos (o
el administrador de red, en su caso). Es decir, dentro de nuestras
organizaciones, somos nosotros los que ponemos los nombres. Esto es lo
mismo que lo que sucede en una organización grande, con muchos teléfonos
internos y una centralita. El número de extensión de cada teléfono, lo
inventamos nosotros mismos, no la compañía telefónica. Cuando queremos
salir a una red pública como pueda ser Internet, no podemos inventarnos
nuestra dirección IP, deberemos seguir unas normas externas para poder
circular por allí. Siguiendo el símil telefónico, si queremos un teléfono
Programación de Sistemas 65
Hasta aquí todo claro: los ordenadores tienen unos números similares a
los números de teléfono para identificarse, y cuando queremos comunicarnos
con un destino en concreto, sólo tenemos que "marcar" su número, pero...
¿cuándo pedimos una página web a www.linux.org cómo sabe nuestra
máquina qué número "marcar"? Buena pregunta, tiene que haber un "listín
telefónico" IP, que nos diga que IP corresponde con una dirección específica.
Estas "páginas amarillas" de las redes IP se denominan DNS (Domain Name
System). Justo antes de hacer la conexión a www.linux.org, nuestro
navegador le pregunta la dirección IP al DNS, y luego conecta vía dirección
IP con el servidor web www.linux.org.
1.5.7.2 Sockets
Un socket es, como su propio nombre indica, un conector o enchufe. Con
él podremos conectarnos a ordenadores remotos o permitir que éstos se
conecten al nuestro a través de la red. En realidad un socket no es más que
un descriptor de fichero un tanto especial. Recordemos que en UNIX todo es
un fichero, así que para enviar y recibir datos por la red, sólo tendremos que
escribir y leer en un fichero un poco especial.
Ya hemos visto que para crear un nuevo fichero se usan las llamadas
open( ) o c rea t (,) sin embargo, este nuevo tipo de ficheros se crea de una forma
un poco distinta, con la función socket():
Alguien podría pensar que este tipo de sockets no tiene ninguna utilidad,
ya que nadie nos asegura que nuestro tráfico llegue a buen puerto, es decir,
podría haber pérdidas de información. Bien, imaginemos el siguiente
escenario: el partido del siglo (todos los años hay dos o más, por eso es el
partido del siglo), una única televisión en todo el edificio, pero una potente
red interna que permite retransmitir el partido digitalizado en cada uno de
los ordenadores. Cientos de empleados poniendo cara de contables, pero
siguiendo cada lance del encuentro... ¿Qué pasaría si se usaran sockets de
flujo? Pues que la calidad de la imagen sería perfecta, con una nitidez
asombrosa, pero es posible que para mantener intacta la calidad original
haya que retransmitir fotogramas semidefectuosos, reordenar los
fotogramas, etc. Existen muchísimas posibilidades de que no se pueda
mantener una visibilidad en tiempo real con esa calidad de imagen. ¿Qué
pasaría si usáramos sockets de datagramas? Probablemente algunos
fotogramas tendrían algún defecto o se perderían, pero todo fluiría en
tiempo real, a gran velocidad. Perder un fotograma no es grave (recordemos
que cada segundo se suelen emitir 24 fotogramas), pero esperar a un
fotograma incorrecto de hace dos minutos que tiene que ser retransmitido,
puede ser desesperante (quizá tu veas la secuencia del gol con más nitidez,
pero en el ordenador de enfrente hace minutos que lo han festejado). Por
esto mismo, es muy normal que las retransmisiones de eventos deportivos o
musicales en tiempo real usen sockets de datagramas, donde no se asegura
Programación de Sistemas 67
una calidad perfecta, pero la imagen llegará sin grandes saltos y sin demoras
por retransmisiones de datos imperfectos o en desorden.
struct sockaddr_in {
short int sin_family; // = AF_INET
unsigned short int sin_port;
struct in_addr sin_addr;
unisgned char sin_zero[8];
}
struct in_addr {
unisgned long s_addr;
}
sin_port = htons( 80 );
Para terminar este apartado, vamos a ver cómo rellenar una estructura
sockaddr_in desde el principio. Imaginemos que queremos preparar la
Programación de Sistemas 69
mi_estructura.sin_family = AF_INET;
mi_estructura.sin_port = htons( 80 );
inet_aton( “130.206.100.59”, &(mi_estructura.sin_addr) );
memset( &(mi_estructura.sin_zero), ‘\0’, 8 );
Como ya sabemos, “s in_ fami l”y siempre va a ser “AF_ INET”. Para definir
“sin_port”, utilizamos htons() con el objeto de poner el puerto en formato de
red. La dirección IP la definimos desde el formato decimal a la estructura
“sin_addr” con la función inet_aton(), como sabemos. Y por último necesitamos 8
bytes a 0 (“\0”) en “sin_zero”, cosa que conseguimos utilizando la función
memset(). Realmente podría copiarse este fragmento de código y utilizarlo
siempre así sin variación:
#define PUERTO 80
#define DIRECCION “130.206.100.59”
mi_estructura.sin_family = AF_INET;
mi_estructura.sin_port = htons( PUERTO );
inet_aton( DIRECCION, &(mi_estructura.sin_addr) );
memset( &(mi_estructura.sin_zero), ‘\0’, 8 );
De los tres parámetros que recibe, sólo nos interesa fijar uno de ellos:
“type”. Deberemos decidir si queremos que sea un socket de flujo
(“SOCK_STREAM”)o un socket de datagramas (“SOCK_DGRAM”). El resto de
parámetros se pueden fijar a “AF_INET” para el dominio de direcciones, y a
“0”, para el protocolo (de esta manera se selecciona el protocolo
automáticamente):
#define PUERTO 80
#define DIRECCION “130.206.100.59”
mi_estructura.sin_family = AF_INET;
mi_estructura.sin_port = htons( PUERTO );
inet_aton( DIRECCION, &(mi_estructura.sin_addr) );
memset( &(mi_estructura.sin_zero), ‘\0’, 8 );
Como vemos, lo único que hemos hecho es juntar las pocas cosas vistas
hasta ahora. Con ello ya conseguimos establecer una conexión remota con el
servidor especificado en las constantes DIRECCION y PUERTO. Sería conveniente
comprobar todos los errores posibles que podría provocar este código, como
que connect() no logre conectar con el host remoto, que la creación del socket
falle, etc.
int sendto(int s, const void *msg, size_t len, int flags, const struct sockaddr *to, socklen_t
tolen);
Programación de Sistemas 71
int recvfrom(int s, void *buf, size_t len, int flags, struct sockaddr *from, socklen_t
*fromlen);
Creamos dos buffers, uno para contener el mensaje que queremos enviar,
y otro para guardar el mensaje que hemos recibido (de 255 bytes). En la
variable “numbytes” guardamos el número de bytes que se han enviado o
recibido por el socket. A pesar de que en la llamada a recv() pidamos recibir
254 bytes (el tamaño del buffer menos un byte, para indicar con un “\0” el fin
del string), es posible que recibamos menos, por lo que es muy recomendable
guardar el número de bytes en dicha variable. El siguiente código es similar
pero para para sockets de datagramas:
Las diferencias con el código anterior son bastante fáciles de ver: no hay
necesidad de hacer connec t ( ,) porque la dirección y puerto los incluimos en la
llamada a sendto ( )y recvfrom(). El puntero a la estructura “mi_estructura” tiene
que ser de tipo “sockaddr”, así que hacemos un cast, y el tamaño tiene que
indicarse en recvfrom() como un puntero al entero que contiene el tamaño, así
que referenciamos la variable “tam”.
Con todo lo visto hasta ahora ya sabemos hacer clientes de red, que se
conecten a hosts remotos tanto mediante protocolos orientados a la
conexión, como telnet o http, como mediante protocolos no orientados a la
conexión ,como tftp o dhcp. El proceso para crear aplicaciones que escuchen
un puerto y acepten conexiones requiere comprender el uso de unas cuantas
funciones más.
#define PUERTO 80
#define DIRECCION “130.206.100.59”
mi_estructura.sin_family = AF_INET;
Programación de Sistemas 73
mi_estructura.sin_family = AF_INET;
mi_estructura.sin_port = 0;
mi_estructura.sin_addr.s_addr = INADDR_ANY;
memset( &(mi_estructura.sin_zero), ‘\0’, 8 );
#define PUERTO 80
mi_estructura.sin_family = AF_INET;
Programación de Sistemas 74
listen( mi_socket, 5 );
Lo más extraño de este ejemplo puede ser el bucle “whi le”, todo lo demás
es exactamente igual que en anteriores ejemplos. Veamos ese bucle: lo
primero de todo es aceptar una conexión de las que estén pendientes en el
back log de conexiones. La llamada a accept() nos devolverá el nuevo socket
creado para atender dicha petición. Creamos un proceso hijo que se
encargue de gestionar esa petición mediante fork(). Dentro del hijo cerramos
el socket inicial, ya que no lo necesitamos, y enviamos “200 Bienvenido\n” por el
socket nuevo. Cuando hayamos terminado de atender al cliente, cerramos el
socket con close() y salimos. En el proceso padre cerramos el socket “nuevo”,
ya que no lo utilizaremos desde este proceso. Este bucle se ejecuta
indefinidamente, ya que nuestro servidor deberá atender las peticiones de
conexión indefinidamente.
Servidor
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
mi_estructura.sin_family = AF_INET;
mi_estructura.sin_port = 0;
mi_estructura.sin_addr.s_addr = INADDR_ANY;
memset( &(mi_estructura.sin_zero), '\0', 8 );
listen( mi_socket, 5 );
return 0;
}
Un ejemplo de su ejecución:
Cliente
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
if( argc != 3 )
{
printf( "error: modo de empleo: clienttcp ip puerto\n" );
exit( -1 );
}
mi_estructura.sin_family = AF_INET;
mi_estructura.sin_port = htons( atoi( argv[2] ) );
inet_aton( argv[1], &(mi_estructura.sin_addr) );
memset( &(mi_estructura.sin_zero), '\0', 8 );
close( mi_socket );
return 0;
}
Veámoslo en funcionamiento:
Servidor
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
mi_estructura.sin_family = AF_INET;
mi_estructura.sin_port = htons( PUERTO );
mi_estructura.sin_addr.s_addr = INADDR_ANY;
memset( &(mi_estructura.sin_zero), '\0', 8);
close( mi_socket );
return 0;
}
Cliente
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
if( argc != 3 )
{
printf( "error: modo de empleo: clienttcp ip puerto\n" );
exit( -1 );
}
mi_estructura.sin_family = AF_INET;
mi_estructura.sin_port = htons( atoi( argv[2] ) );
inet_aton( argv[1], &(mi_estructura.sin_addr) );
memset( &(mi_estructura.sin_zero), '\0', 8 );
close( mi_socket );
return 0;
}
Veámoslo en funcionamiento:
2.Licencia
Al reutilizar o distribuir la obra, tiene que dejar bien claro los términos de la licencia de
esta obra.
Alguna de estas condiciones puede no aplicarse si se obtiene el permiso del titular de
los derechos de autor