Documentos de Académico
Documentos de Profesional
Documentos de Cultura
FICHEROS ORDINARIOS:
Los ficheros ordinarios contienen datos, textos y programas ejecutables (comandos). Y se pueden
realizar siguientes operaciones con los datos de los ficheros:
-No se pueden insertar bytes en un fichero excepto como hemos dicho anteriormente al final de
este.
Varios procesos a la vez pueden escribir y lee un mismo fichero. Esto dependerá del orden de las
llamadas de entrada y salida individuales de cada proceso de la gestión que el planificar haga de los
procesos.
Actualmente hay en el mercado algunas versiones de unix que tienen bloqueo de ficheros y
gestión de semáforos, esto se hace para controlar el acceso simultáneo a un mismo fichero.
El acceso a un fichero ordinario es mediante los nodos-i.
DIRECTORIOS:
Los directorios lo que nos permiten es dar una estructura jerárquica a los sistemas de ficheros de
Unix. Su mayor cometido es la de establecer la relación que existe entre el nombre de un fichero y su
nodo-i correspondiente.
El Unix System V es una versión de unix en la cual un directorio cuyo datos están organizados
como secuencias de entradas. Cada secuencia contiene un número de nodo-i y el nombre de un fichero
que pertenece al directorio. A este conjunto se les llama enlace y puede haber varios nombres de ficheros
que estén enlazados con un mismo nodo-i.
Las entradas de directorios tienen un tamaño de 16 bytes, de los cuales 2 son dedicados al nodo-i y
14 al nombre del fichero.
ESTRUCTURA DE UN DIRECTORIO
Las dos primeras entradas de un directorio reciben los nombres de .y ... Al fichero se le asocia el
nodo-i del directorio actual y el fichero .. tiene asociado el nodo-i del directorio padre actual. Mediante el
programa mkfs (make file system), q es un programa mediante el cual se crea un sistema de ficheros, se
encarga también de que el fichero .. se refiera al propio directorio raíz.
El núcleo maneja los datos de un directorio usando la estructura nodo-i y los bloques de acceso
directo e indirecto.
Esta versión de unix tiene como objetivo fundamental establecer enlaces entre los nombres de los
ficheros y los nodos-i al igual que la versión System V, la diferencia es que en BSD los nombres pueden
ser más largos y no se reserva un espacio fijo de bytes para cada entrada del directorio.
El resto de la entrada contiene una cadena de caracteres terminada con el carácter nulo, este es un
campo variable.
FORMATO DE LAS ENTRADAS DE UN DIRECTORIO EN EL SISTEMA BSD
Cuando la entrada de un directorio es borrada, el propio sistema añade un espacio libre a la entrada
anterior. Si la primera entrada de un bloque estuviera libre, el numero de nodo-i que almacenaría esa
entrada sería cero, esto se realiza para indicar que no esta reservada por ningún fichero.
Para leer un directorio utilizamos: open, read lseek,close, etc. Moverse por el interior de las
jerarquías de directorios del sistema BSD es mas cómodo. Las funciones del interfaz son:
opendir,readdir,rewindir, closedir, seekdir y telldir. Estas funciones pueden codificarse a partir de
llamadas de manejo de los ficheros, para así poder ser emuladas sobre una red o un sistema no UNÍX.
Dependerá de la ruta para que el nodo-i realice la búsqueda desde un punto u otro; por ejemplo: si
la ruta es absoluta, la búsqueda del nodo-i del fichero se iniciará desde el directorio raiz; si la ruta es
relativa, la búsqueda se iniciará en el directorio de trabajo actual, que tienen asociado el proceso que
quiere acceder al fichero.
Mientras se van recorriendo los nodos-i intermedios se va comprobando que el proceso tiene
derechos de acceso a los directorios intermedios.
FICHEROS ESPECIALES:
Los ficheros especiales o también llamados ficheros de dispositivos son utilizados para que los
procesos se comuniquen con los dispositivos periféricos (discos, cintas, impresoras...).
Existen dos familias de ficheros de dispositivo:
Los módulos del núcleo que gestionan la comunicación con los dispositivos se conocen como
manejadores de dispositivos. Los mas corriente es que cada dispositivo tenga su propio manejador,
aunque hay excepciones en las cuales un manejador puede controlar toda una familia de dispositivos con
características comunes.
El sistema también puede soportar dispositivos software que no tienen asociado dispositivo físico.
Como ya sabemos todos los ficheros tienen asociado un nodo-i. En los ficheros ordinarios y los
directorios, el nodo-i nos indica los bloques donde se encuentran los datos del fichero. En cambio, en los
ficheros de dispositivo no hay datos a los que referenciar, en este caso, el nodo-i contiene dos números
conocidos como major number y minor number. El primero indica el número de dispositivo de que se
trata y el segundo indica el número de unidad dentro del dispositivo. Lo que en realidad hacen estos
números es buscar dentro de unas tablas una colección de rutinas que permiten manejar el dispositivo,
esta es la que realmente construye el manejador de dispositivos.
Para realizar una operación de entrada / salida sobre un fichero especial el núcleo se encarga de
llamar al manejador de dispositivos.
TEMA 3. MANEJO DE FICHEROS ORDINARIOS.
Open es la llamada para indicarle al núcleo que habilite las estructuras necesarias para trabajar con
un fichero especificado con una ruta. El núcleo devolverá un descriptor de fichero con el que podremos
referenciar el fichero para las llamadas posteriores. La declaración de open es:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open (char *path, int flag [, mode_t mode]);
path puntero a la ruta del fichero que se quiere abrir. Puede ser absoluta o relativa y la longitud no
puede exceder de PATH_MAX bytes.
Flags es una mascara de bits que le indica al núcleo como queremos que se abra el fichero.
Solamente uno de los bits debe estar presente ( O_RDONLY, O_WRONLY u O_DRWR ) al componer la
mascara, sino el modo de apertura quedaría indefinido. Los flags más significativos son:
En relación con O_NDELAY, cuando abrimos una tubería con nombre y activamos el modo
O_RDONLY u O_WRONLY:
Si el fichero que queremos abrir está asociado con una línea de comunicaciones:
- Si O_NDELAY está activo, open regresa sin esperar por la portadora -llamada no
bloqueante--.
- Si O_NDELAY está inactivo, open no regresa hasta que detecta la portadora -llamada
bloqueante-
- O_APPEND, el puntero de lectura/escritura del fichero se sitúa al final del mismo antes de
empezar la escritura. Así garantizamos que lo escrito se añade al final del fichero.
- O_CREAT, si el fichero que queremos abrir ya existe, este indicador no tiene efecto, excepto en
lo que se indicará para el indicador O_EXCL.El fichero es creado en caso de que no exista y se
creara con los permisos indicados en el parametro mode.
- O_EXCL, si está presente el indicador O_CREAT,open devuelve un código de error cuando el
fichero ya existe.
- O_TRUNC, si el fichero existe, trunca su longitud a cero bytes, incluso si el fichero se abre para
leer.
Mode es el tercer parámetro de open y sólo tiene significado cuando está activo el indicador
O_CREAT. Le indica al núcleo qué permisos tendrá el fichero que va a crear. Mode es también una
máscara de bits y se suele expresar en octal mediante un número de dígitos. El primero de los dígitos hace
referencia a los permisos de lectura, escritura y ejecución para el propietario del fichero; el segundo se
refiere a los mismos permisos para el grupo de usuarios al que pertenece el propietario, y el tercero se
refiere a los permisos del resto de usuarios. Así, por ejemplo, 0644 -110 100 100- indica permisos de
lectura y escritura para el propietario, y permiso de lectura para el grupo y para el resto de usuarios.
int fd;
…
fd = open ("mifichero", O_RDONLY); / / Abre un fichero para leer datos de él.
/ / Abre un fichero para escribir datos en él. Si el fichero existe, trunca su tamaño
/ / a 0 bytes. Si el fichero no existe, lo crea con permiso de lectura y escritura para
/ / el propietario y ningún permiso para el grupo y demás usuarios.
fd = open ("miiichero", RDWR | O_APPEND);
/ / Abre un fichero en modo lectura/escritura y fuerza a que el puntero de
/ / Lectura/escritura se sitúe al final del fichero.
Read es la llamada que emplearemos para leer datos de un fichero. Su declaración es la siguiente:
#include <unistd.h>
int read (int i ildes, char *bui, unsigned nbyte) ;
Read lee nbyte bytes del fichero asociado al descriptor fildes y los coloca en la memoria
intermedia apuntada por buf. Si la lectura se lleva a cabo correctamente, read devuelve el número de bytes
realmente leídos y copiados en la memoria intermedia. Este número puede ser menor que nbyte en el caso
de que el fichero esté asociado a una línea de comunicaciones, o de que quedasen menos de nbyte bytes
por leer.
Cuando se intenta leer más allá del final del fichero, read devuelve el valor O. Sólo en el caso de
que read falle, devuelve el valor -1y errno contendrá el tipo de error que se ha producido.
En los ficheros con capacidad de acceso aleatorio, la lectura empieza en la posición indicada por el
puntero de lectura/escritura del fichero. Este puntero queda actualizado después de efectuar la lectura. En
los ficheros asociados a dispositivos sin capacidad de acceso aleatorio -por ejemplo, líneas serie-, read
siempre lee de la misma posición y el valor del puntero no tiene significado.
Los siguientes ejemplos muestran algunas formas de invocar a read. En estos ejemplos suponemos
que id es el descriptor de un fichero correctamente abierto.
char mem [4096];
int nbytes, fd;
.......
nbytes = read (fd, mem, sizeof (mem));
/ / Lee 4.096 bytes que se almacenan en mem
La lectura no tenemos por qué hacerla siempre sobre un array de caracteres, también se puede
hacer sobre una estructura. Supongamos que queremos leer 40 registros con un formato concreto de un
fichero de datos. Si la composición de cada registro la tenemos definida en una estructura de nombre
REGISTRO, una secuencia de código para efectuar esta lectura puede ser:
Utilizaremos la llamada write para escribir datos en un fichero. Su declaración es muy parecida a
la de read:
write escribe nbyte bytes de la memoria referenciada por buf en el fichero asociado
al descriptor fildes. Si la escritura se lleva a cabo correctamente, write devuelve el número de bytes
realmente escritos; en caso contrario, devuelve -1 y errno contendrá el
tipo del error producido.
En los ficheros con capacidad de acceso aleatorio, la escritura se realiza en la posición indicada
por el puntero de lectura/escritura del fichero. Después de la escritura, el puntero queda actualizado. En
los ficheros sin capacidad de acceso aleatorio, la escritura siempre tiene efecto sobre la misma posición.
En los ficheros ordinarios, la escritura se realiza a través del buffer caché, por lo que una llamada a
write no implica una actualización inmediata del disco. Este mecanismo acelera la gestión del disco, pero
presenta problemas de cara a la consistencia de los datos.
Si no ocurre ningún imprevisto, no hay nada que temer, pero en el caso de fallo no previsto -un
corte de la alimentación del equipo, por ejemplo- es posible que se pierdan datos del buffer caché que no
habían sido actualizados. Si al abrir el fichero estaba presente el indicador O_SYNC, forzamos que las
llamadas a write no devuelvan el control hasta que se escriban los datos en el disco, asegurando así la
consistencia. Naturalmente, este modo de trabajo está penalizado con un mayor tiempo de ejecución de
nuestro proceso.
Utilizaremos la llamada close para indicarle al núcleo que dejamos de trabajar con un fichero
previamente abierto. El núcleo se encargará de liberar las estructuras que había montado para trabajar con
el fichero. La declaración de close es:
#include <unistd.h>
int close (int fildes) ;
Si fildes es un descriptor de fichero correcto devuelto por una llamada a creat, open, dup, fcntlo
pipa, close cierra su fichero asociado y devuelve el valor O; en caso contrario, devuelve -1 y errno
contendrá el tipo de error producido. El único error que se puede producir en una llamada a close es que
fildes no sea un descriptor válido.
Al cerrar un fichero, la entrada que ocupaba en la tabla de descriptores de ficheros del proceso
queda libre para que la pueda utilizar una llamada posterior a open. Por otro lado, el núcleo analiza la
entrada correspondiente en la tabla de ficheros del sistema y, si el contador que tiene asociado este fichero
es 1 -esto quiere decir que no hay más procesos que estén unidos a esta entrada-, esa entrada también se
libera.
Si un proceso no cierra los ficheros que tiene abiertos, al terminar su ejecución el núcleo analiza la
tabla de descriptores y se encarga de cerrar los ficheros que aún estén abiertos.
La llamada creat permite crear un fichero ordinario o rescribir sobre uno existente. Su declaración
es:
#include <fcntl.h>
int creat (char *path, mode - t moda) ;
Mode es una máscara de bit s con el mismo significado que vimos para la llamada open. En esta
máscara se especifican los permisos de lectura, escritura y ejecución para el propietario, grupo al que
pertenece el propietario y el resto de los usuarios.
La llamada a creat tiene la misma funcionalidad que una llamada a open con los indicadores
O_WRONLY | O_CREAT | O_TRUNC activos. Así, las siguientes llamadas tienen la misma
funcionalidad:
La llamada dup duplica un descriptor de fichero que ya ha sido asignado y que está ocupando una
entrada en la tabla de descriptores de fichero. Su declaración es:
#include <unistd.h>
int dup Cint fildes);
fildes es un descriptor obtenido a través de una llamada previa a creat, open, dup,
fcntl o pipe.
La llamada a dup recorre la tabla de descriptores y va a marcar como ocupada la primera entrada
que encuentre libre, pasando a devolvemos el descriptor asociado a esa entrada. Si falla en su ejecución,
devolverá el valor -1, indicando a través de errno el error producido.
Los dos descriptores -original y duplicado- tienen en común que comparten el mismo fichero, por
lo que a la hora de leer o escribir podemos usados indistintamente. Cuando estudiemos las tuberías sin
nombre, veremos la utilidad de esta llamada.
ACCESO ALEATORIO (LSEEK)
#include <sys/types.h> .
#include <unistd.h> / / Para las constantes simbólicas.
Off_t lseek C int f ildes, off - t off set, int whence) ;
Lseek modifica el puntero de lectura/escritura del fichero asociado a fildes de la siguiente forma:
. Si whence vale SEEK_SET, el puntero avanza offset bytes con respecto al inicio del fichero.
. Si whence vale SEEK_CUR, el puntero avanza offset bytes con respecto a su posición actual.
. Si whence vale SEEK_END,el puntero avanza offset bytes con respecto al final del fichero.
Si offset es un número positivo, los avances deben entenderse en su sentido natural; es decir, desde
el inicio del fichero hacia el final del mismo. Sin embargo, también se puede conseguir que el puntero
retroceda pasándole a lseekun desplazamiento negativo.
Cuando lseek se ejecuta correctamente devuelve un numero entero no negativo que es la nueva
posición del puntero medida con respecto al principio del fichero. Si falla devuelve -1 y en errno estará el
código del error producido.
En algunos ficheros no esta permitido el acceso aleatorio y por lo tanto la llamada a lseek no tiene
sentido. Ejemplos de esto son las tuberías con nombre y los ficheros de dispositivo en los que la lectura se
realice siempre a través de un mismo registro o posición de memoria.
CONSISTENCIA DE UN FICHERO
La entrada—salida con el disco se realiza a través del buffer caché para agilizar la transferencia de
datos. Hay aplicaciones cuyas especificaciones obligan a que se prescinda del buffer caché y que las
escrituras en un fichero se reflejen de forma inmediata en el disco.
Se consigue pasándole a open, dependiendo del sistema, alguno de los indicadores (O_SYNC,
O_SYNCW). Otra solución es hacer llamadas a fsync.
Referencian los ficheros mediante punteros a estructuras de tipo FILE. Las cuatro primeras
funciones son fopen, fread, fwrite y fclose.
Apertura (fopen)
Abre el fichero cuyo nombre esta apuntado por file_name y le asocia un flujo. El modo de acceso
al fichero ,entre otros, puede tomar diferentes valores como estos:
La tres primeras entradas de la tabla de flujos están ocupadas por los ficheros estándar (#define
stdin, #define stdout, #define stderr). El resto de los elementos quedaran iniciados a cero ya que se
produce una variante global y esa zona además la memoria se inicializa a cero.
La declaración es:
#include <stdio.h>
size_t fread (char ptr, size_t size,size___t nitems, FILE * stream);
Fread copia en el array apuntando por ptr un total de nitems bloques de datos procedentes del
fichero apuntado por stream. Fread termina su lectura cuando encuentra el fichero final y su lectura se
realiza correctamente.
#include <stdio.h>
size_t fwrite(const char * ptr, size_t size,size_t nitems, FILE* streams);
Fwrite copia en el fichero apuntado por stream el número de bloques indicado en nitems, cada
uno de un tamaño byte.
Cierre (fclose)
#include<stdio.h>
int fclose (FILE*stream);
fclose hace que toda memoria intermedia de datos asociada a streams sea escrita en el disco, que el
espacio de memoria reservado para las memorias intermedias sea liberado y que el flujo sea cerrado,
devuelve cero si la llamada funciona correctamente y EOF si se produce algún error.
Hay dos funciones para leer y escribir caracteres y son fgetc(lectura de caracteres) y
fputc(escritura de caracteres).
Fgetc devuelve el carácter siguiente al ultimo leído del fichero asociado a stream. Fgetc lee
caracteres del fichero, devuelve un entero y consigue 2 objetivos: el byte leido se devuelve como un
carácter sin signo, o que, se detecta el final del fichero se puede devolver EOF(-1) sin que haya lugar a
confundirlo con un dato valido.
En fputc se quiere escribir en el fichero y tiene dos marcos equivalentes: getc y putc. Estos actúan
sobre la entrada estándar y la salida estándar, pueden codificarse como marcos a partir de getc y putc:
Para que nuestras funciones no interfieran con las que ya existen, las nombraremos de igual forma
que a las funciones estándar pero anteponiendo en carácter m a cada nombre.
La declaración es:
#include<sys/types.h>
#include<stdio.h>
#include<fcntl.h>
int fcntl (int fildes, int cmd, union {int val; struct flock*lockdes}arg);
Un cerrojo de lectura indica que el proceso actual esta leyendo del fichero y ningún otro proceso
podrá escribir en el área bloqueada.
Un cerrojo de escritura indica que el proceso esta escribiendo en el fichero y ningún otro proceso
se debe leer o escribir del área bloqueada.
Los cerrojos fijados por un proceso sobre un fichero se borran cuando el proceso termina. Si fcntl
no se ejecuta satisfactoriamente, devuelve el valor -1 y en errno estará codificado el tipo de error
producido.
La función getpid, devuelve el valor del identificador del proceso que la llama. La ejecución del
programa arroja resultados comos los siguientes:
4. ADMINISTRACION DE FICHEROS
STAT, LSTAT Y FSTAT
Estas llamadas devuelven la información que se almacena en la tabla de nodos-i sobre el estado
de un fichero concreto. Su declaración:
#include <sys/types.h>
#include <sys/stat.h>
int stat (char *path, struct stat *buf);
int lstat (int fildes, struct stat *buf);
int fstat (int fildes, struct stat *buf);
La diferencia entra stat y fstat es que la primera recibe como primer parámetro un puntero al
nombre del fichero, mientras que la segunda trabaja con un fichero ya abierto y le debemos pasar su
descriptor.
Lstat trabaja de forma parecida a stat. , menos cuando el nombre del fichero corresponde a un
enlace simbólico.
La información administrativa del fichero se almacena en una estructura de tipo struct stat. Esta
definido en el fichero <sys/stat.h>.
Algunos de los campos estándar de esta estructura junto con su tipo asociado:
- dev_t st_dev, número del dispositivo que contiene al nodo-i. Aquí están codificados el minor
number y el major number del dispositivo.
- gid_t st_gid, identificador del grupo –GID- al que pertenece el propietario del fichero.
- dev_t st_rdev, identificador de dispositivo. Tiene significado únicamente para los ficheros
especiales en modo carácter y en modo bloque.
- time_t st_ctime, fecha del último cambio de la información administrativa del fichero.
MODOS DE UN FICHERO
Si queremos saber si un fichero es un directorio o no, se debe usar una expresión como:
Porque si utilizamos,
Nos dará también el valor lógico VERDAD cuando ese fichero sea de tipo especial modo bloque.
- S_ISUID – Cambiar el identificador del usuario en ejecución. Esto tiene aplicación cuando
intentamos acceder a ficheros que son de otro usuario y no tenemos permiso para escribir en
ellos. Como por ejemplo la orden passwd.
Constantes definidas en <sys/stat.h> para el modo de un fichero:
- S_ISVTX – Bit pertinaz. Indica al núcleo que este fichero es un programa con capacidad para
que varios procesos compartan su segmento de código y que este segmento se debe mantener en
memoria, aun cuando alguno de los procesos que lo utiliza deje de ejecutarse o pase al área de
intercambio.
Estas llamadas se utilizan para cambiar el modo de un fichero. Sus declaraciones son:
#include <sys/types>
#include <sys/stat>
int chmod (char *path, mode_t mode);
int fchmod (int fildes, mode_t mode) ;
En chmod especificamos el fichero por su ruta, path, y con fchmod actuamos sobre un fichero ya
abierto y que tiene asociado el descriptor fildes.
Accesibilidad (access)
#include <unisted.h>
int access (char *path, int amode);
Path es un puntero a la ruta del fichero al que queremos acceder, amode es una máscara que
codifica el tipo de acceso por el que preguntamos. En <unistd.h> están definidos los siguientes valores
para mode:
La usamos para definir la máscara de permisos que tendrá asociado un proceso a la hora de crear
ficheros. Declaración:
#include <sys/types.h>
#include <sys/stat.h>
mode_t umask (mode_t cmask);
La nueva máscara por defecto se indica en cmask y umask devuelve el valor que tenía la máscara
anterior.
Declaración:
#include <stdio.h>
int rename (const char *source, const char *target);
El argumento source apunta al nombre inicial del fichero y target al nuevo nombre.
Sirven tanto para cambiar el identificador del propietario de un fichero como el identificador del
grupo. Declaración:
#include <sys/types.h>
int chown (char *path, uid_t owner, gid_t group);
int fchown (int fildes, uid_t owner, gid_t group);
La diferencia entre chown y fchown es que la primera trabaja con la ruta –path- de un fichero
mientras que la segunda lo hace con el descriptor –fildes- de un fichero ya abierto.
Declaración:
#include <sys/types.h>
#include <utime.h>
int utime (char *path, struct utimebuf *times);
path es el puntero al nombre del fichero cuyas fechas queremos cambiar, times es un puntero a
una estructura de tipo struct utimebuf definida en <utime.h>.
Tanto actime como modotime se expresan en segundos. El cambio de fecha solo puede ser
ejecutado por el propietario del fichero y por el superusuario.
Si utime se ejecuta correctamente nos devuelve el valor 0 pero si por el contrario no se ejecuta
correctamente devuelve el valor –1 junto con errno (contiene el código del tipo del error).
Se puede modificar la longitud de un fichero para que este tome cualquier valor comprendido entre
la longitud nula y la actual del fichero mediante las siguientes sintaxis:
En donde length es la nueva longitud del fichero ( bytes ). Truncate trabaja con un fichero
mediante la especificación de su nombre ( path ) y ftruncate trabaja con un fichero ya abierto en modo
lectura ( fildes ).
Getpwuid y getgrgid son dos funciones de biblioteca. La primera de ellas sirve para leer
información relativa al propietario del fichero y se ubica en / etc / passwd
La función getgrgid sirve para buscar información sobre el grupo al que pertenece su propietario.
Esta búsqueda se realiza en / etc / group.
#include <grp.h>
struct group *getgrgid ( gid_t gid);
Bloqueo consultivo: el sistema conoce que recursos están bloqueados y que procesos los
bloquean pero permite que estos recursos sean usados por otros procesos. Solo se puede trabajar con los
recursos en caso de que estos se encuentren libres. Bloqueo adecuado a procesos cooperativos.
Bloqueo obligatorio: el sistema comprueba los accesos a los recursos compartidos para
denegarle el acceso a procesos no autorizados. No hace falta mirar el estado del recurso ya que el sistema
impedirá utilizarlos.
La función lockf bloquea total o parcialmente un fichero impidiendo que oros procesos accedan a
esa región. La sintaxis es:
#include <unisted.h>
int lockf ( int fildes, int function, long size );
Cuando un proceso termina su ejecución se eliminan todos los cerrojos definidos sobre el fichero.
Los ficheros deben estar en modo escritura o lectura / escritura para poder definir los cerrojos:
F_LOCK: bloquea una región para uso exclusivo del proceso que invoca a lockf.
F_TLOCK: comprueba si la región esta disponible, en caso contrario devuelve –1 y errno con el
código del error.
F_TEST: ver si la región esta bloqueada por otro proceso. Si es accesible devuelve 0, en caso
contrario –1 y errno con el error.
Size indica los bytes contiguos que se van a bloquear o desbloquear. El bloqueo empieza en el
puntero hasta donde indique size. Si size vale 0 el bloqueo es hasta el final del fichero.
La mejor solución para realizar cerrojos es con F_TLOCK ya que sus cerrojos son no bloqueantes
y si el proceso no puede seguir adelante con el bloqueo no se queda durmiendo en espera de fijar el
cerrojo.
Con las funciones creat y open la única forma de bloquear un fichero es haciendo uno auxiliar. Si
las llamadas a creat y open fallan significa que el fichero ha sido bloqueado por otro proceso, pero en
caso contrario, el fichero se bloqueo por nuestro proceso. Para quitar el bloqueo basta con borrar el
fichero auxiliar.
La función lockf que se ha implementado es compatible con todas las versiones UNÍX que
admitan la llamada open con parámetros como O_CREAT | O_EXCL. Pero lockf tiene también
inconvenientes como:
- Con una caída del sistema los ficheros auxiliares de bloqueo no se borran, por lo que hay
que hacerlo manualmente. La solución es situarlos en el directorio / usr / tmp.
- El proceso que tiene bloqueado un fichero puede terminar su ejecución sin desbloquearlo
por lo que la solución es escribir en el fichero auxiliar el PID del proceso que bloquea y
mediante la llamada a kill determinar si el proceso existe.
TEMA 4. MANEJO DE DIRECTORIOS Y FICHEROS
ESPECIALES
1 ACCESO A DIRECTORIOS
Los directorios son ficheros que le proporcionan al sistema una estructura jerárquica de árbol
invertido.
Los directorios se pueden abrir mediante una llamada open y pueden leerse mediante la llamada
read, pero ningún usuario puede escribir en los directorios con llamadas a write. Para poder leer de un
directorio es necesario conocer su estructura, pero la organización del mismo depende del sistema, para
solucionar este problema existe una biblioteca estándar de funciones de manejo de directorios; y aunque
no esta permitido modificar la estructura de un directorio, existen llamadas para crear, borrar ...
directorios que actúan de manera indirecta sobre ellos.
Para crear directorios en UNIX existen dos llamadas mknod y mkdir, aunque cada una de ellas
actúa de una manera:
#include <sys/types.h>
#include <sys/stat.h>
int mknod (char *path, mode_t mode, int dev);
Esta llamada para la creación de un directorio solo puede ser usada por un usuario con
privilegios de súper usuario, por lo tanto no es muy útil su utilización.
#include <sys/types.h>
#include <sys/stat.h>
int mkdir (char *path, mode_t mode);
La diferencia respecto a la anterior llamada, es que mkdir puede escribirse a partir de mknod y
cualquier usuario que tenga privilegios de súper usuario podrá ejecutarlo.
#include <unistd.h>
int rmdir (char *path);
Path: es el puntero que contiene la dirección del directorio que queremos borrar.
Inconveniente: el directorio que deseamos borrar debe estar completamente vacío y no puede ser ningún
directorio de trabajo de otro proceso.
En UNIX, en realidad los directorios se utilizan para asignar nombres de ficheros a un nodo-i,
por lo tanto al poder haber varios nombres conectados al mismo nodo-i, se utiliza la llamada link, para
poder enlazar un nombre con un nodo-i.
#include <unistd.h>
int link (char *path1, char *path2);
Para deshacer un enlace que hayamos creados con anterioridad se utiliza la llamada unlink.
#include <unistd.h>
int unlink (char *path);
En UNIX existen multitud de directorios, por ejemplo el directorio raíz, directorio de trabajo
actual, etc... por lo tanto para cambiar de uno a otro utilizamos la llamada chdir.
#include <unistd.h>
int chdir (char *path);
La biblioteca estándar contiene una función que devuelve la ruta del directorio actual:
#include <unistd.h>
char *getcwd (char *buf, int size);
Buf: es el puntero a la zona de memoria donde se guarda la ruta del directorio actual.
Size: es el tamaño de buf.
Si se ejecuta correctamente la llamada devolverá el mismo puntero que le pasamos por buf, de lo
contrario devolverá NULL.
#include <unistd.h>
int chroot (char *path);
Path: apunta a la cadena con la ruta del directorio, que actuara como nuevo directorio raíz.
El inconveniente de esta llamada es que, hay que tener privilegios del súper usuario para su
utilización.
Existen ciertas llamadas especificas para leer el contenido de un directorio sin tener que
preocuparse de la estructura del mismo. Son las siguientes:
#include <sys/types.h>
#include <dirent.h>
DIR *opendir (char *dirname);
Al utilizar esta llamada para abrir un directorio necesitamos reservar un espacio en memoria para
guardar el directorio que devuelve. Si se produce un error devolverá NULL y errno contendrá el código.
#include <sys/types.h>
#include <dirent.h>
struct dirent *readdir (DIR *dirp);
#include <sus/types.h>
#include >dirent.h>
int closedir (DIR *dirp);
Esta llamada además de cerrar el fichero abierto se encarga a su vez de liberar el espacio en
memoria reservado con anterioridad con la llamada a opendir. Si se realiza debidamente devolverá un 0,
sino un -1.
Seekdir, telldir y rewinddir: La primera función sitúa el puntero de lectura de un directorio, telldir
devuelve la posición de ese puntero y rewinddir permite situar el puntero al principio del directorio y deja
todo como quedo después de la llamada a opendir.
#include <sys/types.h>
#include <dirent.h>
void seekdir (DIR *dirp, long loc);
long telldir (DIR *dirp);
void rewinddir (DIR *dirp);
De estas tres funciones la única que devuelve algún valor es telldir, las otras dos nada, si se
ejecuta correctamente devuelve la posición actual del puntero de lectura, de lo contrario devolverá -1.
2. ACCESO A FICHEROS ESPECIALES
Dentro de los ficheros especiales se engloban prácticamente todos los periféricos que hay
conectados a un ordenador.
Un acierto del sistema de UNIX es no darle trato especial a los periféricos y que puedan ser
manipulados como cualquier otro fichero.
Los terminales son dispositivos especiales que trabajan en modo carácter. Todo proceso que se
ejecuta en UNIX tiene asociados 3 descriptores de fichero que le dan acceso a su terminal de control y
son los 3 siguientes:
El fichero de dispositivo que permite a un proceso acceder a su terminal de control es /dev/tty. Con
las llamadas siguientes reservaríamos manualmente nosotros los descriptores anteriores (si el sistema no
los ha reservado él de forma automática):
Close (0);
Open (“/dev/tty”, O_RDONLY); //Reserva del descriptor 0
Close (1);
Open (“/dev/tty”, O_RDONLY); //Reserva del descriptor 1
Close (2);
Open (“/dev/tty”, O_RDONLY); //Reserva del descriptor 2
Cada usuario que inicia una sesión de trabajo interactiva lo hace a través de un terminal, el cual
tiene asociado un fichero de dispositivo que localmente se puede abrir como /dev/tty y que visto por otros
usuarios tiene la forma /dev/tty##, donde ## equivale a dos dígitos.
La orden WRITE es empleada para enviar mensajes a los usuarios que hay conectados al sistema.
Su forma de uso es la siguiente:
$ write usuario
línea de texto 1
línea de texto 2
...
línea de texto n
^D /Fin de fichero/
Esta secuencia hace que Usuario reciba en su terminal las n líneas de texto que le hemos enviado,
pero para ello, antes tenemos que saber si el usuario existe y ha iniciado una sesión de trabajo y también
tenemos que conocer cuál es el fichero de dispositivo que tiene asociado su terminal; para ello, hay que
consultar el fichero /etc/utmp que es gestionado por el sistema y contiene información administrativa de
los procesos que hay ejecutándose en un instante determinado.
TEMA 5. ESTRUCTURA DE UN PROCESO.
1. PROGRAMAS Y PROCESOS
Un programa es una colección de instrucciones y de datos que se encuentran almacenados en un
fichero ordinario y ejecutable.
1- Editar un fichero de texto que contenga una serie de líneas que puedan ser interpretadas por un
intérprete de órdenes ( sh, csh, y ksh).
2- Cambiar los atributos del fichero para indicar que es ejecutable, esto lo realizamos con la orden
chmod.
Esta forma descrita, es una forma sencilla para crear ficheros y presenta grandes ventajas a la hora
de realizar programas cortos, pero supone una limitación a la hora de desarrollar aplicaciones y
programas mas complejos, de mayor envergadura.
Por ello , en la mayoría de las ocasiones, vamos a generar ficheros ejecutables mediante lenguajes
de programación, de alto o bajo nivel. Vamos a emplear en nuestro análisis, el compilador del lenguaje
C. Primero se debe crear un fichero de texto que contenga el código fuente de nuestro programa. El
compilador de C se encarga de traducir el código fuente a código objeto que entiende nuestra máquina y
crea un fichero de salida, que se llama por defecto a.out, y lo marca como ejecutable.
La estructura de un programa ejecutable a grosso modo, consta de las siguientes partes:
Un bloque donde se encuentran las instrucciones en lenguaje máquina del programa. En Unix, este
bloque se conoce como texto del programa.
Un bloque dedicado a la representación en lenguaje máquina de los datos que deben ser
inicializados cuando arranca la ejecución del programa, ( bloque bss).
Cuando un programa es leído del disco por el núcleo y es cargado en memoria para ejecutarse, se
convierte en lo que se llama a un proceso. Un proceso se compone de tres bloques fundamentales
conocidos como segmentos:
El segmento de texto, que contiene las instrucciones que entiende la CPU de nuestra máquina.
El segmento de datos, que contiene los datos que deben ser inicializados al arrancar el proceso.
El segmento de pila, que lo crea el núcleo al arrancar el proceso y su tamaño es gestionado por
éste. La pila se compone de una serie de bloques lógicos, llamados marcos de pila, los cuales son
introducidos cuando se llama a una función y son sacados cuando se vuelve de la función. Un marco de
pila se compone de los parámetros de la función, de las variables locales de la función y la información
necesaria para restaurar el marco de pila anterior a la llamada a la función.
Debido a que los procesos se pueden ejecutar en dos modos: usuario y supervisor ( o kernel ); el
sistema maneja dos pilas por separado, la pila del modo usuario y la pila del modo supervisor.
Unix es un sistema de tiempo compartido que permite la ejecución de varios procesos a la vez (
multiproceso). El planificador es la parte del núcleo encargada de gestionar la CPU y determinar qué
proceso pasa a ocupar la CPU en un determinado instante.
Se puede decir, que un proceso en Unix es una entidad creada tras la llamada fork,. Todos los
procesos, excepto el primero ( proceso número 0), son creados mediante una llamada a fork. El proceso
que llama a fork se conoce como proceso padre, y el proceso creado es el proceso hijo. Todos los
procesos pueden tener varios procesos hijos, pero el proceso padre es único.
El núcleo identifica cada proceso mediante su número PID; el proceso hijo creado se llama init y
su PID vale 1. Este proceso hijo será el encargado de arrancar los demás procesos del sistema según la
configuración que se indica en el fichero / etc / inittab.
2. ESTADOS DE UN PROCESO
El tiempo de vida de un proceso se puede dividir en un conjunto de estados, cada uno con unas
características determinadas.
Un proceso no permanece siempre en un mismo estado, sino que está continuamente cambiando de
acuerdo con unas reglas bien definidas. Estos cambios de estado vienen impuestos por la competencia que
existe entre los procesos para compartir un recurso escaso como es la CPU.
A continuación, este diagrama nos muestra la transición completa de estados que realiza un
proceso de UNIX; en el cual, los nodos representan los diferentes estados que pueden alcanzar los
procesos, y las ramas representan los eventos que hacen que un proceso cambie de un estado a otro:
Los estados que se reflejan en el diagrama son los siguientes:
3.- El proceso no se está ejecutando, pero está listo para ejecutarse tan pronto como
el planificador lo ordene.
4.- El proceso está durmiendo cargado en memoria. Un proceso entra en este estado cuando no
puede proseguir su ejecución porque está esperando a que se complete una operación de entrada / salida.
5.- El proceso está listo para ejecutarse, pero el intercambiador ( proceso 0), debe
cargar el proceso en memoria antes de que el planificador pueda ordenar que pase a ejecutarse.
6.-El proceso está durmiendo y el intercambiador ha descargado el proceso hacia una memoria
secundaria ( área de intercambio del disco), para crear espacio en la memoria principal donde poder
cargar otros procesos.
7.- El proceso está volviendo del modo supervisor al modo usuario, pero el núcleo se apropia del
proceso y hace un cambio de contexto, pasando otro proceso a ejecutarse en modo usuario.
8.- El proceso acaba de ser creado y está en un estado de transición. Este estado es inicial para
todos los procesos, excepto el proceso 0.
9.- El proceso ejecuta la llamada exit y pasa al estado zombi. El proceso ya no existe; el estado
zombi es el estado final del proceso.
La tabla de procesos tiene campos que son accesibles desde el núcleo, pero los campos del área de
usuario sólo necesitan ser visibles por el proceso. Las áreas de usuario se reservan cuando se crea un
proceso y no es necesario que una entrada de la tabla de procesos que no aloja ningún proceso tenga
reservada un área de usuario.
• PID (identificadores de proceso). Determinan las relaciones entre procesos. Son fijados
cuando se crea el proceso mediante una llamada a fork.
• Un campo de señales que enumera las señales recibidas que no han sido tratadas
todavía.
• Temporizadores que indican el tiempo de ejecución del proceso y del uso del
núcleo. Se usan para llevar la contabilidad del proceso y determina prioridades. El usuario
los puede programar con la señal SIGALRM.
El área de usuario contiene información que es necesaria sólo cuando un proceso se está
ejecutando. Campos:
• Registro de errores.
• Tabla de descriptores de fichero. Identifica los ficheros que tiene abiertos el proceso.
• Campos de límite. Restringen el tamaño del proceso y algún fichero sobre el que puede
escribir.
4 Contexto de un proceso.
Es el estado de un proceso definido por: su código, los valores de sus variables de usuario globales
y sus estructuras de datos, el valor de los registros de la CPU, los valores almacenados en su entrada de la
tabla de procesos y en su área de usuario, y el valor de sus pilas de usuario y supervisor. El código del
S.O y sus estructuras de datos globales son compartidos por todos los procesos, pero no son considerados
como parte del contexto del proceso.
Desde un punto de vista formal, el contexto de un proceso es la unión de su contexto del nivel
usuario, nivel de sistema y registro.
El contexto de nivel usuario se compone de los segmentos de texto, datos y pila del proceso, así
como de las zonas de memoria compartida que se encuentran en la zona de direcciones virtuales del
proceso. Las partes del espacio de direcciones virtuales que periódicamente no residen en memoria
principal debido al intercambio o a la paginación también son parte del contexto del nivel de usuario.
• Registro de estado del procesador (RP). Especifica el estado del hardware en relación con
un proceso determinado.
• Puntero de pila. Apunta a la cima de la pila de usuario o de supervisor. La arquitectura de
la máquina dicta si el puntero de pila debe apuntar a la siguiente entrada libre o a la
última entrada usada, así como si la pila crece hacia direcciones altas o bajas de memoria.
El contexto de nivel de sistema de un proceso tiene una parte estática y una parte dinámica. Todo
proceso tiene una única parte estática del contexto del nivel de usuario, pero puede tener un número
variable de partes dinámicas. La parte dinámica es vista como una pila de capas de contexto que el
núcleo puede introducir y sacar según los eventos que se produzcan.
• Área de usuario. Contiene información de control del proceso que necesita ser accedida
sólo en el contexto del proceso.
• Pila de supervisor. Contiene los marcos de pila de las funciones ejecutadas en modo
supervisor. Si bien todos los procesos ejecutan el mismo código del núcleo, hay una copia
privada de la pila del núcleo para cada uno de ellos que da cuenta de las llamadas que
cada proceso hace a las funciones del núcleo. El núcleo debe ser capaz de recuperar el
contenido de la pila del modo supervisor y la posición del puntero de la pila para reanudar
la ejecución de un proceso en modo supervisor. La pila del núcleo está vacía cuando el
proceso se está ejecutando en modo usuario.
• La parte dinámica del contexto del nivel de sistema se compone de una serie de capas que
se almacenan a modo de pila. Cada capa contiene la información necesaria para recuperar
la capa anterior, incluyendo el contexto de registro de la capa anterior.
El núcleo introduce una capa de contexto cuando se produce una interrupción, una llamada al
sistema o un cambio de contexto. Las capas de contexto son extraídas de la pila cuando el núcleo vuelve
del tratamiento de una interrupción, cuando el proceso vuelve al medo usuario después de ejecutar una
llamada al sistema o cuando se produce un cambio de contexto. La capa introducida es la del último
proceso que se estaba ejecutando y la extraída es la del proceso que se pasará a ejecutar.
Un proceso se ejecuta en su capa de contexto actual. El nº de capas de contexto está limitado por el
nº de niveles de interrupción que soporte la máquina.
TEMA 6. GESTIÓN DE PROCESOS E HILOS
Int execl (char *path, char *arg0,... char *argn, (char *)0);
Int execv (char *path, char *argv[ ]);
Int execle(char *path, char *arg0,... char *argn, (char *)0, char *envp[ ]);
Int execve(char *path, char *argv[ ], char *envp[ ]);
Int execlp(char *file, char *argv[ ], char *argn, (char *)0);
Int execvp(char *file, char *argv[ ]);
Si exec no se ejecuta bien nos devolverá –1 y en errno estará el código del tipo de error que se ha
producido.
#include <sys/types.h>
pid_t fork ();
La llamada a fork hace que el proceso actual se duplique. A la salida de fork, los dos procesos
tienen una copia idéntica del contexto de nivel de usuario excepto el valor de pid, que para el proceso
padre toma el valor del PID del proceso hijo, mientras que para el proceso hijo toma el valor 0. El único
proceso que no se crea con la llamada a fork es el proceso 0 creado por el núcleo del sistema. Si fork falla
nos devolverá –1.
Cuando realizamos una llamada a fork, el núcleo del sistema realiza las siguientes operaciones:
Buscara una entrada libre en la tabla de procesos y la reserva para el proceso hijo.
Asigna un PID al proceso hijo, el cual es invariable y único durante toda la vida del proceso y
además constituirá la clave para poder controlarlo desde otros procesos.
Realiza una copia del contexto del nivel de usuario del proceso padre para el proceso hijo.
También se copiaran las tablas de control de ficheros locales del proceso padre al proceso hijo.
Vuelve al proceso padre el PID del proceso hijo y el proceso hijo le devuelve el valor 0.
#include <stdlib.h>
void exit (int status);
Esta llamada termina la ejecución de un proceso y le devuelve el valor de status al sistema. Para
consultarlo podemos utilizar la variable entorno?. Si efectuamos el retorno sin devolver ningún valor en
concreto, el resultado devuelto al sistema estará indefinido.
• Las funciones registradas por atexit son invocadas en orden inverso a como fueron
registradas.
• El contexto del proceso es descargado de memoria por lo que todo relacionado con el
proceso quedara cerrado.
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait (int *stat_loc);
Esta llamada suspende la ejecución del proceso que la invoca hasta que alguno de los procesos
hijos termina. Puede ocurrir que algún proceso hijo termine de forma anormal, exigiendo unas macros que
muestran la terminación del proceso:
WIFEXITED, devuelve verdad a cualquier valor distinto de 0 cuando el proceso termina con una
llamada a exit o wait.
WIXTSTATUS, devuelve los 8 bits menos significativos que exit le pasa al proceso padre.
IDENTIFICADORES DE PROCESO
Todos los procesos tienen dos números, el identificador de proceso que es el PID y el
identificador del proceso padre que es el PPID que a diferencia del PID este puede variar. Esto ocurre
cuando el proceso padre muere y el PPID pasa al proceso hijo poniéndose el valor a 1.
#include <types.h>
pid_t getpid ();
pid_t getppid ();
#include <sys/types.h>
pid_t getpgrp();
#include <sys/types.h>
pid_t setpgrp ();
El UID identifica al usuario que es responsable de la ejecución del proceso y el GID al grupo al
cual pertenece el usuario.
El EUID se usa para determinar el propietario de los ficheros recién creados, comprobar la
mascara de permisos de acceso a ficheros y los permisos para enviar señales a otros procesos. El UID y
EUID coinciden, pero si un proceso ejecuta un programa que pertenece a otro usuario y que tiene activo
el bit S_ISUID el proceso cambia su EUID que toma el valor del UID del nuevo usuario.
1 CONCEPTO DE SEÑAL
Las señales son interrupciones que pueden ser enviadas a un proceso para informale de algun
evento asíncrono o situación especial, se emplea tambien para referirse al evento.
Los procesos pueden enviarse señales unos a otros a través de la llamada kill. Cuando un proceso
recibe una señal puede reaccionar de tres formas distintas:
1- Ignorar la señal.
- Señales relacionadas con las excepciones inducidas por los procesos. Ejemplos: el intento de
acceder fuera del espacio de direcciones virtuales, los errores producidos al manejar numeros en coma
flotante, etc
- Señales relacionadas con los errores irrecuperables originados en el transcurso de una llamada
al sistema.
- Señales originadas desde un proceso que se está ejecutando en modo usuario. Ejemplos: cuando
un proceso envia una señal a otro via kill, cuando un proceso activa un temporizador y se queda en espera
de la señal de alarma,etc.
- Señales relacionadas con la interacción con el terminal. Ejemplo: pulsar las teclas Ctrl+C.
- Señales para ejecutar un proceso paso a paso. Son usadas por los depuradores.
En el fichero de cabecera <signal.h> estan definidas las señales que puede manejar el sistema.
Una señal puede asociarse con una función que procesa el evento que ha ocurrido, el evento no
interrumpe el fuljo del programa.
ENVÍO DE SEÑALES (KILL Y RAISE)
La subrutina Kill envía una señal a uno o varios procesos desde otro proceso.
Su formato es:
#include <signal.h>
int kill (pid_t pid,int sig)
Pid que es el identificador de los procesos que recibirán la señal. Los valores que puede tomar
son:
>0 : identificador del proceso al que le enviamos la señal.
=-1 : la señal es recibida por aquellos procesos cuyo identificador real sea igual al identificador efectivo
del proceso que la envía.
En todos estos casos el proceso que envía la señal tiene que tener el pid de súper usuario o tener
privilegios sobre el proceso al que envía la señal sino la llamada kill falla.
SIG es el número de la señal que queremos enviar. Si vale 0 efectúa una comprobación de
errores, pero no se envía ninguna señal. Si se envía satisfactoriamente Kill devuelve un 0 si no es así
devolverá un -1. También sirve para verificar la validez del Pid.
#include <signal.h>
main()
{
int pid;
if (( pid = fork()) == 0) {
while(1) {
printf("HIJO.PID = %d\n", pid);
sleep(1);
}
}
sleep(10);
printf("PADRE. Terminación del proceso %d\n", pid);
kill (pid,SIGTERM);
exit(0);
}
Este ejemplo crea un hijo y mientras exista debe mostrar la palabra hijo cada segundo
y el padre a los 10 segundos debe mostrar la palabra PADRE y terminar el proceso del hijo con la señal
SIGTERM.
La subrutina raise envía señales al proceso actual, es decir un proceso así mismo.
Su formato es:
#include<signal.h>
int raise(int sig);
La subrutina signal asocia una acción determinada con una señal, es del tipo función que devuelve
un puntero a una función void y recibe dos parámetros :
Su formato es:
#include<signal.h>
void (*signal 8int sig, void(*action) ())) ();
-ACTION Puntero a la rutina asociada con la señal o uno de los valores siguientes:
SIG_DFL : acción por defecto para dicha señal, consiste en terminar el proceso y a veces generar
un fichero core.
#include<signal.h>
void handler (int sig [, int code, struct sigcontext *scp]);
La rutina handler es la rutina a la cual el núcleo llama y la pasa los parámetros sig,code y scp, su
llamada es asíncrona, es decir, se puede dar en cualquier momento de la ejecución del programa y debe
estar codificada para tratar las situaciones en las que se produce el envío de señales.
code, palabra que contiene información del estado del hardware en el momento de invocar a
handler (parámetro opcional).
Los valores SIG_DFL, SIG_IGN Y SIG_ERR son direcciones de funciones para que puedan
devolver signal y deben estar siempre vacías. Se soluciona definiéndolas de esta forma:
La conversión de las constantes hace que estas sean tratadas como direcciones de inicio de
funciones que no contienen ninguna función porque en todas las arquitecturas UNIX son zonas reservadas
para el núcleo.
#include <stdio.h>
#include<signal.h>
/***
main:inicializa el manejador de la señal SIGINT y se pone en espera para recibir la señal.
***/
main()
{
void manejador_SIGINT();
if (signal(SIGINT, manejador_SIGINT) == SIG_ERR) {
perror ("signal");
exit(-1);
}
while(1) {
printf ("En espera de Ctrl-C\n");
sleep (999);
}
}
/***
manejador_SIGINT : rutina de tratamiento de la señal SIGINT.
***/
void manejador_SIGINT (int sig)
{
printf("Señal número %d recibida.\n", sig);
}
El ejemplo anterior hace que la primera vez que se pulse Ctrl-C aparezca un mensaje que ponga En
espera de Ctrl-C y que espere 999 segundos pero la segunda vez que se pulsa Ctrl-C hace que se muestre
el numero de la señal y que ha sido recibida y termina la ejecución del proceso, esto es porque el nucleo
llama a la rutina de tratamiento, se restaura la rutina por defecto, que se encarga de terminar el proceso.
Para resolver el problema se hace este tratamiento:
Esto hace que la rutina de tratamiento siga siendo la misma y se terminara a los 20 segundos.
Si recibimos una señal mientras que tratamos otra del mismo tipo con el caso anterior se terminaría
el proceso ya que al recibirse la señal por primera vez la nueva rutina de tratamiento pasa a ser la rutina
por defecto.
Para bloquear la recepción de señales de un tipo mientras tratamos otra haremos una llamada a
signal pasándole el parámetro SIG_IGN. Como en el siguiente ejemplo:
Otra opción es que una señal interrumpa a la rutina de tratamiento sin que haga terminar el
proceso, pero no es aconsejable ya que puede que la fuente de señales las genere a una velocidad mayor
que la de tratamiento del manejador y esto haría que se desbordara el programa. Por ejemplo:
void manejador_SIGINT(int sig)
{
static cnt = 0;
signal(SIGINT,manejador_SIGINT);
printf("Señal número %d recibida.\n", sig);
if (cnt <20)
print ("Contador = %d\n", cnt++);
else
exit(0);
}
TEMA 9. COMUNICACIÓN MEDIANTE TUBERÍAS
Los mecanismos que se van a tratar ahora pretender dar soluciones más eficientes, empleando
como canal de transmisión la memoria principal, por lo que se provoca un mayor aumento de velocidad
de transferencia de datos.
A la hora de comunicar dos procesos, vamos a considerar dos situaciones claramente diferentes:
• Que los procesos se estén ejecutando bajo el control de una misma maquina.
La primera situación se viene utilizando para comunicar dos o más procesos a nivel local,
mediante un mecanismo como son las tuberías, y después pasaremos a ver las facilidades IPC del UNÍX
System V. Estas engloban tres mecanismos de comunicación: semáforos, memoria compartida y colas de
mensajes.
El segundo escenario es más complejo, porque se ven involucradas las redes de ordenadores y la
comunicación entre ellos.
Las tuberías son una de las primeras formas de comunicación implantadas en UNÍS y muchos
sistemas se ofrecen hoy día con esta facilidad. Incluso sistemas monoproceso como Dos ofrecen
posibilidad de montar tuberías desde el punto de vista del intérprete de órdenes. Una tubería se puede
considerar como un canal de comunicación entre dos procesos, y las hay de dos tipos: tuberías con
nombre –FIFOS- y tuberías sin nombre.
#include <unistd.h>
int pipe(int fildes [2]);
Si llamada funciona correctamente, devolverá el valor 0 y creará una tubería sin nombre; en caso
contrario, devolverá –1 y en errno estará el código del error producido.
La tubería creada podrá ser manejada a través del array fildes. Los dos elementos de fildes se
comportan como dos descriptores de fichero y los vamos a usar para escribir en la tubería y leer de ella.
Al escribir en fildes[1] estamos escribiendo datos en la tubería y al leer fildes[0] extraeremos datos de
ella. Naturalmente, fildes[1] se comporta como un fichero de sólo escritura y fildes[0] como un fichero de
sólo lectura.
Como el núcleo trata la tubería igual que a un fichero del sistema, al crearla debe asignarle un
nodo-i. También le asigna un par de descriptores de fichero –fildes[0] y fildes[1]- y reserva las
correspondientes entradas en la tabla de ficheros del sistema y en la tabla de descriptores del proceso.
Todo esto facilita el manejo de la tubería, ya que al recibir el mismo tratamiento que un fichero,
podremos leer y escribir en ella con las llamadas read y write que empleamos para los ficheros ordinarios,
los directorios y los ficheros especiales.
Los descriptores de fichero se heredan de padres a hijos tras la llamada a fork o a exec. Así, para
que se comuniquen padre e hijo mediante una tubería, la abriremos en el padre y tanto padre como hijo
podrán compartirla.
La sincronización entre los accesos de escritura y lectura la lleva a cabo el núcleo, de tal manera
que las llamadas a read para sacar datos de la tubería no devolverán el control hasta que no haya datos
escritos por otro proceso mediante la correspondiente llamada a write. También es el núcleo el encargado
de gestionar la tubería para dotarla de una disciplina de acceso en hilera y, así, el proceso sacará los datos
en el mismo orden en que los escribía el proceso emisor.
Los datos escritos en la tubería se gestionan en la memoria intermedia sin que lleguen al disco,
por lo que al producirse la transferencia a través de memoria, las tuberías constituyen un mecanismo de
comunicación mucho más rápido que el uso de ficheros ordinarios. El tamaño de una tubería, es decir, el
bloque de datos más grande que podemos escribir en ella, depende del sistema, pero se garantiza que será
inferior a 4.096 bytes.
Cuando la tubería está llena, las llamadas a write quedan bloqueadas hasta que no se saquen
suficientes datos de la tubería como para escribir el bloque deseado.
Para implementar esta comunicación necesitamos otra tubería que sirva de canal entre el proceso
receptor y el emisor. Podríamos sentirnos tentados a aprovechar una sola tubería como canal
bidireccional, pero esto plantea problemas de sincronismo y tendríamos que ayudarnos de señales o
semáforos para controlar el acceso a la tubería. En efecto, si un proceso escribe en la tubería un mensaje
para otro proceso y se pone a leer de ella la respuesta que le envía éste, puede darse el caso de que lea el
mensaje que él mismo envió. Lo mejor es valernos de dos tuberías: una lleva los mensajes que van del
proceso A al proceso B, y la otra lleva los mensajes en sentido contrario.
Normalmente, los programas pueden servirnos para trabajar con otros ficheros sin necesidad de
modificar su código, pero para ello el intérprete de órdenes que nos comunica con el sistema operativo
debe contemplar la redirección. La redirección hacia otros ficheros se le indica al intérprete mediante los
caracteres < y >.
Otra forma de redirigir es mediante las tuberías. Con ellas, lo que conseguimos es que la salida de
un programa se convierta en entrada para otro. Esto es importante a la hora de aprovechar programas
estándar, que realizan funciones sencillas, para construir otros que realizan funciones más complejas.
5. TUBERÍAS CON NOMBRE
Por medio de las tuberías sin nombre podemos comunicar procesos relacionados entre sí ya que el
proceso que crea la tubería y sus descendientes tienen acceso a la misma. Para los procesos que no
guardan ninguna relación de parentesco, no sirven los canales abiertos mediante tuberías sin nombre. Para
comunicar este tipo de procesos tenemos que recurrir a las tuberías con nombre.
Una tubería con nombre es un fichero con una semántica idéntica a la de una tubería sin nombre,
pero ocupa una entrada en un directorio y se accede a él a través de una ruta.
Un proceso puede abrir una tubería con nombre mediante una llamada a open, de la misma forma
que abre un fichero ordinario. Así, para comunicar dos procesos mediante una tubería con nombre, uno de
ellos debe abrir la tubería para escribir en ella y el otro para leer. La llamada open tiene un
comportamiento ligeramente distinto, sigún se trate de abrir una tubería con nombre o un fichero
ordinario. Así, cuando un proceso abre una tubería con nombre para escribir en ella, se pone a dormir
hasta que no haya otro proceso que la abra para leer de ella. Cuando es el proceso lector el primero en
abrir la tubería, se pone a dormir hasta que algún proceso la abre para escribir. Esto tiene sentido, ya que
no valdría para nada escribir en la tubería cuando nadie va a recoger esos datos.
Otra diferencia que hay entre las tuberías con nombre y los ficheros ordinarios es que, para las
primeras, el núcleo sólo emplea los bloques directos de direcciones de su nodo-i, por lo que la cantidad
total de bytes que se pueden enviar a una tubería con nombre en una sola operación de escritura está
limitada.
Para controlar los accesos de escritura y lectura de la tubería, el núcleo emplea dos punteros de tal
forma que cuando el puntero de escritura llega al ultimo de los bloques, empieza por el primero, y lo
mismo ocurre para el puntero de lectura.
Para poder abrir una tubería con nombre, esta debe existir. Hay dos formas de crearla: desde la
línea de ordenes, mediante una llamada a mknod(1M), y desde programas, mediante una llamada a
mknod(2).
Para crear en nuestro directorio de trabajo actual una tubería de nombre fifo_1, usando
mknod(1M), tenemos que escribir:
$mknod fifo_1 p
Para crear esa misma tuberia con la llamada mknod(2), debemos incluir en nuestro programa unas
lineas parecidas a las siguientes:
Donde mode es la máscara de modo que codifica los permisos habituales de lectura-escritura-
ejecución de la tubería y fifo_name es el nombre de la tubería.
De igual forma, allí donde se requieran privilegios de superusuario para ejecutar la llamdad
mknod, se dispone de la llamada mkfifo que le permite a los usuarios sin privilegios crear tuberías con
nombre. La declaración de esta llamada es:
#include<sys/types.h>
#include<sys/stat.h>
int mkfifo (char *path, mode_t mode);
Donde path es la ruta de la tubería con nombre que se va a crear y mode es la máscara de modo
que codifica los permisos de lectura-escritura-ejecución de la tubería. La llamada devuelve el valor 0 o el
valor –1 dependiendo de que se ejecute correctamente o no. Una posible causa de fallo es el intento de
crear una tubería que ya exista. En este caso, el fallo de la llamada no significa que haya un error en
nuestro programa, sólo significa que la tubería ya existe y por lo tanto no puede ser creada de nuevo;
dependiendo del programa que estamos escribiendo, ante esta situación podemos proceder con
normalidad pasando a abrir la tubería para establecer la comunicación y continuar con la ejecución
normal del programa. En caso de que la llamada falle porque la tubería ya existe, mkfifo devuelve –1 y
errno toma el valor EEXIST.
Como ejemplo de aplicación de las tuberías con nombre, vamos a escribir la pareja de programas
llamar-a y responder-a. Estos programas permitirán que dos usuarios se comuniquen mediante el
intercambio de mensajes. Supongamos que el usuario usr1 desea comunicarse con usr2, entonces deberá
escribir:
$ llamar-a usr2
$ responder-a usr1
Una vez iniciada la conversación, los dos usuarios se intercambiarán mensajes alternativamente,
iniciando el envío usr1. Cada mensaje consta de una serie de líneas de texto, finalizando con la línea clave
cambio. Esta línea servirá para pasarle el turno al otro usuario. Cuando alguno de los usuarios envíe la
línea corto, la conversación terminará.
La comunicación se llevará a cabo mediante dos tuberías con nombre creadas por el programa
llamar-a en el directorio /tmp. Estas tuberías tendrán los nombres:
/tmp/fifo_usr1_usr2 y /tmp/fifo_usr2_usr1.
Una de ellas es para los mensajes que vayan de usr2; la otra, para los mensajes que viajen en
sentido contrario.
TEMA 11. COMUNICACIONES EN RED
La interfaz de acceso a la capa de transporte no esta aislada de las capas inferiores, por lo que es
necesario conocer algunos detalles de estas como la familia o dominio de la conexión y el tipo de
conexión.
- El tipo de conexión nos indica si el circuito por el que se van a comunicar los procesos es virtual
(orientado a la conexión) o datagrama (no orientado a la conexión). En el primer caso se buscan enlaces
libres que unan los ordenadores a conectar. Los datagramas por el contrario trabajan con paquetes que
pueden seguir rutas distintas, por lo que no realizan conexiones permanentes.
DIRECCIONES DE RED
SERVIDOR: Es un proceso que se esta ejecutando en un nodo de la red y que gestiona el acceso a
un determinado recurso.
CLIENTE: Es un proceso que se ejecuta en el mismo o en diferente nodo y que realiza peticiones
al servidor. Las peticiones están originadas por la necesidad de acceder al recurso que gestiona el
servidor.
Es servidor se mantiene a la espera de peticiones, hasta que el cliente realiza una. La cumple y
vuelve al estado inicial. Basándonos en esto podemos considerar dos tipos de servidores:
-Concurrentes: El servidor recoge la petición de servicio, pero en lugar de atenderlas crea otros
procesos que lo hacen. Esto solo se puede aplicar en sistemas multiprocesos como UNÍX. Con este
sistema aumenta la velocidad por lo que es recomendable para las aplicaciones donde los tiempos de
servicio son variables.
ESQUEMA GENERAL DE UN SERVIDOR Y DE UN CLIENTE
Accept: Aceptar una conexión. Bloquea el proceso hasta que se recibe una petición de conexión.
Para el cliente:
Para ambos:
Read: Lectura de la petición de servicio para el servidor, y lectura de la respuesta para el cliente.
Write: Envío de los datos al cliente por parte del servidor y petición de servicio del cliente.
APERTURA DE UN PUNTO TERMINAL EN UN CANAL (SOCKET)
Esta crea un punto terminal para conectarse a un canal y devuelve un descriptor. El descriptor del
conector devuelto se usará en llamadas posteriores a funciones de la interfaz. El parámetro “af” determina
que familia de direcciones o conectores vamos a emplear.
AF_INET: Son los protocolos de internet. Utiliza algunos como TCP o UDP.
El parámetro “type” indica la semántica de la comunicación para el conector y puede tomar los
valores:
SOCK_RAW: Sólo puede ser utilizado por usuarios con permisos de superusuario, ya que facilita
el acceso directo a los protocolos internos de la red.
La llamada bind nos sirve para unir un conector con una dirección de red. Hace que el conector
cuyo descriptor es sfd se una a la dirección de conector especificada en la estructura apuntada por addr,
addrlen indica el tamaño de la dirección.
El servidor indica que esta disponible para recibir peticiones con la llamada listen. El tipo de
conector a de ser SOCK_STREAM y esta llamada suele ejecutarse en el proceso servidor tras socket y
bind.
En los servidores interactivos mientras se esta atendiendo a un cliente pueden llegar peticiones de
otros, por lo que es importante la cola de conexiones que habilita el listen.
Para conectores SOCK_DGRAM: Connect especifica la dirección del conector remoto pero no se
conecta con él. Además solo se podrán recibir mensajes procedentes de la dirección especificada.
Para conectores SOCK_STREAM: Connect intenta contactar con el ordenador remoto con objeto
de realizar una conexión entre el conector remoto y el conector local. La llamada permanece bloqueada
hasta que la conexión se completa.
La llamada accept nos sirve para que los procesos descriptores puedan leer peticiones de servicio.
Se usa con conectores orientados a conexión. Extrae la primera petición de conexión que hay en cola,
creada con una llamada previa la listen. Luego crea un nuevo conector con las mismas propiedades que
sfd y reserva un nuevo descriptor de fichero (nsfd) para él.
Accept permanece bloqueada hasta que reciba una nueva petición de conexión cuando no la tiene.
La llamada select puede usarse para ver si el conector tiene pendiente alguna petición de conexión.
El funcionamiento de la llamada read tiene el mismo interfaz que para el manejo de ficheros. Para
conectores su comportamiento es igual exceptuando que obviamente el descriptor de ficheros en realidad
un descriptor de conector.
Las otras cuatro llamadas son variaciones de read que sólo funcionan con conectores.
Como ocurría para el read, tenemos 5 llamadas para escribir datos en un conector. Write, writev,
send, sedto, sedmsg.
La llamada write se comporta también como cuando se usa con ficheros con la salvedad de que el
descriptor de ficheros es en realidad un descriptor de conector.
Writev es una generalización de write y se puede utilizar para fichero y para conector. Las otras
tres devuelven el total de bytes escritos en el conector.
Para desconectar un proceso de un conector podemos utilizar close. Esta llamada cierra el conector
en ambos sentidos.