SISTEMAS OPERATIVOS II
1 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006
P
ROGRAMACIÓN CON LLAMADAS AL SISTEMA POSIX
(INTRODUCCIÓN A LA PROGRAMACIÓN CON UNIX/LINUX)
1-LLAMADAS AL SISTEMA:
1.1- Interfaz del programador:
La interfaz del sistema operativo con el programador es la que recupera los servicios y llamadas al sistema que los usuarios pueden utilizar directamente desde sus programas. Esta es, quizás, una de las partes más importante de un sistema operativo, ya que recupera la visión que como máquina extendida tiene el usuario del sistema operativo. Las interfaces más utilizadas son: POSIX y Win32.
1.2- Concepto de llamada al sistema
La interfaz con el sistema operativo y los programas de usuario está definida por el conjunto de “operaciones extendidas” que el sistema operativo ofrece. Estas instrucciones se han llamado tradicionalmente “llamadas al sistema”, aunque ahora pueden implementarse de varias formas.
Para esconder realmente lo que los sistemas operativos hace, debemos examinar con detenimiento esta interfaz. Las llamadas disponibles en la interfaz varían de un sistema operativo a otro (aunque los conceptos subyacentes tienden a ser similares).
1.3- Implementación:
Las llamadas al sistema se realizan de diversas formas de acuerdo a cada arquitectura de equipo. En una IBM 360/370 existe una llamada al sistema especial que hace un trap directamente al SO. Los 8 bits menos significativos son un número que identifica el tipo de llamada. El CP/M no posee una instrucción de llamada al sistema especial: se carga en el registro C el número de función y se realiza un salto a la dirección 5 de memoria.
A menudo se necesita pasar al SO información adicional al tipo de llamada al sistema. Por ejemplo, para leer una imagen de entrada en tarjetas debemos especificar el archivo o dispositivo a usar y la dirección y longitud del buffer de memoria del cual será leído. El dispositivo o archivo puede ya estar implícito en la llamada al sistema y como las tarjetas tienen siempre 80 caracteres, podría no ser necesario el dato de longitud.
Dos métodos generales son usados para pasar parámetros al SO, el más simple es a través de registros, y el otro, a través de bloques o tabla en memoria.
Las llamadas al sistema están disponibles generalmente en assembler. La mayoría de los sistemas permiten realizar llamadas al sistema desde lenguajes de alto nivel (Pascal, Fortran, C, etc.), en los cuales una llamada realmente involucra funciones predefinidas o llamadas a subrutinas. Ellas pueden generar una llamada a una rutina especial de ejecución que realiza una o varias llamada al sistema.
Algunos lenguajes como el C se han definido de forma tal de reemplazar el assembler por instrucciones de alto nivel, permitiendo que las llamadas sean accedidas directamente.
Un ejemplo de cómo se usan las llamadas al sistema es considerar escribir un programa que lea datos de un archivo y los copie a otro.
1) El programa necesita primeramente los nombres de los 2 archivos: el de entrada y el de salida. Esto puede especificarse de 2 formas: que el programa pregunte al usuario los nombres, el cual en un sistema interactivo requiere un secuencia de llamadas al sistema, primero para escribir la pregunta en la pantalla y luego para leer lo que se contesta vía teclado; en un sistema batch se especifican los nombres con tarjetas de control, en el cual debe haber un mecanismo para pasar estos nombres al programa.
SISTEMAS OPERATIVOS II
2 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 2) Luego se debe abrir el archivo de entrada y crear el de salida. Cada una de estas operaciones requiere llamadas al sistema. Debe también preverse errores como la no existencia del archivo de entrada o la existencia de protección de acceso al mismo.
(Se necesitan llamadas al sistema para manejar cada situación).
3) Una vez abiertos los archivos, se entra en un loop de lectura que lee el archivo de entrada (una llamada al sistema) y escribe sobre el de salida (otra llamada al sistema). Cada una de estas operaciones debe retornar información sobre posibles errores. (Lectura: fin de archivo, error de paridad en la lectura; Escritura: sin espacio en disco, fin de cinta físico, impresora sin papel).
4) Luego de copiar todo el archivo, el programa debe cerrar los mismos (otra llamada al sistema), escribir un mensaje en la consola (otra llamada) y terminar normalmente
Como vemos, los programas hacen uso intensivo de los servicios del SO: toda interacción entre el programa y su ambiente de trabajo lo realizan a través de pedidos al SO. Sin embargo los usuarios, normalmente no alcanzan a ver estos detalles.
1.4- Llamadas al sistemas en Linux:
Una llamada al sistema es normalmente una demanda al sistema operativo (núcleo) para que haga una operación de hardware/sistema especifica o privilegiada. Por ejemplo, en Linux-1.2, se han definido 140 llamadas al sistema. Las llamadas al sistema como close() se implementan en la libc de Linux. Esta aplicación a menudo implica la llamada a una macro que puede llamar a syscall(). Los parámetros pasados a syscall() son el número de la llamada al sistema seguida por el argumento necesario. Los números de llamadas al sistema se pueden encontrar en <linux/unistd.h> mientras que <s ys/syscall.h> actualiza con una nueva libc. Si aparecen nuevas llamadas que no tienen una referencia en libc aun, puede usar syscall(). Como ejemplo, puede cerrar un fichero usando syscall() de la siguiente forma (no aconsejable):
#include <syscall.h> extern int syscall(int, ...); int my_close(int filedescriptor) {
return syscall(SYS_close, filedescriptor); }
En la arquitectura i386, las llamadas al sistema están limitadas a 5 argumentos además del número de llamada al sistema debido al número de registros del procesador. Si usa Linux en otra arquitectura puede comprobar el contenido de <asm/unistd.h> para las macros syscall, para ver cuántos argumentos admite su hardware o cuantos escogieron los desarrolladores.
Estas macros syscall se pueden usar en lugar de syscall(), pero esto no se recomienda ya que esa macro se expande a una función que ya puede existir en una biblioteca. Por consiguiente, sólo los desarrolladores del núcleo deberían jugar a con las macros syscall. Como demostración, aquí tenemos el ejemplo de close() usando una macro syscall.
#include <linux/unistd.h>
_syscall1(int, close, int, filedescriptor);
La macro syscall1 expande la función close(). Así tenemos close() dos veces, una vez en libc y otra vez en nuestro programa. El valor devuelto por syscall() o un una macro syscall es -1 si la llamada al sistema falló y 0 en caso de éxito. Un vistazo a la variable global errno sirve para comprobar que ha ocurrido si la llamada al sistema falló.
Las siguiente llamadas al sistema están disponibles en BSD y SYS-V pero no están disponibles en Linux: audit(), auditon(), auditsvc(), fchroot(), getauid(), getdents(), getmsg(),
SISTEMAS OPERATIVOS II
3 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 mincore(), poll(), putmsg(), setaudit(), setauid().
1.5- POSIX:
POSIX (IEEE96) es un estándar de interfaz de sistemas operativos portables basado en el sistema operativo UNIX. Aunque UNIX era prácticamente un estándar industrial, había bastantes diferencias entre las distintas implementaciones de UNIX, lo que provocaba que las aplicaciones no se pudieran transportar fácilmente entre distintas plataformas UNIX. Este problema motivó a los desarrolladores y usuarios a implementar un estándar internacional con el propósito de conseguir la portabilidad de las aplicaciones en cuanto a código fuente.
POSIX se ha desarrollado entro de la IEEE con la referencia 1003 y también está siendo desarrollado como estándar internacional con la referencia ISO/IEC 9945.
POSIX es una familia de estándares en evolución, cada uno de los cuales cubre diferentes aspectos de los sistemas operativos.
POSIX es una interfaz ampliamente utilizada. Se encuentra disponible en todas las versiones de UNIX y Linux, inclusive Windows NT ofrece un subsistema que permite programar aplicaciones POSIX.
POSIX en una especificación estándar, no define una implementación. Los distintos sistemas operativos pueden ofrecer los servicios POSIX con diferentes implementaciones.
Características más relevantes:
§ Algunos tipos de datos utilizados por las funciones no se definen como parte del estándar, pero se defiene
como parte de la implementación. Estos tipos se encuentran definidos en el archivo de cabecera <sys/types.h>. Estos tipos acaban con el sufijo “_t”. Por ejemplo: uid_t, es un tipo que se emplea para almacenar un identificador de usuario (UID).
§ Los nombres de las llamadas al sistemas en POSIX son en general cortos y con todas sus letras en minúsculas. Ejemplo: fork, close, read.
§ Las funciones, normalmente devuelven cero si se ejecutaron con éxito, o –1 en caso de error. Cuando una función devuelve –1, se almacena en una variable global, denominada errno, el código de error. Este código de error es un valor entero. La variable errno se encuentra definida en el archivo de cabecera <errno.h>.
§ La mayoría de los recursos gestionado por el sistema operativo se referencias mediante descriptores. Un
SISTEMAS OPERATIVOS II
4 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006
G
ESTIÓN DEP
ROCESOS/T
HREAD CONPOSIX
1- SERVICIOS PARA LA GESTIÓN DE PROCESOS:(CARRETERO PEREZ)
1.1- Identificación de procesos:
POSIX identifica cada proceso por medio de un entero único denominado “Identificador de Proceso” (Process ID, PID) de tipo pid_t.
a- Obtener el identificador de un proceso: pid_t getpid(void);
b- Obtener el identificador del proceso padre: pid_t getppid(void) Ejemplo: #include <sys/types.h> #include <stdio.h> main(){ pid_t id_proceso; pid_t id_padre; id_proceso = getpid(); id_padre = getppid();
printf("Identificador de proceso: %d\n", id_proceso); printf("Identificador del proceso padre %d\n", id_padre); }
1.2- Entorno de un proceso:
Viene definido por una lista de variables que se pasan al mismo en el momento de comenzar su ejecución. Estas variables se denominan variables de entorno y son accesibles a un proceso a través de la variable externa “environ”, declarada de la siguiente forma.
extern char **environ;
Esta variable apunta a una lista de variables de entorno. Esta lista no es más que un vector de punteros a cadenas de caracteres de la forma nombre=valor, donde nombre hace referencia al nombre de una variable de entorno y valor al contenido de la misma.
Ejemplo: Programa que imprime el entorno de un proceso #include <stdio.h>
#include <stdlib.h> extern char **environ;
SISTEMAS OPERATIVOS II
5 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 {
int i;
printf("Lista de variables de entorno de %s\n",argv[0]); for (i=0 ; environ[i] != NULL ; i++)
printf("environ[%d] = %s\n", i, environ[i]); }
Cada aplicación interpreta la lista de variables de entorno de forma específica. POSIX establece el significado de determinadas variables de entorno:
HOME: directorio de trabajo inicial del usuario
LOGNAME: nombre del usuario asociado al un proceso PATH: prefijo de directorios para encontrar ejecutables TERM: tipo de terminal
TZ: información de la zona horaria
El servicio “getenv” permite buscar una determinada variable de entorno dentro de la lista de variables de entorno de un proceso.
char *getenv(const char *name);
Esta función devuelve un puntero al valor asociado a la variable de entorno de nombre name. si la variable no se encuentra definida, la función devuelve NULL.
Ejemplo: Programa que imprime el valor de la variable HOME #include <stdio.h>
#include <stdlib.h> int main(){
char *home = NULL; home = getenv("HOME"); if (home == NULL)
printf("$HOME no se encuentra definida\n"); else
printf("El valor de $HOME es %s\n", home); }
1.3- Creación de procesos:
La única forma es invocando la llamada al sistema fork. El SO realiza un clonación del proceso que lo solicite. El proceso que solicita el servicio se convierte en el proceso padre del nuevo proceso.
pid_t fork()
La clonación se realiza copiando la imagen de memoria y la PCB. El proceso hijo es una copia del proceso padre en el instante en que éste solicita al servicio fork. Esto significa que los datos y la pila del proceso hijo son los que tiene el padre en ese instante de ejecución. Es más, dado que, al entrar el sistema operativo a tratar el servicio, lo primero que
SISTEMAS OPERATIVOS II
6 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 hace es salva los registro en la PCB del padre, al copiarse la PCB se copian los valores salvado de los registro, por lo que el hijo tiene los mismos valores que el padre.
Esto significa que el contador de programa de los dos procesos tiene el mismo valor, por lo que van a ejecutar la misma instrucción máquina. No hay que caer en el error de pensar que el proceso hijo empieza la ejecución del código en su punto de inicio, sino que el proceso hijo comienza a ejecutar, al igual que el padre, la sentencia que se encuentra después de fork().
Las diferencias que existen entre el proceso hijo y el padre son:
§ El proceso hijo tiene su propio identificador(PID).
§ El proceso hijo tiene una nueva descripción de la memoria. Aunque el hijo tenga los segmentos con el mismo contenido, no tienen por que esta en la misma zona de memoria.
§ El tiempo de ejecución del proceso hijo es igual a cero.
§ Todas la alarmas pendientes se desactiva en el proceso hijo.
§ El conjunto de señales pendientes se pone en cero.
§ El valor que retorna el sistema operativo como resultado de fork() e distinto en el hijo que el padre (el hijo recibe un 0 y el padre el PID del hijo).
Este valor de retorno se puede utilizar mediante una cláusula de condición para que le padre y el hijo sigan flujos de ejecución distintos.
Las modificaciones que realice el proceso padre sobre sus registros e imagen de memoria después de fork no afectan al hijo y, viceversa. Sin embargo, el proceso hijo tiene su propia copia de los descriptores del proceso padre.
SISTEMAS OPERATIVOS II
7 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 Esto hace que el hijo tenga acceso a los archivos abiertos por el proceso padre. El padre y el hijo comparte el puntero de posición de los archivos abiertos en el padre.
Ejemplo: Programa que crea un proceso #include <sys/types.h> #include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main(){ pid_t pid; pid = fork();
if(pid==-1) /* error del fork() */ printf(“Error\n”);
else if(pid==0)
printf("Soy el HIJO, Proceso %d; padre = %d \n", getpid(), getppid()); else
printf("Soy el PADRE, Proceso %d\n", getpid());
}
// Ejemplo: Programa que crea una cadena de procesos
// En cada ejecución del bucle crea un proceso. El proceso padre obtiene el
// identificador del hijo, que será distinto de cero y saldrá del bucle utilizando // la sentencia “break”. El proceso hijo continuará la ejecución, repitiéndose
// este proceso hasta que se llegue al final del bucle #include <sys/types.h> #include <stdio.h> #include <unistd.h> int main() { pid_t pid; int i; int n = 10; for (i = 0; i < n; i++){ pid = fork(); if (pid != 0) break; }
printf("El padre del proceso %d es %d\n", getpid(), getppid()); }
SISTEMAS OPERATIVOS II
8 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006
1.4- Ejecutar un programa:
El servicio “exec” de POSIX tiene por objetivo cambiar el programa (el código) que se está ejecutado. Se puede considerar que el servicio tiene dos fases. En la primera se vacía el proceso de casi todo su contenido, mientras en la segunda se carga un nuevo programa.
“exec” no genera ningún proceso nuevo, simplemente cambia el programa que ejecuta el proceso que lo invocó. En el proceso de vaciado de la imagen de memoria se conservan algunos datos como ser:
§ Entorno del proceso que el SO lo incluye en la nueva pila.
§ Ciertos datos de la PCB (PID, PPID, identificador de usuario y descriptores de archivos abiertos).
Fase de carga:
§ Asignar al proceso un nuevo espacio de memoria.
§ Cargar el texto y los datos iniciales en los segmentos correspondientes.
§ Crear la pila inicial del proceso con el entorno y los parámetros que se pasa al programa.
§ Llenar la PCB con los valores iniciales de los registro y la descripción de los nuevo segmentos de memoria.
Prototipos de la familia de funciones exec: int excl(char *path, char *arg, ...); int execv(char *path, char *arg[]); int execle(char *path, char *arg, ...);
int execve(char *path, char *arg[], char *envp[]); int execlp(char *file, const char *arg, …);
int execvp(char *file, char *argv[]);
La nueva imagen se construye a partir de un archivo ejecutable. Si la llamada se ejecuta con éxito, ésta no devolverá ningún valor puesto que la imagen del proceso habrá sido reemplazada, caso contrario devuelve –1.
La función main() del nuevo programa tendrá la forma: int main(int argc, char **argv)
SISTEMAS OPERATIVOS II
9 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 donde argc representa el número de argumentos que se pasan al programa, incluido el propio nombre del programa, y argv es un vector de cadenas de caracteres, conteniendo cada elemento de este vector un argumento pasado al programa. El primer componente de este vector (argv[0]) representa el nombre del programa.
Argumentos:
path: apunta al nombre del archivo ejecutable donde reside la nueva imagen del proceso.
file: se utiliza para construír el nombre del archivo ejecutable. Si el argumento file contiene el carácter /, entonces el argumento file constituye el nombre del archivo ejecutable. Casa contrario, el prefijo del nombre para el archivo se construye por medio de la búsqueda en los directorios pasados en la variable de entrono PATH.
argv contiene los argumentos pasadas al programa y debería acabar con un puntero NULL.
envp apunta al entorno que se pasará al nuevo proceso y se obtiene de la variable externa environ.
Los descriptores de los archivos abiertos previamente por le proceso que realiza la llamada exec permanecen abiertos en la nueva imagen del proceso, excepto aquellos abiertos con el valor FD_CLOEXEC.
Las señales con la acción por defecto seguirán por defecto. Las señales ignoradas seguirán ignoradas por el nuevo proceso y las señales con manejadores activados tomaran la acciones por defecto en la nueva imagen del proceso. Atributos que se mantienen después de la llamada:
§ PID § PPID § UID § GID § EUID § EGID
§ Directorio de trabajo actual (Work Directory) § Máscara de creación de archivos.
§ Máscara de señales del proceso. § Señales pendientes.
// Ejemplo: Programa que ejecuta el comando ls –l #include <sys/types.h> #include <sys/wait.h> #include <stdio.h> #include <unistd.h> int main() { pid_t pid; int status; pid = fork(); switch(pid) {
case -1: /* error del fork() */ exit(-1);
case 0: /* proceso hijo */ execlp("ls","ls","-l",NULL); perror("exec");
break;
default: /* padre */ printf("Proceso padre\n");
SISTEMAS OPERATIVOS II
10 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 while(pid != wait(&status));
} }
// Ejemplo: Programa que ejecuta el comando ls –l mediante execvp #include <sys/types.h>
#include <stdio.h> #include <unistd.h>
int main(int argc, char **argv) { pid_t pid; char *argumentos[3]; argumentos[0] = "ls"; argumentos[1] = "-l"; argumentos[2] = NULL; pid = fork(); switch(pid) {
case -1: /* error del fork() */ exit(-1);
case 0: /* proceso hijo */
execvp(argumentos[0], argumentos); perror("exec"); break; default: /* padre */ printf("Proceso padre\n"); } }
// Ejemplo: Programa que ejecuta el comando desde el shell #include <sys/types.h>
#include <stdio.h> #include <unistd.h>
int main(int argc, char **argv) {
pid_t pid; pid = fork(); switch(pid) {
case -1: /* error del fork() */ perror("fork");
break;
SISTEMAS OPERATIVOS II
11 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 if (execvp(argv[1], &argv[1])< 0) perror("exec"); break; default: /* padre */ printf("Proceso padre\n"); } }
2-SERVICIOS POSIX PARA LA GESTIÓN DE HILOS (THREAD):(CARRETERO PEREZ) 2.1- Atributos de un thread:
Cada hilo en POSIX tiene asociado una serie de atributos que representa sus propiedades. Los valores de estos se almacenan en un objeto atributo de tipo pthread_attr_t.
a) Crear un atributo:
Permite iniciar una estructura atributo que se puede utilizar para crea nuevos procesos ligeros. int pthread_attr_init(pthread_attr_t *attr);
b) Destruir atributos:
int pthread_attr_destroy(pthread_attr_t *attr);
c) Asignar el tamaño de la pila:
Cada hilo tiene una pila cuyo tamaño se pude establecer mediante esta función. int pthread_attr_setstacksize(pthread_attr_t *attr, int stacksize);
d) Determinar el tamaño de la pila:
int pthread_attr_getstacksize(pthread_attr_t *attr, int *stacksize)
e) Establecer el estado de terminación:
int phtread_attr_setdatachstate(pthread_attr_t *attr, int detachstate;
Si el valor del argumento detachstate es PTHREAD_CREATE_DETACHED, el proceso ligero que se cree con esos atributos se considerará como independiente y liberará sus recursos cuando finalice su ejecución. Si el valor del argumento detachstate es PTHREAD_CREATE_JOINABLE, el proceso ligero se crea como no independiente y no liberará sus recursos cuando finalice su ejecución. En este caso es necesario que otro proceso ligero espere por su finalización. Esta espera se consigue mediante el servicio pthread_join.
SISTEMAS OPERATIVOS II
12 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006
2.2- Creación e identificación de procesos ligeros: a) Crear un proceso ligero:
int pthread_create(pthread_t *thread, pthread_attr_r *attr, void *(*start routine) (void *), void *arg); El primer argumento de la función apunta al identificador del proceso ligero que se crea, este identificador viene determinado por el tipo pthread_t. El segundo argumento especifica los atributos de ejecución asociados al nuevo proceso ligero. Si el valor de este segundo argumento es NULL, se utilizarán los atributos por defecto, que incluyen la creación del proceso como no independiente. El tercer argumento indica el nombre de la función a ejecutar cuando el proceso ligero comienza su ejecución. Esta función requiere un solo parámetro que se especifica con el cuarto argumento, arg.
b) Obtener el identificador de un proceso ligero: pthread_t pthread_self()
2.3- Terminación de procesos ligeros:
a) Esperar la terminación de un proceso ligero:
Este servicio es similar a wait, pero a diferencia de éste, es necesario especificar el proceso ligero por el que se quiere esperar, que no tiene por qué ser un proceso ligero hijo.
int pthread_join(pthread thid, void **value);
La función suspende la ejecución del proceso ligero que la invoca hasta que el proceso ligero con identificador thid finalice su ejecución. La función devuelve en el segundo argumento el valor que pasa el proceso ligero que finaliza su ejecución en el servicio pthread_exit, que se verá a continuación. Únicamente se pude solicitar el servicio pthread_join sobre procesos ligeros creados como no independientes.
b) Finalizar la ejecución de un proceso ligero: int pthread_exit(void *value)
Incluye un puntero a una estructura que es devuelta al proceso ligero que ha ejecutado la correspondiente llamada a pthread_join, lo que es mucho más genérico que el parámetro que permite el servicio wait.
SISTEMAS OPERATIVOS II
13 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 En la figura se muestra una jerarquía de procesos ligeros. Se supone que el proceso ligero A es el primario, por lo que corresponde a la ejecución del main. Los proceso B, C, y D se han creado mediante pthread_create() y ejecutan respectivamente los procedimientos b(), c() y d(). El proceso ligero D se ha creado como “no independiente” por lo que otro proceso puede hacer una operación join sobre él. La figura muestra que le proceso ligero C hace una operación join sobre D, por lo que queda bloqueado hasta que termine.
//Ejemplo
//Programa que crea dos procesos ligeros no independientes #include <pthread.h> #include <stdio.h> void *func(void * jj){ printf("Thread %d \n", pthread_self()); pthread_exit(0); } int main() { pthread_t th1, th2;
/* se crean dos procesos ligeros con atributos por defecto */ pthread_create(&th1, NULL, func, NULL);
pthread_create(&th2, NULL, func, NULL);
printf("El proceso ligero principal continua ejecutando\n"); /* se espera su terminación */
pthread_join(th1, NULL); pthread_join(th2, NULL);
SISTEMAS OPERATIVOS II
14 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 exit(0);
}
/* Ejemplo
Programa que crea diez procesos ligeros independientes, que liberen sus recursos cuando finalizan ( se han creado con el atributo PTHREAD_CREATE_DETACHED). En este caso no se puede esperar la terminación de los procesos ligeros, por lo que el proceso ligero principal que ejecuta el código de la función main debe continuar su ejecución en paralelo con ellos. Para evitar que el proceso ligero principal finalice la ejecución de la función main, lo que supone la ejecución del servicio exit y, por lo tanto, la finalización de todo el proceso, junto con todos los procesos ligeros, el proceso ligero principal suspende su ejecución durante cinco segundo para dar tiempo a la creación y destrucción delos procesos ligeros que se han creado.
La ejecución de exit supone la finalización del proceso que ejecuta la llamada. Esto supone, por lo tanto, la finalización de todos sus procesos ligeros, ya que éstos sólo tienen sentido dentro del contexto de un proceso. */
#include <pthread.h> #include <stdio.h> #include <unistd.h> #define MAX_THREADS 10 void *func(void *jj){ printf("Thread %d \n", pthread_self()); pthread_exit(0); } int main(void) { int j; pthread_attr_t attr; pthread_t thid[MAX_THREADS];
/* Se inicial los atributos y se marcan como independientes */ pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); for(j = 0; j < MAX_THREADS; j ++)
pthread_create(&thid[j], &attr, func, NULL);
/* El proceso ligero principal no puede esperar la finalización */ /* de los procesos ligeros que creado y se suspende durante un */ /* cierto tiempo esperando su finalización */
sleep(5); }
//Ejemplo
//Programa que crea un proceso ligero por cada número introducido #include <pthread.h>
#include <stdio.h> #define MAX_THREADS 10 struct pal{
SISTEMAS OPERATIVOS II
15 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 int n;
};
void *imprimir(void *n){
struct pal *j = (struct pal *) n;
printf("Thread %d %d \n", pthread_self(), j->n); pthread_exit(0); } int main(void) { pthread_attr_t attr; pthread_t thid; int num; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); while(1){
printf("Escribir numero entero :\n"); scanf("%d", &num); /* espera */
pthread_create(&thid, &attr, &imprimir, &num); }
SISTEMAS OPERATIVOS II
16 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006
COMUNICACIÓN ENTRE PROCESOS EN LINUX
1- INTRODUCCIÓN
Los medios IPC (Inter-Process Communication) de Linux proporcionan un método para que múltiples procesos se comuniquen unos con otros. Hay varios métodos de IPC disponibles para los programadores Linux en C:
§ Pipes UNIX Half-duplex § FIFOs (pipes con nombre) § Colas de mensajes estilo SYSV § Semáforos estilo SYSV
§ Segmentos de memoria compartida estilo SYSV § Sockets (estilo Berkeley)
§ Señales
Los tubos, mensajes y memoria compartida proporcionan un medio de comunicación de datos entre procesos, mientras que los semáforos y las señales se usan para provocar acciones en otros procesos.
Estos medios, cuando se usan de forma efectiva, proporciona una base sólida para el desarrollo de cliente/servidor en cualquier sistema UNIX (incluido Linux).
2- IPC EN SISTEMA V
2.1 Conceptos fundamentales:
Con Unix Sistema V, AT&T introdujo tres nuevas formas de las facilidades IPC (colas de mensajes, semáforos y memoria compartida). Mientras que el comité POSIX aun no ha completado su estandarización de estas facilidades, la mayoría de las implementaciones soportan éstas. Además, Berkeley (BSD) usa sockets como su forma primaria de IPC, más que los elementos del Sistema V. Linux tiene la habilidad de usar ambas formas de IPC (BSD y System V).
Identificadores IPC
Cada objeto IPC tiene un único identificador IPC asociado con él. Cuando decimos “objeto IPC”, hablamos de una simple cola de mensaje, semáforo o segmento de memoria compartida. Se usa este identificador, dentro del núcleo, para identificar de forma única un objeto IPC. Por ejemplo, para acceder un segmento particular memoria compartida, lo único que requiere es el valor del ID que se le ha asignado a ese segmento.
La unicidad de un identificador es importante según el tipo de objeto en cuestión. Para ilustrar esto, supondremos un identificador numérico “12345". No puede haber nunca dos colas de mensajes con este mismo identificador, pero existe la posibilidad que existan una cola de mensajes y un segmento de memoria compartida que poseen el mismo identificador numérico.
Claves IPC
Para obtener un identificador único, debe utilizarse una clave. Esta debe ser conocida por ambos procesos cliente y servidor. Este es el primer paso para construir el entorno cliente/servidor de una aplicación.
Cuando usted llama por teléfono a alguien, debe conocer su número. Además, la compañía telefónica debe conocer cómo dirigir su llamada al destino. Una vez que el receptor responde a su llamada, la conexión tiene lugar.
En el caso de los mecanismos IPC de Sistema V, el “teléfono" coincide con el tipo de objeto usado (semáforo, cola de mensaje, etc). El “número de teléfono”, se puede comparar con la clave IPC.
La clave puede ser el mismo valor cada vez cada vez que se ejecuta el programa, incluyendo su valor en código. Esta es una desventaja pues la clave requerida puede estar ya en uso. Por eso, la función ftok() nos será útil para generar claves no utilizadas para el cliente y el servidor.
FUNCIÓN DE LIBRERÍA: ftok();
PROTOTIPO: key_t ftok ( char *nombre, char proj );
SISTEMAS OPERATIVOS II
17 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 La clave devuelta por ftok() se genera por la combinación del número del i-nodo y el número menor de dispositivo del archivo argumento, con el carácter identificador del segundo argumento. Este no garantiza la unicidad, pero una aplicación puede comprobar las colisiones y reintentar la generación de la clave.
key_t miclave;
miclave = ftok("/tmp/miaplic", 'a');
En el caso anterior el directorio /tmp/miaplic se combina con la letra 'a'. Otro ejemplo común es usar el directorio actual:
key_t miclave;
mykey = ftok(".", 'a');
El algoritmo de la generación de la clave usado está completamente a la discreción del programador de la aplicación. Mientras que tome medidas para prevenir las condiciones críticas, bloqueos, etc, cualquier método es viable. Para nuestros propósitos de demostración, usaremos ftok().
El valor clave que se obtiene, se usa en las llamada al sistema IPC para crear u obtener acceso a los objetos IPC. El comando ipcs
ipcs puede utilizarse para obtener el estado de todos los objetos IPC Sistema V. ipcs -q: Mostrar solo colas de mensajes
ipcs -s: Mostrar solo los semáforos
ipcs -m: Mostrar solo la memoria compartida ipcs --help: Otros argumentos
Por defecto, se muestran las tres categorías. Considérese el siguiente ejemplo de salida del comando ipcs: --- Shared Memory Segments ---
shmid owner perms bytes nattch status --- Semaphore Arrays ---
semid owner perms nsems status --- Message Queues ---
msqid owner perms used-bytes messages 0 root 660 5 1
Aquí vemos una simple cola mensaje que tiene un identificador “0”. Es propiedad del root, y tiene permisos 660, o -rw-rw--. Hay un mensaje en la cola, y ese mensaje tiene un tamaño total de 5 bytes. Los comandos ipcs son una herramienta muy potente que proporciona una introducción a los mecanismos de almacenamiento del núcleo para objetos IPC.
El comando ipcrm
Se puede usar el comando ipcrm para quitar un objeto IPC del núcleo.
Mientras que los objetos IPC se pueden quitar mediante llamadas al sistema en el código del usuario, aparece a menudo la necesidad, sobre todo en ambientes de desarrollo, de quitar objetos IPC a mano. Su uso es simple:
SISTEMAS OPERATIVOS II
18 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 ipcrm <msg | sem | shm> <IPC ID>
Simplemente especifique si el objeto a eliminar es una cola de mensaje (msg), un semáforo (sem), o un segmento de memoria compartida (shm).
El identificador de IPC se puede obtener mediante los comandos ipcs. Tiene que especificar el tipo de objeto, dado que los identificadores son únicos dentro de un mismo tipo, pero no entre distintos tipos de objetos.
2.2 Semáforos: Conceptos Básicos
Los semáforos se pueden describir mejor como contadores que se usan para controlar el acceso a recursos compartidos por múltiples procesos. Se usan con más frecuencia como un mecanismo de cierre para prevenir que los procesos accedan a un recurso particular mientras otro proceso lo está utilizando. Los semáforos son a menudo considerados como el más difícil asir de los tres tipos de objetos Sistema V IPC. Para comprender totalmente los semáforos, los discutiremos brevemente antes de comenzar cualquier llamada al sistema y teoría operacional.
El nombre de semáforo es realmente un término viejo del ferrocarril, que se usaba para prevenir accidentes, en el cruce de las vías de los viejos carros.
Exactamente lo mismo se puede decir sobre un semáforo. Si el semáforo está abierto (los brazos en alto), entonces un recurso está disponible (los carros cruzarían las vías). Sin embargo, si el semáforo está cerrado (los brazos están abajo), entonces el recurso no están disponible (los carros deben esperar).
Mientras que con este ejemplo simple nos introduce el concepto, es importante darse cuenta de que los semáforos en POSIX se llevan a cabo realmente como conjuntos, en lugar de como entidades solas. Por supuesto, un conjunto de semáforos dado puede tener sólo un semáforo, como en nuestro ejemplo del ferrocarril.
Quizás otra aproximación al concepto de semáforos, sería pensar en ellos como contadores de recursos. Apliquemos este concepto a otro caso del mundo real. Considere un spooler de impresión, capaz de manipular impresoras múltiples, con cada manejo de la impresora con demandas de la impresión múltiples. Un hipotético manejador del spool de impresión utilizará un conjunto de semáforos para supervisar el acceso a cada impresora.
Suponemos que en nuestro cuarto de impresoras, tenemos 5 impresoras conectadas. Nuestro manejador del spool asigna un conjunto de 5 semáforos a él, uno por cada impresora del sistema. Como cada impresora es capaz de imprimir físicamente un único trabajo en un instante, cada uno de nuestros cinco semáforos se inicializará con un valor de 1 (uno), lo que significa que están todas en línea, y aceptan trabajos.
Juan envía una petición de impresión al spooler. El manejador de la impresión mira los semáforos, y encuentra que el primer semáforo tiene un valor de uno. Ante enviar la petición de Juan al aparato físico, el manejador de impresión decrementa el semáforo de la impresora correspondiente. Ahora, el valor de ese semáforo es cero. En nuestro ejemplo no se pueden enviar a esa impresora ninguna otra petición hasta que sea distinto de cero.
Cuando el trabajo de Juan se ha realizado, el gestor de impresión incrementa el valor del semáforo correspondiente. Su valor vuelve a ser uno (1), lo que indica que el recurso vuelve a estar disponible. Naturalmente, si los cinco semáforos tienen valor cero, indica que todas las impresoras están ocupadas con peticiones y no se pueden atender más.
Aunque este es un ejemplo simple, procure no confundirse con el valor inicial (1) dado a los semáforos. En realidad, cuando se ven como contadores de recursos, pueden ser iniciados con cualquier valor positivo, y no están limitados a valer 0 o 1. Si las impresoras de nuestro ejemplo fuesen capaces de aceptar 10 trabajos de impresión, habríamos iniciado sus semáforos con el valor 10, decrementándolo en 1 cada vez que llega un trabajo nuevo y reincrementándolo al terminar otro. Como descubriremos en este capítulo, el funcionamiento de los semáforos tiene mucha relación con el sistema de memoria compartida, actuando como guardianes para evitar múltiples escrituras en la misma zona de memoria.
SISTEMAS OPERATIVOS II
19 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 Antes de entrar en las llamadas al sistema relacionadas, repasemos varias estructuras de datos internas usadas en las operaciones con semáforos.
Estructuras de datos internas
Veamos brevemente las estructuras de datos mantenidas por el núcleo para los conjuntos de semáforos.
El núcleo mantiene unas estructuras de datos internas especiales por cada conjunto de semáforos dentro de su espacio de direcciones. Esta estructura es de tipo semid_ds y se define en linux/sem.h como sigue:
/* Hay una estructura semid_ds por cada juego de semáforos */ struct semid_ds {
struct ipc_perm sem_perm; /* permisos .. ver ipc.h */ time_t sem_otime; /* ultimo instante semop */ time_t sem_ctime; /* ultimo instante de cambio */
struct sem *sem_base; /* puntero al primer semáforo del array */ struct wait_queue *eventn;
struct wait_queue *eventz;
struct sem_undo *undo; /* deshacer peticiones del array*/ ushort sem_nsems; /* no. de semaforos del array */ };
Como con las colas de mensaje, las operaciones con esta estructura son ejecutados por llamadas especiales al sistema especial, y no se deben usar directamente. Aquí tenemos las descripciones de los campos más interesantes: sem_perm
Este es un caso de la estructura ipc perm, que se define en linux/ipc.h. Toma la información de los permisos para el conjunto de semáforos, incluyendo permisos de acceso e información sobre el creador del conjunto (uid, etc).
sem_otime
Instante de la última operación semop(). sem_ctime
Instante del último cambio de modo. sem_base
Puntero al primer semáforo del array (ver siguiente estructura). sem_undo
Número de solicitudes de deshacer en el array. sem_nsems
Número de semáforos en el conjunto (el array) Estructura sem del núcleo
En la estructura semid_ds, hay un puntero a la base del array del semáforo. Cada miembro del array es del tipo estructura sem. También se define en linux/sem.h:
/* Una estructura por cada juego de semáforos */ struct sem {
SISTEMAS OPERATIVOS II
20 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 short sempid; /* pid de ultima operación */
ushort semval; /* valor actual */
ushort semncnt; /* num. procesos esperando para incrementarlo */ ushort semzcnt; /* num. procesos esperando que semval sea 0 */ };
sempid
El PID (identificador del proceso) que realizó la última operación. semval
Valor actual del semáforo. semncnt
Número de procesos esperando la disponibilidad del recurso. semzcnt
cantidad de semáforos esperando que semval = 0 LLAMADA AL SISTEMA: semget()
Se usa para crear un nuevo conjunto o acceder a uno existente.
El primer argumento de semget() es el valor clave (en nuestro caso devuelto por la llamada a ftok()). Este valor clave se compara con los valores clave existentes en el núcleo para otros conjuntos de semáforos. Ahora, las operaciones de apertura o acceso depende del contenido del argumento semflg.
Desafortunadamente, otro proceso podría haber elegido la misma clave, por lo que ambos accedería al mismo semáforo. Usando la constante especial IPC_PRIVATE como valor de la clave se busca garantizar que un nuevo semáforo sea crado.
Un nuevo conjunto de nsems semáforos se crea si key tiene el valor IPC_PRIVATE, o si key no vale IPC_PRIVATE, no hay un conjunto de semáforos asociado a key, y el bit IPC_CREAT vale 1 en semflg
IPC_CREAT
Crea el juego de semáforos si no existe ya en el núcleo. IPC_EXCL
Al usarlo con IPC CREAT, falla si el conjunto de semáforos existe ya. Si se usa IPC_CREAT solo, semget(), bien devuelve el identificador del semáforo para un conjunto nuevo creado, o devuelve el identificador para un LLAMADA AL SISTEMA: semget();
PROTOTIPO: int semget ( key_t key, int nsems, int semflg ); RETORNA: Identificador IPC del conjunto, si tuvo éxito.
-1 si existió un error y errno=
EACCESS (permiso denegado)
EEXIST (no puede crearse pues existe (IPC_EXCL)) EIDRM (conjunto marcado para borrarse)
ENOENT (no existe el conjunto ni se indicó IPC_CREAT) ENOMEM (No hay memoria suficiente para crear) ENOSPC (Limite de conjuntos excedido)
SISTEMAS OPERATIVOS II
21 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 conjunto que existe con el mismo valor clave. Si se usa IPC_EXCL junto con IPC_CREAT, entonces se crea un conjunto nuevo, o si el conjunto existe, la llamada falla con –1. IPC_EXCL es inútil por sí mismo, pero cuando se combina con IPC_CREAT, se puede usar como una facilidad garantizar que ningún semáforo existente se abra accidentalmente para accederlo.
Como sucede en otros puntos del IPC del Sistema V, puede aplicarse a los parámetros anteriores, un número octal para dar la máscara de permisos de acceso de los semáforos. Debe hacerse con una operación OR binaria. El argumento nsems especifica el número de semáforos que se deben crear en un conjunto nuevo. Este representa el número de impresores en nuestro cuarto de impresión ficticio descrito antes. El máximo número de semáforos en un conjunto se define en\linux/sem.h" como:
#define SEMMSL 32 /* <=512 max num de semáforos por id */
Observe que el argumento nsems se ignora si abre explícitamente un conjunto existente. Creemos ahora una función de cobertura para abrir o cerrar juegos de semáforos: int abrir_conj_semaforos( key_t clave, int numsems ){
int sid;
if ( numsems > 0)
return semget(clave, numsems, IPC_CREAT | 0660 ); else
return(-1); }
Vea que se usan explícitamente los permisos 0660. Esta pequeña función retornará, bien un identificador entero del conjunto de semáforos, o bien un -1 si hubo un error.
En el ejemplo del final de esta sección, observe la utilización del flag IPC_EXCL para determinar si el conjunto de semáforos existe ya o no.
LLAMADA AL SISTEMA: semop()
El primer argumento de semop() es el identificador IPC (en nuestro caso devuelto por una llamada a semget(). El segundo argumento (sops) es un puntero a un array de operaciones para que se ejecuta en el conjunto de semáforo, mientras el tercer argumento (nsops) es el número de operaciones en ese array.
LLAMADA AL SISTEMA: semop();
PROTOTIPO: int semop ( int semid, struct sembuf *sops, unsigned nsops); RETURNS: 0 si tuvo éxito (todas las operaciones realizadas)
-1 si existió un error y la variable “errno” =
E2BIG (nsops mayor que máx. número de opers. permitidas atómicamente) EACCESS (permiso denegado)
EAGA IN (IPC_NOWAIT incluido, la operación no terminó) EFAULT (dirección no v_alida en el parámetro sops) EIDRM (el conj. de semáforos fue borrado)
EINTR (Recibida señal durante la espera) EINVAL (el conj. no existe, o semid inválido)
ENOMEM (SEM_UNDO incluido, sin memoria suficiente crear la estructura de retroceso necesaria)
SISTEMAS OPERATIVOS II
22 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 El argumento sops apunta a un array del tipo sembuf. Se declara esta estructura en linux/sem.h como sigue:
/* La llamada al sistema semop usa un array de este tipo */ struct sembuf {
ushort sem_num; /* posición en el array */
short sem_op; /* operación del semáforo */
short sem_flg; /* flags de la operación */
}; sem_num
Número de semáforo sobre el que desea actuar, recuerde que en Linux no se crean semáforos individuales, sino conjuntos de semáforos. Este atributo permite elegir o seleccionar el semáforo del conjunto sobre el que se desea actual
sem_op
Operación a realizar (positiva, negativa o cero) sem_flg
Flags (parámetros) de la operación
Si sem_op es negativo, entonces su valor se resta del valor del semáforo. Este pone en correlación con la obtención de recursos que controla el semáforo o los monitores de acceso. Si no se especifica IPC_NOWAIT, entonces proceso que efectúa la llamada duerme hasta que los recursos solicitados están disponible en el semáforo, es decir, que si al Decrementar el semáforo este queda con un valor negativo el proceso se bloquea.
Si sem_op es positivo, entonces su valor se añade (se suma) al semáforo. Este se pone en correlación con los recursos devueltos al conjunto de semáforos de la aplicación. Siempre se deben devolver los recursos al conjunto de semáforos cuando ya no se necesiten más.
Finalmente, si sem_op vale cero (0), entonces el proceso que efectúa la llamada dormiría hasta que el valor del semáforo sea 0.
Para explicar la llamada de semop, volvamos a ver nuestro ejemplo de impresión. Supongamos una única una impresora, capaz de único un trabajo en un instante. Creamos un conjunto de semáforos con único semáforo en él (sólo una impresora), e inicializa ese semáforo con un valor de uno (único un trabajo en un instante).
Cada vez que deseemos enviarle un trabajo a esta impresora, primeros necesitamos asegura que el recurso está disponible. Hacemos este para intentar obtener una unidad del semáforo. Cargamos un array sembuf para realizar la operación:
struct sembuf sem_lock = { 0, -1, IPC_NOWAIT };
La traducción de la inicialización de la anterior estructura indica que se decrementarà en una unidad el semáforo 0 del conjunto de semáforos.
En otras palabras, se obtendría una unidad de recursos del único semáforo de nuestro conjunto (miembro 0). Se especifica IPC_NOWAIT, así la llamada o se produce inmediatamente, o falla si otro trabajo de impresión está activo en ese momento. Aquí hay un ejemplo de como usar esta inicialización de la estructura sembuf con la llamada al sistema semop:
if((semop(sid, &sem_lock, 1) == -1) perror("semop");
SISTEMAS OPERATIVOS II
23 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 El tercer argumento (nsops) dice que estamos sólo ejecutando una (1) operación (hay sólo una estructura sembuf en nuestra array de operaciones).
El argumento sid es el identificador IPC para nuestro conjunto de semáforos.
Cuando nuestro trabajo de impresión ha terminado, debemos devolver los recursos al conjunto de semáforos, de manera que otros puedan usar la impresora.
Esto es similar a hacer un wait() en la teoría de los semáforos, con la única diferencia es que se esta especificando como IPC_NOWAIT, lo que significa que la función a decrementar el semáforo y encontrarse con un valor negativo no bloquea el proceso, simplemente falla y deja que continúe su ejecución. Para que esta llamada al sistema sea equivalente a la operación wait() en teoría de semáforos, no se debe indicar IPC_NOWAIT.
struct sembuf sem_unlock = { 0, 1, IPC_NOWAIT };
La traducción de la estructura anteriormente inicializada indica que un valor de “1” se agrega a semáforo número 0 en el conjunto de semáforos. En otras palabras, una unidad de recursos se devolverá al conjunto. Esto es equivalente a hacer un signal() en la teoría de los semáforos
LLAMADA AL SISTEMA: semctl()
LLAMADA AL SISTEMA: semctl();
PROTOTIPO: int semctl ( int semid, int semnum, int cmd, union semun arg ); RETURNS: entero positivo si _exito
-1 si error: errno = EACCESS (permiso denegado) EFAULT (dirección inválida en el argumento arg) EIDRM (el juego de semáforos fue borrado) EINVAL (el conj. no existe, o semid no es válido)
EPERM (El EUID no tiene privilegios para el comando incluido en arg) ERANGE (Valor para semáforo fuera de rango)
NOTAS: Realiza operaciones de control sobre conjuntos de semáforos
La llamada al sistema semctl se usa para desempeñar operaciones de control sobre un conjunto de semáforo. Esta llamada es análoga a la llamada al sistema msgctl que se usa para operaciones sobre las colas de mensaje. Si usted compara las listas de argumento de las dos llamadas al sistema, notará que la lista para semctl varía ligeramente con la de msgctl.
Las llamados al sistema utilizan un argumento cmd, para la especificación del comando a ser realizado sobre el objeto IPC. La diferencia con las colas de mensajes es que con los semáforos, se soportan los comandos operacionales adicionales, así requieren unos tipos de estructuras de datos más complejos en el argumento final. El uso del tipo unión confunde a muchos programadores de forma considerable. Nosotros estudiaremos esta estructura cuidadosamente, en un esfuerzo para impedir cualquier confusión.
El argumento cmd representa el comando a ejecutar con el conjunto. Como puede ver, incluye los conocidos comandos IPC_STAT/IPC_SET, junto a otros específicos de conjuntos de semáforos:
IPC_STAT
Obtiene la estructura semid ds de un conjunto y la guarda en la dirección del argumento buf en la unión semun.
SISTEMAS OPERATIVOS II
24 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 IPC_SET
Establece el valor del miembro ipc perm de la estructura semid ds de un conjunto. Obtiene los valores del argumento buf de la unión semun.
IPC_RMID
Elimina el conjunto de semáforos. GETALL
Se usa para obtener los valores de todos los semáforos del conjunto.
Los valores enteros se almacenan en un array de enteros cortos sin signo, apuntado por el miembro array de la unión.
GETNCNT
Devuelve el número de procesos que esperan recursos. GETPID
Retorna el PID del proceso que realizó la última llamada semop. GETVAL
Devuelve el valor de uno de los semáforos del conjunto. GETZCNT
Devuelve el número de procesos que esperan la disponibilidad del 100% de recurso. SETALL
Coloca todos los valores de semáforos con una serie de valores contenidos en el miembro array de la unión. SETVAL
Coloca el valor de un semáforo individual con el miembro val de la unión.
El argumento arg representa un ejemplo de tipo semun. Esta unión particular se declara en linux/sem.h como se indica a continuación:
/* argumento para llamadas a semctl */ union semun {
int val; /* valor para SETVAL */
struct semid_ds *buf; /* buffer para IPC_STAT e IPC_SET */ ushort *array; /* array para GETALL y SETALL */
struct seminfo *__buf; /* buffer para IPC_INFO */ void *__pad;
}; val
Se usa con el comando SETVAL, para indicar el valor a poner en el semáforo. buf
Se usa con los comandos IPC_STAT/IPC_SET. Es como una copia de la estructura de datos interna que tiene el núcleo para los semáforos.
array
Puntero que se usa en los comandos GETALL/SETALL. Debe apuntar a una matriz de números enteros donde se ponen o recuperan valores de los semáforos.
SISTEMAS OPERATIVOS II
25 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 Los demás argumentos, buf y pad, se usan internamente en el núcleo y no son de excesiva utilidad para el programador. Además son específicos para el sistema operativo Linux y no se encuentran en otras versiones de UNIX.
Ya que esta llamada al sistema es de las más complicadas que hemos visto, pondremos diversos ejemplos para su uso. La siguiente función devuelve el valor del semáforo indicado. El último argumento de la llamada (la unión), es ignorada con el comando GETVAL por lo que no la incluimos:
int obtener_sem_val( int sid, int semnum ){ return( semctl(sid, semnum, GETVAL, 0)); }
Considérese la siguiente función, que se debe usar para iniciar un nuevo semáforo: void iniciar_semaforo( int sid, int semnum, int initval){
union semun semopts; semopts.val = initval;
semctl( sid, semnum, SETVAL, semopts); }
Observe que el argumento final de semctl es una copia de la unión, más que un puntero a él. Mientras nosotros estamos en el tema de la unión como argumento, me permito demostrar una equivocación más bien común cuando usa este llamado de sistema.
Recordamos del proyecto msgtool que los comandos IPC_STAT e IPC_SET se usaron para alterar los permisos sobre la cola. Mientras estos comandos se soportan, en la implementación de un semáforo implementación, su uso es un poco diferente, como las estructuras de datos internas e recuperan y copian desde un miembro de la unión, más bien que una entidad simple. ¿Puede encontrar el error en este código?
/* Los permisos se pasan como texto (ejemplo: "660") */ void changemode(int sid, char *mode){
int rc;
struct semid_ds mysemds; /* Obtener valores actuales */
if((rc = semctl(sid, 0, IPC_STAT, semopts)) == -1){ perror("semctl");
exit(1); }
printf("Antiguos permisos: %o\n", semopts.buf->sem_perm.mode); /* Cambiar los permisos del semaforo */
sscanf(mode, "%o", &semopts.buf->sem_perm.mode); /* Actualizar estructura de datos interna */
semctl(sid, 0, IPC_SET, semopts); printf("Actualizado...\n"); }
El código intenta de hacer una copia local de las estructuras de datos internas estructura para el conjunto, modifica los permisos, e IPC_SET los devuelve al núcleo. Sin embargo, la primera llamada a semctl devuelve EFAULT, o dirección errónea para el último argumento (¡la unión!). Además, si no hubiéramos verificado los errores de la llamada, nosotros habríamos conseguido un fallo de memoria. ¿Por qué?
SISTEMAS OPERATIVOS II
26 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 Recuerde que los comandos IPC_SET/IPC_STAT usan el miembro buf de la unión, que es un puntero al tipo semid_ds. ¡Los punteros, son punteros, son punteros y son punteros! El miembro buf debe indicar alguna ubicación válida de almacenamiento para que nuestra función trabaje adecuadamente.
Considere esta versión:
void cambiamodo(int sid, char *mode){ int rc;
struct semid_ds mysemds;
/* Obtener valores actuales de estructura interna */ /* !Antes de nada apuntar a nuestra copia local! */ semopts.buf = &mysemds;
/* !Intentemos esto de nuevo! */
if((rc = semctl(sid, 0, IPC_STAT, semopts)) == -1){ perror("semctl");
exit(1); }
printf("Antiguos permisos: %o\n", semopts.buf->sem_perm.mode); /* Cambiar permisos */
sscanf(mode, "%o", &semopts.buf->sem_perm.mode); /* Actualizar estructura interna */
semctl(sid, 0, IPC_SET, semopts); printf("Actualizado...\n"); }
3- Memoria Compartida Conceptos básicos
La memoria compartida se puede describir mejor como el mapeo (mapping) de un área (segmento) de memoria que se combinaría y compartirá por más de un de proceso. Esta es por mucho la forma más rápida de IPC, porque no hay intermediación (es decir, un tubo, una cola de mensaje, etc). En su lugar, la información se combina directamente en un segmento de memoria, y en el espacio de direcciones del proceso llamante. Un segmento puede ser creado por un proceso, y consecutivamente escrito a y leído por cualquier número de procesos.
Estructuras de datos internas y de usuario
Estructuras de datos que mantiene el núcleo para cada segmento de memoria compartida.
Estructura shmid_ds del núcleo Como con la cola de mensaje y los conjuntos de semáforos, el núcleo mantiene unas estructuras de datos internas especiales para cada segmento compartido de memoria que existe dentro de su espacio de direcciones. Esta estructura es de tipo shmid_ds, y se define en linux/shm.h como se indica a continuación:
/* Por cada segmento de memoria compartida, el núcleo mantiene una estructura como esta */ struct shmid_ds {
struct ipc_perm shm_perm; /* permisos operacion */
int shm_segsz; /* tamanyo segmento (bytes) */
time_t shm_atime; /* instante ultimo enlace */
time_t shm_dtime; /* instante ult. desenlace */
time_t shm_ctime; /* instante ultimo cambio */
unsigned short shm_cpid; /* pid del creador */
SISTEMAS OPERATIVOS II
27 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006
short shm_nattch; /* num. de enlaces act. */
/* lo que sigue es privado */ unsigned short shm_npages; /* tam. segmento (paginas) */
unsigned long *shm_pages; /* array de ptr. a marcos -> SHMMAX struct vm_area_struct *attaches; /* descriptor de enlaces */
};
Las operaciones sobre esta estructura son realizadas por una llamada especial al sistema, y no deberían ser realizadas directamente. Aquí se describen de los campos más importantes:
shm_perm
Este es un ejemplo de la estructura ipc_perm, que se define en linux/ipc.h. Esto tiene la información de permisos para el segmento, incluyendo los permisos de acceso, e información sobre el creador del segmento (uid, etc).
shm_segsz
Tamaño del segmento (en bytes). shm_atime
Instante del último enlace al segmento por parte de algún proceso. shm_dtime
Instante del último desenlace del segmento por parte de algún proceso. shm_ctime
Instante del último cambio de esta estructura (cambio de modo, etc). shm_cpid
PID del proceso creador. shm_lpid
PID del último proceso que actuó sobre el segmento. shm_nattch
Número de procesos actualmente enlazados con el segmento. LLAMADA AL SISTEMA: shmget()
Para crear un nuevo segmento de memoria compartida, o acceder a una existente, tenemos la llamada al sistema shmget().
LLAMADA AL SISTEMA: shmget();
PROTOTIPO: int shmget ( key_t key, int size, int shmflg );
RETORNA: si tuvo exíto retorna el identificador (ID) de segmento de memoria compartida -1 existió un error: errno = EINVAL (Tam. de segmento invalido)
EEXIST (El segmento existe, no puede crearse) EIDRM (Segmento borrado o marcado para borrarse) ENOENT (No existe el segmento)
EACCES (Permiso denegado)
SISTEMAS OPERATIVOS II
28 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 Es parecido a las correspondientes para las colas de mensaje y conjuntos de semáforos.
El argumento primero de shmget() es el valor clave (en nuestro caso vuelto por una llamada a ftok()). Este valor clave se compara entonces con valores claves existentes dentro de el núcleo de otros segmentos compartidos de memoria. En esta situación, las operaciones de apertura o de acceso dependen de los contenidos del argumento shmflg.
IPC_CREAT
Crea un segmento si no existe ya en el núcleo. IPC_EXCL
Al usarlo con IPC CREAT, falla si el segmento ya existe. Si se usa IPC_CREAT sin nada más, shmget() retornará, bien el identificador del segmento recién creado, o bien el de un segmento que existía ya con la misma clave IPC. Si se añade el comando IPC_EXCL, en caso de existir un segmento con la misma clave, la llamada al sistema fallará, y si no se creará. De nuevo, puede añadirse un modo de acceso en octal, mediante la operación OR.
Preparemos una función para crear o localizar segmentos de memoria compartida: int abrir_segmento( key_t clave, int size ){
int shmid;
if((shmid = shmget( clave, size, IPC_CREAT | 0660 )) == -1){ return(-1);
}
return(shmid); }
Observe el uso de los permisos explícitos 0660. Esta sencilla función retornará un entero con el identificador del segmento, o -1 si existió un error.
Los argumentos son, el valor de la clave IPC y el tamaño deseado para el segmento (en bytes).
Una vez que un proceso obtiene un identificador de segmento válido, el siguiente paso es mapear (attach) el segmento en su propio espacio de direcciones.
LLAMADA AL SISTEMA: shmat()
Si el argumento addr es nulo (0), el núcleo intenta encontrar una zona no mapeada. Es la forma recomendada de hacerlo. Se puede incluir una dirección, pero es algo que solo suele usarse para facilitar el uso con hardware propietario o resolver conflictos con otras aplicaciones. La constante SHM_RND puede pasarse con un OR lógico en el argumento shmflg para forzar una dirección pasada para ser página (se redondea al tamaño más cercano de página).
Además, si se hace OR con la constante SHM_RDONLY y con el argumento de banderas, entonces el segmento compartido de memoria se mapea, pero marcado como sólo lectura.
Esta llamada es quizás la más simple de usar. Considere esta función de envoltura, que se pasa un identificador IPC válido para un segmento, y devuelve la dirección a la que el segmento está enlazado:
LLAMADA AL SISTEMA : shmat();
PROTOTIPO: int shmat ( int shmid, char *shmaddr, int shmflg); RETORNA: dirección de acceso al segmento, o -1 si hubo error:
errno = EINVAL (Identificador o dirección inválidos) ENOMEM (No hay memoria suficiente para ligarse) EACCES (Permiso denegado)
SISTEMAS OPERATIVOS II
29 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 char *ligar_segmento( int shmid ){
return(shmat(shmid, 0, 0)); }
Una vez que un segmento ha sido adecuadamente adjuntado, y un proceso tiene un puntero al comienzo del segmento, la lectura y la escritura en el segmento se realizan referenciando el puntero.
¡Tenga cuidado de no perder el valor del puntero original! Si esto sucede, no habrá ninguna manera de acceder a la base (comienzo) del segmento.
LLAMADA AL SISTEMA: shmctl()
LLAMADA AL SISTEMA: shmctl();
PROTOTYPE: int shmctl ( int shmqid, int cmd, struct shmid_ds *buf ); RETURNS: 0 si éxito, -1 si error:
errno = EACCES (No hay permiso de lectura y cmd es IPC_STAT)
EFAULT (Se ha suministrado una dirección inválida para los comandos IPC_SET e IPC_STAT) EIDRM (El segmento fue borrado durante esta operación)
EINVAL (shmqid inválido)
EPERM (Se ha intentado, sin permiso de escritura, el comando IPC_SET o IPC_RMID) Los que valores válidos de comando son:
IPC_STAT
Obtiene la estructura shmid_ds de un segmento y la almacena en la dirección del argumento buf. IPC_SET
Ajusta el valor del miembro ipc_perm de la estructura, tomando el valor del argumento buf. IPC_RMID
Marca un segmento para borrarse. El comando IPC_RMID no quita realmente un segmento del núcleo. Más bien, marca el segmento para eliminación. La eliminación real del mismo ocurre cuando el último proceso actualmente adjunto al segmento termina su relación con él. Por supuesto, si ningún proceso está actualmente asociado al segmento, la eliminación es inmediata.
Para separar adecuadamente un segmento compartido de memoria, un proceso invoca la llamada al sistema shmdt.
LLAMADA AL SISTEMA: shmdt() LLAMADA AL SISTEMA: shmdt();
PROTOTIPO: int shmdt ( char *shmaddr );
SISTEMAS OPERATIVOS II
30 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006 Cuando un segmento compartido de memoria no se necesita más por un proceso, se debe separar con una llamado al sistema. Como mencionamos antes, esto no es lo mismo que eliminar un segmento desde el núcleo! Después de separar con éxito, el miembro shm_nattch de la estructura shmid_ds se decrementa en uno. Cuando este valor alcanza el cero (0), el núcleo quitaría físicamente el segmento.
SISTEMAS OPERATIVOS II
31 DE 48–CÁTEDRA SISTEMAS OPERATIVOS II–2006
LLAMADAS AL SISTEMA DE LINUX EN ORDEN ALFABÉTICO: exit - como exit pero con menos acciones (m+c) accept - aceptar conexiones en un socket (m+c!)
access - comprobar permisos de usuario en un fichero (m+c) acct - no implementada aun (mc)
adjtimex - obtener/ajustar variables de tiempo internas (-c) afs syscall - reservada para el sistema de ficheros Andrew (-) alarm - envió de SIGALRM tras un tiempo especificado (m+c) bdush - vuelca buffers modificados al disco (-c)
bind - nombrar un socket para comunicaciones (m!c) break - no implementada aun (-)
brk - cambiar el tamaño del segmento de datos (mc) chdir - cambiar el directorio de trabajo (m+c) chmod - cambiar permisos en un fichero (m+c) chown - cambiar propietario de un fichero (m+c) chroot - cambiar el directorio raíz (mc)
clone - ver fork (m-)
close - cerrar un fichero (m+c) connect - enlazar dos sockets (m!c) creat - crear un fichero (m+c)
create module - reservar espacio para un modulo del núcleo (-) delete module - descargar modulo del núcleo (-)
dup - crear un duplicado de un descriptor de fichero (m+c) dup2 - duplicar un descriptor (m+c)
execl, execlp, execle, ... - vease execve (m+!c) execve - ejecutar un fichero (m+c)
exit - terminar un programa (m+c)
fchdir - cambiar directorio de trabajo por referencia () fchmod - vease chmod (mc)
fchown - cambiar propietario de un fichero (mc) fclose - cerrar un fichero por referencia (m+!c) fcntl - control de ficheros/descriptores (m+c) ock - cambiar bloqueo de fichero (m!c) fork - crear proceso hijo (m+c)
fpathconf - obtener info. de fichero por referencia (m+!c) fread - leer matriz de datos de un fichero (m+!c)
fstat - obtener estado del fichero (m+c)
fstatfs - obtener estado del sistema de ficheros por referencia (mc) fsync - escribir bloques modificados del fichero a disco (mc) ftime - obtener fecha del fichero, en segundos desde 1970 (m!c) ftruncate - cambiar tamaño del fichero (mc)
fwrite - escribir matriz de datos binarios a un fichero (m+!c)
get kernel syms - obtener tabla de símbolos del kernel o su tamaño (-) getdomainname - obtener nombre de dominio del sistema (m!c)
getdtablesize - obtener tamaño de la tabla de descriptores de fich. (m!c) getegid - obtener id. de grupo efectivo (m+c)
geteuid - obtener id. de usuario efectivo (m+c) getgid - obtener id. de grupo real (m+c) getgroups - obtener grupos adicionales (m+c) gethostid - obtener identificador del huesped (m!c) gethostname - obtener nombre del huesped (m!c)