Post on 25-Feb-2021
BENEMERITA UNIVERSIDAD
AUTONOMA DE PUEBLA
Facultad de Ciencia de la Computacion
Edificio 135, 14 Sur y Av. San Claudio,
Ciudad Universitaria
Puebla, Pue. C.P. 72570
Notas de Programacion de Sistemas
Hilario Salazar Martınez & Mariano Larios Gomez
BUAP Puebla-MexicoVersion 1.2
verano 2008
Indice general
Indice de figuras VI
Indice de Tablas VIII
I Aspectos Basicos 1
1. Parametros main 2
2. Archivos en UNIX 4
2.1. Que son ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.2. Tipos de archivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.3. Creacion de archivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.4. Los directorios mas comunes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.5. El directorio HOME . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2.6. Elementos de la lınea de comandos (que, como y quien) . . . . . . . . . . . . . . . . . . . . . . . 6
2.7. Comandos utiles con archivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.7.1. ls . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
2.7.2. find . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.7.3. more . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.7.4. cat . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.7.5. pwd . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.7.6. cd . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.7.7. mkdir . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.7.8. rmdir . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.7.9. who . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.7.10. cp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.7.11. mv(move) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.7.12. rm (remove) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.7.13. echo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.7.14. grep . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.7.15. sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.7.16. Ejecucion en background . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.7.17. Las entradas/salidas estandar . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.7.18. Redireccion de las entradas/salidas estandar . . . . . . . . . . . . . . . . . . . . . . . . . . 14
i
2.7.19. Restricciones de las redirecciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
3. Autorizacion de acceso a archivos 15
3.1. Categorias de usuarios en UNIX . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
II Apuntadores y arreglos 17
4. Que es un apuntador? 18
5. Para que usar punteros? 21
6. Artimetica de Punteros 25
7. Cadena de caracteres 28
8. Estructuras 31
8.1. Referenciando elementos estructurados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
8.2. Declarando un puntero de estructuras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
8.3. Usando punteros de estructura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
9. Puntero a Matrices 38
10.Asignacion dinamica de memoria 41
11.Puntero a funciones 44
III Archivos 48
12.Archivos 49
12.1. I/O archivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
12.2. sprintf y sscanf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
12.3. Peticion del estado del flujo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
12.4. E/S de bajo nivel o sin almacenamiento intermedio . . . . . . . . . . . . . . . . . . . . . . . . . . 51
IV NCURSES 54
V llamadas al sistema y gestion de procesos 55
13.Funcion system 56
14.La familia exec 58
15.Atributos de proceso 60
15.1. Estados de un proceso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
15.2. Creacion de un proceso(fork) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
15.3. Terminacion de procesos (exit y wait) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
15.3.1. exit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
ii
15.3.2. wait . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
15.4. Identificadores de proceso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
15.5. Ejemplo terminacion normal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
15.6. Ejemplo de terminacion anormal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
15.7. Ejemplo usando llamadas al sistema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
VI Senales 70
16.Concepto de senal 71
16.1. Como utilizar la funcion Kill . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
16.2. Tratamiento de senal(signal) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
16.3. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
VII Comunicacion Interprocesos (IPC) 79
17.Condiciones de competencia 80
18.Conceptos basicos IPC 85
18.1. El comando ipcs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
18.1.1. Salida del comando ipcs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
18.2. El comando ipcrm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
18.3. Pipes UNIX Semi-duplex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
19.Pipe sin nombre 89
19.1. Creacion de un ((pipe)) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
19.2. Escritura en un ((pipe)) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
19.3. Cierre de un ((pipe)) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
19.4. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
20.Pipes con nombre 94
20.1. Creacion de una FIFO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
20.2. Operaciones con FIFOs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
20.3. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
20.4. Acciones bloqueantes en un FIFO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
21.Colas de mensajes 98
21.1. Crear y abrir una cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
21.2. Enviar a una cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
21.3. Recibir desde la cola . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
21.4. Como eliminar cola de mensajes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
21.5. Ejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
22.Semaforos 109
22.1. Caracterısticas de los semaforos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
22.2. Semaforos generales o contadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
iii
22.3. Semaforos Binarios I . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
22.4. Semaforos sincronizadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
22.5. Semaforos Binarios II . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
22.5.1. Peticion de semaforo (semget()) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
22.5.2. Operaciones P y V (semop()) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
22.5.3. lock de un semaforo P(S) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
22.5.4. Unlock de un semaforo V(S) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
22.5.5. Destruir un semaforo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
22.6. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121
23.Memoria compartida 127
23.1. llamada shmget() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
23.2. llamada shmat() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 129
23.3. llamada shmdt() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
23.4. llamada shmctl() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
23.5. Ejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
24.Sockets 138
24.1. Obteniendo informacion: los resolvers y otras llamadas . . . . . . . . . . . . . . . . . . . . . . . . 138
24.1.1. Obteniendo datos del host . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 138
24.1.2. Obteniendo datos de los servicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
24.1.3. Obteniendo el nombre del host local . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
24.1.4. Conviertiendo diferentes formatos de direccion . . . . . . . . . . . . . . . . . . . . . . . . 143
24.2. Comunicacion con sockets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
24.3. Dominios y direcciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
24.4. Estilos de comunicacion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146
24.5. Protocolos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
24.6. Las llamadas de sistema para la creacion y uso de sockets . . . . . . . . . . . . . . . . . . . . . . 147
24.6.1. Creacion del socket: socket( ) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
24.6.2. Escuchando en el puerto listen() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
24.6.3. Aceptando una llamada accept() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
24.6.4. Conectandose al puerto connect() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
24.6.5. Recepcion de mensajes: read(), recv() y recvfrom() . . . . . . . . . . . . . . . . . . . . . . 149
24.6.6. Enviando mensajes: write(), send() y sendto() . . . . . . . . . . . . . . . . . . . . . . . . 150
24.6.7. Cerrando la comunicacion close() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
24.7. La comunicacion cliente-servidor con sockets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
24.8. Aspectos a considerar en el diseno de un servidor . . . . . . . . . . . . . . . . . . . . . . . . . . . 151
24.8.1. Servidor serial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
24.8.2. Servidor padre . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 153
24.8.3. Aspectos a considerar en el diseno de un cliente . . . . . . . . . . . . . . . . . . . . . . . 154
24.9. Comunicacion con SOCKETPAIRS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154
24.10.Comunicacion orientada conexion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
24.10.1.El servidor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 156
24.10.2.El cliente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
iv
24.11.Comunicacion orientada no conexion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
24.11.1.El servidor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
24.11.2.El cliente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
24.12.Ejemplo sobre datagrama en el dominio UNIX . . . . . . . . . . . . . . . . . . . . . . . . . . . . 160
24.13.Ejemplo sobre datagrama en el dominio INTERNET . . . . . . . . . . . . . . . . . . . . . . . . . 163
24.13.1.Receptor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163
24.13.2.Emisor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 165
24.13.3.Ejecucion del ejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 166
24.14.Conexion en el dominio INTERNET . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
24.14.1.Cliente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167
24.14.2.servidor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169
24.14.3.Ejecucion de Ejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
24.15.Conexion monoproceso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172
24.15.1.Ejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174
24.16.Conexiones simultaneas (multiprocesos) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
24.16.1.Ejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
VIII Creacion de demonios 181
25.Proceso para la creacion de un demonio 182
25.1. Demonios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
25.2. Como crear un demonio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 182
25.3. Como comunicarse con un demonio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
25.3.1. Como leer un archivo de configuracion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185
25.3.2. Como anadir manipulacion de senales a un demonio . . . . . . . . . . . . . . . . . . . . . 188
IX Driver 190
26.Proceso para la creacion de un driver 191
26.1. El primer driver: carga y descarga del driver en el espacio de usuario . . . . . . . . . . . . . . . . 191
26.2. El driver “Hola mundo”: carga y descarga del driver en el espacio de kernel . . . . . . . . . . . 191
26.3. El driver completo “memoria”: parte inicial del driver . . . . . . . . . . . . . . . . . . . . . . . . 192
26.4. El driver “memoria”: conexion de dispositivos con sus ficheros . . . . . . . . . . . . . . . . . . . . 193
26.5. El driver “memoria”: eliminando el modulo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 194
26.6. El driver “memoria”: abriendo el dispositivo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195
Bibliografıa 196
v
Indice de figuras
1.1. Representacion de los argumentos argc y argv. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
4.1. Representacion de la Memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
4.2. Memoria reservada para una variable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
4.3. Uso del operador & . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
5.1. Resultados listado 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
5.2. Copia del valor de pi en vpi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
5.3. Cambio de valor de vpi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
5.4. Resultados listado 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
5.5. Paso por referencia de pi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
5.6. Asignacion de 0 al contenido de la memoria apuntado por vpi . . . . . . . . . . . . . . . . . . . 24
6.1. Incremento de punteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
6.2. Resultados listado 6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
7.1. Resultados listado 7 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
8.1. Resultados del listado 9 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
9.1. Representacion de matriz bidimensional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
11.1. Resultado listado 20 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
14.1. execlp . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
15.1. Estados de un proceso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
15.2. Procesos y llamadas al sistema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
17.1. Spooler de impresiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
17.2. Visualizacion de dos procesos estan al mismo tiempo en su SC . . . . . . . . . . . . . . . . . . . . . . 81
17.3. Proceso activando y desactivando interrupciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
18.1. Ejemplo del comando ipcs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
18.2. Ejemplo del comando ipcrm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
19.1. Comunicacion unidirecional utilizando una tuberia . . . . . . . . . . . . . . . . . . . . . . . . . . 89
19.2. Tuberia entre dos procesos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
vi
21.1. suma de dos vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
22.1. Los semaforos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
22.2. Tipos de semaforos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 110
22.3. Estructura del semaforo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
22.4. Estructura del semaforo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
22.5. Relacion estructuras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
22.6. Definicion de semop() . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
22.7. operaciones semop(). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
22.8. Resultado del ejemplo 1 (semaforos) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123
23.1. memoria compartida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
23.2. pasos para accesar a una memoria compartida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
23.3. parametros de entrada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
23.4. resultado ejemplo memoria compartida . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137
24.1. Ejemplo de un resolver . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139
24.2. conexion con sockets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 144
24.3. Pasos para crear una conexion con sockets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 152
24.4. Pasos a seguir para establecer una comunicacion en modo conexion . . . . . . . . . . . . . . . . . 157
24.5. Pasos a seguir para establecer una comunicacion en modo no conexion . . . . . . . . . . . . . . . 158
24.6. resultado emisor-receptor sockets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 162
vii
Indice de Tablas
3.1. Permiso de usuarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
3.2. Ejemplo de permiso de usuarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
16.2. Senales usadas por Kill . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
18.1. Archivos cabecera y llamadas al sistema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
22.1. operaciones de sem op . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
24.1. Secciones de procesos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155
viii
Parte I
Aspectos Basicos
1
Capıtulo 1
Los argumentos argc, argv y env de
main()
Se usan dos argumentos especiales predefinidos argv y argc para recibir los argumentos de la linea de ordenes.
Prototipo:
main(int argc, char *argv, char env);
Donde:
argc Mantiene el numero de argumentos de la lınea de ordenes y es un entero. Siempre
es al menos uno ya que el nombre del programa se erige en el primer argumento.x
argv Es un puntero a un array de puntero de caracteres. Esto es, argv,
el puntero de un array de cadenas.
env Se usa para acceder a los parametros del entorno.
Ejemplo 1:
#copiar fichero1 fichero2
argc toma el valor de 3, y los contenidos de argv son argv[0]=’’compiar’’, argv[1]=’’fichero1’’ y
argv[2]=’’fichero2’’. Como vemos argv[0] contiene el nombre delprograma.
Ejemplo 2:
/* nombre.c */
#include <stdio.h>
#include <process.h>
int(int argc, char *argv[])
if(argc!=2)
printf("le falta especificar su nombre \n");
exit(1);
printf("Hola %s",argv[1]);
2
Figura 1.1: Representacion de los argumentos argc y argv.
return 0;
Si se llama nombre y su nombre es Javier, entonces se deberia teclear nombre Javier para ejecutar el programa.
La salida deberia ser hola Javier.
#nombre Javier
Hola Javier
#
3
Capıtulo 2
Archivos en UNIX
2.1. Que son ?
Informacion asociada a un nombre
consideracion como una serie de bytes:
• Caracteres ASCII
• Codigo maquina
Almacenados en un disquete, disco duro, o banda
2.2. Tipos de archivos
1. Ordinarios
Contiene una serie de bytes (ASCII,binarios,ambos)
Estructura interna definida por el usuario
2. Directorios
3. Especiales perifericos, (descriptores)
a) Indican a UNIX cual driver se debe utilizar
b) Le proporcionan al driver las opciones a utilizar
c) Se encuentran dentro del directorio /dev
2.3. Creacion de archivos
Pueden ser creados por:
• un editor (ed, vi, emacs, etc ...)
• la redireccion de la salida estandar de un programa
#ls -l tarea >salida.txt
• algunos comandos (cp, sort, cc, mkdir)
La administracion de los archivos es efectuada por UNIX y es totalmente transparente al usuario.
4
Los archivos son generalmente escritos sobre disco.
Cada archivo esta asociado con el menos un nombre
2.4. Los directorios mas comunes
/bin contiene los principales comandos
/etc contiene los comandos y los archivos utilizados para el mantenimiento y administracion del sistema
/lib contiene librerıas y programas de los compiladores
/tmp contiene los archivos temporales los usuarios pueden usarla como zona de trabajo
/dev contiene los archivos especiales de descripcion de perifericos
/usr es, en teorıa, un sistema de archivos completo. Contiene los comandos locales, algunas librerias, los
comandos de usuario, los archivos de correo, etc. los directorios de los usuarios pueden encontrarse en ese
directorio
/usr/bin contiene los comandos complementarios
/usr/ucb contiene los comandos especificos BSD 4.x
/usr/lib contiene las librerias y los programas suplementarios.
Tambien contiene los programas de administracion de las impresoras y comunicacion
2.5. El directorio HOME
Cada usuario tiene un directorio HOME
Directorio creado por el administrador del sistema, generalmente es creado derntro del directorio /usr,
/u o /udd
Despues del “login”el usuario se encuentra dentro de este directorio, (que se convierte en su directorio de
trabajo)
Normalmente, el usuario trabajara en los directorios que se encuentran dentro del directorio HOME
Para posicionarse en el basta con teclear:
#cd <ret>
Una forma de accesarlo desde cualquier directorio es a traves del caracter “∼´´, por ejemplo:
# pwd
/home/usr/too/tareas
# cp ~/bin/xvi
5
2.6. Elementos de la lınea de comandos (que, como y quien)
El comando: que hacer?
• Es la primera palabra de la lınea
• Corresponde al nombre de un archivo ejecutable
Las opciones: como hacerlo?
• Siguen al comando (separados por espacios)
• Generalmente precedidos por un ’-’ (a veces por un ’+’)
$ls -/
$date +%d%m%y
Los argumentos: sobre quien actuar? Generalmente uno, o varios, nombres de archivos
$cat capitulo
$archivo nuevo
$ls -l tarea
2.7. Comandos utiles con archivos
2.7.1. ls
despliega los nombres de los archivos que se encuentran dentro del directorio actual.
Sintaxis: ls [opciones]
algunas opciones:
-a lista los archivos que comienzan con un “.”
-l listado en formato largo
-d si el argumento es un directorio lista el nombre del archivo directorio y no su contenido
-s da el tamano de los archivos en kilo-bytes
-u despliega la ultima hora de acceso en lugar de la ultima hora de modificacion
-t acomoda los archivos en funcion de la hora de la ultima modificacion
-i imprime el numero de referencia (i-node) de los archivos
-C lista los archivos en columnas
-g muestra el propietario del grupo de un archivo en un formato largo
Nota las opciones pueden estar mezcladas:
# ls -al
Ejemplo :
6
#ls -l
total 14464
-rwxr-xr-x 1 root root 624 dic 12 16:26 biblio.bib
-rwxr-xr-x 1 root root 141558 dic 10 10:41 buap.bmp
-rwxr-xr-x 1 root root 164694 dic 10 10:41 calendario2006.BMP
-rwxr-xr-x 1 root root 0 dic 16 09:50 comandos.txt
-rwxr-xr-x 1 root root 4410054 dic 14 18:40 Dibujo.bmp
-rwxr-xr-x 1 root root 1021622 ene 14 2006 ejemplo.bmp
-rwxr-xr-x 1 root root 23215 jul 28 2007 fdl.tex
drwxr-xr-x 8 root root 16384 dic 10 12:02 GnuWin32
Despliega la siguiente informacion
• El tipo de archivo:
- normal
d directorio
c especial(perifericos en modo caracter)
• las autorizaciones de acceso de los archivos:
r lectura
w escritura
x ejecucion
• Numero de ligas
• Propietario
• Tamano
• Fecha y hora de la ultima modificacion
• Nombre del archivo o directorio
2.7.2. find
Busca archivos y carpetas
Sintaxis: find [camino] [expresion]
Descripcion: Con find podemos realizar todo tipo de busqueda de archivos y directorios en todo el sistema
de archivos y atendiendo una serie de caracterısticas y patrones de archivos y directorios que se quiere
localizar. Tambien nos permite ejecutar un comando a cada archivo localizado. Por defecto se toma el
directorio actual y la opcion dentro de la expresion de -print.
Cuando tenemos varias expresiones se aplica el operador logico Y. Por lo que se deben satisfacer todas las
expresiones.
Opciones:
camino: Lista de directorios separados por comas donde queremos que se realize la busqueda. Esta se
ralizara recursivamente en todos los subdirectorios de los directorios de comienzo que se especifiquen.
7
expresion: Muestra los tamanos en bytes.
-name ”archivo”: Busca el archivo cuyo nombre coincida con el que se encierra entre comillas, se
puede usar los comodines > y *.
-type t: Busca archivos cuyo tipo coincida con el indicado:
b: Especial de bloques.
c: Especial de caracteres.
d: Directorio.
p: Pipe.
l: Ligadura simbolica
s: Socket.
f: Fichero normal.
-print Muestra los nombres de los archivos que va encontrando en la salida estandar.
Solo se ha mostrado algunas de las expresiones, las que consideramos mas interesantes, de las muchas
que soporta el comando.Para mas informacion consule el manual del sistema (#man find).
Ejemplo: find / -name documento.txt -print
Busca a partir del directorio raız todos los archivos que se llamen documento.txt y los muesta en
pantalla.
2.7.3. more
Despliega el contenido de un archivo parandose entre cada pantalla
Sintaxis more nombre-archivo
Ejemplo
# more .cshrc
2.7.4. cat
Copia uno o varios archivos hacia la salida estandar (la pantalla por default)
Sintaxis: cat nombre-archivo
Ejemplo
#cat archivo.txt
2.7.5. pwd
Despliega el camino de acceso del directorio actual(donde se encuentra dentro del sistema de archivos).
Despliega el nombre de un directorio nunca el de un archivo
Sintaxis: pwd
Ejemplo
8
$ pwd
/home/dic/rogomez/Perso
$
2.7.6. cd
permite cambiar de directorio
Sintaxis: cd [nombre-directorio]
cd sin nombre lo posiciona en el directorio HOME
Ejemplo:
#cd tareas
2.7.7. mkdir
creacion de directorios
Sintaxis: # mkdir directorio [directorio]
Ejemplos:
2.7.8. rmdir
Borra directorios
Sintaxis: #rmdir directorio [directorio]
rmdir no borrara el directorio si este no se encuentra vacio
Ejemplo:
#rmdir tareas
2.7.9. who
Despliega los usuarios conectados
Ejemplo:
#who
root :0 Dec 16 09:12
root pts/0 Dec 16 09:12
root pts/1 Dec 16 09:21
root pts/2 Dec 16 09:23
9
2.7.10. cp
copia una archivo ordinario
Sintaxis:
#cp archivo1 archivo2
#cp archivo [archivos] directorio
ejemplos
#cp arch1 arch2
#cp arch1 direc
#cp arch1 direc/arch2
#cp arch1 arch2 arch3 direc
#cp -r
cp no modifica los archivos originales, tan solo los duplica
opcion -r =copia recursivamente: si es un directorio copia el contenido de este
2.7.11. mv(move)
desplaza un archivo o lo renombra
Sintaxis:
#mv antiguo-nombre nuevo-nombre
#mv archivo [archivos] directorio
ejemplos:
#mv arch-a arch-b
#mv dire1 direc2
#mv arch1 arch2 arch3 arch4 direc
#mv arch1 .../bin/fx
2.7.12. rm (remove)
Borra el nombre de un archivo
Si ese nombre fuera el ultimo (numero de ligas=1). el archivo sera “fisicamente´´suprimido
Sintaxis: #rm archivo [archivos]
10
Ejemplo:
#rm arch1
Opciones:
-r recursivamente (si directorio contiene otro, borra contenido de este)
-f forza (no despliega errores, ni hace preguntas)
-i interactivo (pregunta)
2.7.13. echo
Muestra una linea de texto
Sintaxis
echo [-n][-e] cadena
Este comando sirve para mostrar a la salida estandar la cadena que se le pasa como argumento. Su utilidad
esta en los shell-scripts para dar informacion.
Opciones:
-n: No pone el caracter de control final de lınea, al final de la cadena.
-e: Permite la utilizacion de las siguientes caracteres en la cadena.
\a: Beep.
\b: Retroceso.
\c: Suprime salto de lınea.
\f: Avance de pagina.
\n: Salto de lınea.
\r: Retorno de carro.
\t: Tabular.
\v: Tabular vertical.
cadena: cadena a mostrar en la salida estandar.
2.7.14. grep
Sirve para encontrar dentro de un conjunto de archivos, todas lıneas que contienen una cadena de caracteres
especificadas por una expresion regular.
Sintaxis: #grep [opciones] expr-reg [archivos]
Opciones:
11
-v despliega las lıneas que no contienen la expresion
-c imprime solo el numero de lıneas que contienen la expresion
-i no hace diferencia entre mayusculas y minusculas
-n despliega el numero de lıneas
Dentro de la misma familia, se encuentran los comandos siguientes:
• fgrep no admite las expresiones regulares
• egrap admiten expresiones regulares extendidas
2.7.15. sort
Permite ordenar las lineas de un arcchivo de texto. Por default, sort ordena en funcion de todos los
caracteres de la lınea, en orden creciente de los valores de caracteres ASCII
Sintaxis: #sort [opciones] [llave de ordenamiento] [archivos]
Opciones
-u suprime las lıneas conteniendo las llaves identicas
-n ordenamiento numerico
-b ignorar los blancos rn principio de la lınea
Ejemplos:
#sort archivo
#sort arch1 arch2>ordenado.txt
#sort -n numeros
2.7.16. Ejecucion en background
Concepto de job en cshell: Cuando se teclea un comando se le asigna un numero de job.
Para los comandos lentos en su ejecuıon resulta interesante poder disponer de la terminal, de tal forma
que se pueda ejecutar otros comandos.
Ponemos un “&”despues de la lınea de comandos, este se ejecutara en background, de una forma
sincronica.
Ejemplo
#sleep 10 &
[1] 712
#
1 es el numero de job
712 identificador del proceso (PID)
12
Una vez concluida el proceso, el sistema imprimira el siguiente mensaje luego de la proxima tecla ENTER
presionada:
[1]+ Done sleep 10
Para obtener un listado de los proceso que se estan ejecutando en segundo plano, solo hay que escribir el
comando
#jobs
[1]- Running sleep 20 &
[2]+ Running sleep 30 &
La tabla contiene tres columnas:
Primera el numero de proceso en background.
Segunda el estado (Running ejecutandose, Stopped parado).
Tercera la cadena correspondiente al proceso en cuestion.
Nota: La salida estandar de estos comandos, (ası como la salida estandar de errores) es la pantalla, salvo
si hay una redireccion, (lo cual es aconsejable).
Para regresarlo a foreground se usa el comando fg seguido del numero de job
#fg %1
si se quiere regresarlo a backgrond
#bg %1
2.7.17. Las entradas/salidas estandar
cuando se realiza una conexion a un sistema UNIX tres archivos, (descriptores), son abiertos:
Entrada estandar(stdin): el teclado por donde se teclean los comandos
Salida estandar (stdout): la pantalla donde son desplegados los resultados de los coamndos
Salida de errores (stderr): la pantalla donde son desplegados los errores eventuales
Un numero (el descriptor de archivo) es asignado a cada archivo:
0 entrada estandar
1 salida estandar
2 salida de errores
13
2.7.18. Redireccion de las entradas/salidas estandar
< redireccion de la entrada estandar
> redireccion de la salida estandar(creacion)
À redireccion de la salida estandar (anadir y agregar)
La redireccion de la salida de error depende del shell
bshell cshell
2> redireccion de la salida de error >& redireccion de la salida y de error
2>&1 redireccion de la salida estandar y de error No existe opcion para redireccionar solo la salida error
2.7.19. Restricciones de las redirecciones
Es posible modificar la redireccion de la salida estandar en shell con la variable noclobber
Existe el archivo?
SI NO
< lee el archivo error
> error creacion del archivo
À anadir al final del archivo error
14
Capıtulo 3
Autorizacion de acceso a archivos
3.1. Categorias de usuarios en UNIX
En UNIX el acceso a los archivos y directorios se diferencia en funcion de tres categorias de usuarios:
1. Propietario
El propietario del archivo
En principio se refiere al que creo el archivo
2. Grupo
En principio se refiere al grupo al cual pertenece el creador del archivo
3. Otros
El resto del mundo
A traves de ls -l podemos ver las autorizaciones de los archivos:
r w x r w x r w x
0 0 0 0 0 0 0 0 0
o o o
1 1 1 1 1 1 1 1 1
propietario grupo otros
Tabla 3.1: Permiso de usuarios
Para los archivos:
r autorizacion lectura
w autorizacion escritura
x autorizacion ejecucion
Para los directorios:
r autorizacion para leer el directorio (ls)
w autorizacion de escribir en el directorio (creacion, modificacion o supresion de archivos)
x autorizacion de se posicionar en el directorio (cd)
15
Por default cuando se crea un directorio se le asigna las autorizaciones siguiente
rwx rwx rwx (o sea 777 en octal)
En el caso de los archivos:
rw- rw- rw- (o sea 666 en octal)
Ejemplos:
octal binario propietario grupo otros
0600 000 110 000 000 rw- - - - r- -
0644 000 110 100 100 rw- r- - r- -
0666 000 110 110 110 rw- rw- rw-
Tabla 3.2: Ejemplo de permiso de usuarios
Cambio de autorizacion de acceso (chmod)
• El comando chmod permite modificar las autorizaciones de acceso de los archivos y de los directorios
ya creados.
• Sintaxis: chmod nuevo-modo [archivos] [directorios]
• Existen dos formas de especificar el nuevo nodo
F en octal:
chmod ooo archivo
Ejemplo: #chmod 660 tarea
F en modo simbolico:
#chmod [ugoa][+=][rwx]
u permiso del usuario
g permiso de grupo
o permiso de los otros
a todos los permisos
• Ejemplos de chmod
#la -lg e1
-rw-rw-rw- 1 toto daemon 0 Oct 12 18:20 e1
#chmod 755 e1
#-ls -lg e1
-rwxr-xr-x 1 toto daemon 0 Oct 12 18:20 e1
#ls -lg e1
-rw-r--r-- 1 toto daemon 0 Oct 12 18:20 e1
#chmod g+x e1
#chmod o-r e1
#ls -lg e1
-rw-r-x--- 1 toto daemon 0 Oct 12 18:20 e1
16
Parte II
Apuntadores y arreglos
17
Capıtulo 4
Que es un apuntador?
Suele resultar dificil dominar el concepto de punteros. Especialmente a los programadores que aprendieron
con lenguajes de alto nivel, donde no se permite trabajar directamente con posiciones de memoria, El mayor
problema en C surge al tratar de detectar esos “segmentation fault” maquiavelicos que aveces aparecen ,
aveces no.
En el libro de Kernighan y Ritchie “el elnguaje de la programacion en C”, se define un puntero Puntero!definicion
como “una variable que contiene la direccion de una variable”. Tras esta definicion clara y precisa, podemos
remarcar que un puntero es un tipo especial de variable que almacena el valor de una direccion de memoria.
La representacion anterior se utilizara a lo largo del texto para ilustrar los distintos ejemplos. De esta forma, si
Figura 4.1: Representacion de la Memoria
ejecutamos el siguiente ejemplo:
01 #include <stdio.h>
02 int main(void)
03
04 float pi=3.14;
05 printf("%.2f\n",pi);
06 return 0;
07
listado 1
Obtenemos en pantalla el esperado 3.14. Sin embargo, que operaciones han hecho el ordenador hasta mostrar
el resultado?. En la lınea 4, la instruccion reserva memoria para la variable pi. En este ejemplo, asumimos que
un decimal de tipo float ocupa 4 bytes. Dependiendo de la arquitectura del computador, la cantidad de bytes
18
requerida para cada tipo de datos puede variar.
En la figura 4.2 muestra el estado de la memoria despues de la declaracion de la variable pi. la representacion
elegida facilita el seguimiento de los ejemplos, sin embargo, la representacion real en la memoria de ordenador
no tendrıa esa disposicion. Cuando se utiliza pi en la lınea 5, ocurre dos pasos diferenciables. En primer lugar,
Figura 4.2: Memoria reservada para una variable
el programa buscala direccion de memoria reservada para pi (en nuestro ejemplo. serıa la direccion 146). Hecho
esto, en segundo lugar, el programa recupera el contenido de esa direccion de memoria.
Asi, distinguimos por un lado la direccion de memoria asignada a una variable, y por otro lado el contenido
de la posicion de memoria reservada. Podemos acceder a la direccion de una variable utilizando el operador &.
Mediante este operador, obtenemos la direccion donde se almacena la variable (un numero!). En el ejemplo del
listado 2, se ha utilizado %u y una conversion forzada a entero sin signo para que la salida por pantalla sea
mas entendible. Se podıa haber utilizado el conversir estandar para punteros %p y haber quitado la conversion
forzada a entero sin signo.
01 #include <stdio.h>
02 int main(void)
03
04 float pi=3.14;
05 printf("Direccion de pi %u\n",(unsigned int)&pi);
06 return 0;
07
listado 2
Si ejecutamos el programa del listado 2 en la memoria del ejemplo anterior, deberıa mostrar la direccion 146.
Recordemos que una direccion de memoria es unicamente un numero. De hecho, podrıamos almacenarla en una
variable entera sin signo (ver listado 3).
01 #include <stdio.h>
02 int main(void)
03
04 float pi=3.14;
05 unsigned int dir=(unsigned int)π
06 printf("Direccion de pi %u\n",dir);
07 return 0;
19
08
listado 3
el codigo anterior demuestra que no hay nada de magia ni efectos especiales en el tema de las direcciones.
Simplemente son numeros que pueden ser almacenados en variables enteras1. Esto se puede ver graficamente
en la siguiente figura 4.3 (suponiendo que las variables de tipo entero sin signo requiere 3 bytes en memoria).
Figura 4.3: Uso del operador &
lo que se quiere representar simplemente es que mediante el operador & accedemos a la direccion de memoria
de variables. Esta direccion es un simple numero entero con el que podemos trabajar directamente.
Para diferenciar una variable entera que almacene valores de nuestro programa (por ejemplo, ındices, contadores,
valores...) de las variables que almacena direcciones, en C se creo un nuevo tipo de variable para alamcenar
direcciones. Este tipo es, precisamente, el puntero. Ası, podemos tener algunas declaraciones de punteros como
las siguientes:
char *nombre;
float *altura;
Como interpretar las lıneas anteriores? Podemos leer que nombre es un entero, pero podemos anadir que
es un entero especialmenter disenado para guardar la direccion de un caracter. Por su parte, altura es un
entero, ademas es un entero especialmente disenado para guardar la direccion de un float. Es lo que decimos
resumidamente con “nombre es un puntero a char” o “altura es un puntero a float”.
El operador * tsambien se utiliza para recuperar el contenido de una posicion de memoria indicada por una
direccion. No debemos confundir su uso en la declaracion de un puntero, como hemos visto antes, con los casos
en que se emplea para recuperar el contenido de una posicion de memoria. Este uso lo veremos a continuacion.
1Recordemos que el modificador unsigned simplemente indica que no se permiten numeros negativos. Las direcciones no pueden
ser negativas
20
Capıtulo 5
Para que usar punteros?
Hagamos un programa que cambie el valor de la variable pi del ejemplo anterior a cero. Para ello, realizaremos
el codigo que se muestra en el listado 4. En ese caso intentamos pasar el parametro vpi por referencia, para
que cambie el contenido de pi dentro de la funcion. Si ejecutaramos el programa, el resultado obtenido no es el
esperado, Porque?
01 void cambiavalor(float vpi)
02 vpi=0;
03 printf("Direccion de vpi: %u\n",(unsigned int)&vpi);
04 printf("Valor de vpi: %f\n",vpi);
05
06 int main(void)
07 float pi=3.14;
08 cambiavalor(pi);
09 printf("Direccion de pi: %u\n",(unsigned int)&pi);
10 printf("Valor de pi: %f\n",pi);
11 return 0;
14
Listado 4
la ejecucion del programa muestra que el parametro pi ha sido pasado por valor (en lugar de por referencia). Si
hacemos un seguimiento mas exhaustivo del programa, vemos que cuando llamamos a la funcion cambiavalor
en la lınea 8, el programa salta a la lınea 1 para ejecutar la funcion. Allı, se crea una nueva variable llamada
vpi, y se copia el valor de pi en ella. Esto se representa en el esquema de la figura 5.2.
queda clara la razon por la que vpi y pi tiene diferentes direcciones en memoria; se ha realizado una copia del
contenido de la memoria que tiene pi, se ha llevado a otra zona de memoria y se le ha puesto de nombre vpi.
De esta formna, cuando se asigna 0 a la variable vpi en la lınea 2, se esta asignado el contenido de memoria de
vpi ver figura 5.3.
Los argumentos de la funcion en C se pasan siempre por valor. No existe un a forma directa para que la
funcion que se invoca altere una variable de la funcion de la funcion que la llama. Es imprescindible el uso de
los punteros en el caso de querer modificar el valor de una variable pasada como parametro a una funcion.
21
Figura 5.1: Resultados listado 4
Figura 5.2: Copia del valor de pi en vpi
Figura 5.3: Cambio de valor de vpi
Ası, podemos modificar el programa de Listado 4, para que el paso del argumento se realice por referencia.
Tendremos que utilizar los operadores * y & que hemos visto anteriormente. El nuevo programa se muestra en
Listado 5.
00 #include <stdio.h>
01 void cambiavalor(float *vpi)
02 *vpi=0;
03 printf("Direccion de vpi: %u\n",(unsigned int)vpi);
04 printf("Valor de vpi: %f\n",*vpi);
22
05
06 int main(void)
07 float pi=3.14;
08 cambiavalor(&pi);
09 printf("Direccion de pi: %u\n",(unsigned int)&pi);
10 printf("Valor de pi: %f\n",pi);
11 return 0;
12
Listado 5
Figura 5.4: Resultados listado 5
En esta nueva version del programa, la funcion cambiavalor , recibe el parametro como un puntero (lınea 1).
Le estamos diciendo al compilador que pase como argumento a la funcion una direccion de memoria. Es decir,
queremos que le pase un entero; ese entero sera una direccion de memoria de un float.
En un seguimiento en detalle del programa vemos que, en la lınea 8, cuando llamamos a la funcion cambiavalor,
le tenemos que pasar una direccion de memoria. Para ello, tenemos que hacer uso del operador & , para pasarle
la direccion de la variable pi . Con esto, cuando llamamos a la funcion cambiavalor en 1, lo que se hace es una
copia de la direccion de memoria de pi en la variable (ver 5.5). Ası mediante el operador * podremos acceder
al contenido de la posicion de memoria de pi y cambiar su valor.
En la lınea 2 del programa se cambia el valor de la zona de memoria apuntada por vpi . Podemos leer esa
instruccion como “asignamos cero al contenido de la direccion de memoria apuntada por vpi”. Recordemos que
esa zona de memoria tiene que ser de tipo float. En el ejemplo de la figura 5.5, esta direccion de memoria es la
de 146. Podemos ver el esquema de l resultado de la operacion de la figura 5.6
Ahora, cuando acabe la funcion cambiavalor y retornemos a la funcion principal main, el valor de vi sera. 0.
Hemos realizado un pase de parametros por referencia.
23
Figura 5.5: Paso por referencia de pi
Figura 5.6: Asignacion de 0 al contenido de la memoria apuntado por vpi
A continuacion examinaremos en profundidad algunos tipos de datos complejos (como vectores y estructuras)
y su utilizacion con punteros, ası como algunos operadores nuevos especıficos de punteros.
24
Capıtulo 6
Artimetica de Punteros
Como hemos mencionado anteriormente, podemos considerar el tipo de un puntero como el tipo de datos que
contiene la zona de memoria a la que este apunta. Es decir, si tenemos:
int *ptr;
*ptr=2;
La primera lınea indica que tenemos una variable llamada ptr , cuyo contenido es una direccion de memoria a
una zona donde hay un entero. E la segunda lınea asignamos al contenido de esa direccion de memoria el valor 2.
Una operacion interesante que podemos realizar con punteros es incremetarlos. Obviamente, la operacion de
incremento en un puntero no es igual que en una variable normal. Un incremento en un puntero
hara que apunte al siguiente elemento del mismo tipo de datos que se encuentran en memoria.
en el ejemplo de la figura 6.1, inicialmente el puntero ptr apunta al tercer entero almacenado en el bloque
de memoria representado (suponiendo que cada entero ocupa 3 posiciones de memoria). Ası, la asignacion
*ptr=168 hara que el contenido de esa posicion de memoria sea 168 hara que el contenido de esa posicion
de memoria sea 168. Si incrementamos el puntero (cuidado!, no hablamos del contenido del puntero, sino del
propio puntero), haremos que apunte al siguiente entero que hay en memoria. Este incremento del puntero no
implica sumar 1 a la direccion que contiene el puntero. En este ejemplo, serıa necesario sumar 3 (tres posiciones
de memoria que ocupa el entero para acceder al siguiente).
Naturalmente esta “suma de posiciones de memoria” no la tendra que hacer el programador. Sera el
compilardor el que se encargue de realizar las cuentas para ver cuantas posiciones de memoria debe avanzar.
En general, el compilador tendra sumar tantas posiciones de memoria como requiera el tipo de datos sobre el
que se haya definido el puntero. En el caso de que incrementemos alegremente el puntero y nos salgamos de la
zona de memoria que tengamos reservada recibiremos la visita de nuetro amigo ¡¡segmentation fault¿¿.
Vamos a ilustrar el uso de punteros con vectores mediante un ejemplo. Supongamos que definimos un array de
entero (que, a fin de cuentas, es un conjunto de enteros a los que se reserva posiciones de memoria consecutiva)
como se muestra en la lınea 2 del Listado 6.
01 #include<stdio.h>
02 int vector[]=1,56,47,-89,10;
03 int *ptr;
04 int main(void)
05
25
Figura 6.1: Incremento de punteros
06 int i;
07 ptr=&vector[0];
08 for(i=0; i<5; i++)
09 printf("Vect[%d]= %d\n", i, vector[i]);
10 printf("Ptr + %d= %d\n",i,*(ptr+i));
11
12 return 0;
13
Listado 6
Figura 6.2: Resultados listado 6
Tenemos un vector de 5 enteros. Podemos acceder a ellos mediante el ındice que ocupa en el array (como por
ejemplo, vector[3] nos darıa el cuarto entero del array). De igual forma podemos acceder a el mediante un
puntero. En la lınea 7, asignamos al puntero la direccion del primer elemento del vetor. En C, el nombre del
arreglo apunta al primer elemento de array; por lo que podriamos cambiar la linea 7 por la instruccion
ptr=vector; con identicos resultados.
26
De cualquier forma, debemos de tener muy claro que, una vez definido un array, el nombre del array identifica
a la posicion del primer elemento, pero es una constante. Es decir, podemos utilizarlo como hemos indicado
antes para asignarselo a un puntero (copiamos la direccion de memoria del primer elemento), pero no podemos
asignarle a un array ya creado una direccion distinta; vector=otro puntero provocarıa un error. Algunos
programadores llaman al nombre de un array como “puntero constante”, indicando que no puede cambiarse
su valor. Es simplemente un puntero al que no podemos cambiar su direccion.
27
Capıtulo 7
Cadena de caracteres
Recordemos que en C, las cadenas de caracteres son simplemente arrays de caracteres. No hay un tipo de datos
especial, como en Basic o Pascal. Una cadena en C terminara siempre con un caracter especial reservado para
indicar “fin de cadena”, que es un cero binario, representado por “ \0 ”.
Vamos a implementar nuestr4a funcion de copia de cadenas, similares a la de ANSI C: strcpy ().
01 #include <stdio.h>
02 char origen[40]="Apuntes de Punteros";
03 char destino[40];
04 char *mi_strcpy(char *dest, const char *orig)
05
06 char *p=dest;
07 while (*orig != ’\0’)
08 *p++=*orig++;
09 *p=’\0’;
10 return dest;
11
12 int main(void)
13
14 int i;
15 mi_strcpy(destino, origen);
16 printf("%s",destino);
17 return 0;
18
Listado 7
En el listado anterior, el espacio necesario para las dos cadenas se reserva en origen y destino. Cuando llamamos
a la funcion mi strcpy, en la lınea 15 lo unico que se pasa es la direccion de memoria de ambas cadenas. Este
plantemiento nos permite trabajar con arrays de gran tamano sin tener que mover gran cantidad de informacion
en memoria. como las cadenas de caracteres son exactamente iguales que los arrays de cualquier otro tipo de
datos, el enfoque utilizado es el mismo en todos los casos.
Recordemos que en la figura 5.5, lo que pasamos como argumento a la funcion es una copia de la direccion
28
Figura 7.1: Resultados listado 7
de memoria. Ası, si cambiamos el puerto dentro de la funcion mi strcpy, a la vuelta de la funcion los punteros
originales seguiran apuntando a donde estaban. Esto se puede ver en el ejemplo anterior; cuando llamamos a la
funcion en la lınea 15, los punteros origen y destino apuntan al comienzo de las cadenas. Dentro de la funcion
mi strcpy, los punteros van avanzando en la (lınea 8), y al final ambos estan situados al final de las cadenas.
Al ser copias, cuando volvemos de la funcion, los punteros origen y destino siguen apuntando al inicio de la
cadena.
El modificar const usado en la lınea 4 hace que, dentro de la funcion, no podamos modificar el contenido de la
memoria apuntada por orig, tomando el contenido como valor constante.
En la lınea 6, hacemos que un puntero auxiliar apunte al inicio de la cadena destino (dest). Despues, mientras
el caracter apuntado por el puntero por orig sea distinto del fin de cadea, copiamos el caracter en la zona de
memoria apuntada por p y avanzamos. Esto lo realizamos con una unica instruccion (lınea 8 ). Es equivalente
*p++ a (*p)++?. No, segun la precedencia de los operadores, *p++ es equivalente a *(p++). Y,
que decimos en cada caso?. Pues con la primera instruccion, *p++, decimos que primero veamos el contenido
del puntero y despues incrementemos la posicion del puntero (utilizado en la funcion de arrays), mientras que
con (*p)++ decimos que incremente el contenido de la posicion de memoria donde apunta p. Si p apuntara a
un caracter “C”, harıa que ese caracacter pasara a ser “D”. Si p apunta a un array cuyo valor es 24, despues
de esa instruccion serıa 25.
Queda claro, por tanto, la necesidad de establecer correctamente el orden de aplicacion entre operadores.
Una buena practica que mantiene el codigo mas legible es utilizar un parentesis siempre que haya dudas.
Naturalmente, siempre dentro de unos lımites razonables. Es necesario el apuntador auxiliar p? Claramente
No. se ha utilizado unicamente para dejar bien claro que trabajamos con direcciones de memoria , y que *p y
*dest es exactamente lo mismo. Por eso, al retornar de la funcion devolveremos dest. El codigo de la funcion
mi strcpy del Listado 7 podıan simplificarse como muestra el Listado 8.
01 while (*orig != ’\0’)
29
02 *(dest++) = *(orig++);
03 *dest = ’\0’;
04 return dest;
Listado 8
En esta ultima version se ha hecho uso de parentesis para indicar de forma clara, que queremos copiar el
contenido de la memoria apuntada por orig en dest y, despues, incrementar los punteros. Finalmente hay que
anadir la constante \0 en la ultima posicion de dest (para que cupla el convenio utilizado con cadenas de
caracteres en ANSCI C). Incluso podrıamos optimizar algo mas el codigo poniendo en la definicion del bucle
while(*orig), ya que sera cierto mientras el valor apuntado por orig se distinto de cero.
finalmente recordemos que podemos utilizar tambien la notacion de array para trabajar con cadenas de
caracteres. Ası, tednrıamos la expresion dest[i] y *(dest + 1) como equivalentes.
30
Capıtulo 8
Estructuras
En C una Estructura es una coleccion de variables que se referencian bajo el mismo nombre. Las variables que
conforman la estructura son llamados elementos estructurados.
La palabra clave struct dice al compilador que se esta definiendo una plantilla de estructura.
struct dire
char nombre[40];
char calle[40];
char ciudad[20];
char estado[3];
unsingned long int codigo;
;
Note que la definicion termina en un punto y coma. La razon para el punto y coma es que una definicion de
estructura es una sentencia.
En este momento del codigo, realmente no ha sido declarada ninguna variable. El codigo solo ha definido el
formato de los datos. Para declara una variable real con esta estructura, se escribirıa.
struct dire info_dire;
Se puede declara una o mas variables cuando se define una estructura. Veamos el siguiente listado como ejemplo
de lo anterior.
struct dire
char nombre[40];
char calle[40];
char ciudad[20];
char estado[3];
unsingned long int codigo;
info_dire, binfo, cinfo;
Si solo necesita una variable estructurada, no se necesita incluir el nombre de la estructura. Esto significa que
el codigo
struct
char nombre[40];
char calle[40];
31
char ciudad[20];
char estado[3];
unsingned long int codigo;
info_dire;
El formato general de una definicion de estructura es
struct nombre tipo de estructura
type nombre_elemento1;
type nombre_elemento1;
type nombre_elemento1;
.
.
.
type nombre_elementoN;
variables_estructura;
8.1. Referenciando elementos estructurados
Se referencia elementos individuales de la estructura usando el operador “.”, que se llama a veces
operador de punto. Por ejemplo el siguiente codigo asignara el codigo postal 12345 al campo correspondiente
de la variable estructura info_dire que se declaro antes.
info_dire.codigo=12345;
El formato gerneral para acceder a los elementos es:
nombre_estructura.nombre_elemento
Por tanto, para imprimir el codigo postal en pantalla, se escribirıa
printf("%u",info_dire.codigo);
8.2. Declarando un puntero de estructuras
Se declara un puntero de estructura poniendo el * delante del nombre de la variable de estructura. Por ejemplo,
si se supone la estructura dire definida antes, los siguiente declara puntero_dire para puntero de datos de ese
tipo:
struct dire *puntero_dire;
32
8.3. Usando punteros de estructura
Para encontrar la direccion de una variable de estructura, se pone el operador & antes del nombre de variable
de estructura. Por ejemplo el fragmento siguiente
struct bal
float balance;
char name[80]
persona;
struct bal *p;
Se vera que este codigo
p=&persona;
pone la direccion de persona en el puntero p. Para referenciar el elemento balance, se escribiria
(*p).balance
Los parentesis son necesarios alrededor del puntero porque el operador de punto tiene una prioridad mas alta
que el operador *.
Existe otro metodo de acceso a los elementos de estructuras usando punteros utiliza operador flecha (->), que
es esencialmente un atajo para el metodo anterior.
Para ver como se puede usar un puntero de estructura, examinar este sencillo programa que imprime horas,
minutos y segundos en la pantalla usando un temporizador por software.
Ejemplo 1:
/*visualiza un temporizador software */
#include <stdio.h>
#include <conio.h>
struct time_struct
int hours;
int minutes;
int seconds;
;
void update(struct time_struct *t);
void display(struct time_struct *t);
void delay(void);
33
main()
struct time_time;
time.hours=0;
time.minutes=0;
hora.second=0;
for(; !kbhit(); )
update(&hora);
display(&hora);
/* version 1 explica las referencias de puntero */
void update(struct time_struct *t)
(*t).seconds++;
if((*t).seconds==60)
(*t).seconds=0;
(*t).minutes++;
if((*t).minutes==60)
(*t).minutes=0;
(*t).hours++;
if((*t).hours==24) (*t).hours=0;
delay();
void display(struct time_struct *t)
printf("%d:", (*t).hours);
printf("%d:", (*t).minutes);
printf("%d:", (*t).seconds);
void delay(void)
long int t;
for(t=1; t<128000; ++t);
Listado 30
34
Se usa el operado flecha en lugar del operador punto cuando se accede a un elemento de estructura, dado un
puntero a la variable estructura. Por ejemplo:
(*t).hours
es lo mismo que
t->hours
por lo tanto en el listado 30 se podrıa reescribir update() como:
/* version 2 con el operador flecha */
void update(struct time_struct *t)
t->seconds++;
if(t->seconds==60)
t->seconds=0;
t->minutes++;
if(t->minutes==60)
t->minutes=0;
t->hours++;
if(t->hours==24) t->hours=0;
delay();
Ejemplo 2:
Vamos a realizar un programa que muestre por pantalla un elemento de tipo estructura.
01 #include <stdio.h>
02 #include <string.h>
03 struct alumno
04 char nombre[20];
05 char apell[40];
06 int edad;
07 float nota;
08 ;
09 void ver_alumno(struct alumno *a)
10 printf("%s %s \n",a->nombre,a->apell);
11 printf("\n Calificacion: %f",a->nota);
12
35
13 int main(void)
14
15 struct alumno alum;
16 strcpy(alum.nombre,"Perico");
17 atrcpy(alum.apell,"Palotes");
18 alum.edad=22;
19 alum.nota=8.75;
20 ver_alumno(&alum);
21 return 0;
22
Listado 9
Figura 8.1: Resultados del listado 9
Notas:
Un error tipico es olvidar el puntero y coma al terminar la definicion de una estructura (lınea 8) del
Listado 9. Despues de declarar una varible del tipo estructura alumno, dentro de main.
Podemos acceder a sus campos con el operador punto “.” siempre que estemos trabajando con la estructura
directamente.
Cuando estemos utilizando un puntero a estructura, utilizaremos el operador ->. Este operador es
equivalente a utilizar (*puntero_estructura).campo.
ANSI C permite pasar por valor las estructuras completas. Sin embargo, esto implica que se copia el
contenido de toda la estructura (completa) desde la funcion llamante a la funcion llamada. Esto, para
estructuras con un gran numero de elementos, puede ser un problema. Por cuestiones de eficiencia y
comodidad, pasaremos las estructuras a la funciones mediante un puntero.
36
Ası, en la lınea 11 del Listado 9 podıamos haber accedido al campo nota del puntero a estructura ‘‘a’’,
mediante la instruccion (*a).nota sin embargo, es mas comodo utilizar la notacion flecha “− >”para estos
casos.
37
Capıtulo 9
Puntero a Matrices
Anteriormente hemos visto la relacion entre punteros y array; vamos a ampliar nuestro cmapo de vision utilizando
arrays de varias dimensiones (matrices).
01 #include <stdio.h>
02 #define NFILAS 5
03 #define NCOLUMNAS 8
04 int main(void)
05
06 int fil, col;
07 int matriz[NFILAS][NCOLUMNAS];
08 /*asignamos valores a los elementos */
09 for(fil=0; fil<NFILAS; fil++)
10 for(col=0; col<NCOLUMNAS; col++)
11 matriz[fil][col]=fil*col;
12 /*Mostramos los valores de 2 formas */
13 for(fil=0; fil<NFILAS; fil++)
14 for(col=0; col<NCOLUMNAS; col++)
15 printf("%d - ",matriz[fil][col]);
16 printf("%d\n", *(*(matriz+fil)+col));
17
18 return 0;
19
Listado 10
El resultado es el siguiente:
En el ejemplo del listado 10, tenemos una declaracion de matriz en la linea 7. Que significa esa instruccion?
Por un lado tenemos un array de 5 elementos (filas), pero a su vez cada uno de estos elementos son arrays de 8
enteros (columnas).
Sabemos que los array se alamacenan contiguos en memoria. Ası, el estado de nuestra memoria pordrıa
representarse como:
38
Figura 9.1: Representacion de matriz bidimensional
000000000123456702468101214036912151821...
el primer cero indica la direccion de &matriz[0][0] (que podemos nombrar tambien unicamente con el nombre
de matriz). De esta forma, si quicieramos mostrar el tercer elemento de la segunda fila (el numero 2 marcado
en rojo), podriamos hacerlo como matriz[1][1] o bien como *(*(matriz+1)+2). La primera forma es sencilla;
utilizamos la notacion de array a la que estamos acostumbrados. la notacion de punteros podemos leerla como
“Dame el contenido de una direccion. Esa direccion esta formada por el contenido de donde apunta matriz mas
uno. A ese resultado sumale dos”. Podemos ver cada fila de la matriz como un puntero cuyos elementos son
punteros a arrays de enteros (ver figura 9.1).
Ası, podemos acceder al elemento de la segunda fila, segunda columna pidiendo primero el contenido de
matriz+1. Esto nos dara el segundo puntero del array (recordemos que los arrays en C empiezan a numerarse
en 0). Con esto, tenemos una direccion. Si a esa direccion de memoria le sumamos dos, accedemos al tercer
elemento del segundo array (en el ejemplo de la figura 9.1, tendriamos el 11). En caso de querer acceder al primer
elemento del segundo array, bastaria con escribir *(*(matriz+1)) o, de forma equivalente **(matriz+1).
Queda claro, por tanto, que el primer componente de la matriz bidimensional es equivalente a un array
del siguiente tipo: int *filas[NFILAS]. En esta declaracion estamos diciendo que tenemos un vector cuyos
elementos son punteros a enteros, los mismo que tenıamos en la parte de filas de la declaracion anterior.
Que tiene que hacer el compilador para acceder a cada uno de los elementos de la matriz? Debe clacular la
posicion del elemento en memoria. Para ello, debera multiplicar la fial en la que se encuentra por el numero de
elementos por columna y sumarle la columna en la que esta actualmente. Es decir:
Posicion en memoria=(filas * NCOLS) + columna
Como puede verse en la expresion anterior, es necesario que el compilador sepa a priori, el numero de elementos
que tiene cada columna de nuestra matriz. En egernal, es necesario especificar, al menos, la segunda dimension
de la matriz en forma fija. Puede interesarnos implementar una funcion que nos permita trabajar con nuestro
array multidimensional. Un ejemplo muy sencillo (aunque inutil) de esto puede verse en el listado 11.
39
void reset_matriz(int mat[][NCOLUMNAS])
int fil, col;
for(fil=0; fil<NFILAS; fil++)
for(col=0; col<NCOLUMNAS; col++)
mat[fil][col]=1;
Listado 11
En general, en arrays de mas de una dimension es necesario indicar, en la definicion los parametros a la funcion,
las dimensiones segunda, tercera, etc . . .
40
Capıtulo 10
Asignacion dinamica de memoria
En muchas ocasiones no podemos saber a priori el espacio que vamos a necesitar para nuestra estructura de
datos. Esto debera decidirse en tiempo de ejecucion y llevarse a cabo mediante funciones de gestion de memoria
como malloc, calloc, u otras similares.
Cuando pedimos memoria con alguna funcion de gestion (como malloc()), esta nos devuelve un puntero. En
compiladores ANSI, este puntero es de tipo void. Como void es un tipo generico que a apunta a “cualquier
cosa”, tendremos que indicar al compilador que haga una conversion de tipo (cast) al tipo de datos que nos
interese; es decir, a un puntero al tipo de datos que queramos.
Ademas, sera necesario indicarle a la funcion que llamemos para pedir memoria el numero de bytes que
necesitamos. En C, el numero de bytes que se necesitan para almacenar cada tipo de datos es dependiente
de la maquina sobre la que ejecutamos el programa. Por ejemplo, en dos maquinas distintas una puede requerir
2 bytes para un entero y otra utilizar 4 bytes para el mismo entero. como podemos decirle el numero de bytes
que vamos a necesitar? Utilizando la funcion sizeof(). Ası, el tamano que queremos reservar estara en funcion
del tamano del tipo de datos que utilicemos. Esto puede verse en el ejemplo del Listado 12.
01 #include <stdio.h>
02 #include <stdlib.h>
03 int main(void)
04
05 int *ptr, i;
06 ptr=(int *)malloc(10*sizeof(int));
07 if (ptr==NULL) printf("Error de Mem.");
08 for(i=0; i<10; i++)
09 ptr[i]=1;
10 return 0;
11
Listado 12
Utilizando la equivalencia entre la notacion de punteros y de array, en la lınea 9 asignamos a todos los elementos
de array creados anteriormente un valor constante 1.
El ejemplo anterior premite crear un arrreglo de una dimension dinamico en tiempo de ejecucion, pero ’?Como
creariamos un arraymultidimensional?. Hemos visto en el puntero anterior que los arrays bidimensionales se
41
gestionan como arrays de punteros a arrays del tipo de datos que queremos utilizar. Como reservamos memoria
para estas matrices?. En un planteamiento general, necesitaremos generar la matriz dinamicamente, tanto el
numero de filas como de columnas. En los siguientes ejemplos utilizaremos como tipo de datos base el entero,
pero podrıa utilizarse cualquier otro.
01 #include <stdio.h>
02 #include <stdlib.h>
03 int main(void)
04
05 int nfilas=5, ncols=10;
06 int fila;
07 int **filaptr;
08 filaptr=malloc(nfilas * sizeof(int *));
09 if(filaptr==NULL) printf("Error");
10 for(fila=0; fila<nfilas; fila++)
11 filaptr[fila]=(int *)malloc(ncols *sizeof(int));
12 if(filaptr==NULL)printf("Error");
13
14 return0;
15
Listado 13
En el Listado 13, filaptr es un puntero a puntero a entero. En este caso, el programa crea una matriz de 5
filas por 10 columnas. Esos datos, naturalmente, podrıa gestionarse en tiempo de jecucon.
La primera llamada a malloc (lınea 8) reserva espacio para array de punteros a los arrays de enteros. Po esta
razon, a la funcion sizeof le pasamos el tipo “puntero a entero”. Con el primer array de punteros creado,
pasamos a reservar memoria para los arrays que son apuntados por cada elemento “fila” (en al Lınea 11).
En el ejemplo anterior se necesitan en total 6 llamadas a malloc; una para reservar el array de filas y una para
cada array de enteros asociado a cada fila. Ademas no nos aseguramos de tener toda la matriz en un bloque
contiguo de memoria. Se puede comprobar, utilizando un puntero auxiliar al primer elemento de la primera fila
(es decir, usando int *ptr_aux que apunte a filaptr[0] e incrementado *(testptr++) no accedemos a los
valores introducidos a la matriz). Tal vez obtenemos un bonito “segmentation fault”.
Con este planteamiento, deberemos acceder a los elementos de la matriz unicamente utilizando la notacion de
corchetes. No podemos utilizar un puntero para reccorrer la memoria.
01 #include <stdio.h>
02 #include <stdlib.h>
03 int main(void)
04
05 int nfilas=5, ncols=10;
42
06 int *mem_matriz;
07 int **filaptr;
08 int *testptr;
09 int fila, col;
10 mem_matriz=malloc(nfilas * ncols * sizeof(int));
11 if(mem_matriz == NULL) printf("Error");
12 filaptr=malloc(nfilas * sizeof(int *));
13 if(filaptr == NULL) printf("Error");
14 for(fila=0; fila<nfilas; fila++)
15 filaptr[fila] = mem_matriz+(fila*ncols);
16 for(fila=0; fila<nfilas; fila++)
17 for(col=0; col<ncols; col++)
18 filaptr[fila][col]=1;
19
20 testptr=filaptr[0];
21 for(fila=0; fila<nfilas; fila++)
22 for(col=0; col<ncols; col++)
23 printf("[%d][%d]%d \n",fila, col, *(testptr));
24 return 0;
25
Listado 14
En el ejemplo del Listado 14 se reserva la memoria para la matriz en un bloque contiguo. En la lınea 10 se
reserva la memoria para todas las celdas de la matriz. En la lınea 12 hacemos el array de punteros correspondiente
a la fila de la amtriz. en la lınea 14-15 hacemos que cada puntero de las filas apunte a la celda donde empieza
cada fila. Esto puede calcularse con un simple producto (fila*ncols).
Las lıneas siguientes (16. . . 23) desmuestran que el bloque creado es contiguo, y que podemos utilizar un
punterom auxiliar para rrecorrer las posiciones de memoria.
Con este metodo se ha tenido que utilizar unicamente dos llamadas a la funcion malloc; una para crear el array
de filas y otro para reservar el espaciode todas las celdas. Ademas , serıa posible utilizar funciones que trabajan
con zonas de memoria para su inicializacion de una vez (como memset()).
43
Capıtulo 11
Puntero a funciones
Loa apuntadores a funciones son quiza uno los usos mas confusos de los apuntadores en C. Los apuntadores a
funciones no son tan comunes como otros usos que tienen los apuntadores. Sin embargo, un uso comun es cuando
se pasan apuntadores a funciones como parametros en la llamada a una funcion. Lo anterior es especialmente
util cuando se deben distintas funciones quizas para realizar tareas similares con los datos. Por ejemplo, se
pueden pasar los datos y la funcion que sera usada por alguna funcion de control. como se vera mas adelante
la biblioteca estandar de C da funciones para ordenamiento (qsort) y para realizar busqueda (bsearch), a las
cuales se les puede pasar funciones.
Para declara una apuntador a una funcion se debe hacer:
int (*pf)();
Lo cual declara un apuntador pf a una funcion que regresa un tipo de dato int. Todavia no se ha indicado a
que funcion apunta.
Suponiendo que se tiene una funcion int f(), entonces simplemente se debe escribir:
pf=&f;
Para que pf apunte a la funcion f()
Para que trabaje en forma completa el compilador es conveniente que se tengan los prototipos completos de las
funciones y los apuntadores a las funciones, por ejemplo:
int f(int);
int (*pf)(int)=&f;
Ahora f() regresa un entero y toma un entero como parametro.
Se pueden hacer cosas como:
ans=f(5);
ans=pf(5);
Los cuales son equivalentes.
Las funciones de biblioteca estandar qsort es muy util y esta disenada para ordenar un arreglo usando un valor
44
como llave de cualquier tipo para ordenar en forma ascendente.
El prototipo de la funcion qsort de la biblioteca stdlib.h es:
void qsort(void *base, size_t nmiemb, size_t tam, int(*compar)(const void *, const void *));
Donde:
base apunta al comienzo del vector que sera ordenado.
nmiemb indica el tamano del arreglo
tam es el tamano en bytes de cada elemento del arreglo
compar es un apuntador a una funcion
la funcion qsort llama a la funcion compar la cual es definida por el usuario para comparar los datos cuando se
ordenen. Observar que qsort conservar su independencia respecto al tipo de dato al dejarle la responsabilidad al
usuario. La funcion compar debe regresar un determinado valor entero de acuerdo al resultado de comparacion
que debe ser:
A continuacion se muestra un ejemplo que ordena un arreglo de caracteres, observar que en la funcion comp,
< si el primer valor es menor que el segundo
0 el primer valor es igual al segundo
> si el primer valor es mayor que el segundo
se hace un cast para forzar el tipo void * al tipo char *.
#include <stdlib.h>
#include <stdio.h>
int comp(const void *i, const void *j);
main()
int i;
char cad[]="Facultad de ciencias fisico-matematicas";
printf("\n\n Arreglo original: \n");
for(i=0; i<strlen(cad); i++)
printf("%c",cad[i]);
qsort(cad, strlen(cad), sizeof(char), comp);
printf("\n\n Arreglo ordenado: \n");
for(i=0; i<strlen(cad); i++)
printf("%c",cad[i]);
printf("\n");
45
int comp(const void *i, const void *j)
char *a, *b;
a = (char *) i; /*para forzar void * al tipo de char *, se hace cast */
b = (char *) j; /* empleando (char *) */
return *a - *b;
Listado 20
Figura 11.1: Resultado listado 20
bsearch() realiza una busqueda binaria en datos ordenados. Tiene el siguiente prototipo:
void *bsearch(const *clave, const void *comienzo, tamano_t num, tamano_t anchura,
int (*cmp)(const void *, const void *));
clave es un puntero a la clave que se esta buscando.
comienzo es un puntero al principio del array ordenado
num tamano del arreglo
anchura El numero de bytes usado por cada elemento, todos los elementos del mismo tamano
cmp() es un apuntador a una funcion
Si tiene que trabajar solo con datos no ordenados se debe usar la funcion lsearch().
Tiene este prototipo:
void *lsearch(const void *clave, const void *comienzo, tamano_t num,
46
tamano_t anchura, int (*cmp)(const void *, const void *));
01 #include <stdio.h>
02 #include <stdlib.h>
03 #include <search.h>
04 int cmp(char *, char *);
05 main()
06
07 char s[10]="abcdefghij";
08 int num;
09 num=10;
10
11 if(lsearch("d", s, &num, 1, cmp)) printf("lsearch lo he encontrado \n");
12 if(bsearch("d", s, num, 1, cmp)) printf("bsearch lo he encontrado \n");
13
14 return 0;
15
16 int cmp(char *a, char *b)
17
18 return *a-*b;
19
Listado 21
47
Parte III
Archivos
48
Capıtulo 12
Archivos
Los archivos son la forma mas comun de los flujos.
Lo primero que se debe hacer es abrir el archivo. La funcion fopen() hace lo siguiente:
FILE *fopen(const char *nomb, const char *modo);
fopen regresa un apuntador a un FILE. En la cadena nomb se pone el nombre y la trayectoria del archivo que
se desea accesar. La cadena modo controla el tipo de acceso. Si un archivo no puede ser accesado por alguna
razon un apuntador NULL es devuelto.
Los modos son:
”r”lectura;
”w”escritura; y
”a”
Para abrir un archivo se debe tener un flujo (apuntador tipo archivo) que apunte a la estructura FILE. Por lo
tanto, para abrir un archivo denominado miarch.dat para lectura haremos algo como lo siguiente;
FILE *flujo; /* Se declara un flujo */
flujo = fopen("miarch.dat","r");
es una buena practica revisar si un archivo se pudo abrir correctamente
if ( (flujo = fopen("miarch.dat","r")) == NULL )
printf("No se pudo abrir %s\n","miarch.dat");
exit(1);
.....
12.1. Lectura y escritura de archivos
Las funciones fprintf y fscanf son comunmente empleadas para accesar archivos.
int fprintf(FILE *flujo, const char *formato, args ... );
int fscanf(FILE *flujo, const char *formato, args ... );
49
Las funciones son similares a printf y scanf excepto que los datos son leıdos desde el flujo, el cual debera ser
abierto con fopen().
El apuntador al flujo es automaticamente incrementado con todas las funciones de lectura y escritura. Por lo
tanto, no se debe preocupar en hacer lo anterior.
char *cadena[80];
FILE *flujo;
if ( (flujo = fopen( ... )) != NULL)
fscanf(flujo,"%s",cadena);
Otras funciones para archivos son:
int getc(FILE *flujo) int fgetc(FILE *flujo)
int putc(char ch, FILE *s) int fputc(char ch, FILE *s)
Estas son parecidas a getchar y putchar.
getc esta definida como una macro del preprocesador en stdio.h. fgetc es una funcion de la biblioteca de C.
Con ambas se consigue el mismo resultado.
Para el volcado de los datos de los ujos a disco, o bien, para disasociar un ujo a un archivo, haciendo previamente
un volcado, usar:
int fflush(FILE *flujo);
int fclose(FILE *flujo);
Tambien se puede tener acceso a los ujos predeterminados con fprintf, etc. Por ejemplo:
fprintf(stderr,"<<No se puede calcular!!\n");
fscanf(stdin,"%s",string);
12.2. sprintf y sscanf
Son parecidas a fprintf y fscanf excepto que escriben/leen una cadena.
int sprintf(char *cadena, char *formato, args ... )
int sscanf(char *cadena, cahr *formato, args ... )
Por ejemplo:
float tanque_lleno = 47.0; /* litros */
float kilometros = 400; char km_por_litro[80];
sprintf( km_por_litro, "Kilometros por litro = %2.3f", kilometros/tanque_lleno);
50
12.3. Peticion del estado del flujo
Existen unas cuantas funciones utiles para conocer el estado de algun flujo y que tienen los prototipos siguientes:
int feof(FILE *flujo);
int ferror(FILE *flujo);
void clearerr(FILE *flujo);
int fileno(FILE *flujo);
feof() devuelve verdadero si el ujo indicado esta en el fin del archivo. Por lo tanto para leer un ujo, fp,
lınea a lınea se podrıa hacer algo como:
while ( !feof(fp) )
fscanf(fp,"%s",linea);
ferror() inspecciona el indicador de error para el flujo indicado, regresando verdadero si un error ha
ocurrido.
clearerr() limpia los indicadores de fin-de-fichero y error para el flujo indicado.
fileno() examina el argumento flujo y devuelve su descriptor de fichero, como un entero.
12.4. E/S de bajo nivel o sin almacenamiento intermedio
Esta forma de E/S es sin buffer cada requerimiento de lectura/escritura genera un acceso al disco (o dispositivo)
directamente para traer/poner un determinado numero de bytes.
No hay facilidades de formateo ya que se estan manipulando bytes de informacion.
Lo anterior significa que se estan usando archivos binarios (y no de texto).
En vez de manejar apuntadores de archivos, se emplea un manejador de archivo de bajo nivel o descriptor de
archivo, el cual da un entero unico para identificar cada archivo. Para abrir un archivo usar:
int open(char* nomb, int flag);
que regresa un descriptor de archivo o -1 si falla.
flag controla el acceso al archivo y tiene los siguientes macros definidas en fcntl.h:
O_APPEND el archivo se abrira en modo de solo anadir
O_CREAT si el archivo no existe, sera creado.
O_EXCL abrir en forma exclusiva.
O_RDONLY solo lectura
O_RDWR lectura y escritura
O_WRONLY solo escritura
51
para ver otras opciones usar man. La funcion:
int creat(char* nomb, int perms);
puede tambien ser usada para crear un archivo.
Otras funciones son:
int close(int fd);
int read(int fd, char *buffer, unsigned longitud);
int write(int fd,char *buffer, unsigned longitud);
que pueden ser usadas para cerrar un archivo y leer/escribir un determinado numero de bytes de la
memoria/hacia un archivo en la localidad de memoria indicada por buffer.
La funcion sizeof() es comunmente usada para indicar la longitud.
Las funciones read y write regresan el numero de bytes leıdos/escritos o -1 si fallan.
Se tiene a continuacion dos aplicaciones que usan algunas de las funciones indicadas:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
main(int argc, char **argv)
float buffer[]=23.34,2.34,1112.33;
int df;
int bytes_esc;
int num_flot;
/* Primeramente se crea el archivo */
if ( (df = open(argv[1], O_CREAT, S_IRUSR | S_IWUSR) ) == -1)
/* Error, archivo no abierto */
perror("Archivo datos, apertura");
exit(1);
else printf("Descriptor de archivo %d\n",df);
/* Despues se abre para solamente escribir */
if ( (df = open(argv[1], O_WRONLY) ) == -1)
/* Error, archivo no abierto */
perror("Archivo datos,apertura");
exit(1);
else printf("Descriptor de archivo %d\n",df);
/* En primer lugar se escribe el numero de flotantes que seran escritos */
num_flot = 3;
if ( (bytes_esc = write(df, &num_flot, sizeof(int)) ) == -1)
/* Error en la escritura */
52
perror("Archivo datos, escritura");
exit(1);
else printf("Escritos %d bytes\n",bytes_esc);
/* Se escribe el arreglo de flotantes */
if ( (bytes_esc = write(df, buffer, num_flot*sizeof(float)) )== -1)
/* Error en la escritura */
perror("Archivo datos, escritura");
exit(1);
else printf("Escritos %d bytes\n",bytes_esc);
close(df);
Ejemplo de lectura del ejemplo anterior:
/* Este programa lee una lista de flotantes de un archivo binario. */
/* El primer byte del archivo es un entero indicando cuantos */
/* flotantes hay en el archivo. Los flotantes estan despues del */
/* entero, el nombre del archivo se da en la linea de comandos. */
#include <stdio.h>
#include<fcntl.h>
main(int argc, char **argv)
float buffer[1000];
int fd;
int bytes_leidos;
int num_flot;
if ( (fd = open(argv[1], O_RDONLY)) == -1)
/* Error, archivo no abierto */
perror("Archivo datos");
exit(1);
if ( (bytes_leidos = read(fd, &num_flot, sizeof(int))) == -1)
/* Error en la lectura */
exit(1);
if ( num_flot > 999 )
/* arch muy grande */ exit(1);
if ( (bytes_leidos = read(fd, buffer, num_flot*sizeof(float))) == -1)
/* Error en la lectura */
exit(1);
53
Parte IV
NCURSES
54
Parte V
llamadas al sistema y gestion de
procesos
55
Capıtulo 13
Funcion system
La funcion system, cuyo prototipo se ofrece mas abajo, ejecuta el comando especificado por string, que puede
incluir opciones y argumentos, pasandolos a /bin/sh -c, que en su momento pasa la lınea decomando entera
(/bin/sh -c string) a la llamada del sistema execve sobre la que leeremos en poco tiempo. Devuelve 127 si
no se encuentra /bin/sh, -1 si se produce otro error o el codigo de retorno de string. Sin embargo, si string
es NULL, system devuelve un valor distinto de 0, o, en otro caso, 0.
#include <stdlib.h>
int system(const char *string);
Ejemplo:
/*system.c */
#include <stdio.h>
#include <stdlib.h>
int main(void)
int retval;
retval=system("ls -l");
if(retval==127)
fprintf(stderr, "/bin/sh no disponible \n");
exit(127);
else if(retval==-1)
perror("system");
exit(EXIT_FAILURE);
else if(retval != 0)
fprintf(stderr,"comando devuelto %d\n",retval);
perror("ls");
56
else puts("comando ejecutado con exito");
exit(EXIT_SUCCESS);
57
Capıtulo 14
La familia exec
La funcion exec es realmente una familia de seis funciones, cada una con convenciones y usos de llamada
ligereamente diferentes. A pesar de las multiples funciones, convencionalmente se les llama funciones exec. Se
declara en <unistd.h>. Los prototipos son:
int execl (char *ruta, char *arg0, char * arg1,..., char *argn, (char *)0);
int execv (char *ruta, char *argv[]);
int execle (char *ruta, char *arg0, char * arg1,...,char *argn, (char *)0, char *envp[]);
int execve (char *ruta, char *argv[], char *envp[]);
int execlp (char *fichero, char *arg0, char * arg1,..., char *argn, (char*)0);
int execvp (char *fichero, char *argv[]);
Donde:
ruta Es una cadena con el path (absoluto o relativo) de un archivo ejecutable.
fichero Es el nombre de un fichero ejecutable.
arg0, arg1,...,argn Son cadenas de caracteres que constituyen la lista de parametros que se le pasa al
nuevo programa. Por convenio, al menos arg0 esta presente siempre y coincide con ruta o
con el ultimo componente de ruta. Hay que destacar que tras argn se pasa un puntero
NULL Para indicar el final de los argumentos.
argv Es un array de cadenas de caracteres que constituye la lista de argumentos que va a recibir
el nuevo programa. Por convenio, argv debe tener al menos un elemento, que coincide
con ruta o con el ultimo componente de ruta. El
argv Se indica colocando un puntero NULL detras del ultimo parametro.
Ejemplo 1 (execve):
/*execve.c */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
char *args[]="/bin/ls", NULL;
58
if(execve("/bin/ls",args,NULL)==-1)
perror("execve");
exit(EXIT_FAILURE);
puts("no deberiamos estar aqui");
exit(EXIT_SUCCESS);
Ejemplo 2 (execlp):
Figura 14.1: execlp
59
Capıtulo 15
Atributos de proceso
¿Que es exactamente un proceso? Un proceso es una instancia de un programa en ejecucion y tambien la unidad
basica de planificacion de Linux.
15.1. Estados de un proceso
Figura 15.1: Estados de un proceso
15.2. Creacion de un proceso(fork)
La llamada fork crea un nuevo proceso. El nuevo proceso, o proceso hijo sera una copia del proceso que llama,
o proceso padre. La sintaxis es de fork es:
#include <unistd.h>
pid_t fork( );
60
y la forma de invocarla es pid=fork(). La llamada a fork hace que el proceso actualse duplique. A la salida de
fork, los dos procesos tiene una copia identica del contexto del nivel de usuario excepto el valor de pid, que
para el proceso padre toma el valor del PID del proceso hijo y para el proceso hijo toma erl valor 0, El proceso
0, creado por el nucleo cuando arranca el sistema, es el unico que no se crea con una llamada a fork. Si la
llamada a fork falla, devolvera el valor -1 y en errno estara el codigo de error producido.
Una secuencia de codigo tıpica para manejar la llamada a fork es la siguiente:
int pid;
...
if((pid=fork())==-1)
perror("Error en la llamada a fork");
else if(pid==0)
//Codigo que ejecutara el proceso hijo
else
//Codigo que ejecutara el proceso padre
Si tengo un programa que contiene:
puts("Antes");
creado = fork();
puts("Despues");
Como primer ejemplo de uso de fork veamos un programa que crea un proceso hijo y a continuacion tanto el
padre como el hijo escribiran continuamente en pantalla.
Soy el proceso PADRE
Soy el proceso HIJO
Soy el proceso PADRE
61
Soy el proceso HIJO
...
Programa
#include <sys/types.h>
main()
int i=0;
switch(fork())
case -1:
perror("Error al crear procesos");
exit(-1);
break;
case 0: /*Codigo para el hijo. */
while(i<10)
sleep(1);
printf("\t\t Soy el proceso hijo: %d\n",i++);
break;
default: /*Codigo para el padre */
while(i<10)
printf("Soy el proceso padre: %d\n",i++);
sleep(2);
exit(0);
Cuando llamamos a fork, el nucleo realiza las siguientes operaciones:
1. Busca una entrada libre en la tabla de procesos y la reserva para el proceso hijo.
2. Asigna un identificador de proceso PID para el proceso hijo. Este numero es unico e invariable durante
toda la vida del proceso y es la clave para poder controlar desde otro proceso.
3. Realiza una copia del contexto del nivel de usuario del proceso padre para el proceso hijo. Las secciones
que deben ser compartidas, como el codigo o las zonas de memoria compartida, no se copia, sino que se
incrementan los contadores que indican cuantos procesos comparten esa zonas.
4. Las tablas de control de fichero locales al proceso -como pueden ser la tabla de descriptores de archivos-
tambien se copian del proceso padre al proceso hijo, ya que forman parte del contexto del nivel usuario.
En las tablas globales del nucleo -tabla de ficheros y tabla de inodes- se incrementan los contadores que
indican cuantos procesos tienen abiertos esos ficheros.
5. Rertorna el proceso padre el PID del proceso hijo, y al proceso hijo le devuelve el valor 0.
62
15.3. Terminacion de procesos (exit y wait)
15.3.1. exit
Una situacion muy tipica en programacion concurrente es que el procerso padre espere a la terminacion del
proceso hijo antes de terminar su ejecucion. Un ejem plo de esta situacion es la forma de operar de los interpretes
de ordenes. Cuando escribimos una orden, el interprete arranca un proceso para ejecurtarla y no devuelve el
control hasta que no se ha ejecutado completamente. Naturalmente, esto no se apllica cuando la orden se jecuta
en segundo plano.
Para sincronizar los procesos padre e hijo se emplean las llamadas exit y wait. La declaracion de exit es la
siguiente:
#include <stdio.h>
void exit(int status);
Esta llamada termina la ejecuacion de un proceso y le devuelve el valor de status al sistema.
La llamada exit tien ademas las siguientes consecuencias:
Las funciones registradas por atexit son invocadas en orden inverso a como fueron resgistradas. atexit
permite indicarle al sistemas las acciones que se deben ejecutar al producirse la terminacion de un proceso.
El contexto del proceso es descargado de memoria, lo que implica que la tabla de descriptores de fichero
es cerrada y sus ficheros asociados cerrados, si no quedan mas procesos que los que tengan abiertos.
Si el proceso padre esta ejecutando una llamada a wait, se le notifica la terminacion de su proceso hijo
y se le envia 8 bits menos significativos de status. Con esta informacion, el proceso padre puede saber en
que condiciones ha terminado el proceso hijo.
Si el proceso padre no esta ejecutando una llamada a wait, el proceso hijo se transforma en un proceso
zombi. Un proceso zombi solo ocupa una entrada en la tabla de proceso del sistema y su contexto es
descargarlo de memoria.
exit es uno de los pocos ejemplos de llamadas que no devuelven ningun valor. Es logico, ya que el proceso que
la llama deja de existir despues de haberla ejecutado.
15.3.2. wait
La declaracion de wait es la siguiente:
#include <sys/types.h>
#include <sys/wait>
pid_t wait(int *stat_loc);
Esta llamada suspende la ejecucion del proceso que la invoca hasta alguno de sus procesos hijo termina. La
forma de invocar a wait es:
63
pid_t pid;
int estado;
...
pid=wait(&estado);
//Tambien vale
pid =wait(NULL);
Donde:
pid es el identificador de alguno de los procesos hijo zombi.
estado es la variable donde se almacena el valor que el proceso hijo le envia al proceso padre mediante la
llamada exit y que da la condicion de finalizacion del proceso hijo. Si queremos ignoarar este valor, podemos
pasarle a wait un puntero NULL.
Puede ocurrir que el proceso hijo termine de forma anormal. Para estos caso hay definidas, en el fichero
<sys/wait.h>, unas macros que analizan el valor de estado para determinar la causa de terminacion del
proceso. Estos macros son:
WIFEXITED, devuelve VERDAD -cualquier valor distinto de 0- cuando el proceso termina con una llamada a
exit o a _exit.
WEXITSTATUS, si WIFEXITED devuelve VERDAD, esta macro devuelve el valor de los 8 bits menos significativos
que exit le pasa al proceso padre.
WIFSIGNALED, devuelve VERDAD cuando el proceso termina debido a la accion por defecto de alguna senal.
WTERMSIG, si WIFSIGNALED devuelve VERDAD, esta macro devuelve el numerode la senal que ha causado
la terminacion del proceso.
COREDUMP, devuelve VERDAD si ha generado un ficherocon un volcado de la memoria -core image- del
proceso.
WIFSTOPPED, devuelve VERDAD si el proceso esta parado.
WSTOPSIG, si WIFSTOPPED devuelve VERDAD, esta macro devuelve el numero de la senal que ha causado la
parada del proceso.
Como se ha indicado antes, exit y wait se suelen usar para conseguir que el proceso padre espere a la terminacion
de su proceso hijo. Una secuencia de codigo para realizar esta operacion puede ser:
pid_t pid;
int estado;
...
if((pid = fork()) == -1)
//Error en la creacion del proceso hijo
else if(pid == 0)
//Codigo del proceso hijo
64
exit(10);
else //codigo del proceso padre
pid = wait(&estado);
//cuando el proceso hijo llame a exit,
//le pasara al padre el valor 10,
//que este puede recibir a traves de estado (estado=10)
15.4. Identificadores de proceso
Todo proceso tiene asociado dos numeros desde el momento de creacion: el didentificador del proceso y el
identificador del proceso padre.
El identificador de proceso PID es un nuimero entero positivo que actua a modo de nombre de proceso. El
identificador del proceso padre PIDD, es el PID del proceso que ha creado el actual. El PID de un proceso no
cambia durante el tiempo de vida de este, sin embargo, su PPID si puede variar. Esta situacion se da cuando
el proceso padre muere, pasando el PPID del proceso hijo a tomar el valor 1 (PPID del proceso init).
Para leer los valores de PID y PPID utilizaremos las llamadas getpid y getppid respectivamente:
#include <types.h>
pid_t getpid();
pid_t getppid();
65
15.5. Ejemplo terminacion normal
/************* toto.c ****************/
main()
int m,n;
if(fork()==0)
printf("Proceso hijo id: %d \n",getpid());
exit(3);/***********/
else
m=wait(&n); /***********/
printf("Fin del proceso %d valor regreso: %d n",m,n);
#gcc toto.c -o toto
#./toto
Proceso hijo id:9227
Fin del proceso 9227 valor regreso:768
#
15.6. Ejemplo de terminacion anormal
/************ toto2.c ****************/
main()
int m,n;
if(fork()==0)
printf("Proceso hijo id: %d \n",getpid());
for(;;);/***********/
else
m=wait(&n); /***********/
printf("Fin del proceso %d valor regreso: %d n",m,n);
#gcc toto2.c -o toto2
66
#./toto2 &
Proceso hijo id:9286
#kill -9 9286
Fin del proceso 9286 valor regreso:9
#./toto2 &
Proceso hijo id:9290
##kill -5 9290
Fin del proceso 9286 valor regreso:133
(se crea un core)
15.7. Ejemplo usando llamadas al sistema
Suponiendo que existe una archivo llamado suma o en caso contrario se crear usando #vi suma, y esta organizado
de la siguiente manera:
1
56
3
10
40
#include <stdio.h>
#include <wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
int process_fork(int nproc)
int i;
for(i=1; i<=nproc-1;i++) if(fork()==0) return(i);
return(0);
static char *cmd[]="who","ls","date","ps","uname";
int j,i,pid,status,proc;
int buf; FILE *fp;
main()
j=rand()%5; /* utilizado por el proceso uno*/
pid=process_fork(6);
switch(pid)
67
case 0: printf("estoy en proceso");
wait(&status);
fprintf(stdout,"\n\n ********* padre con ID=%ld \n\n",getpid());
exit(1);
case 1: fprintf(stdout,"\n soy el proceso 1 con ID=%ld \n\n",getpid());
execlp(cmd[j],cmd[j],0);
exit(0);
case 2: fprintf(stdout,"\n soy el proceso 2 con ID=%ld \n\n",getpid());
system("sort -n suma");
exit(0);
case 3: fprintf(stdout,"\n soy el proceso 3 con ID=%ld \n\n",getpid());
execlp("sort","sort","-n","suma",0);
exit(0);
case 4: fprintf(stdout,"\n soy el proceso 4 con ID=%ld \n\n",getpid());
if((fp=fopen("suma","r"))==NULL)
printf("error al abri el archivo");
exit(0);
else printf("buscando archivo \n");
while(!feof(fp))
fscanf(fp,"%d",&buf);
printf("%d \n",buf);
fclose(fp);
exit(0);
case 5: fprintf(stdout,"\n soy el proceso 5 con ID=%ld \n\n",getpid());
if((fp=fopen("suma","a"))==NULL)
printf("error al abrir el archivo/escritura");
exit(0);
else printf("escribiendo archivo \n");
fprintf(fp,"%d\n",2000);
fclose(fp);
exit(0);
default: printf("no hay tal comando \n");
exit(0);
wait(&status);
68
El resultado es el siguiente:
Figura 15.2: Procesos y llamadas al sistema
69
Parte VI
Senales
70
Capıtulo 16
Concepto de senal
Las senales son interrupciones software que pueden ser enviadas a un proceso para informarle de algun evento
asıncrono o situacion especial. El termino senal se emplea tambien para referirse al evento.
Los proceso pueden enviarse senales unos a otros a traves de la llamada kill.
16.1. Como utilizar la funcion Kill
En el fichero de cabecera <signal.h> estan definidas las senales que pueden manejar el sistema. Las senales
del UNIX System V son:
SIGHUP Desconexion. Es enviada cuando una terminal se desconectade todo proceso
de que es terminal de control. Tambien se envia a tocos los procesos de un
grupo cuando el lıder del grupo termina su ejecucion. La acccion por defecto
de esta senal es terminar la ejecucion del proceso que la recibe.
SIGINT Interrupcion. Se envia a todo proceso asociado con una terminal de control
cuando se pulsa la tecla de interrupcion Ctrl+C. Su accion por defecto es
terminar la ejecucion del proceso que la recibe.
SIGQUIT Salir. Similar a SIGINT, pero es generada al pulsar la tecla de salida
Control+\. Su acccion por defecto es generar un fichero core y terminar el
proceso.
SIGKILL Intruccion ilegal. Es enviada cuando el procesador detecta una instruccion
que no forma parte de su repertorio. En los programas escritos en C suele
producirse este tipo de error cuando manejamos punteros a funciones que
no han sido correctamente inicializados. Su acccion por defecto es generar
un fichero core y terminar el proceso.
SIGTRAP Trace trap. Cuando un proceso se esta ejecutando paso a paso, esta senal
es enviada despues de ejecutar cada instruccion. Es empleada por los programas
depuradores. Su accion por defecto es generar un fichero core y
terminar el proceso.
SIGBUS Error de bus. Se produce cuando se da un error de acceso a memoria.
Las dos situaciones tıpicas que la provocan suele ser intentar acceder a
una direccion que fısicamente no existe o intentar acceder a una direccion
impar, violando asi las reglas de alineacion que impone el procesador.
Su accion por defecto es generar un archivo core y terminar el proceso.
71
SIGKILL Terminacion abrupta. Esta senal provoca irremadiablemente la terminacion
del proceso. No puede ser ignorada y siempre que se recibe se ejecuta.
Su accion por defecto, que consite en generar un archivo core y termina
el proceso
SIGSEGV Violacion de segmento. Es enviada a un proceso cuando intenta acceder
a datos que que se encuentran fuera de su segmento de datos.
Su accion por defecto es generar un fichero core y terminar el proceso.
SIGTERM Finalizacion controlada. Es la senal utilizada para indicarle a un proceso
que debe terminar su ejecucion. Esta senal no es tajante como SIGKILL
y puede ser ignorada. Lo correcto es que la rutina de tratamiento de esta senal
se encargue de tomar las acciones necesarias para dejar al proceso en un estado
coherente y a continuacion finalizar su ejecucion con una llamada a exit.
Esta senal es enviada a todos los proceso cuando se emiten las ordenes shutdown
reeboot. Su acccion por defecto es terminar el proceso.
SIGCLD Terminacion del proceso hijo. Es enviada al proceso padre cuando alguno
de sus procesos hijo termina. Esta senal es ignorada por defecto.
Un proceso se puede terminar con otro uilizando la funcion kill cuyo prototipo es:
#include<sys/types.h>
#include <sys/types.h>
int kill(pid_t pid, int sig);
pid especifica el proceso que queremos eliminar y sig es la senal que queremos enviar. Como esta seccion
especifica la eliminacion de un proceso, la unica senal de la que nos tenemos que preocupar por ahora es SIGKILL.
Con Kill -l podemos ver las posibles senales que se les puede envia a los procesos.
En la siguiente tabla tenemos un resumen de ellas:
Por lo tanto, para eliminar un proceso, primero buscaremos su PID con un comando ps y despues ejecutarıa el
comando kill de la siguiente forma:
$kill -9 PID_del proceso
Si queremos que un proceso se envie una senal a si mismo, podemos usar la llamada raise:
#include <signal.h>
int raise (int sig);
donde sig es el numero de la senal que queremos enviar; raise se puede codificar a partir de kill de la siguiente
forma:
int raise(int sig)
return kill (getpid(), sig);
72
Senal Nombre Capturable Sgnificado Restaura rutina
por defecto
1 SIGHUP Sı Cierre de lınea ?
2 SIGINT Sı Interrupcion ?
3 SIGQUIT Sı Salir ?
5 SIGTRAP Sı Senal de traza de proceso
7 SIGBUS No Error en el bus del sistema ?
9 SIGKILL No Muerte de un proceso ?
11 SIGSEGV No Violacion de segmento de memoria ?
15 SIGTERM Sı Terminacion de un proceso ?
17 SIGCHLD Sı Senal enviada al padre por el hijo cuando este termina ?
18 SIGCLD Sı Senal enviada al padre por ?
el hijo cuando este termina
Tabla 16.2: Senales usadas por Kill
16.2. Tratamiento de senal(signal)
Para especificar que tratamiento debe realizar un proceso al recibir una senal, se emplea la llamada signal:
#include <signal.h>
void (*signal (int sig, void (*action)()))();
La declaracion de signal puede resultar extrana, pero es frecuente en los programas C. Como vemos, signal
es del tipo funcion que devuelve un puntero a una funcion void y recibe dos parametros.
sig es el numero de la senal cuya forma de tratamiento queremos especificar.
action es la accion que se tomara al recibir la senal y puede tomar tres clases de valores:
• SIG_DFL, indica que la accion a realizar cuando se recibe la senal es la accion por defecto asociada
a la senal -manejador por defecto-. Por lo general, esta accion consiste en terminar el proceso y en
algunos casos tambien incluye generar un fichero core.
• SIG_IGN, indica que la senal se debe ignora.
• direccion, es la direccion de la rutina de tratamiento de la senal -manejador suministrado por el
usuario-; la declaracion de esta funcion debe ajustarse al siguiente modelo:
#include <signal.h>
void handler(int sig, int code, struct sigcontext *scp);
73
16.3. Ejemplos
Ejemplo 1:
En este ejemplo el proceso padre envia una senal al proceso hijo a traves de su pid para su terminacion
#include<signal.h>
main()
int pid;
if((pid = fork()) == 0)
/* codigo del proceso hijo */
while(1)
printf("HIJO PID=%d \n",getpid());
sleep(1);
sleep(10);
printf("PADRE. Terminacion del proceso %d\n",pid);
kill(pid,SIGTERM);
exit(0);
Ejemplo 2:
/* SenalProceso.c */
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#define TIMEOUT 30
/*** Codigo manejador de se~nales*/
void manejador(int senal)
fprintf(stderr, "Llego: %d\n", senal);
int main(int argc, char *argv[])
/*** Establece los manejadores de se~nales */
signal(SIGINT, manejador);
74
signal(SIGQUIT, manejador);
signal(SIGTERM, manejador);
/*** Entra en un ciclo infinito esperando ser
* terminado
*/
for (;;)
fprintf(stdout, "Esperando se~nal\n");
sleep(TIMEOUT);
Ejemplo 3:
/*
* Ejemplo de como una proceso puede enviar una se~nal a otro proceso mediante
* kill()
*/
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
/* Prototipo de la funcion para tratamiento de la se~nal */ void trataSenhal (int);
/* Programa principal.
* Crea un proceso hijo y le envıa una se~nal SIGUSR1 cada segundo.
*/
main()
/* Identificador del proceso hijo */
pid_t idProceso;
/* Se crea el proceso hijo y se comprueba el error */
idProceso = fork();
if (idProceso == -1)
perror ("No se puede lanzar proceso");
exit (-1);
/* Camino que sigue el proceso hijo.
* Pone trataSenhal() para tratar la se~nal SIGUSR1 y se mete en un bucle
* de espera
*/
if (idProceso == 0)
75
signal (SIGUSR1, trataSenhal);
while (1)
pause ();
/* Camino que sigue el proceso padre.
* Cada segundo envıa una se~nal SIGUSR1 a su proceso hijo.
*/
if (idProceso > 0)
while (1)
sleep (1);
kill (idProceso, SIGUSR1);
/* Funcion de tratamiento de SIGUSR1.
* Escribe en pantalla un aviso de que ha llegado la se~nal.
*/
void trataSenhal (int numeroSenhal)
printf ("Recibida se~nal del padre\n");
Ejemplo 4:
#include <stdio.h>
#include <signal.h>
#include <time.h>
#define MAX 256
int pid_emisor, pid_receptor; void enviar(sig)
char str[MAX];
FILE *fp;
printf("Proceso emisor mensaje : ");
if(gets(str)!=NULL)
if((fp=fopen("buzon","w"))==NULL)
76
perror("enviar");
kill(pid_receptor,SIGTERM);
fputs(str,fp);
fclose(fp);
signal(SIGUSR1,enviar);
kill(pid_receptor,SIGUSR1);
else
kill(pid_receptor,SIGTERM);
exit(0);
void recibir(sig)
char str[MAX];
FILE *fp;
if((fp=fopen("buzon","r"))==NULL)
perror("enviar");
kill(pid_emisor,SIGTERM);
fgets(str,MAX,fp);
fclose(fp);
printf("PROCESO RECEPTOR MENSAJE : %s\n",str);
signal(SIGUSR1,recibir);
kill(pid_emisor,SIGUSR1);
main(int argc, char *argv[])
//struct tms bt1,bt2;
int estado;
pid_emisor=getpid();
//t1=times(&bt1);
if((pid_receptor=fork())==-1)
perror(argv[0]);
exit(-1);
else if(pid_receptor==0)
/*codigo del proceso hijo */
77
signal(SIGUSR1,recibir);
while(!0) pause();
else
/*codigo del proceso padre*/
//wait(&estado);
//t2=times(&bt2);
sleep(2);
enviar();
while(!0) pause();
exit(0);
78
Parte VII
Comunicacion Interprocesos (IPC)
79
Capıtulo 17
Condiciones de competencia
Los procesos requieren con frecuencia la comunicacion entre ellos(IPC). Esto es porque en un sistema los procesos
tienden a compartir recursos de espacio comun de almacenamiento(condiciones de competencia). En algunos
S.O., los proceso que trabajan juntos comparten con frecuencia un espacio en comun para almacenamiento,
principalmente cuando leen y escriben en un espacio compartido que puede estar en la memoria principal o un
archivo.
Por ejemplo.
Supongamos que se desea imprimir un archivo, entonces se ejecuta la instruccion y el kernel manda a un procesos
A a imprimir el archivo, colocando el nombre de este en un directorio spooler, mientras otro proceso verifica
frecuentemente si hay archivos por imprimir, si existe entonces lo imprime y lo elimina. Tambien el spooler
tiene un numero enorme de entradas de peticiones, teniendo dos operaciones basicas en las peticiones:
OUT : Apunta al siguiente archivo a imprimir.
IN : Apunta al la siguiente entrada libre dentro del directorio.
Cabe la posibilidad de que otro proceso B solicite imprimir su archivo a la vez (i.e.) El proceso A y el proceso
B deciden colocar un archivo en la fila de impresiones como se muestra en la figura siguiente.
Figura 17.1: Spooler de impresiones
El proceso A lee IN y almacena su valor en una variable local llamada ”next-free-slot”. Cuando ocurre
una interrupcion de reloj y la CPU decide que el proceso A ya ha estado en ejucucion el tiempo suficiente,
entonces altera con el proceso B. El proceso B tambien lee IN, y obtiene su mismo valor por lo que almacena el
80
nombre de su archivo en el registro de impresiones(IN ). El porceso A vuelve a ajecutarse a partir del momento
en que abandono y revisa el next-free-slot y sobreescribe el nombre de su archivo con el nombre que tenia
anteriormente escrito en la lista de impresion. y por ultimo incrementa next-free-slot++. Y es por eso que el
deamon de impresion no detecta algo raro en este procedimiento. La consecuencia es que el proceso B nunca
imprime su archivo, porque el proceso A sobreescribio en la peticion del proceso B como se muestra en la figura
17.1.
Definiendo las condiciones de competencia: Cuando dos o mas procesos leen o escriben en ciertos datos
campartidos y el resultado final depende de quien ejecute ¿que? y en ¿que momento?.
Si un proceso utiliza una variable V o un archivo A compartido, los demas procesos no pueden utilizarlo,
segun la definicion 1 evitara las condiciones de competencia y se llama ”Exclusion Mutua”.
Definicion 1 Exclusion Mutua
Cuando dos o mas procesos no pueden entrar a su seccion crıtica al mismo tiempo
Ahora hay que definir lo que es la seccion crıtica .
Definicion 2 La seccion crıtica o region critica
Determina una forma de prohibir que mas de un proceso lea o escriba en los datos compartidos a la vez.
La seccion crıtica (SC) se puede ver como un estado del proceso en el momento que esta usando el recurso
compartido.
Una vez que se sabe que es la exclusion mutua y la seccion crıtica , ahora hay que mencionar que los
procesos no siempre estan compitiendo por la SC , tambien trabajan, en otras palabras, hacen calculos o
procesan informacion que no conducen a las condiciones de competencia, sin embargo otros procesos comparte
recursos o realizan labores crıticas que pueden llevar a conflictos.
Figura 17.2: Visualizacion de dos procesos estan al mismo tiempo en su SC
Se pude observar que los dos procesos entran a su SC al mismo tiempo, mas no se debe confundir con el hecho
de que los procesos utilicen(escribir) el recurso compartido al mismo tiempo, esto no se puede por razones fısicas
en la memoria. Ademas se debe agregar que no se comunican los procesos para poder realizar una sincronizacion
entre ellos.
Para garantizar la exclusion mutua se proponen cuatro condiciones para obtener mejores resultados:
1. Dos procesos no deben encontrarse al mismo tiempo dentro de sus SC.
81
2. No se deben hacer hipotesis sobre la velocidad y numero de CPU.
3. Ningun proceso que este en ejecucion fuera de su seccion crıtica puede bloquear a otros procesos
4. Ningun proceso debe esperar eternamente para entrar a su seccion crıtica
Los metodos que no garantizan correctamente la exclusion mutua son:
Desactivacion de iterrupciones: La forma mas simple de evitar que dos procesos entren a su SC es
desactivando todas sus iterrupciones justo antes de entrar a su SC y activarlas de nuevo una ves qyue salga
de su SC, al desactivar las interrupciones no puede ocurrit una interrupcion de reloj. Este metodo es poco
confiable porque el programador tiene en sus manos el control de activar y desactivar las interrupciones,
ası tambien el problema se agrabia cuando el proceso recive una senal kill, estando en su S.C. Esto
probocarıa que nadie pueda usar el recurso compartido y ademas no cumpliria las condiciones 3 y 4.
Donde el proceso P1 que fue eliminado por una senal no dejaria entrar a los procesos Pi a su SC(regla 4).
Ademas el proceso P1 que no esta en su SC, esta bloqueando a los demas procesos esto se puede ver como
el no cumplimiento de la regla 3. Acontinuacion ilustraremos el caso en la figura 17.3. Donde el proceso
que toma el recurso compartido antra a su SC y desactiva las interrupciones y al salir las vueleve a activar
Figura 17.3: Proceso activando y desactivando interrupciones
Variables de Cerradura: En este metodo se utiliza una variable compartida llamada cerradura. Si se desea
que un proceso entre a su SC primero se hace una prueba de cerradura, i.e se pregunta por el valor de la
variable cerradura, si es cero, entonces el proceso cambia el valor de la variable a uno y entra a su SC.
en caso contrario, si el valor de la cerradura es uno , entoces el proceso espera hasta obtener el valor cero
de nuevo. El problema de este metodo es que se tiene probleas de sincronizacion puesto que tambien la
variable de cerradura es un recurso campartido y no se garantiza la exclusion mutua en esta zona y por
ende falla.
Alternancia estricta. En esta metodo el acceso a la Sc de los procesos es alterno y va pasando uno por uno
utilizando un turno. Al proceso que no le toca se mantiene en espera hasta detectar cuando conmuta la
variable ”turno”y entrar a su SC.
Este chequeo continuo de una variable se denomina espera activa. la incovenecia de este metodo es que
los procesos deben alternarse de forma estricta en el uso de sus SC´s. No es un buen enfoque cuando un
proceso es mucho mas rapido que el otro. Se viola la condicion 3 mencionada: un proceso esta bloqueado
por otro que no esta en su SC. Es decir, si un porceso falla, entonces bloqueara a otro, como se ilustra en
el siguiente codigo.
82
Proceso 1 Proceso 2
(1) while(true) while(true)
(2) while(turn!=0) while(turn!=1)
(3) SC(); SC();
(4) turn=1; turn=0;
(5) Not_SC(); Not_SC();
Algoritmo de Alternancia Estricta
Algoritmo de Peterson: El algoritmo de Peterson consiste en utilizar una variable global llamada ”senal”que
indica el interes de los procesos por entra a su SC y una variabble llamada ”turno”que resuelve los confrictos
de simultaneidad. este algoritmo da una solucion sencilla para la exclusion mutua. En este algoritmo, un
proceso debe de entrar a su SC como un proceso que llama a una funcion ”enter region”con su propio
numero de proceso 0 o 1 como parametro, hasta que pueda entrar(fuerza). Despues de usar la variable
compartida, el proceso llama a otra funcion, llamada ”leave region”para indicar que ha terminado y
permita la entrada de otro proceso. Dichas funciones se pueden contemplar a continuacion.
private boolean ban = true;
private final int n = 2;
int turn, interested[n];
(1) public void enter_region(int process)
(2) Int other;
(3) other =1-process;
(4) interested[process]=ban;
(5) turn=process;
(6) while(turn==process&& intrested[other]==ban);
(7) private void leave_region(int process)
(8) interested[process]=!ban;
Algoritmo de Peterson
En la lınea 6 del codigo 2 ningun proceso esta en su region critica. El proceso P0 llama a la funcion
enter region. Determina el elemento de su arreglo y asigna turn =0. Puesto que el proceso P1 no
esta interesado, regresa inmediatamente, Si P1 llama a enter region, espera hasta que interested[0]=!ban,
este evento sucede solo cuando el P0 llama a leave region. En caso que ambos procesos llaman a enter region
en forma simultanea, ambos tienen su turno con turn.
83
Cuando se llega a la linea (6) WHILE, P0 se ejecuta 0 veces y P1 hace un ciclo y no entra a su region
critica
TSL (Test and set lock). Lee el contenido de una palabra de memoria en un registro para despues almacenar
su valor distinto de cero en una direccion de memoria. Las instrucciones son a bajo nivel y el nemonico
tsl que esta en la lınea 2 del codigo 3 rutina ”Enter region”bloquea un registro de banderas y por ultimo
en la lınea 7, limpia el registro de vanderas para que otro proceso pueda entrar a su SC.
(1) Enter_region:
(2) Tsl register,flag
(3) Cmp register,#0
(4) Jnz enter_region
(5) Ret
(6) Leave_region:
(7) Mov flag,#0
(8) ret
Algoritmo TSL
84
Capıtulo 18
Conceptos basicos IPC
IPC=Interprocess Communication
IPC es un termino generico que describe los metodos que utilizan los procesos para comunicarse los unos
con los otros. Sin IPC, los proceso pueden intercambiar datos u otra informacion solo a traves del sistema de
archivos o, en el caso de los procesos que tiene un ancestro en comun (como la relacion padre-hijo despues de un
fork), a traves de descriptores de archivos heredados. En particular, conoceremos los conductos, FIFO, memoria
compartida, semaforos y colas de mensajes.
Cola de Mensajes Semaforos Memoria Compartida
Archivo cabecera #include <sys/msg.h> <sys/sem.h> <sys/shm.h>
Llamada sistema crear o abrir msgget semget shmget
Llamada para operaciones de control msgctl semctl shmctl
Llamada operaciones IPC msgsnd semop shmat
msgrcv shmdt
Tabla 18.1: Archivos cabecera y llamadas al sistema
18.1. El comando ipcs
ipcs da un reporte del status de las facilidades de comunicacion entre procesos
Sintaxis:
#ipc [-abcmopqsr] [-C corefile] [-N namelist]
Sin opciones la informacion corta para las colas de mensajes, la memoria compartida y los semaforos que se
encuentran activos en el sistema.
Algunas opciones:
85
-m informacion acerca de segmentos activos de memoria compartida
-s informacion acerca de semaforos activos
-a utiliza todas las opciones de salida
-t imprime informacion de tiempo de la ultima operacion de control que
cambio los permisos de acceso para todas las facilidades
-p muestra informacion de los numeros de procesos
18.1.1. Salida del comando ipcs
Salida del tipo:
T ID KEY MODE OWNER GROUP
Donde:
T Tipo de facilidad:
q cola de mensajes
m memoria compartida
s semaforos
ID identificador para la entrada del facilitador
KEY llave usada como argumento para la creacion acceso de la facilidad
MODE modos de acceso a la facilidad y banderas
Dos primeros caracteres:
R proceso espera un msgrcv
S proceso espera un msgsnd
D segmento de memoria asociado ha sido suprimido
C segmento memoria asociado sera limpiado (cleared)
- la bandera correspondiente no esta activa
Los siguiente nueve caracteres son permisos agrupados en conjunto de tres (propietario, grupo y el resto).
El ultimo caracter generalmente no se usa.
r permiso de lectura
w permiso de escritura
a permiso de alteracion
- permiso no autorizado
OWNER nombre de firma (login) del propietario de la entrada de la facilidad
GROUP nombre del grupo del grupo del propietario de la entrada de la facilidad
86
Figura 18.1: Ejemplo del comando ipcs
18.2. El comando ipcrm
Borra uno o mas mensajes, semaforos o indicadores de memoria compartida
Sintaxis:
ipcrm [-m shmid][-q msqid][-s semid][-M shmkey][-Q msqkey][-S semkey]
Opciones:
-m shmid Borra el segmento de memoria compartido con identificador shmid
-q msgid Borra la cola de mensaje con identificador msgid
-s semid Borra el semaforo con identificador semid
-M shmkey Borra la memoria compartida, creada con la llave shmkey
-Q msqkey Borra la cola de mesajes, creada con la llave msqkey
-S semkey Borra el semaforo, creado con la llave semkey
18.3. Pipes UNIX Semi-duplex
La forma mas simple de IPC en Linux son los pipes o tuberıas, han estado presentes desde los primeros
orıgenes del sitema operativo UNIX y proporcionan un metodo de comunicaciones en un sentido (unidirecional,
semiduplex entre procesos).
Una tuberıa (pipe) es simplemente un metodo de conexion que une la salida estandar de un proceso a la entrada
estandar de otro. Para esto se utilizan “descriptores de archivos”reservados, los cuales en forma general son:
0: entrada estandar (stdin).
87
Figura 18.2: Ejemplo del comando ipcrm
1: salida estandar (stdout).
2: salida de error(stderr).
Este mecanismo es ampliamente usado, incluso en la lınea de coamndos UNIX (en la Shell)
ls | sort | lp
Lo anterior es un ejemplo de “pipeline”, donde se toma la salida de un comando ls como entrada de un
comando sort, quien a su vez entrega su salida en la entrada lp. Los datos corren por la tuberia semi-duplex,
viajando(virtualmente) de izquierda a drecha por la tuberia.
Cuando un proceso crea una tuberia, el kernel instala dos descriptores de archivos para que los use la tuberia.
Un descriptor se usa para permitir un camino de entrada a la tuberia(write), mientras que la otra se usa para
obtener los datos de la tuberia(read). A estas alturas, la tuberia tiene un pequeno uso practico, ya que la
creacion del proceso solo usa tuberias para comunicarse consigo mismo. Se podrıa considerar esta presentacion
de un proceso y el kernel despues de que se haya creado una tuberıa.
En la figura19.1, es facıl ver como se conectan los descriptores. Si el proceso envia datos por la tuberıa
(fildes[0]), tiene la habilidad de obtener (leer) es informacion de fildes[1]. Sin embargo, hay un objetivo
mas amplio sobre el es esquema anterior. Mientras una tuberia conecta inicialmente un proceso a si mismo, los
datos que viajan por la tuberıa se mueven por el kernel.
Veamos como un proceso hijo hereda cualquier descriptor de archivo abierto del padre, ahora tenemos la base
para la comunicacion multiprocesos (entre padre e hijo).
88
Capıtulo 19
Pipe sin nombre
Para crear una tuberia simple con C, se usa la llamada al sistema pipe(). Toma un argumento solo, que es un
arreglo de dos enteros, y si tiene exito, la tabla contendra dos nuevos descriptores de archivos para ser usados
por la tuberia.
Figura 19.1: Comunicacion unidirecional utilizando una tuberia
Un pipe no tiene nombre y, por tanto, solo puede ser utilizado entre los procesos que lo hereden a traves de
la llamada fork(). A continuacion se describen los servicios que permiten crear y acceder a los datos de un pipe.
19.1. Creacion de un ((pipe))
El servicio que permite crear un pipe es el siguiente:
int pipe(int fildes[2]);
Esta llamada devuelve dos descriptores de archivos (vease figura 19.1) que se utilizan como identificadores.
89
El prime elemento del arreglo fildes (elemento 0) esta fijado para lectura, mientras el segundo entero (elemento
1) esta fijado y abierto para escritura. Visualmente hablando, la salida de fildes[1] se vuelve la entrada
para fildes[0].
Todos los datos que se mueven por la tuberia lo hacen por el kernel. Se debe recordar que un nombre de arreglo
en C es un puntero a su primer elemento miembro. Es decir, fildes es equivalente a &fildes[0]. Una vez
establecida la tuberia, se puede crear(mediante fork) un nuevo proceso hijo.
Si el padre quiere recibir datos del hijo (ver figura 19.2), debe cerrar fildes[1], y el hijo debe cerrar fildes[0].
Si el padre quiere enviarle datos al hijo, debe cerrar fildes[0], y el hijo debe cerrar fildes[1]. como los
descriptores se comparten entre el padre y el hijo, siempre se debe cerrar el extremo de la tuberia que no
interesa, nunca se devolvera EOF si los extremos innecesarios de la tuberia no son explicitamente cerrados. Como
se menciono previamente, una vez que se ha establecido la tuberia, los descriptores de archivos se tratan como
descriptores a archivos normales.
En resumen:
fildes[0], descriptor de archivo que se emplea para leer del pipe.
fildes[1], descriptor de archivo que se utiliza para escribir en el pipe.
La llamada pipe devuelve 0 si fue bien y −1 en caso de error.
19.2. Escritura en un ((pipe))
El servicio para escribir datos en un pipe es el siguiente:
int write(int fd, char *buffer, int n);
Figura 19.2: Tuberia entre dos procesos
90
19.3. Cierre de un ((pipe))
El cierre de cada uno de los descriptores que devuelve la llamada pipe se consigue mediante el servicio close,
que tambien se emplea para cerrar cualquier archivo. Su prototipo es:
int close(int fd);
El argumento de close indica el descriptor de archivo que se desea cerrar. La llamada devuelve 0 si se ejecuto con
exito. En caso de error, devuelve −1
19.4. Ejemplos
Ejemplo 1:
Este primer ejemplo (bastante inutil), crea, escribe y lee desde un pipe:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
int main()
int fd[2];
char buffer[30];
if(pipe(fd) == -1) /*pipe() devuelve 0 en caso
de exito o -1 en caso de error */
perror("pipe");
exit(1);
printf("Escribiendo en el descriptor de archivo #%d\n",fd[1]);
write(fd[1],"probando 1 2 3",5);
printf("Leyendo desde el descriptor de archivo #%d\n",fd[0]);
read(fd[0],buffer,5);
printf("Leido \ "%s\"\n",buffer);
En este ejemplo no tiene mucho sentido ya que se esta comunicando consigo mismo.
91
Ejemplo 2:
Veamos que pasa si se llama fork().
El hijo recibe una copıa de todos los descriptores de archivos del padre, incluyendo una copia de los descriptores
de archivos del pipe. Esto permite que el hijo mande datos al extremo de escritura del pipe, y el padre los reciba
del extremo de lectura:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
int fd[2];
char buffer[30];
pipe(fd);
if(!fork())
printf("HIJO: Escribiendo \n");
write(fd[1],"hola",5);
printf("HIJO: adiossssssss \n");
exit(0);
else
printf("PADRE: leyendo desde el pipe \n");\
read(fd[0],buffer,5);
printf("PADRE: He leido \"%s\"\n",buffer);
wait(NULL);
En este caso, el padre intento leer desde el pipe antes que el hijo escribiera. Cuando esto ocurre, se dice que el
padre se bloquea, o duerme, hasta que llegan datos que leer. Al parecer el padre intento leer, se durmio, el hijo
escribio y termino, y el padre desperto y leyo el dato.
Veamos un ejemplo mas completo y con mas verificacion de errores.
#include <stdio.h>
#include <unistd.h>
#include <sys/type.h>
int main(void)
92
int fd[2], nbytes:
pid_t childpid;
char string[] = "Hola a todos! \n";
char readbuffer[80];
pid(fd);
if((childpid = fork()) == -1)
perror("fork");
exit(1);
if(childpid == 0)
/*cierra el descriptor de entrada en el hijo*/
close(fd[0]);
/*enviar el saludo vıa descriptor de salida */
write(fd[1]), string, strlen(string));
exit(0);
else
/*cierre del descriptor de salida en el padre */
close(fd[1]);
/*leer algo de la tuberia... el saludo */
nbytes = read(fd[0], readbuffer, sizeof(readbuffer));
printf("He recibido el string: %s", readbuffer);
return(0);
93
Capıtulo 20
Creacion de tuberias con nombre
(FIFO) First In First Out
Un FIFO tambien se conoce como una tuberia con nombre. El nombre es el de una archivo que multiples proceso
pueden abrir, leer y escribir. Una tuberia con nombre funciona como una tuberia normal, pero tiene algunas
diferencias notables:
Las tuberias con nombre existen en el sistema de archivos como un archivo de dispositivo especial.
Los procesos de diferentes padres pueden compartir datos mediante una tuberia con nombre.
Una vez finalizada toda las operaciones de E/S, la tuberia con nombre permanece en el sistema de archivos
para un uso posterior.
20.1. Creacion de una FIFO
Hay varias formas de crear una tuberia con nombre. Las dos primeras se pueden hacer directamente desde el
shell:
mknod MIFIFO p
mkfifo a=rw MIFIFO
Los dos comandos anterior realizan operaciones identicas, con una excepcion. El comando mkfifo proporciona
una posibilidad de alterar los permisos del archivo FIFO directamente tras la creacion. Con mknod sera necesaria
una llamada al comando chmod.
Los archivos FIFO se pueden identificar rapidamente en un archivo fisico por el identicador “p”que aparece en
la lista del directorio.
$ ls -l MIFIFO
prw-r--r-- 1 root root 0 Dec 14 22:15 MIFIFO|
Tambien hay que observar que la barra vertical (|), simbolo de pipe, esta situada inmediatamente detras del
nombre de archivo.
Para crear un FIFO en C, se puede hacer uso de la llamada del sistema mknod() con sus respectivos argumentos:
94
mknod("/tmp/MIFIFO", S_IFIFO|0644, 0);
En este caso el archivo “/tmp/MIFIFO”se crea como un archivo FIFO. El segundo argumento es el modo de
creacion, que sirve para indicarle a mknod() que crea un FIFO (con S_IFIFO) y pone los permisos de acceso a
ese archivo al igual como se puede hacer con el comando chmod. Finalmente, el tercer argumento de mknod() se
ignora (al crear un FIFO) salvo que se este creando un archivo de dispositivo. En ese caso, se deberia especificar
los numeros mayores y menor del archivo de dispositivo.
Los permisos se ven afectados por la configuracion de umask de la siguiente forma:
umask_definitiva = permisos_solicitados & ~umask_inicial
Un truco comun es usar la llamada del sitema umask() para borrar temporalmente el valor de umask:
umask(0);
mknod("/tmp/MIFIFO", S_IFIFO|0666, 0).
20.2. Operaciones con FIFOs
Las operaciones E/S sobre un FIFO son esencialmente las mismas que para las tuberias normales, con una gran
excepcion. Se debe usar una llamada al sistema open() o una funcion de librerias para abrir fisicamente una
canal para la tuberia. Con las tuberias semi-duplex, esto es innecesario, ya que la tuberia reside en el kernel y
no en un sistema de archivos fisico. En nuestro ejemplo trataremos la tuberia como un stream, abriendolo con
fopen(), y cerrandose con fclose().
20.3. Ejemplos
Consideremos un proceso servidor simple:
/*******************************
MODULO: fifoserver.c
*******************************/
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
#include <linux/stat.h>
#define FIFO_FILE "MIFIFO"
int main(void)
95
FILE *fp;
char readbuf[80];
/*crea el FIFO si no existe*/
umask(0);
mknod(FIFO_FILE, S_IFIFO|0666, 0);
while(1)
fp = fopen(FIFO_FILE, "r");
fgets(readbuf, 80, fp);
printf("Cadena recibida: %s\n", readbuf);
fclose(fp);
return(0);
Como un FIFO bloquea por defecto, se debe ejecutar el servidor en segundo plano tras compilarlo:
$ fifoserver &
Se discutira la accion de bloqueo de un FIFO mas adelante. Primero consideremos el siguiente proceso cliente:
/*******************************
MODULO: fifoclient.c
*******************************/
#include <stdio.h>
#include <stdlib.h>
#define FIFO_FILE "MIFIFO"
int main(int argc, char *argv[])
FILE *fp;
if(argc != 2)
printf("USO: fifoclient [cadena]\n");
exit(1);
if((fp=fopen(FIFO_FILE,"w")) == NULL)
perror("fopen");
exit(1);
96
fputs(argv[1],fp);
fclose(fp);
return(0);
20.4. Acciones bloqueantes en un FIFO
Normalmente, el bloqueo ocurre en un FIFO. En otras palabras, si se abre el FIFO para lectura, el proceso
estara “bloqueado”hasta que cualquier otro proceso lo abra para escritura. Esta aacion funciona al reves tambien.
si este comportamiento no nos interesa, se puede usar la bandera O_NONBLOCK en la llamada Open() para
desactivar la accion del bloqueo por defecto.
En el caso del servidor simple, se ha puesto en segundo plano, y permitido hacer su bloqueo alli.
97
Capıtulo 21
Colas de mensajes
Una cola de mensaje funciona como un FIFO, pero con algunas diferencias. Generalmente, los mensajes son
sacados de la cola en el orden en que se pusieron. Sin embargo, hay manera de sacar cierto mensaje de la cola
antes de que alcance llegar al inicio de la cola.
Un proceso puede crear una nueva cola de mensajes, o se puede conectar a una ya existente. De esta forma, dos
procesos pueden compartir informacion mendiante la misma cola de mensajes.
Una vez que se crea una cola de mensajes, esta no desaparece hasta que se destruya. Todos los procesos que
alguna vez se usaron pueden finalizar, pero la cola todavıa existira. Una buena costumbre serıa usar el comando
ipcs para verificar que si existe alguna cola de mensajes que ya no este en uso y destruirla con el comando
ipcrm.
Primero que nada queremos conectarnos a una cola, a crearla si no existe.
21.1. Crear y abrir una cola
Para hacer esto se utiliza la llamada al sistema msgget():
Su prototipo es:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key,int msgflg);
Donde:
msgget() Devuelve el ID de la cola de mensajes en caso de exito, o -1 en caso de error.
key Es un identificador unico que describe la cola a la cual uno se quiere conectar
(o crear). Cualquier otro proceso que quiera conectarse a esta cola debera usar
este mismo key.
msgflg Le dice a msgget() que hacer con la cola. Para crear una cola este campo
debe ser igual a IPC_CREAT junto a los permisos para la cola. (Los permisos
de la cola son los mismos que los permisos estandares de archivos- las colas reciben
los user-id y group-id del programa que los creo).
98
Como crear el identificador key?
1. Ya que el tipo key_t es long, se puede usar cualquier numero que se quiera (siempre y cuando otro
programa no utilize el mismo numero), entonces es recomendable la siguiente alternativa.
2. Usando la funcion ftok() la cual genera un identificador a partir de dos argumentos:
key_t ftok(const char *path, int id);
Donde:
path Debe ser un archivo que el proceso pueda leer.
id Es normalmente un char escogido arbitrariamente, como por ejemplo ‘A’.
Ejemplo:
#include <sys/msg.h>
key=ftok("/home/hola",’b’);
msqid=msgget(key,0666 | IPC_CREAT);
Ejemplo:
/* mkq.c - crea una cola de mensajes */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
int qid; /*identificador de la cola */
key_t key; /*clave de la cola */
key=123;
/*crea la cola */
if((qid=msgget(key, IPC_CREAT |0666))<0)
perror("msgget:create");
exit(EXIT_FAILURE);
99
printf("creado ID de cola = %d \n ",qid);
exit(EXIT_SUCCESS);
La salida de este programa deberıa paracecer a la siguiente.
#./mkq
creado ID de la cola = 128
#
21.2. Enviar a una cola
Una vez uno se conecta a la cola de mensajes usando msgget(), se puede mandar y recibir mensajes.
Para mandar mensajes:
Cada mensaje consiste en dos partes, las cuales estan definidas en la estructura struct msgbuf, tal como aparece
en sys/msg.h:
struct msgbuf
long mtype;
char mtext[1];
Donde:
mtype Es usado para recibir mensajes, y puede ser cualquier numero positivo.
mtext Es el dato que se anadira a la cola.
Se puede usar cualquier estructura para mandar mensajes a la cola, siempre y cuando el primer elemento es un
long. Por ejemplo, se podrıa usar la siguiente estructura para lamacenar todo tipo de informacion:
struct pirata_msgbuf
long mtype; /*debe ser positivo */
char nombre[30];
char tipo_de_barco;
int crueldad;
;
Una vez definida la estructura, solo resta enviar la informacion a la cola usando msgsnd(): Su prototipo es:
#include <sys/types.h>
100
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
Donde:
msqid Es el identificador de la cola de mensajes devuelta por msgget()
msgp Es un puntero a los datos que se quiere poner en la cola
msgsz Es el tamano de los datos que se quieren enviar
msgflg Permite poner parametros adicionales, que se pueden ignorar por el momento en 0.
Este es el codigo que muestra como nuestros datos son anadidos a la cola de mensajes;
/* qsnd.c - envia un mensaje a una cola previamente abierta*/
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define BUFSZ 512
/* estructura del mensaje*/
struct msg
long msg_type;
char msg_text[BUFSZ];
pmsg; /*puntero a la estructura de mensaje*/
int main(int argc, char *argv[])
int qid; /*identificador de la cola */
int len; /*el tama~no de los datos enviados*/
/*Espera el ID de cola pasado en la linea de comandos*/
if(argc!=2)
puts("USAGE: qsnd <ID de la cola>");
exit(EXIT_FAILURE);
qid=atoi(argv[1]);
/*Obtiene el mensaje a a~nadir a la cola*/
101
puts("Intoducir el mensajea enviar:");
if((fgets((&pmsg)->msg_text, BUFSZ, stdin)) == NULL)
puts("no hay mensaje para enviar");
exit(EXIT_SUCCESS);
/*asocia el mensaje con este proceso*/
pmsg.msg_type=getpid();
/*a~nade el mensaje a la cola*/
len=strlen(pmsg.msg_text);
if((msgsnd(qid, &pmsg, len, 0))<0)
perror("msgsnd");
exit(EXIT_FAILURE);
puts("mensaje enviado");
exit(EXIT_SUCCESS);
Una ejecucion de ejemplos de este programa tiene como resultado la siguiente salida.
Observese que el programa utiliza el ID de cola devuelto por mkq.
#./qsnd 128
Introduzca el mensaje a enviar
Anadiendo el mensaje al ID de cola 128
Mensaje enviado
#
21.3. Recibir desde la cola
La contraparte de msgsnd() es msgrcv(). Esta serıa la forma de recibir usando la llamada msgrcv().
Su prototipo es:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgrcv(int msqid, void *ptr, size_t nbytes, long type, int flags);
Si msgrcv tien exito elimina el mensaje devuelto de la cola. Los argumentos son los mismo que acepta msgsnd
excepto que rellena la estructura con el tipo de mensaje y hasta nbytes de datos. El argumento adicional, type
determina que mensaje se devuelve, como podemos ver en la siguiente lista.
Muchas veces se requiere simplemente recibir el proximo mensaje de la cola, sin importar type. En ese caso, se
dejarıa el parametro type en 0.
102
type Efecto en msgrcv()
0 Recibe el proximo mensaje de la cola(el de arriba).
> se devuelve el primer mensaje cuyo msg_type sea igual a type.
< se devuelve el primer mensaje cuyo msg_type sea el menor valor o igual que el valor absoluto de type.
El siguiente listado qrd.c lee un mensaje de una cola poblada y creada previamente. La cola de la que leer se
pasa como argumento de la lınea de comandos.
/* qrd.c lee todos los mensajes de una cola de mensajes */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#define BUFSZ 512
/*estructura del mensaje*/
struct msg
long msg_type;
char msg_text[BUFSZ];
pmsg; /* una estructura de mensajes*/
int main(int argc, char *argv[])
int qid; /* el identificador de la cola */
int len; /*el tamano del mensaje*/
/* espera el ID de cola pasado en la linea de comandos */
if(argc!=2)
puts("USAGE: qrd <ID de cola>");
exit(EXIT_FAILURE);
qid=atoi(argv[1]);
/*recupera y muestra un mensaje de la cola */
len=msgrcv(qid, &pmsg, BUFSZ, 0,0);
if(len>0)
(&pmsg)->msg_text[len]= ’\0’;
printf("leyendo el ID de la cola: %05d\n",qid);
printf("tipo de mensaje: %05ld\n",(&pmsg)->msg_type);
printf("tamano del mensaje: %d bytes \n",len);
printf("texto del mensaje: %s \n", (&pmsg)->msg_text);
103
else
perror("msgrcv");
exit(EXIT_FAILURE);
exit(EXIT_SUCCESS);
A continuacion tenemos la salida de una ejecucion de este programa. Como antes, utiliza el ID de la cola creado
por la ejecuacion de mkq anteriormente. Ademas lee el mensaje enviado por el ejemplo anterior qsnd.
#./qrd 128
tipo de mensaje: 06360
tamano del mensaje: 34 bits
texto del mensaje: Anadiendo un mensaje al ID de cola 128.
#
21.4. Como eliminar cola de mensajes
Llega un momento cuando se quiere eliminar una cola de mensajes. Como ya se menciono, las colas de mensaje
quedan a menos que se eliminen explicitamente; es importante hacer esto para no gastar recursos del sistema.
1. Usar el comando de UNIX ipcs para obtener una lista de colas de mensajes definidas, luego usar el
comando ipcrm para eliminar la cola.
2. Escribir el codigo necesario.
Generalmente, es mas recomendada la segunda opcion, ya que se puede desear que el programa limpie la cola
en cierto momento. Para hacer esto se utiliza otra funcion: msgctl().
Su prototipo es:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
Donde:
msqid Es el ID de cola existente.
cmd Puede ser uno de los siguientes:
104
IPC_RMID. Elimina la cola msqid
IPC_STAT. Rellena buf con la estructura msqid_ds de la cola y nos permite ver su contenido sin eliminar
ningun mensaje.
IPC_SET. Nos permite cambiar el UID, GID, modo de acceso y maximo numero bits permitidos de la cola.
El siguiente listado qctl.c utiliza la llamada msgctl para eliminar una cola cuyo ID se pasa en lınea de
comandos.
/* qctl.c - Elimina una cola de mensajes */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[])
int qid;
if(argc!=2)
puts("USAGE: qctl <qid>");
exit(EXIT_FAILURE);
qid=atoi(argv[1]);
if(msgctl(qid, IPC_RMID, NULL)) <0
perror("msgctl")
exit(EXIT_FAILURE);
printf("cola %d eliminada \n", qid);
exit(EXIT_SUCCESS);
#ipcs -q
..............
#./qctl 128
cola 128 eliminada
#ipcs -q
.................
#
105
21.5. Ejemplo
Este programa realiza la suma de dos vectores:
Por ejemplo:
A=[1,2,3,4]
B=[1,1,1,1]
A+B=[2,3,4,5]
/* msg-A+B.c */
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<time.h>
#define tam_buf 10
struct mensaje
long tipo_msj;
int vec[tam_buf];
;
main(int argc,int *argv[])
struct mensaje A,B;
int k,l,i,j;
int id_c,tam,pid,estado,x,arr[10];
srand(time(NULL));
struct mensaje buf_msj;
key_t key;
key=666;
if((id_c=msgget(key,IPC_CREAT|0666))<0)
perror("msgget:create");
exit(-1);
if((id_c==msgget(key,0))<0)
perror("msgget:open");
exit(-1);
106
pid=fork();
switch(pid)
case -1: perror("fork");
exit(1);
case 0: msgrcv(id_c,&B,10*sizeof(struct mensaje),0,0);
msgrcv(id_c,&A,10*sizeof(struct mensaje),0,0);
printf("\t HIJO ");
printf("\nMENSAJE RECIBIDO: ");
printf("suma de A + B \n");
for(i=0;i<10;i++)printf("%d ", A.vec[i]+B.vec[i]);
printf("\n\n");
exit(1);
default: printf("\n EL PADRE\n");
/* vector A */
for(i=0;i<10;i++) A.vec[i]=rand()%20;
printf("A %d \n",A.vec[k]);
msgsnd(id_c,&A,sizeof(struct mensaje),0);
/* vector B */
for(j=0;j<10;j++) A.vec[j]=rand()%50;
printf("B %d \n",A.vec[k]);
msgsnd(id_c,&A,sizeof(struct mensaje),0);
puts("\nMENSAJES ENVIADOS");
wait(&estado);
msgctl(id_c,IPC_RMID,NULL);
exit(1);
/*fin del swicht*/
/*fin del programa*/
107
El resultado se muestra en la siguiente figura:
Figura 21.1: suma de dos vectores
108
Capıtulo 22
Semaforos
Los semaforos se pueden utilizar para controlar el acceso a los archivos, memoria compartida, y en general
cualquier otro recurso compartido.
Figura 22.1: Los semaforos
109
22.1. Caracterısticas de los semaforos
1. Desbloqueando procesos
El semaforo informa a los procesos que se encuentran bloqueados
Todos los procesos se desbloquean, y pasan a estado listo, es el administrador de procesos quien elije quien
pasa a ejecucion. (Dependiendo de la implementacion: FIFO, LIFO, etc).
2. Atomicidad
Se garantiza que al iniciar una operacion con semaforo, ningun otro proceso puede tener acceso al semaforo
hasta que la operacion termine o se bloquee. En este tiempo ningun otro proceso puede simultaneamente
modificar el mismo valor de semaforo.
Figura 22.2: Tipos de semaforos
22.2. Semaforos generales o contadores
Utiles cuando un recurso sera asignado, tomandolo de un conjunto de recurso identicos.
Semaforo es inicializado con el numero de recursos existentes: P(S) decrementa a S en 1; indicando que
un recursos ha sido suprimido del conjunto.
110
Si S=0 entonces no hay mas recursos y el proceso se bloquea
V(S) incrementa a S en 1; que un recurso ha sido regresado al conjunto.
Si un proceso esperaba por un recurso, este despierta.
22.3. Semaforos Binarios I
Solo puede tomar dos valores: 0 o 1
Generalmente se inicializan con un valor de 1.
Son usados por dos o mas procesos para garantizar que solo uno puede entrar en seccion critica.
Antes de entrar en seccion crıtica un proceso ejecuta un P(S) y un V(S) antes de salir de ella.
La variable de tipo semaforo se llama entrar y es inicializada en 1. Cada proceso tiene la estructura
siguiente:
while(1)
P(entrar)
<seccion critica>
V(entrar)
do
se detalla ampliamente en la seccion 22.5.
22.4. Semaforos sincronizadores
Solucion de varios problemas de sincronizacion.
Sean dos procesos concurrente P1 y P2 que se encuentran corriendo:
P1 con enunciado S1
P2 con enunciado S2
Se desea que S2 sea ejecutado despues de que S1 haya terminado
Solucion:
Semaforo sincrono (inicializado en 0)
P1: ———- P2:———-
S1 P(sincro)
V(sincro) S2
—————- ——————
111
22.5. Semaforos Binarios II
Obtener semaforos con IPC de System V, se obtienen conjuntos de semaforos. Obviamente, se puede obtener
un conjunto de semaforos que solo contiene un semaforo, pero la idea es que se puede tener una gran cantidad
de semaforos al crear un solo conjunto de semaforos.
Figura 22.3: Estructura del semaforo
112
Figura 22.4: Estructura del semaforo
113
Figura 22.5: Relacion estructuras
22.5.1. Peticion de semaforo (semget())
Como se hace para crear un conjunto de semaforos? Se hace con la llamada semget(), que devuelve el id
(identificador) del semaforo (de aquı en adelante sera referido como semid):114
#include <sys/sem.h>
int semget(key_t key, int nsems, int semflg);
Donde:
key Es un identificador unico que es usado por procesos diferentes para identificar este conjunto de
semaforos (El key sera generado usando ftok(), descrito en los apuntes sobre las colas
de mensajes).
nsems Es la cantidad de semaforos en este conjunto de semaforos.
semflg Le indica a semget() entre otras cosas; que permisos tendra el nuevo conjunto de semaforos,
si se esta creando un nuevo conjunto o si solo se quiere conectar a un conjunto ya existente. Para
crear un nuevo conjunto, se puede combinar los permisos con el argumento IPC_CREAT.
Aquı hay un ejemplo donde se crea el key con ftok() y luego se crea un conjunto de 10 semaforos, con los
permisos 666 (rw-rw-rw-):
#include <sys/ipc.h>
#include <sys/sem.h>
key_t key;
int semid; key = ftok(".", ’E’);
semid = semget(key, 10, 0666 | IPC_CREAT);
Una vez ejecutado este ejemplo, se puede verificar la existencia del conjunto de semaforos con el comando ipcs.
(No se olviden eliminar el semaforo con el comando ipcrm!)
115
22.5.2. Operaciones P y V (semop())
Figura 22.6: Definicion de semop()
Todas las operaciones sobre semaforos utilizan la llamada al sistema semop(). Esta llamada al sistema es de
proposito general, y su funcionalidad es dictada por una estructura que se le pasa, struct sembuf:
116
struct sembuf
ushort sem_num;
short sem_op;
short sem_flg;
;
Donde:
sem_num Es el numero del semaforo en el conjunto que se quiere manipular.
sem_op Es lo que se quiere hacer con ese semaforo. Esto tiene varios significados,
dependiendo si sem_op es positivo, negativo, o cero, tal como se muestra en la tabla 22.5.2:
sem_flg Permite al programa especificar modificadores que modifican mas aun los efectos de una llamada
a semop().
y puede ser uno de los siguiente modificadores:
IPC_NOWAIT, la llamada a sem_op devuelve el control en caso de que no se pueda
satisfacer la operacion especificada en sem_op. La forma de trabajar por defecto es IPC_WAIT
SEM_UNDO este causa que semop() grabe, de cierta forma, el cambio realizado en el semaforo.
Cuando el programa termina su ejecucion, el kernel automaticamente deshace todos los cambios
que fueron marcados con el modificador SEM_UNDO.
sem op accion
> (V) El valor de sem_op es sumado al valor del semaforo y el recurso controlado
por el semaforo es liberado
< (P) El proceso que llama esta indicando que quiere esperar hasta que el recurso controlado
este disponible, en cuyo momento el valor del semaforo disminuira y el recurso sera bloqueado
por el proceso.
0 El proceso que llama se bloqueara hasta que el semaforo sea 0; si ya es 0, la llamada
vuelve inmediatamente
Tabla 22.1: operaciones de sem op
Ası que, basicamente, lo que se hace es cargar un struct sembuf con los valores que se quieran, y luego llamar
a semop(), de esta manera:
int semop(int semid ,struct sembuf *sops, unsigned int nsops);
Donde:
semid Es el numero obtenido de la llamada a semget().
sops Es un puntero a una estructura struct sembuf que ya se haya rellenado
con los comandos de los semaforos. Incluso se puede crear un arreglo de struct sembufs,
para realizar muchas operaciones al mismo tiempo.
nsops Es el total de elementos que tiene el array de operaciones
117
Figura 22.7: operaciones semop().
118
22.5.3. lock de un semaforo P(S)
/*poniendo un candado al semaforo */
void P(sem,n)
int sem,n;
struct sembuf sop;
/* construccion arreglo operaciones de un elemento */
sop.sem_num=n;
sop.sem_op=-1; /*bloquea solo una unidad */
sop.sem_flg=0;
/*bloquea hasta que el recurso es liberado */
semop(sem,&sop,1);
22.5.4. Unlock de un semaforo V(S)
/*Quitando un candado al semaforo */
void V(sem,n)
int sem,n;
struct sembuf sop;
/* construccion arreglo operaciones de un elemento */
sop.sem_num=n;
sop.sem_op=-1; /* +1 -> desbloquea*/
sop.sem_flg=0;
/*bloquea hasta que el recurso es liberado */
semop(sem,&sop,1);
22.5.5. Control de las estructuras de semaforo (semctl())
Hay dos maneras de deshacerse de un conjunto semaforos: una es usando el comando ipcrm de UNIX. La otra
manera es a traves de la llamada semctl() con los argumentos adecuados.
119
Esta es la union union semun, junto con la llamada a semctl() para destruir el semaforo:
union semun
int val; /* usado solo para SETVAL */
struct semid_ds *buf; /* para IPC_STAT y IPC_SET */
ushort *array; /* usado para GETALL y SETALL */
;
Sintaxis:
int semctl(int semid, int semnum, int cmd, union semun arg);
Donde:
semid El ID del semaforo que se quiere destruir.
semnum Indica cual es el semaforo, de los que hay bajo semid, al que
queremos acceder.
cmd Debe estar en IPC_RMID, lo que le indica a semctl()
que debe sacar este conjunto de semaforos.
semnum arg No tienen ningun significado en el contexto de
IPC_RMID y se puede dejar en cualquier cosa.
Los siguientes valores son validos para cmd:
VALOR DESCRIPCION
GETVAL Se utiliza para leer el valor de un semaforo.
el valor se devuelve a traves del nombre de la funcion
SETVAL Permite inicializar un semaforo a un valor determinado que se
especifica en arg.
GETPID Se usa para leer el PID del ultimo proceso que actuo sobre el
el semaforo. Este valor se devuelve a traves del nombre de la funcion.
GETNCTN Permite leer el numero de procesos que hay esperando a que se incremente el valor
del semaforo. Este numero se devuelve a traves del nombre de la funcion.
GETSCNT Permite leer el numero de procesos que hay esperando a que el semaforo tome el
el valor 0. Este numero se devuelve a traves del nombre de la funcion
GETALL Permite leer el valor de todos los semaforos asociados a identificados
semid. Estos valores se almancenan en arg.
SETALL sirve para inicializar el valor de todos los smaforos asociados al identificados
semid. Los valores de inicializacion debe estar en arg.
IPC_STAT e IPC_SET Permiten leer y modificar la informacion administrada asociada
al identificador semid. Para obtener mas informacion puede consultar el manual
de LINUX.
IPC_RMID Le indica al nucleo que debe borrar el conjunto de semaforos agrupados bajo el
identificador semid. La operacion de borrado no tendra efecto mientras haya
algun proceso que este usando los semaforos.
120
Esto es un ejemplo de como eliminar un semaforo:
union semun dummy;
int semid;
.
.
semid = semget(...);
.
.
semctl(semid, 0, IPC_RMID, dummy);
Cuando recien se crearon los semaforos, estos quedan inicializados en cero. Esto es grave, ya que significa que
todos estan marcados como ocupados; y se necesita otra llamada (ya sea a semop() o a semctl() para marcarlos
como libres).
Que significa todo esto? Esto significa que la creacion de semaforos no es atomica. Si dos procesos estan
tratando de crear, inicializar y usan un semaforo al mismo tiempo, se puede producir una condicion de carrera.
Este problema se puede resolver teniendo un solo proceso que crea e inicializa el semaforo. El proceso principal
solo lo accesa, pero nunca lo crea o lo destruye.
22.6. Ejemplos
Ejemplo 1:
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define SEM_HIJO 0
#define SEM_PADRE 1
main(int argc, char *argv[])
int i=10, semid, pid;
struct sembuf operacion;
key_t llave;
/*Peticion de un identificador con dos semaforos */
llave=ftok(argv[0],’K’);
if ((semid=semget(llave,2,IPC_CREAT|0600))==-1)
perror("semget");
exit(-1);
121
/*Inicializacion de los semaforos*/
/*Cerramos el semaforo del proceso hijo*/
semctl(semid,SEM_HIJO,SETVAL,0);
/*Abrimos el semaforo del proceso hijo*/
semctl(semid,SEM_PADRE,SETVAL,1);
/*Creacion del proceso hijo*/
if((pid=fork())==-1)
perror("fork");
exit(-1);
else if (pid==0) /*codigo del proceso hijo*/
while(i)
/*cerrramos el semaforo del proceso hijo*/
operacion.sem_num=SEM_HIJO;
operacion.sem_op=-1;
operacion.sem_flg=0;
semop(semid,&operacion,1);
/*abrimos el semaforo del proceso padre*/
printf("PROCESO HIJO: %d \n",i--);
operacion.sem_num=SEM_PADRE;
operacion.sem_op=1;
semop(semid,&operacion,1);
/*borrado del semaforo*/
semctl(semid,0,IPC_RMID,0);
else /*codigo del proceso padre*/
operacion.sem_flg=0;
while(i)
/*cerramos el semaforo del proceso padre*/
operacion.sem_num=SEM_PADRE;
operacion.sem_op=-1;
semop(semid,&operacion,1);
printf("PROCESO PADRE: %d \n", i--);
/*abrimos el semaforo del proceso hijo*/
operacion.sem_num=SEM_HIJO;
operacion.sem_op=1;
semop(semid,&operacion,1);
/*borrado del semaforo*/
122
semctl(semid,0,IPC_RMID,0);
el resultado es el siguiente:
Figura 22.8: Resultado del ejemplo 1 (semaforos)
Ejemplo 2:
Esta compuesto por tres programas:
1. seminit.c crea e inicializa el semaforo.
2. semdemo.c controla el acceso a un supuesto archivo.
3. semrm.c es usado para destruir el semaforo (esto podrıa realizarse con el comando ipcrm).
La idea es ejecutar seminit.c para crear el semaforo. Se puede usar el comando ipcs desde la lınea de
comandos para verificar que el semaforo existe. Luego ejecutar semdemo.c en un par de ventanas y ver como
ellos interactuan. Finalmente, se debe usar semrm.c para eliminar el semaforo.
Este es el codigo de seminit.c (debe ejecutarse de los primeros!):
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int main(void)
key_t key;
int semid;
123
union semun arg;
if ((key = ftok(".", ’J’)) == -1)
perror("ftok");
exit(1);
/* crear un conjunto de semaforos con 1 semaforo: */
if ((semid = semget(key, 1, 0666 | IPC_CREAT)) == -1)
perror("semget"); exit(1);
/* inicializar el semaforo #0 en 1: */
arg.val = 1;
if(semctl(semid, 0, SETVAL, arg) == -1)
perror("semctl");
exit(1);
return 0;
Este es el codigo de semdemo.c:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
int main(void)
key_t key;
int semid;
struct sembufsb = 0, -1, 0; /* para reservar recursos */
if ((key = ftok(".", ’J’)) == -1)
perror("ftok");
exit(1);
/* conectarse al conjunto de semaforos creado por seminit.c: */
if ((semid = semget(key, 1, 0)) == -1)
perror("semget");
124
exit(1);
printf("Presione ENTER para bloquear: ");
getchar();
printf("Tratando de bloquear...\n");
if (semop(semid, &sb, 1) == -1)
perror("semop");
exit(1);
printf("Bloqueado.\n");
printf("Presione ENTER para desbloquear: ");
getchar();
sb.sem_op = 1;
/* liberar recurso */
if (semop(semid, &sb, 1) == -1)
perror("semop");
exit(1);
printf("Desbloqueado\n"); return 0;
Este es el codigo de semrm.c:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include<sys/sem.h>
int main(void)
key_t key;
int semid;
union semun arg;
if ((key = ftok(".", ’J’)) == -1)
perror("ftok");
exit(1);
/* conectarse al semaforo creado por seminit.c: */
if ((semid = semget(key, 1, 0)) == -1)
125
perror("semget");
exit(1);
/* eliminarlo */
if (semctl(semid, 0, IPC_RMID, arg) == -1)
perror("semctl");
exit(1);
return 0;
Los semaforos son muy utiles en una situacion de concurrencia. Sirven para controlar el acceso a un archivo,
pero tambien a otro recurso como por ejemplo la memoria compartida.
Siempre cuando se tienen multiples procesos ejecutandose dentro de una seccion crıtica de codigo, se necesitan
los semaforos.
126
Capıtulo 23
Memoria compartida
La forma mas rapida de comunicar dos procesos es hacer que compartan una zona de memoria. Para enviar
datos de un proceso a otro, solo hay que escribir en memoria y automaticamente estos datos estaran disponibles
para que los lea otro proceso.
Figura 23.1: memoria compartida
La memoria convencional que puede direccionar un proceso a traves de su espacio de direcciones virtuales es
local a ese proceso y cualquier intento de direccionar esa memoria desde otro proceso provocara una violacion
de segmento.
127
Pasos para accesar una memoria compartica
Figura 23.2: pasos para accesar a una memoria compartida
128
23.1. llamada shmget()
Con shmget obtenemos un identificador con el que podemos realizar futuras llamadas al sistema para controlar
una zona de memoria compartida. su declaracion es:
id=shmget(key,size,flag)
Donde:
int key
llave numerica de identificacion del segmento
int size
tamano del segmento en bytes
int flag
bandera para los derechos del segmento. 0 si el segmento ya esta creado
Numerico Simbolico Descripcion
0400 SHM_R Read by owner
0200 SHM_W Write by owner
0040 SHM_RÀ3 Read by group
0020 SHM_WÀ3 Write by group
0004 SHM_WÀ6 Read by world
0002 SHM_WÀ6 Write by world
IPC_CREAT Special IPC flag
IPC_EXCL Special IPC flag
int id
manejador del segmento
23.2. llamada shmat()
Antes de usar una zona de memoria compartida tenemos que asignarle un espacio de direcciones virtuales de
nuestro proceso. Esta accion se conoce como unirse o atarse al segmento de memoria compartida.
ptr=shmat(id,addr,flag)
Donde:
int id
manejador del proceso, (obtenido a partir de shmget()).
int addr
especificacion de una direccion de mapeo, generalmente cero, (el sistema se ocupa)
int flag
bandera
129
ptr type *ptr apuntador del tipo de informacion almacenada en el segmento de memoria
Ejemplo
struct info
char *nombre;
int edad;
;
struct info *ptr;
ptr=(struct info *)shmat(id,0,0)
23.3. llamada shmdt()
shmdt(addr)
Desata un segmento cuando el proceso termino de utilizarlo
No borra el segmento de memoria
El segmento deja de estar accesible para el proceso.
Donde:
int *addr
#include algo.h
int id;
struct info *ctrl;
int main()
.
.
.
id=shmget(KEY,SEGSIZE,IPC_CREAT|0666);
ctrl=(struct info *) shmat(id,0,0);
<codigo uso memoria compartida>
shmdt(ctrl);
return 0;
23.4. llamada shmctl()
Con shmctl podremos realizar operaciones de control sobre una zona de memoria previamente creada por un a
llamada a shmget:
130
Sintaxis
int shmctl(shmid,cmd,buf)
Donde:
int shmid
identificador, (manejador), del segmento de memoria compartida.
int cmd
que puede ser alguno de los siguiente:
IPC_STAT
asigna cada uno de los valores de los campos de la estructura de datos asociada con shmid, en la
estructura apuntada por buf
IPC_SET
asigna el valor de los campos:
shm_perm.uid shm_perm.gid shm_perm.mode
en la estructura apuntada por buf
IPC_RMID
borra el identificador del segmento de memoria del sistema especificado por shmid, y destruye el
segmento de memoria y la estructura de4 datos asociados a el.
SHM_LOCK
bloquea el segmento de memoria especificado por el identificador shmid.
SHM_UNLOCK
desbloquea el segmento de memoria especificado por el identificador shmid.
struct shmid ds *buf
estructura en la que se almacena informacion del estatus del segmento de memoria, (no es usada por todas
las opciones).
131
Acceso a memoria compartida
Figura 23.3: parametros de entrada
132
23.5. Ejemplo
#include <stdlib.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <stdio.h>
#include <errno.h>
#define array_size 1000
extern char *shmat();
void spin_lock_init();
void spin_lock();
void spin_unlock();
int main()
key_t shm_key;
int shmid, semid, pid;
char *shm;
int *A, *addr, *sum;
int partial_sum;
int i;
printf("Before spin lock init \n");
spin_lock_init(&semid);
shm_key=0x567;
shmid=shmget(shm_key,(array_size*sizeof(int)+1),(IPC_CREAT|0600));
if(shmid==-1)
perror("shmget");
exit(1);
shm=shmat(shmid,NULL,0);
if(shm==(char*)-1)
perror("shmat");
exit(1);
133
addr=(int*)shm;
sum=addr;
addr++;
A=addr;
*sum=0;
for(i=0;i<array_size; i++) *(A+i)=i+1;
printf("Before fork \n");
pid=fork();
if(pid==0)
partial_sum=0;
for(i=0;i<array_size; i=i+2)
partial_sum+=*(A+i);
printf("%d ",i);
else
addr++;
A=addr;
*sum=0;
for(i=0;i<array_size; i++) *(A+i)=i+1;
printf("Before fork \n");
pid=fork();
if(pid==0)
partial_sum=0;
for(i=0;i<array_size; i=i+2) partial_sum+=*(A+i);
else
partial_sum=0;
for(i=1;i<array_size; i=i+2)
partial_sum+=*(A+i);
partial_sum=0;
for(i=1;i<array_size; i=i+2)
partial_sum+=*(A+i);
printf("after fork \n");
134
spin_lock(&semid);
*sum+=partial_sum;
spin_unlock(&semid);
printf("\n process pid=%d, partial sum=%d \n",pid,partial_sum);
if(pid==0) exit(0);
else wait(0);
printf("\n sum of 1 to %i is %d\n",array_size, *sum);
if(semctl(semid,0,IPC_RMID,1)==-1)
perror("semctl");
exit(1);
if(shmctl(shmid,IPC_RMID,0)==-1)
perror("shmctl");
exit(1);
exit(0);
void spin_lock(int *lok)
struct sembuf sembuffer, *sops;
sops=&sembuffer;
sops->sem_num=0;
sops->sem_op=-1;
sops->sem_flg=0;
if(semop(*lok,sops,1)<0)
perror("semop");
exit(1);
135
/*end of spin_lock */
void spin_lock_init(int *lok)
int init_sem_value=1;
*lok=semget(IPC_PRIVATE,1,(0600|IPC_CREAT));
if(*lok==-1)
perror("semget");
exit(1);
if(semctl(*lok,0,SETVAL,init_sem_value)<0)
perror("semctl");
exit(1);
/*end of spin_lock_init */
void spin_unlock(int *lok)
struct sembuf sembuffer, *sops;
sops=&sembuffer;
sops->sem_num=0;
sops->sem_op=1;
sops->sem_flg=0;
if(semop(*lok,sops,1)<0)
perror("semop");
exit(1);
/*end of spin_unlock */
136
Figura 23.4: resultado ejemplo memoria compartida
137
Capıtulo 24
Sockets
24.1. Obteniendo informacion: los resolvers y otras llamadas
En la seccion anterior se establecio que para poder utilizar sockets es necesario conocer el
numero IP de una maquina. En varias aplicaciones este tipo de informacion varıa de acuerdo a
la red o a la maquina sobre la cual se este ejecutando la aplicacıon.
Para poder obtener informacion acerca de las diferentes maquinas que componen una red de
computadoras, se cuenta con funciones conocidas con el nombre de resolvers. Los resolvers
tienen como caracterıstica principal el hecho de que no regresan informacion puntual, sino un
apuntador a una estructura que contiene toda la informacion asociada con la entidad relacionada
con el resolver. En la figura 24.1 se muestra el uso de los resolvers asociados para la obtencion
de informacion asociada a un host.
Los resolvers forman parte de NIS (Network Information Service) que se encarga de distribuir
a todas las computadoras de una red local, la informacion contenida en los archivos de con
guracion. La funcion principal de los resolvers es proporcionar, a nivel programacion, un medio
para obtener esta informacion. En lo que concierne a este capıtulo se hara referencia a dos
archivos /etc/hosts y /etc/services. El primero almacena las direcciones, nombres y alias de las
computadoras que forman la red local. El segundo archivo almacena los nombres de los servicios,
sus protocolos, y el numero de puerto a traves del cual se va a proporcionar el servicio.
24.1.1. Obteniendo datos del host
Los nombres de las computadoras que forman una red local estan reunidos en un archivo
llamado /etc/hosts. En ese archivo se encuentran las direcciones de las maquinas, sus nombres
y sus alias. Un ejemplo del contenido de dicho archivo se presenta abajo.
#
#Internet host table
#
127.0.0.1 localhost
148.241.61.15 brasil loghost
138
Figura 24.1: Ejemplo de un resolver
Para poder obtener informacion acerca de los hosts de una red se cuenta con diferentes funciones.
Esta funciones difieren entre ellas por el parametro de entrada a partir del cual se va a buscar
la informacion. La sintaxis de las llamadas es la siguiente:
1. struct hostent *gethostbyaddr(const void *addr, size_t len,
int type);
2. struct hostent *gethostbyname(const char *name);
3. struct hostent *gethostent(void);
La llamada gethostbyaddr() obtiene informacion de un host a partir de una direccion, del tamano
de dicha direccion y del tipo de la direccion) Tambien es posible conseguir informacion a partir
139
del nombre de la maquina con la llamada gethostbyname(). Si se quiere recorrer toda la base
de datos, registro por registro, se debe de utilizar gethostent() Dicha llamada lee el siguiente
registro en la base de datos.
Como puede constarse todas las llamadas regresan un apuntador a un estructura. Dicha
estructura contiene la informacion acerca de las computadoras de la red local y se ecuentra
organizada de la siguiente forma:
struct hostent
char *h_name; /*nombre oficial de un host */
char **h_aliases; /*lista de alias */
int h_addrtype; /*tipo de direccion del host */
int h_length; /*longitud de la direccion */
char **h_addr_list; /*lista de direcciones del servidor de nombres */
#define h_addr h_addr_list[0] /*direccion para compatibilidad */
;
Es importante no olvidar que los resolvers regresan un apuntador a esta estructura, y que si
se quiere un campo determinado es necesario accederlo directamente. Tambien vale la pena
aclarar que para agregar nuevos campos al archivo /etc/hosts es necesario contar con ciertos
privilegios.
24.1.2. Obteniendo datos de los servicios
Un servicio esta definido por un nombre, un numero de puerto y un protocolo. Existen servicios
(ftp, telnet, portmapper) usados frecuentemente que tienen establecidos por default un numero
de puerto (0-1024) y un protocolo. Dichos servicios se encuentran de nidos en el archivo
/etc/services. En la parte de abajo se muestra un ejemplo del contenido de dicho archivo.
#ident "@(#)services 1.6
#
#Network services, Internet style
tcpmux 1/tcp
echo 7/tcp
discard 9/tcp sink null
discard 9/udp sink null
systat 11/tcp users
daytime 13/tcp
daytime 13/udp
140
netstat 15/tcp
chargen 19/tcp ttytst source
chargen 19/udp ttytst source
ftp-data 20/tcp
ftp 21/tcp
telnet 23/tcp
smtp 25/tcp mail
time 37/tcp timserver
time 37/udp timserver
domain 53/udp
domain 53/tcp
bootps 67/udp #BOOTP/DHCP server
bootpc 68/udp #BOOTP/DHCP client
hostnames 101/tcp hostname # usually to sri-nic
sunrpc 111/udp rpcbind
sunrpc 111/tcp rpcbind
Las siguientes funciones permiten conocer informacion acerca de un servicio (o de todos si es
necesario) que proporciona una red. La sintaxis de cada una de las funciones es la siguiente:
1. struct servent *getservbyname(const char *name, const char *proto);
2. struct servent *getservbyport(int port, const char *proto);
3. struct servent *getservent(void);
La primera de ellas nos permite conseguir informacion a partir del nombre del nombre del
servicio y del protocolo. La funcion gerservbyport() recibe como parametros un numero de
puerto y un protocolo. Por ultimo la funcion getservent() lee la siguiente entrada de la base de
datos, lo cual nos puede servir para obtener informacion acerca de todos los servicios existentes.
Todas las llamadas regresan un apuntador a la estructura struct servent, la cual cuenta con los
siguientes campos:
struct servent
char *s_name; /* nombre oficial del servicio */
char **s aliases; /* lista de alias */
int s_port; /* numero de puerto */
char *s_proto; /* protocolo a usar */
141
Al igual que el archivo /etc/hosts se requiere de permisos especiales para poder dar de alta un
servicio en el archivo de configuracion /etc/services.
24.1.3. Obteniendo el nombre del host local
Una de las caracterısticas principales de un sistema cliente-servidor es la de portabilidad. Para
lograr esto es necesario que los programas conozcan el nombre de la maquina sobre la cual se
estan ejecutando. Existen cuatro formas de obtener el nombre de la maquina sobre la cual se
esta ejecutando un proceso:
El comando hostname El comando regresa el nombre de la maquina sobre la que se esta
ejecutando;el problema es que es a nivel comando y no de llamada de sistema. Una posible
solucion es utilizar la llamada system() para redireccionar la salida del comando a un
archivo y despues leer el contenido de dicho archivo. El codigo siguiente es un ejemplo de
obtencion de nombre a partir del comando hostname:
#include <stdio.h>
main()
FILE *fd;
char nombre[50];
system("hostname > /tmp/name");
fd=fopen("/tmp/name","r");
fscanf(fd,"%s",nombre);
unlink("/tmp/name");
El archivo /etc/hostname.e01 Este archivo contiene el nombre de la maquina, en realidad
lo que hace el comando anterior es leer el contenido de este archivo. A nivel programa es
posible abrir el archivo y leer su contenido para conocer el nombre de la maquina.
El comando uname Con la opcion -n del comando uname es posible obtener el nombre
de la maquina. El problema es el mismo que en el primer de los casos y la solucion es la
misma redireccionar la salida a un archivo y despues leer su contenido.
La llamada de sistema gethostname(char *name, int namelen) Es la forma mas simple de
obtener el nombre. La llamada regresa el nombre estandar de la maquina sobre la cual
142
se esta ejecutando. Recibe como parametros el tamano de la variable donde va a regresar
el nombre namelen y la variable donde se va a almacenar el nombre de la computadora,
(name). La llamada regresa 0 si todo salio bien y -1 si hubo algun error. Un ejemplo de
uso se presenta a continuacion:
#include <errno.h>
main()
char nombre[256];
int n=256;
if (gethostname(nombre, n))
printf("Error no. %d \n", errno);
else
printf("El nombre es: %s \n", nombre);
La eleccion entre una y otra dependera de la aplicacion y del sistema en que se este trabajando.
24.1.4. Conviertiendo diferentes formatos de direccion
En una comunicacion es importante que los datos que viajan a traves de la red sean
independientes de la arquitectura sobre la cual se esta ejecutando. Un mensaje esta formado por
una secuencia de bytes la cual representa cierto tipo de informacion, y la forma de interpretar
esos bytes puede diferir entre dos arquitecturas. Para no tener que realizar traducciones entre
todas las arquitecturas existentes, se eligio realizar tan solo dos traducciones del host a red y
de red a host.
Los nombres de estas funciones tiene forma XtoYT(), donde X puede ser ’h’(host, computador)
o ’n’(network, red), indicando el orden original; Y puede ser tambien ’h’o ’n’, indicando el
orden resultado de la traduccion, y T puede ser ’s ’(short, entero corto) o ’l ’(long, entero largo),
indicando el tamano del dato sobre el que se trabaja.
Para llevar a cabo lo anterior se cuenta con las siguientes llamadas:
1. in_addr_t htonl(in_addr_t hostlong); /*host to net - long */
2. in_port_t htons(in_port_t hostshort); /*host to net - short */
3. in_addr_t ntohl(in_addr_t netlong); /*net to host - long */
143
4. in_port_t ntohs(in_port_t netshort); /*net to host - short*/
Estas funciones convierten cantidades de 16 y 32 bits entre formatos red y host. Las dos primeras
transforman un formato host a un formato red htonl() para enteros largos y htons() para enteros
cortos. Las dos Ultimas convierten de red a host ntohl() se utiliza en el caso de enteros largos
y ntohs() para enteros cortos. Todas reciben como parametro el valor a convertir y regresan
el valor en el formato solicitado. Para poder utilizar estas llamadas se requiere del archivo de
biblioteca #include <arpa/inet.h>
24.2. Comunicacion con sockets
Como se pueden comunicar dos procesos que no tienen parientes comunes?
Para conseguir esto aparece el concepto de conector o socket, dos procesos distintos crean cada
uno su conector.
1. Cada conector esta ligado a una direccion.
2. Un proceso puede enviar informacion, a traves de un socket propio, al socket de
otro proceso, siempre y cuando conozca la direccion asociada (la del otro socket). La
comunicacion se realiza entre una pareja de sockets.
Figura 24.2: conexion con sockets
24.3. Dominios y direcciones
Definimos un dominio de comunicacion como una familia de protocolos que se pueden emplear
para conseguir el intercambio de datos entre sockets.
144
La estructura general de una familia de direcciones es:
struct sockaddr
u_short sa_family; /*familia direccion*/
char sa_data[4]
;
Un sistema UNIX particular puede ofertar varios dominios, aunque los mas habituales son estos
dos:
1. Dominio UNIX (PF UNIX). Para comunicarser entre procesos dentro de la misma
maquina1. La estructura donde almacena toda la informacion necesaria para el manejo
de este es:
struct sockaddr_un
short sun_family; /* AF_UNIX */
char sun_path[80]; /* Nombre del path */
;
Donde:
sun_family se le debe asignar el campo el valor AF_UNIX
path recibe el path y el nombre del archivo que va a servir de punte de comunicacion
entre el cliente y el servidor.
2. Dominio Internet(PF INET). Para comunicarse entre procesos en dos computadoras
conectadas mediante los protocolos de Internet (TCP-UDP/IP). La estructura que
almacena los datos necesarios para lograr lo anterior es la siguiente:
struct sockaddr_in
short sin_family; /* familia direccion*/
u_short sin_port; /* numero de puerto */
struct in_addr sin_addr; /* direccion */
char sin_zero[8];
;
1PF significa Protocol Family
145
Donde:
sin_family se usa para almacenar el tipo de socket y debe asignarle el valor AF_INET
sin_port almacena el puerto a traves del cual va establecer comunicacion
sin_addr contiene la direccion de la maquina
sin_zero es un campo de relleno con el fin de que las diferentes familias tenga
el mismo tamano
la estructura struct in_addr es de la forma:
struct in_addr
u_long s_addr; /* familia direccion*/
;
Un sockets del dominio PF UNIX no puede dialogar con sockets del dominio PF INET.
Cada familia de protocolos define una serie de convenciones a la hora de especificar los formatos
de las direcciones. Tenemos, por lo tanto, estas dos familias de direcciones:
1. Formato UNIX(AF UNIX). Una direccion de socket es como nombre de fichero(pathname)2.
2. Formato Internet (AF INET). Una direccion de sockets precisa de estos tres campos:
Una direccion de red de la maquina (Direccion IP).
Un protocolo (TCP o UDP) y
Un puerto correspondiente a ese protocolo.
Es importante resaltar que un socket puede ser referenciado desde el exterior solo si su
direccion es conocida. Sin embargo, se puede utilizar un socket local aunque no se conozca
la direccion que tiene.
Senalaremos que las constantes PF INET y AF INET tienen el mismo valor, y lo mismo
ocurre con PF UNIX y AF UNIX.
24.4. Estilos de comunicacion
Orientados a conexion:- Mecanismo que sirven para conseguir un canal para el intercambio
de octenos. Un proceso pone, a su ritmo, bytes en el canal, que recibe de forma fiable y ordenada
2AF significa una Address Family
146
en el otro extremo, donde hay un proceso que los recoge a su conveniencia.
Ejemplo: pipes y los socketpairs
Comunicacion sin conexion:- tambien denominada comunicacion con datagramas. El
emisor envia mensajes envia un mensaje autonomo que el receptor debe recibir enteros. Por el
camino algun mensajes pueden perderse, retrasarse o llegar desordenados.
La comunicacion entre dos sockets puede realizarse con cualquiera de estas dos formas (aunque
los dos sockets comunicantes deben de hacerse creados con el mismo estilo, ademas de el mismo
dominio). Para ello, al crear un socket se indica el estilo de comunicacion correspondiente:
SOCK STREAM la comunicacion pasa por tres fases:
1. Apertura de conexion
2. Intercambio de datos
3. Cierre de conexion
El intercambio de datos es fiable y orientado a octenos (como los pipes): no hay frontera
de mensajes.
SOCK DGRAM. No hay conexiones. Cada mensaje (o datagrama) es autocontenido. Los
datagramas no se mezclan unos con otros. No hay garantia de entrega: se pueden perder,
estropear o desordenar.
24.5. Protocolos
Un protocolo es un conjunto de reglas, formato de datos y convenciones que es necesario
respetar para conseguir la comunicacion entre dos entidades, ejemplos: IP, TCP, UDP, FTP,
ETC.,
Cuando se crea un socket es necesario indicar cual es el protocolo de trasporte a emplear para
el intercambio de datos que se realizara a traves de el.
24.6. Las llamadas de sistema para la creacion y uso de sockets
Un proceso puede crear un socket utilizando la funcion socket().
24.6.1. Creacion del socket: socket( )
Sintaxis:
147
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
Donde:
domain
∗ PF UNIX
∗ PF INET
type (socket)
∗ SOCK STREAM
∗ SOCK DGRAM
∗ SOCK RAW (root)
protocol sirve para especificar el protocolo de comunicacion a emplear, en general este
argumento es 0, indicando que se usa el protocolo por omision.
24.6.2. Escuchando en el puerto listen()
Utilizado por el servidor en protocolos orientados conexion. Una vez que el socket fue asociado
a una direccion es posible ponerse escuchar a traves de el. Ya que el socket esta asociado a una
direccion basta con accedar al socket para atomaticamente acceder a la direccion. La llamada
de sistema para escuchar tiene la sintaxis siguiente:
int listen(int sockfd, int backbg);
Donde:
sockfd Es el socket a traves del cual se va a escuchar
backbg Es el numero maximo de procesos en espera, es decir la longitud de la cola de espera. Generalmente este
parametro se deja con un valor de cinco.
La llamada regresa un 0 si todo sale bien y -1 si hubo algun error.
24.6.3. Aceptando una llamada accept()
En protocolos orientados conexion el servidor debe esperar una peticion de conexion para
despues crear un canal de comunicacion a traves del cual se emiten y reciben mensajes. La
llamada de sistema que crea este canal de comunicacion es accept(). La sintaxis de la llamada
es:
148
int accept(int sockfd, struct sockaddr *cltaddr, int *addrlen);
Donde:
sockfd Representa al descriptor del socket.
cltaddr Es la direccion del proceso con el que se establecio el canal de comunicacion (el cliente)
addrlen es la longitud de la direccion.
La llamada regresa un descriptor a traves del cual se van a recibir y enviar los mensajes del
cliente. Este descriptor es una copia de las propiedades del descriptor sockfd pasado como
parametro. Si se presenta algun error regresa un valor de -1.
Es una llamada bloqueante desde el punto de vista que el proceso se bloquea hasta que llegue
una demanda de conexion.
24.6.4. Conectandose al puerto connect()
Las dos ultimas llamadas son usadas por el servidor para escuchar demandas de conexion y
crear canalaes de comuniacion. Por el otro lado el cliente cuenta con la llamada connect()
para solicitar una conexion. Esta llamada presenta la sintaxis siguiente:
int connect(int sockfd, struct sockaddr *servaddr, int *addrlen)
Donde:
sockfd Es el descriptor de socket creado a partir de la llamada socket().
servaddr Es la direccion del servidor
addrlen Es el tamano de la direccion.
La llamada regresa 0 si todo salio bien y -1 si no se pudo establecer ninguna conexion.
24.6.5. Recepcion de mensajes: read(), recv() y recvfrom()
Existen tres llamadas que se pueden utilizar para la recepcion de mensajes, y presentan la
siguiente sintaxis:
int read(int fildes, void *buf, size_t nbyte);
int recv(int sockfd, const void *buffer, size_t length, int flags)
ssize_t recvfrom(int sockfd, void *buffer, size_t length, int flags,
struct sockaddr *address, size_t *address_len);
149
La primera es la utilizada para la lectura de archivos, se lee de fildes, nbytes bytes de
informacion y la lectura se alamcena en buf.
La llamada recv() es muy parecida a la anterior, lee un mensaje de longitud length del socket
sockfd, dejando el contenido en la variable buffer, el ultimo parametro es utilizado para
protocolos especiales, pero para nuestras aplicaciones se le asigna un valor de 0.
La ultima llamada, recvfrom() espera por un mensaje de sockfd de tamano length dejando
la lectura en buffer, en la variable address se tiene la informacion del emisor y en address_len
la longitud de esa direccion. El parametro flags es para protocolos especiales, aunque para
las aplicaciones de este capıtulo se le asignara el valor de cero. Si tambien los parametros
address_len y address tienen un valor de cero la llamada se comporta como una llamada
read() .
Todas las llamadas regresan el numero de bytes recibidos si todo salio bien o -1 si hubo algun
error. La eleccion entre una y otra depende del tipo de protocolo que se este usando: conexion
o no conexion. Las dos primeras son utilizadas en protocolos tipo conexion mientras que la
ultima se utiliza en protocolos orientado no conexion.
24.6.6. Enviando mensajes: write(), send() y sendto()
Se cuenta con tres llamadas para el envıo de mensajes, a continuacion se presentan las sintaxis
de estas llamadas
int write(int fildes, void *buf, size_t nbyte);
int send(int sockfd, const void *buffer, size_t length, int flags),
ssize_t sendto(int sockfd, const void *message, size_t length, int flags,
const struct sockaddr *dest_addr, size_t dest_len);
La primera es la utilizada para la escritura de archivos, se lee de fildes, nbytes bytes de
informacion y la lectura se alamcena en buf.
La llamada send() es muy parecida a la anterior, envia un mensaje de longitud length a traves
del socket sockfd, Se asume que el mensaje esta almacenado en la variable buffer el ultimo
parametro es utilizado para protocolos especiales, pero para nuestras aplicaciones se le asigna
un valor de 0.
La ultima llamada, sendto(), envıa el mensaje buffer de tamano length a traves de sockfd.
En la variable address se guarda la informacion del emisor y en address_len la longitud de
150
esa direccion. El parametro flags es para protocolos especiales, aunque para las aplicaciones
de este capıtulo se le asignara el valor de cero 0. Si tambien se asigna un valor de cero a los
parametros address_len y address la llamada se comporta como una llamada write().
Todas las llamadas regresan el numero de bytes enviados si todo salio bien o -1 si hubo algun
error. Las dos primeras se utilizan para en el caso de protocolos orientados conexion mientras
que la ultima se utiliza en protocolos orientados no conexion.
24.6.7. Cerrando la comunicacion close()
El socket es un descriptor por lo que para cerrarlo se debe de utilizar la llamada de sistema
close() La sintaxis de dicha llamada es:
int close(int fildes)
Donde:
fildes es el descriptor del socket.
Si no se cierra el socket el puerto utilizado para la comunicacion no podra ser utilizado de nuevo
por ningun otro proceso. Incluso si por alguna razon se tiene que interrumpir el programa es
conveniente cerrar el socket antes de abortara ya que de lo contrario no se podra reiniciar el
programa, sobretodo el servidor.
24.7. La comunicacion cliente-servidor con sockets
Todo sistema cliente-servidor consta de dos programas un programa servidor que va a escuchar
las peticiones atenderlas y darles una respuesta. El otro programa (el cliente) se encarga
de enviar la peticion, esperar una respuesta y procesar dicha respuesta. Esto trae como
consecuencia que todo sistema construido a partir de sockets conste de dos partes la del cliente
y la del servidor, el diseno de uno sin el otro no tiene sentido. La figura 24.3 presenta los pasos
a seguir por el cliente y por el servidor para que puedan comunicarse.
24.8. Aspectos a considerar en el diseno de un servidor
El servidor es el encargado de esperar las peticiones, de atenderlas y de enviar una respuesta a
quien las envio. En el caso de que exista mas de un cliente realizando peticiones a un servidor,
este debe de ser capaz de diferenciar el origen de las diferentes peticiones.
En el caso de trabajar con protocolos orientados conexion la solucion es proporcionar a cada
cliente un canal privado de comunicacion a traves del cual se reciban las peticiones y se envıen
151
Figura 24.3: Pasos para crear una conexion con sockets
las respuestas. Por el otro lado, con protocolos orientados no conexion, es necesario identificar al
emisor para poder otorgarle, o negarle, un servicio. Esto ultimo se realiza a traves de llamadas
de sistema como recvfrom() o sendto(). Un problema adicional se presenta cuando alguien
se hace pasar por dicho proceso.
Otro problema que se presenta cuando se tiene mas de un cliente es atender a todos. Existen
tres estrategias a seguir para la atencion de varios clientes: servidor serial, servidor padre y
servidor hijo. A continuacion se vera en mas detalle las caracterısticas de cada una de ellas.
24.8.1. Servidor serial
El servidor serial solo atiende un proceso a la vez. Una vez que termina de atender la peticion
solicitada verifica si tiene o no otra peticion por atender. Las peticiones no atendidas son
formadas en un cola de atencion cuya longitud es defnida por el servidor. En caso de que la
152
cola se llene las peticiones, y lleguen mas estas seran rechazadas. El algoritmo de un servidor
serial es el siguiente:
while (true)
begin
escuchar puerto por peticiones
crear canal privado de comunicacion bidireccional
leer peticion cliente
atender peticion
responder al cliente
cerrar el canal de comunicacion
end
24.8.2. Servidor padre
El servidor padre tiene por objeto evitar que la cola se llene y que sean rechazadas las peticiones.
Este servidor crea un hijo, a traves de la llamada fork() que atiende cada una de las peticiones.
Es importante remarcar que el padre crea un canal de comunicacion con el cliente y lo hereda
a su hijo. Una vez heredado el canal, el padre regresa a escuchar el puerto por si llegan nuevas
peticiones.
while (true)
begin
escuchar puerto por peticiones
crear canal privado de comunicacion bidireccional
if (fork()== 0) then
begin
leer peticion cliente
atender peticion
responder al cliente
cerrar el canal de comunicacion
end
end
Este tipo de estrategia ejemplifica la utilidad que tiene el hecho de que los procesos vean el
socket como un descriptor de archivos.
153
24.8.3. Aspectos a considerar en el diseno de un cliente
A diferencia de un servidor, no existen diferentes estrategıas para lo que es el diseno de un
cliente. Debido a la naturaleza del modelo cliente/servidor, el cliente solo se tiene que preocupar
por enviar su peticion y aguardar por la respuesta.
Existen dos aspectos principales a cuidar en el diseno de un cliente: el identificar de quien es
el mensaje que llega y que hacer en caso de que el servidor o la lınea de comunicacion no
funcionen.
Para identifıcar al emisor se deben utilizar las mismas llamadas que se mencionaron en la
seccion anterior, recvfrom() y sendto(). Las consecuencias de estar esperando una respuesta de
un servidor caıdo es que el cliente se quede un tiempo indefınido en espera. Esto se soluciona
con la implementacion de un time-out. La construccion de un time-out se base en la llamada
de sistema signal() el enunciado goto del lenguaje C y la llamada de sistema alarm().
El principio es el siguiente: antes de enviar la peticion se debe de activar la recepcion de la
senal SIGALARM, la accion a realizar cuando llegue dicha senal podrıa ser el desplegar que se
esta esperando una respuesta. El siguiente paso es enviar la peticion y activar un despertador
alarm() por el tiempo que se quiera esperar. Despues de pasado ese tiempo si no ha llegado
respuesta alguna, el proceso recibira SIGALARM ejecutara el procedimiento asociado y podra
abortat o reenviar el mensaje. El pseudocodigo de lo anterior se presenta a continuacion:
signal(SIGALARM, nada);
e: envia(peticion)
alarm(t)
esperando(respuesta)
si (llega(SIGALARM)) entonces
ejecuta(nada);
goto e;
24.9. Comunicacion con SOCKETPAIRS
Es un mecanismo de comunicacion entre procesos muy similar a los pipes, con la particularidad
que son bidirecionales. Comparte sin embargo, la caracterıstica de se utilizables solo por procesos
con un ancestro en comun.
Las secciones realizadas por cada proceso se resumen en la siguiente tabla 24.1:
/* Ejemplo: Ejsp.c */
#include <sys/types.h>
#include <sys/socket.h>
154
PADRE HIJO
Crear un socketpair
Crear el hijo
Leer un mensaje Escribir un mensaje
Escribir un mensaje Leer un mensaje
Terminar Terminar
Tabla 24.1: Secciones de procesos
#include <stdio.h>
#define DATA1 "El rey ordeno a su visir..."
#define DATA2 "TENIA EL NOMBRE DE BELISA..."
main()
int n,sockets[2], child;
char buf[1024];
if(socketpair(PF_UNIX,SOCK_STREAM,0,sockets)<0)
perror("abriendo el par de sockets\n");
exit(1);
if ((child=fork())==-1) perror("creando el proceso hijo\n");
else if(child) //este es el padre
close(sockets[0]);
if(read(sockets[1],buf,1024)<0)
perror("padre leyendo el mensaje \n");
printf("ID padre = %d --> %s\n",getpid(),buf);
if(write(sockets[1],DATA2,strlen(DATA2)+1)<0)
perror("padre escribiendo el mensaje");
close(sockets[1]);
exit(1);
else /*este es el hijo */
close(sockets[1]);
if(write(sockets[0],DATA1,strlen(DATA1)+1)<0)
perror("hijo escribiendo mensaje");
wait(&n);
155
if(read(sockets[0],buf,1024)<0) perror("hijo leyedo mensaje\n");
printf("ID hijo = %d --> %s\n",getpid(),buf);
close(sockets[0]);
24.10. Comunicacion orientada conexion
En este modo de comunicacion es necesario que el cliente le solicite al servidor una conexion.
Una vez establecida dicha conexion el cliente y el servidor pueden empezar a intercambiar
mensajes. Generalmente es el cliente el que emite el primer mensaje pero esto hecho no se
puede generalizar. En la figura 24.4 se presenta un esquema general de los pasos requeridos
para poder comunicar dos procesos en modo conexion.
24.10.1. El servidor
El servidor debe de crear un socket, y asociarlo a un puerto. Una vez asociado el socket al
puerto el servidor debe de esperar por peticiones de conexion al puerto asociado. Cuando la
peticion llegue se debe de crear un canal de comunicacion que sirva de puente de comunicacion.
Una vez establecido el canal, se puede empezar a enviar y a recibir mensajes. Al final el servidor
debe de cerrar el socket para que el puerto pueda ser utilizado por otros procesos. Mientras el
servidor este usando el puerto ningun otro proceso podra hacer uso de el.
La atencion de la peticion depende del servicio solicitado. En la mayor parte de los casos, se
trata de una simple llamada a una funcion que implementa el servicio.
Es logico que si el cliente esta intentanto conectarse con el servidor en un determinado puerto
y el servidor esta escuchando otro puerto, la conexion no podra llevarse a cabo.
1. Creacion del socket
2. Asociando el socket al puerto
3. Escuchando en el puerto
4. Aceptando una peticion
5. Recibiendo la peticion
6. Atendiendo la peticion
7. Enviando la respuesta
8. Cerrando la conexionn
156
Figura 24.4: Pasos a seguir para establecer una comunicacion en modo conexion
24.10.2. El cliente
Un proceso debe de crear un socket, asociarlo a un numero de puerto, el cual varıa de acuerdo a
la aplicacion, solicitar una conexion al servidor. Una vez establecida la conexion el cliente puede
empezar a emitir/recibir mensajes. Por ultimo debe de cerrar el socket, lo que provoca el cierre
del canal de comunicacion. A continuacion se describen los llamadas de sistema necesarias para
llevar a cabo lo anterior, puede compararse esta lista con lo descritos en laa figuras: 24.4 y 24.3.
1. Creacion del socket
2. Asociacion del socket a un puerto
3. Conectarse al servidor
4. Envıo de la peticion
5. Recepcion de la respuesta
6. Cerrando la comuniacion
157
24.11. Comunicacion orientada no conexion
A diferencia de la comunicacion orientada conexion la comunicacion orientada no conexion no
necesita establecer un canal de comunicacion entre las dos entidades comunicantes. Tan solo
es necesario que el cliente envie su peticion, que el servidor la reciba, la atienda y en caso
necesario, envie la respuesta.
Vale la pena aclarar que en esta comunicacion los mensajes contienen la direccion del
destinatario, y pueden perderse, lo cual lo hace poco confıable:
En la figura 24.5 se presenta un esquema general de los pasos necesarios para poder establecer
una comunicacion orientada no conexion entre el cliente y servidor:
Figura 24.5: Pasos a seguir para establecer una comunicacion en modo no conexion
24.11.1. El servidor
Los pasos a seguir por un servidor para atender las peticiones de los clientes difieren de aquellos
utilizados en modo conexion. No es necesario que el servidor espere solicitudes de conexion y
cree un canal de comunicacion para poder empezar a recibir y enviar mensajes. El servidor
debe de crear un socket asociarlo a un puerto y ponerse a esperar que las peticiones lleguen
para despues antenderlas y enviar la respuesta si esta es necesaria.
158
Por el lado del servidor este debe de realizar los siguientes pasos:
1. Creacion de un socket
2. Asociar el socket a un puerto
3. Esperar por un mensaje
4. Atencion de la peticion
5. Envıo de la respuesta
6. Cerrar la comunicacion
Como se puede apreciar el codigo del servidor es mas reducido en el caso del modo no conexion
que el del modo conexion.
24.11.2. El cliente
Los pasos que un cliente debe de realizar para poder establecer una comunicacion con el servidor
en un modo no conexion no difieren mucho de los realizados en modo conexiion. La principal
diferencia radica en que no es necesario enviar una peticion de conexion. En efecto despues
de crear el socket y asociarlo a un puerto, el cliente envia directamente al cliente su mensaje.
Dependiendo de la aplicacion puede esperar por una respuesta o ponerse a hacer otra cosa. Las
llamadas de sistema necesarias para que el cliente se comunique con el servidor son las siguientes:
1. Crear un socket
2. Asociar el socket a un numero de puerto
3. Enviar la respuesta
4. Esperar la respuesta
5. Cerrar la comunicacion
Siguiendo los pasos anteriores el codigo del cliente no puede asegurar que el mensaje llegue al
servidor, ni detectar que el servidor esta caıdo o que la vıa de comunicacion se rompio. Con el
finn de evitar esperas infinitas por parte del cliente se sugiere implementar un time-out para
volver enviar el mensaje o darse por vencido e intentarlo despues si ası se desea.
159
24.12. Ejemplo sobre datagrama en el dominio UNIX
Nota: Primero ejecutamos el receptor, despues el emisor como se muestra en la figura 24.6
Extremo receptor
/* recep-inter.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#define NAME "socketdir"
main()
int sock, length, lon2;
struct sockaddr_un name;
char buf[1024];
sock=socket(PF_UNIX, SOCK_DGRAM,0);
if(sock<0)
perror("abriendo socket de datagramas");
exit(1);
name.sun_family=AF_UNIX;
strcpy(name.sun_path,NAME);
if(bind(sock,(struct sockaddr *)&name,sizeof(name))<0)
perror("asociado con nombre al socket");
exit(1);
printf("Direccion del socket -> %s \n",NAME);
if(recvfrom(sock,buf,1024,0,NULL,&lon2)<0)
perror("recibiendo un datagrama");
printf("--> %s\n",buf);
close(sock);
unlink(NAME);
160
exit(0);
Extremo emisor
/* emisor-inter.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#define DATA "holaaaaa como esta alla"
main(int argc, char *argv [])
int sock;
struct sockaddr_un name;
sock=socket(PF_UNIX, SOCK_DGRAM,0);
if(sock<0)
perror("Abriendo socket de datagrama");
exit(1);
name.sun_family=AF_UNIX;
strcpy(name.sun_path,argv[1]);
if(sendto(sock,DATA,strlen(DATA)+1,0,
(struct sockaddr *)&name,sizeof name)<0) perror("enviando un datagrama");
close(sock);
exit(0);
161
El resultado es el siguiente:
Figura 24.6: resultado emisor-receptor sockets
162
24.13. Ejemplo sobre datagrama en el dominio INTERNET
Una vez que hemos visto como realizar una palicacion con sockets de datagramas en el dominio
UNIX, vamos aver una aplicacion equivalente en el dominio de Internet. Veremos que la logica
del programa no depende del dominio de comunicacion. Los cambios fundamentales radican
en la gestion de las direcciones asociadas a los sockets. en el espacio de direcciones AF UNIX
las direcciones se alamacena en estructuras del tipo ( struct sockaddr_un). En el espacio
AF INET las estructuras son del tipo (struct sockaddr_in). Las funciones que necesitan
direcciones (bind(), send(), recvfrom() y otras) aceptan cualquiera de estos dos formatos siempre
que se realice un cast a la estructura generica (struct sockaddr).
24.13.1. Receptor
Observese como se crea un socket con el estilo SOCK DGRAM en el dominio PF INET.
/* recinetd.c */
/* Internet. Extremo receptor */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
main()
int sock, length, lon2;
struct sockaddr_in name;
char buf[1024];
sock=sock(PF_INET, SOCK_DGRAM,O);
if(sock<0)
perror("abriendo socket de detagrama");
exit(1);
name.sin_family=AF_INET;
name.sin.addr.s_addr=htonl(INADDR_ANY);
name.sin_port=htons(0);
if(bind(sock, (struct sockaddr *)name, sizeof name)<0)
perror("asociando nombre al socket");
exit(1);
163
length=sizeof name;
if(getsockname(sock,(struct sockaddr *)&name,&length)<0)
perror("averiguando el nombre del socket");
exit(1);
printf("puerto del socket --> %d \n", ntohs(name.sin_port));
if(recvfrom(sock,buf,1024,0,(struct sockaddr *)NULL, &lon2)<0)
perror("recibiendo un datagrama");
printf("--> %s \n",buf);
close(sock);
exit(0);
De este programa es importante fijarse en la parte en la que se manipula la estructura name,
de tipo sockaddr in, que contiene la direccion del receptor. Hay tres setencias en las que se
rellenan los tres campos de interes de la estructura:
name.sin family=AF INET;
name.sin addr.s addr=htonl(INADDR ANY);
name.sin port=htons(0);
La primera sentencia rellena el campo correspondiente al formato de la direccion. La
segunda rellena la direccion IP a la que se va asociar el socket. La tercera rellena el puerto
correspondiente. En los dos ultimos campos se introducen, en vez de valores concretos, unos
valores especiales con el siguiente significado:
Poner INADDR ANY en la direccion IP significa que, cuando se utilice bind()para asociar
una direccion a un socket, la direccion IP que se va a emplear es la correspondiente a la
maquina en la que se este ejecutando el programa. Es una forma de indicar ”la direccion
IP de esta maquina”.
Poner un cero(0) en el puerto significa que, cuando se utilice bind(), el puerto que se va
asociar al sockets es uno cualquiera libre asignado por el sistema operativo.
Por lo tanto, la estructura name no define por completo la direccion que se va a asociar al
socket. Los valores finales no lo sabremos hasta despues de la ejecucion de bind(). Por ese
motivo seutiliza la funcion getsockname(): para saber que direccion nos ha correspondido.De
164
aqui lo unico que nos interesa es el puerto, que se imprime por pantalla (una vez convertido al
orden de la maquina con ntohs()). si se imprime es para que, al poner en marcha el emisor, le
podamos indicar exactamente el puerto en el que el receptor esta esperando datos.
El prototipo de getsockname() es:
#include <sys/types.h>
#include <sys/socket.h>
int getsockname(int s, struct sockaddr *name, int *namelen);
Donde:
sock la direccion que queremos conocer
name aqui es donde queda la direccion el tipo puede ser sockadd in o sockaddr un,
siempre que se utilice el cast adecuado.
namelen indica el espacio reservado para contener la estructura con la direccion, y se actualiza
con el tamano real ocupado por dicha estructura.
24.13.2. Emisor
Como pasaba con el receptor, el programa emisor en el dominio Internet es muy similar a su
equivalente en el dominio de UNIX, estando la complejidad en la gestion de direcciones.
/* eminetd.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/in.h>
#include <setdb.h>
#include <stdio.h>
#define DATA "Este es el mensaje..."
main(int argc, char *argv [])
int sock;
struct sockaddr_in name;
struct hostent *hp, *gethostbyname();
sock=socket(PF_INET, SOCK_DGRAM,0);
165
if(sock<0)
perror("Abriendo socket de datagrama");
exit(1);
hp=gethostbyname(argv[1]);
if(hp==0)
fprintf(stderr, %s: host desconocido",argv[1]);
exit(2);
memcpy((char *)&name.sin_addr, (char *)hp->h_addr,hp->h_length);
name.sun_family=AF_INET;
name.sin_port=htons(atoi(argv[2]));
if(sendto(sock,DATA,strlen(DATA)+1,0,(struct sockaddr *)&name,sizeof name)<0)
perror("enviado un data grama");
close(sock);
exit(0);
El emisor consigue la direccion del receptor a partir de dos argumentos facilitados en la lınea
de comandos:
1. El nombre de la maquina receptora (argv[1])
2. El puerto en el que espera el receptor(argv[2])
E nombre de la maquina se convierte en direccion IP utilizando la funcion gethostbyname().
Una vez hecho esto, se procede a rellenar la estructura name con los datos exactos del receptor.
Aquı no se puede utilizar comodines: todos los campos tienen valores perfectasmente definidos.
El emisor no se molesta en asignar direccion a su socket. Esto no quiere decir que no la tenga.
El sistema operativo comprueba,cuando se envıa informaciona traves de un socket, si tiene o
no direccion asociada con bind().Si no es ası, se asigna automaticamente: la direccion IP es de
la maquina local, y el puerto es uno cualquiera libre.
24.13.3. Ejecucion del ejemplo
1. Crear los ejecutables correspondietes de emisor(eminetd) y receptor(recinetd)
166
2. Lanzar tarea como de fondo (recinetd &). Debera escribir por pantalla el string “puerto
del socket –>” y un numero, sea n.
3. Lanzar despues el emisor, facilitandose el nombre del computador donde esta el receptor
y el numero de puerto asignado a su socket (eminetd maquina receptora n). El receptor
deberıa escribir “–> Este es el mensaje ...”. Tras ello ambos programas terminan.
24.14. Conexion en el dominio INTERNET
En esta seccion vamos a ver, por medio de un ejemplo, como realizar comunicacion orientada a
conexion en el dominio Internet. no vemos ningun ejemplo en el dominio UNIX porque todo lo
que expongamos en el contexto del dominio Internet tambien sera aplicable al dominio UNIX,
a exepcion de lo que se refiere a los formatos de las direcciones.
El ejemplo consta de dos programas, a los que llamaremos cliente y servidor. El cliente es el
que inicia la conexion(TCP) de forma activa, mientras que el servidor espera en su puerto a
que le llamen, abriendo las conexiones de forma pasiva. Una vez establecida una comunicacion
entre el cliente y el servidor, la conexion es bidireccional: cualquiera puede leer y escribir por
ella. En este caso, el cliente se limitaa enviar una mensaje, y el servidor a escribirlo. Pasamos
a describir con algo mas de detalle cada uno de los programas.
24.14.1. Cliente
El programa cliente sigue el siguiente esquema:
Crea un socket en el dominio PF_INET, con el estilo SOCK_STREAM.
Calcula la direccion del servidor, usando para ello la lınea de comandos (argv[1] contiene
el nombre de la maquina servidora) y un puerto conocido a priori.
Se conecta, de forma activa, con el servidor
Envıa un mensaje
Cierra el socket y, por lo tanto, la conexion.
/* clinetc.c */
#include <sys/type.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#define SERV_ADDR (IPPORT RESERVED + 1)
167
#define DATA "##--##--##----***----##--##--##"
main(int argc, char *argv[])
int sock;
struct sockaddr_in server;
struct hostent *hp, *gethostbyname();
if(argc <2)
printf("Uso: %s nombre_host \n",argv[0]);
exit(1);
/* creamos un socket orientado a conexion */
sock=socket(PF_INET, SOCK_STREAM, 0 );
if(sock<0)
perror("No se ha podido conseguir un socket");
exit(1);
/* Nos conectamos con el socket de escucha */
/* del servidor. Lo conseguimos claculando */
/* primero su direccion, a partir del nombre */
/* del computador y del numero de puerto */
server.sin_family=AF_INET;
hp=gethostbyname(argv[1]);
if(hp==0)
fprintf(stderr, "%s: No conozco ese computador \n",argv[1]);
exit(2);
memcpy((char *)&server.sin_addr, (char *)hp->h_addr, hp->h_length);
server.sin_port=htons(SERV_ADDR);
if(connect(sock,(struct sockaddr *)&server,sizeof server)<0)
perror("Conexion no aceptada!!!");
exit(1);
if(write(sock,DATA,strlen(DATA)+1)<0)
168
perror("No he podido escribir el mensaje");
close(sock);
exit(0);
Como primer punto a destacar tenemos el uso de la constante SERV_ADDR con el numero de
puerto del servidor. No es necesario, como en el caso anterior, confiar en que nos los dan
como parametro. El inconveniente radica en que, si el servidor cambia de puerto, entonces
hay que modificar y recompilar el cliente. Una alternativa mejor es registrar una asociacion
¡servicio puerto/protocolo¿en el fichero /etc/services y utilizar la funcion getservbyname(),
para obtener, dada una cadena dem caracteres con el nombre del servicio, el numero de puerto
asociado.
Por otra parte, SERV_ADDR esta definido en funcion de otra constante: IPPORT RESERVED.
En un sitema UNIX, todos los puertos menores que esta constante, (cuyo valor puede ser 1024)
estan reservados para procesos privilegiados (del sistema), donde se incluye la mayoria de los
servicios TCP/IP. Los demas puertos estan disponibles para uso temporaltıpicamente para
clientes. El sistema se encarga de no asignar aquellos puertos que ya estan ocupados.
En relacion al puerto TCP asociado al socket del cliente, vemos que no aparece citado
explicitamente en ninguna parte. Sin embargo, tiene que estar ahı, porque una conexion TCP
precisa de dos puertos. Al igual que pasaba en el caso de UDP, el sistema asigna dinamicamente
un puerto cualquiera, que este libre, dentro del rango no reservado. La asignacion se realiza
cuando se intenta establecer una conexion y no ha habido un bind() previo. Si tenemos interes
en saber el puerto del cliente, podemos obtenerlo usando la funcion getsockname().
24.14.2. servidor
El servidor se encarga de hacer las operaciones necesarias para atender a sus clientes : es
importante darse cuenta de que este servidor no se limita establecer una comunicacion con
el cliente y luego terminar. Es capaza de antender a multiples clientes aunque, eso si, de uno en
uno. Es un servidor iterativo. Mas adelante veremos como disenar servidores concurrentes que
permitan la atencion simultanea a varias conexiones o, lo que es lo mismo, a varios clientes.
El diseno de un servidor TCP es mas complejo que el de un cliente. Vamos enumerar ahora
los pasos fundamentales que se dan, y tras ver el codigo analizaremos mas a detalle algunos
aspectos del programa.
169
1. Crea un socket, sock, en dominio PF INET, estilo SOCK STREAM.
2. Calcula la direccion y se la asocia a sock
3. Marca el sock como socket de escucha.
4. Se bloquea en sock, a las espera de una peticion de establecimiento de conexion por parte
de algun cliente.
5. Consigue una conexion a traves de un socket nuevo, msgsock , que es un socket de dialogo
6. Recibe informacion a traves de msgsock.
7. Cierra msgsock (y, con el, la conexion).
8. Se dispone a aceptar nuevas conexiones a traves de sock
/* serinetc.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <stdio.h>
#define STDOUT 1
#define SERV_ADDR(IPPORT_RESERVED+1)
main()
int rval;
int sock, length, msgsock;
struct sockaddr_in server;
char buf[1024];
/* creacion de un socket de escucha, de tipo "stream */
sock=socket(PF_INET, SOCK_STREAM,0);
if(sock<0)
perror("No hay socket de escucha");
exit(1);
/* voy a asignar a ese socket una direccion de transporte */
server.sin_family=AF_INET;
server.sin_addr.s_addr=htonl(INADDR_ANY);
170
server.sin_port=htons(SERV_ADDR);
/*El puerto es uno concreto y fijo */
if(bind(sock,(struct sockaddr *)&server, sizeof server)<0)
perror("direccion no asignada");
exit(1);
/*Me dispongo a escuchar en el socket. */
listen(sock,1);
do
/* Me bloqueo esperando peticion de conexion */
/* Acepto y consigo un socket de dialogo "msgsock" */
msgsock=accept(sock, (struct sockaddr *)0, (int *)0);
if(msgsock==-1)
perror("Conexion no aceptada !!!");
else
do
/* me dispongo a leer datos por la conexion */
memset(buf,0, sizeof buf):
rval=read(msgsock, buf, 1024);
if(rval<0) perror("Mensaje no leido");
else write(STDOUT, buf, rval);
while(rval>0);
printf("\n Cerrando la conexion...\n");
close(msgsock);
while(1);
exit(0);
Lo mas peculiar del programa servidor es que no gestiona un unico socket, como pasa con
el cliente, sino dos (sock y msgsock), cada uno con una funcion diferente. Tambien cambia
la forma en la que se gestionan las conexiones. Hacden falta utilizar dos funciones: listen() y
accept().
24.14.3. Ejecucion de Ejemplo
1. Crear los ejecutables correspondientes al cliente (clinetc) y al servidor(serinetc)
171
2. Lanzar el servidor como de fondo (recinetd &), se puede comprobar con el comando ps
para comprobar que esta activo.
3. Lanzar el cliente indicandole como parametro el nombre del computador donde se
encuentra el receptor (clinetc maquina_servidor). El servidor escribira entonces, en
su salida estandar, estas dos lıneas:
##--##--##----***----##--##--##
Cerrando la conexion..
4. El cliente termina inmediatamente, pero el servidor no: podemos comprobarlo utilizando
de nuevo el comando ps. Podemos realizar tantas ejecuciones del cliente como queramos.
Al terminar habra que matarlo explicitamente con el comando kill.
24.15. Conexion monoproceso
Ahora vamos aver como hacer un servidor que tenga un unico proceso antendiendo a la vez a
varias conexiones. La clave esta en el uso de una nueva llamada al sistema: select().
#include <sys/time.h>
#include <sys/types.h>
int select(int nfds, fd_set *readfds, fds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
Con select podemos:
Bloquearnos en varios sockets a la vez, para despertarnos en cuando se pueda operar en
uno de ellos.
Hacer una encuesta simultanea a varios sockets, sin bloqueo.
Bloquearnos por un tiempo limitado
Para realizar estas operaciones, select() toma cinco argumentos. Los argumentos segundo,
tercero y cuarto son del tipo fd set , que es una forma de representar conjuntos de descriptores
(puede ser de cualquier objeto de E/S, sockets incluidos) constante, FD SETSIZE ). El ultimo
argumento representa un intervalo de tiempo (segundos+microsegundos).
struct timeval
long tv_sec; /* segundos */
172
long tv_usec; /* y microsegundos */
;
Donde:
readfds Indica a select() que objetos tiene que examinar para ver si se puede realizar
una operacion de lectura en ellos.
writefds Indica que objetos tiene que examinar para ver si se puede realizar una operacion
de escritura en ellos
exceptfds Indica que objeto tiene que examinar para ver si se puede escribir por ellos
En este contexto, hay que indicar que la posibilidad de aceptar una conexion por un socket de
escucha tiene la misma consideracion que la posibilidad de leer por un socket de dialogo.
Select() monitoriza todos los objetos a la vez durante el tiempo maximo especificado en su
argumento timeout. En funcion del valor de este temporizador, se consigue tres componentes
distintos:
Si timeout es NULL (no se facilita temporizador), select() funciona de forma totalmente
bloqueante. Solo retorna en cuanto es posible realizar alguna operacion segura sobre alguno
o algunos de los descriptores a su cargo. Entendemos por operacion segura aquella que se
puede completar inmediatamente, sin bloqueo.
Si timeout indica tiempo 0, select() se limita a hacer una encuesta, retornando
inmediatamente.
Si timeout indica tiempo distinto de cero, select() se bloquea en sus descriptores, y
el momento de retornar depende de lo que ocurra antes: (1) que expire el temporizador o
que (2) se pueda realizar una operacion segura sobre algun descriptor.
Select() devuelve(-1) en caso de error; en caso contrario, devuelve el numero de descriptores
preparados para realizar una operacion. Para saber exactamente cuales son los descriptores
utilizables hay que examinar los conjuntos de descriptores, que habran sido convenientemente
modificados.
Para tratar con conjuntos de descriptores se dispone de estas cuatros macros:
void FD_SET(int fd, fd_set &fdset);
173
void FD_CLR(int fd, fd_set &fdset);
void FD_ISSET(int fd, fd_set &fdset);
void FD_ZERO(fd_set &fdset);
FD SET() incluye un desriptor (fd) en un conjunto (fdset).
FD CLR() elimina un descriptor de un conjunto (fdset).
FD ISSET() devuelve 1 si un descriptor esta en un conjunto, y 0 en caso contrario. Por ultimo,
PD ZERO() vacıa un conjunto.
24.15.1. Ejemplo
/* serveco3.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#define STDOUT 1
#define SERV_ADDR (IPPORT_RESERVED+1)
fd_set lecturas;
main()
int rvala, rvalb;
int sock, length, msgsocka, msgsockb;
int dispuestos;
struct sockaddr_in server;
char buf[1024];
sock = socket(PF_INET, SOCK_STREAM,0);
if(sock<0)
perror("no hay socket de escucha");
exit(1);
server.sin_family=AF_INET;
server.sin_addr.s_addr=htonl(INADDR_ANY);
server.sin_port=htons(SERV_ADDR);
if(bind(sock, (struct sockaddr *)&server, sizeof server)< 0)
174
perror("direccion no asignada");
exit(1);
listen(sock, 1);
do
msgsocka=accept(sock, (struct sockaddr *)0, (int *)0);
if (msgsocka == -1)
perror("conxion no aceptada !!!");
exit(-1);
msgsockb=accept(sock, (struct sockaddr *)0, (int *)0);
if (msgsockb == -1)
perror("conxion no aceptada !!!");
exit(-1);
do
FD_ZERO(&lecturas);
FD_SET(msgsocka, &lecturas);
FD_SET(msgsockb, &lecturas);
dispuestos=select(FD_SETSIZE, &lecturas,(fd_set *)NULL,
(fd_set *)NULL, NULL);
if(FD_ISSET(msgsocka, &lecturas))
rvala=read(msgsocka, buf, 1024);
if(rvala<0) perror("mensaj no leido (1)");
else write(STDOUT, buf, rvala);
if(FD_ISSET(msgsockb,&lecturas))
rvalb=read(msgsockb, buf, 1024);
if(rvalb<0) perror("mensaj no leido (2)");
else write(STDOUT, buf, rvalb);
while((rvala>0) || (rvalb >0));
printf(" \n Cerrando las conexiones ... \n");
close(msgsocka);
close(msgsockb);
175
while(1);
exit(0);
El ejemplo muestra un sevidor que, como hemos dicho atiende conexiones de dos en dos. En
primer lugar, se bloquea hasta recibir dos peticiones de conexion; el efecto es que un cliente
que se conecta a este servidor se queda “sin poder hacer nada” hasta que llega un segundo
cliente. Una vez que se ha establecido las dos conexiones, el sevidor utiliza select() para
gestinarlas simultaneamente. El bucle principal del servidor termina cuando ambos usuarios
dan por cerradas sus conexiones respectivamente; observese que, debido al comportamiento de
select(), si un usuario cierra su conexion el otro puede seguir aun trabajando con el servidor.
24.16. Conexiones simultaneas (multiprocesos)
Una alternativa al diseno de servidor concurrente consiste en disenar un programa multiproceso
de tal forma que el servidor tiene constantemente un proceso en cmarcha atendiendo al socket
de escucha. En cuanto se establece una comunicacion, el servidor realiza un fork() y crea un
proceso hijo que se dedica, en exclusiva, a atender el cliente con el que se tiene conexion a
traves del sockets de dialogo. Mientras tanto, el padre o proceso principal sigue atendiendo al
socket de escucha, ignorando los sockets de dialogo, creando tantos hijos como sea necesario.
Esta alternativa funciona bien siempre que los proceso hijos no necesiten compartir informacion
(variables), puesto que cada uno de ellos se ejecuta en un espacio de memoria separado. En
el caso de que el sistema operativo soporte threads el servidor puede ser multithread en vez
de multiprocesos, y la limitacion mencionada desaparece, puesto que diferentes threads sı que
puede compartir memoria.
24.16.1. Ejemplo
A continuacion incluimos un ejemplo completo: un servidor de eco concurrente multiproceso
/* serveco2.c */
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
176
#include <errno.h>
extern int errno;
void do_echo(int);
main()
struct sockaddr_in sin, fsin;
int s, ssock, alen;
sin.sin_family=AF_INET;
sin.sin_addr.s_addr=htonl(INADDR_ANY);
sin.sin_port=htons(3500);
if((s=socket(PF_INET, SOCK_STREAM,0))<0)
perror("No se puede crear el socket");
exit(1);
if(bind(s,(struct sockaddr *)&sin, sizeof sin)<0)
perror("No se puede asignar direccion");
exit(2);
if(listen(s,5)<0)
perror("No puedo poner el socket en modo escucha");
exit(3);
signal(SIGCHLD,SIG_IGN);
while(1)
alen=sizeof(fsin);
if((ssock=accept(s,struct sockaddr *)&fin,&alen))<0)
if(errno==EINTR) continue;
perror("Fallo en accept");
exit(4);
177
switch(fork())
case -1: perror("No se puede crear hijo");
exit(5);
case 0: close(s);
do_echo(ssock);
exit(0);
default:close(ssock);
break;
void do_echo(int fd)
char buf[4096];
int cc, org, faltan, cc2;
while (cc=read(fd,buf, sizeof buf))
if(cc<0)
perror("read");
exit(6);
org=0;
faltan=cc;
while(faltan)
if((cc2=write(fd,&buf[org],faltan))<0)
perror("Fallo al escribir");
exit(7);
org+=cc2;
faltan-=cc2;
178
close(fd);
El servidor reside en puerto fijo, el 3500. Si unj cliente se pone en contacto con este servidor
establece una conexion, recibira por ella una copia de toda la informacion que envıe.
Interesa destacar unos cuantos aspectos del ejemplo:
El que se refiere al control de los errores. Observese que cada vez que se realiza una llamada
al sistema, se comprueba inmediatamente su resultado a fin de terminar el programa si
algo va mal. Esta estrategia es fundamental para conseguir aplicaciones robustas.
la siguiente lınea de codigo tambien resulta interesante:
signal(SIGCHLD, SIG_IGN);
El motivo de su presencia es el siguiente. El servidor esta creando procesos hijos
constantemente, uno por cada conexion. En cuanto una conexion se cierra, el proceso
que la atendia “muere”y devuelve un diagnostico (ejecutando exit(n)). Se supone que,
por cada hijo creado, el proceso padre de realizar una llamada a la funcion wait(), que le
bloquea hasta que el hijo muere y recupera el valor del diagnostico. Si el padre no realiza
esta accion, el resultado es que los hijos no desaparecen del sistema, si no que quedan
como procesos zombies. Adicionalmente cuando un hijo muere se envia al padre unaq
senal SIGCHLD que, en caso de que no se realice ninguna operacion especial, se ignora
y no tiene ningun efecto?
La segunda estrategia es asignar un gestor que atienda la SIGCHLD que se limite a
realizar llamadas a wait(), eliminado ası zombies.
La asignacion del gestor se hace con signal();
signal(SIGCHLD,terminator);
El gestor, terminador(), puede tener este aspecto:
int terminnador()
int status;
179
while(wait(&status))>=0);
180
Parte VIII
Creacion de demonios
181
Capıtulo 25
Proceso para la creacion de un demonio
25.1. Demonios
Los demonios se inician (y se paran)
• Cuando un sistema cambia los niveles de ejecucion;
• Cuando un sistema arranca y, a menos que los obligue a terminar.
• Se ejecutan hasta que se cierra el sistema
No tienen terminal controlador, cualquier salida a stderr o a stdout, requiere una
manipulacion especial en consecuencia no son interactivos, es decir no necesitan entrada
de usuario.
Se ejecutan frecuentemente con privilegios de superusuario.
Generalmente son lideres de grupos de proceso y lideres de sesion.
El padre del proceso es el proceso init, que tiene un PID 1.
25.2. Como crear un demonio
1. Tenemos que hacer fork y hacer que el padre salga (exit), para deshacerse del terminal
controlador. Esto tiene sentido. Los demonios ni leen en una entrada estandar ni escriben
en un salida o error estandar, por lo que necesitan una interfaz de terminal el tiempo
suficiente para iniciarse
2. Crear una sesion en el hijo utilizando la llamada setsid. Con esta llamada conseguimos
varias cosas.
a) Crear una sesion nueva si el proceso que lo llama no es un lider de grupo de proceso.
b) Hace del proceso que llama el lider de grupo de proceso del nuevo grupo.
c) Establece el ID del grupo de proceso (PGID) y el ID de sesion (SID) en el ID de
proceso (PID) del proceso que llama.
182
d) Separa la nueva sesion de cualquier tty y controladora.
3. Hacer del directorio raiz el directorio actual del proceso hijo.
4. Establecer la umask del proceso como 0. Este paso es necesario para evitar que la
umask heredada del demonio interfiera en la creacion de archivos y directorios.
5. Cerrar cuaquier descriptor de archivos innecesarios que haya heredado el hijo. (stdin,
stdout y stderr).
En resumen:
Fork y exit en el proceso padre.
Llamar a setsid en el proceso hijo Hacer el directorio raiz, /, el directorio activo del
proceso hijo.
Cambiar la umask del proceso hijo a 0
Cerrar cualquier descriptor de archivos innecesarios.
Ejemplo:
/*demonio sencillo que marca el tiempo */
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <time.h>
#include <syslog.h>
int main(void)
pid_t pid, sid;
time_t timebuf;
int fd, len;
pid=fork();
183
if(pid<0)
perror("fork");
exit(EXIT_FAILURE);
if(pid>0) /*en el padre, liberamos */
exit(EXIT_SUCCESS);
/*EN EL HIJO*/
/* PRIMERO INICIAMOS UNA NUEVA SECCION */
if((sid=setsid())<0)
perror("setsid");
exit(EXIT_FAILURE);
/*A CONTINUACION, HACEMOS DE / EL DIRECTORIO ACTUAL */
if((chdir("/"))<0)
perror("chdir");
exit(EXIT_FAILURE);
/*RESTABLECEMOS EL MODO DE ARCHIVO */
umask(0);
/*CERRAMOS LOS DESCRIPTORES DE ARCHIVOS INNECESARIOS*/
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
/* POR ULTIMO, HACEMOS NUESTRO TRABAJO */
len=strlen(ctime(&timebuf));
while(1)
char *buf=malloc(sizeof(char)*(len + 1));
if(buf==NULL)
perror("malloc");
exit(EXIT_FAILURE);
184
if((fd=open("/var/log/daemons.log",O_CREAT|O_WRONLY
|O_APPEND,0600))<0)
perror("open");
exit(EXIT_FAILURE);
time(&timebuf);
strncpy(buf,ctime(&timebuf),len + 1);
write(fd, buf,len+1);
close(fd);
sleep(60);
exit(EXIT_SUCCESS);
25.3. Como comunicarse con un demonio
Para comunicarnos con un demonio, le enviamos una senal que hara que responda de una
cierta manera.Por ejemplo, normalmente es necesario obligar a un demonio a que vuelva a leer
su archivo de configuracion. El modo mas comun de hacer esto es enviar una senal SIGHUP
al demonio (tanto el servidor HTTP Apache como el servidor de correo Sendmail interpretan
SIGHUP como la senal para volver a leer sus archivos de configuracion). Otra necesidad comun
es la de alterar el comportamiento de un demonio sin forza a leer su archivo de configuracion.
Los ejemplos que se presentan a continuacion modifican lpudated para que lea un archivo de
configuracion y responda a SIGHUP. Sin embargo primero aprenderemos a leer un archivo de
configuracion y a utilizarlo para controlar el comportamiento de un demonio.
25.3.1. Como leer un archivo de configuracion
La primerera tarea es ensenar a lpudated a leer un archivo de configuracion. Como
demostracion, el archivo de configuracion, /etc/lpudated.conf, considera de un unica lınea
de texto de menos 256 caracteres. en realidad, como veremos en el programa, /etc/lpudated
puede contener cualquier cantidad de texto en tanta lıneas como queremos, pero lpudated solo
leera los primeros 255 caracteres. El listado siguiente muestra el programa modificado y con
nuevo nombre lpudated-rc.
/* lpudated-rc.c Demonio sencillo de marcacion de tiempo
185
que lee un archivo de configuracion en /etc
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <time.h>
#include <syslog.h>
#define RCFILE "/etc/lpudated.conf" /*el archivo de configuracion */
#define BUFLEN 256 /*longitud de lectura de buffer*/
int main(void)
pid_t pid, sid;
time_t timebuf;
int fd, rcfd, len;
pid=fork();
if(pid<0)
syslog(LOG_ERR,"%s\n",perror);
exit(EXIT_FAILURE);
if(pid>0) /*En el padre, liberamos */
exit(EXIT_SUCCESS);
/* en el hijo */
/* abre el archivo de sistema */
openlog("lpudated",LOG_PID,LOG_DAEMON);
/* lee el archivo de configuracion */
if((rcfd =open(RCFILE, O_RDONLY))<0)
syslog(LOG_ERR,"%s\n", "error opening RCFILE \n");
exit(EXIT_FAILURE);
186
else
/* lee el archivo de configuracion */
char rcbuf[BUFLEN]; /*mantiene el valor leido */
int len; /* la longitud del buffer */
if((len=read(rcfd,rcbuf,BUFLEN))<0)
syslog(LOG_ERR,"%s\n", "error opening RCFILE \n");
exit(EXIT_FAILURE);
rcbuf[len]=’\0’;
/*Cierra el archivo de configuracion */
if(close(rcfd)<0)
syslog(LOG_ERR,"%s\n", "error closing RCFILE \n");
exit(EXIT_FAILURE);
/*Escribe el valor leido en el registro del sistema */
syslog(LOG_INFO,"%s\n",rcbuf);
/* Primero inicamos una nueva sesion */
if((sid=setsid())<0)
syslog(LOG_ERR,"%s\n", "setsid");
exit(EXIT_FAILURE);
/* A continuacion, hacemos del / el directorio actual */
if((chdir("/"))<0)
syslog(LOG_ERR,"%s\n", "chdir");
exit(EXIT_FAILURE);
/* Restablecemos el modo de archivo */
umask(0);
/* Cerramos los descriptores de archivos innecesarios */
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
187
/* Por ultimo, hacemos nuestro trabajo */
len=strlen(ctime(&timebuf));
while(1)
char *buf=malloc(sizeof(char) * (len + 1));
if(buf == NULL)
syslog(LOG_ERR,"malloc");
exit(EXIT_FAILURE);
if((fd=open("/var/log/lpudated.log",O_CREAT |
O_WRONLY|O_APPEND, 0600))<0)
syslog(LOG_ERR,"open");
exit(EXIT_FAILURE);
time(&timebuf);
strncpy(buf, ctime(&timebuf),len+1);
write(fd,buf,len+1);
close(fd);
sleep(60)
/* Cerramos el registro del sistema */
closelog();
exit(EXIT_SUCCESS);
Recordemos que el demonio debe iniciarlo el suario root.
25.3.2. Como anadir manipulacion de senales a un demonio
Una vez que el demonio sabe leer un archivo de configuracion, la siguiente leccion es posibilitar
la manipulacion de senales. En su proxima reencarnacion, lpudated-sig, lpudate aprendera
a responder a SIGUP, los que hace que vuelvaa leer su archivode configuracion.
/* lpudated-sig.c Demonio sencillo de marcacion de tiempo que lee un
archivo de configuracion en /etc y manipula se~nales
*/
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
188
#include <string.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <time.h>
#include <syslog.h>
#include <signal.h>
#define RCFILE "/etc/lpudated.conf" /*el archivo de configuracion */
#define BUFLEN 256 /*longitud de lectura de buffer*/
#define NORMAL 1
#define ROT13 2
int main(void)
pid_t pid, sid;
time_t timebuf;
int rcfd, fd, len, o_style;
pid = fork();
if(pid <0)
sys
#define RCFILE "/etc/lpudated.conf" /*el archivo de configuracion */
#define BUFLEN 256 /*longitud de lectura de buffer*/
189
Parte IX
Driver
190
Capıtulo 26
Proceso para la creacion de un driver
26.1. El primer driver: carga y descarga del driver en el espacio de
usuario
Para ello se escribe este programa en un fichero llamado nada.c
#define MODULE
#include <linux/module.h>
Lo compilamos con el siguiente comando:
$ gcc -c nada.c
(en realidad, para que funcione siempre sin problemas el comando para compilar deberıa ser
$ gcc -I/usr/src/linux/include -O -Wall -c nada.c , donde detras de -I debe aparecer el
directorio donde se encuentren los ficheros include del kernel)
Este elemental modulo pertenece al espacio de kernel y entrara a formar parte de el cuando
se cargue. Dentro del espacio de usuario podemos cargar el modulo en el kernel en la lınea
de comandos como usuario root con # insmod nada.o (si este no funciona se puede probar
insmod -f nada.o).
El comando insmod permite instalar nuestro modulo en el kernel, aunque este en concreto
no tenga ninguna utilidad. Podemos comprobar que el modulo ha sido instalado mediante el
comando que lista todos los modulos instalados: # lsmod Finalmente podemos eliminar el
modulo del kernel con el comando # rmmod nada Podemos comprobar que el modulo ya no
esta instalado de nuevo con el comando lsmod.
26.2. El driver “Hola mundo”: carga y descarga del driver en el
espacio de kernel
Para ello existen dos funciones, ini module y cleanup module, dentro del espacio de kernel
correspondientes a las del espacio de usuario, insmod y rmmod, que se utilizan cuando se
instala o quita un modulo. Dichas funciones son llamadas por el kernel cuando se realizan estas
191
operaciones.
Veamos un ejemplo practico con el clasico programa ”Hola mundo”:
#define MODULE
#include <linux/module.h>
int init_module(void)
printk("<1>Hola mundo\n");
return 0;
void cleanup_module(void)
printk("<1>Adios mundo cruel\n");
26.3. El driver completo “memoria”: parte inicial del driver
Ahora realizaremos un driver completo, memoria.c, utilizando la memoria del ordenador que
nos permitira escribir y leer un caracter en memoria. Este dispositivo, aunque no muy util, es
muy ilustrativo dado que es un driver completo y facil de implementar ya que no se necesita
un dispositivo real. Para realizar un driver, en la parte inicial de el, tendremos que definir las
constantes MODULE y KERNEL . Ademas tendremos que incluir, con #include, una serie
de ficheros habituales en los drivers:
<<memoria inicio>>=
/* Definiciones e includes necesarios para los drivers */
#define MODULE
#define __KERNEL__
#include <linux/config.h>
#include <linux/module.h>
#include <linux/kernel.h> /* printk() */
#include <linux/malloc.h> /* kmalloc() */
#include <linux/fs.h> /* everything... */
#include <linux/errno.h> /* error codes */
#include <linux/types.h> /* size_t */
#include <linux/proc_fs.h>
#include <linux/fcntl.h> /* O_ACCMODE */
#include <asm/system.h> /* cli(), *_flags */
192
#include <asm/uaccess.h> /* copy_from/to_user */
/* Declaracion de funciones de memoria.c */
int memoria_open(struct inode *inode, struct file *filp);
int memoria_release(struct inode *inode, struct file *filp);
ssize_t memoria_read(struct file *filp, char *buf, size_t count,
loff_t *f_pos);
ssize_t memoria_write(struct file *filp, char *buf, size_t count,
loff_t *f_pos);
void cleanup_module(void);
/* Estructura que declara las funciones tipicas */
/* de acceso a ficheros */
struct file_operations memoria_fops = read: memoria_read,
write: memoria_write,
open: memoria_open,
release: memoria_release;
/* Variables globales del driver */
/* Numero mayor */
int memoria_major = 60; /* Buffer donde guardar los datos */
char *memoria\_buffer;
Detras de los ficheros # include, aparecen las declaraciones de las funciones que definiremos en
el programa mas adelante. Posteriormente aparece la definicion de la estructura file operations
que define las funciones tıpicas que se utilizan al manipular ficheros y que veremos despues.
Finalmente estan las variables globales del driver, una de ellas es el numero mayor del dispositivo
y la otra un puntero a la region de memoria, memoria buffer, que utilizaremos como almacen
de datos del driver.
26.4. El driver “memoria”: conexion de dispositivos con sus ficheros
En UNIX y Linux se accede a los dispositivos desde el espacio de usuario de identica forma a
como se hace con un fichero. Dichos ficheros suelen colgar del directorio dev. Para ligar ficheros
con dispositivos se utilizan dos numeros: numero mayor y numero menor. El numero mayor
es el que utiliza el kernel para relacionar el fichero con su driver. El numero menor es para
uso interno del dispositivo y por simplicidad no lo veremos aquı. Para conseguir este proposito
primero se tiene que crear el fichero que sirva como dispositivo con el comando, como usuario
root, # mknod devmemoria c 60 donde la c significa que se trata de un dispositivo tipo
193
char, el 60 es el numero mayor y el 0 el numero menor. Para ligar un driver con su fichero dev
correspondiente se utiliza la funcion register chrdev que tiene como argumento el numero mayor
del dispositivo. Esta funcion se llama con tres argumentos: numero mayor, cadena de caracteres
indicando el nombre del modulo y una estructura file operations que asocia esta llamada con
las funciones aplicables a ficheros definida dentro de ella. Se invoca, al instalar el modulo, de
esta forma:
<<memoria init module>>=
int init_module(void)
int result; /* Registrando dispositivo */
result = register_chrdev(memoria_major, "memoria",
&memoria_fops);
if (result < 0) printk("<1>memoria: no puedo obtener numero
mayor %d\n",memoria_major);
return result;
/* Reservando memoria para el buffer */
memoria_buffer = kmalloc(1, GFP_KERNEL);
if (!memoria_buffer) result = -ENOMEM; goto fallo;
memset(memoria_buffer, 0, 1);
printk("<1>Insertando modulo\n");
return 0; fallo: cleanup_module();
return result;
Ademas reservamos espacio en memoria para el buffer de nuestro dispositivo, memoria buffer,
a traves de la funcion kmalloc, la cual es muy similar a la comun malloc. Finalmente actuamos
en consecuencia ante posibles errores al registrar el numero mayor o al reservar memoria.
26.5. El driver “memoria”: eliminando el modulo
Para eliminar el modulo, dentro de la funcion cleanup module, insertamos la funcion
unregister chrdev para liberar el numero mayor dentro del kernel.
<<memoria cleanup module>>=
void cleanup_module(void)
/* Liberamos numero mayor */
194
unregister_chrdev(memoria_major, "memoria");
/* Liberamos memoria del buffer */
if (memoria_buffer)
kfree(memoria_buffer);
printk("<1>Quitando modulo\n");
En esta subrutina tambien liberamos la memoria del buffer del dispositivo para dejar el kernel
limpio al quitar el modulo.
26.6. El driver “memoria”: abriendo el dispositivo
La funcion en el espacio de kernel correspondiente a la apertura de un fichero en el espacio
de usuario (fopen) es el miembro open: de la estructura file operations en la llamada a
registe chrdev. En este caso se trata de memoria open. Tiene como argumentos una estructura
inode que pasa informacion del kernel al driver tal como el numero mayor y el numero menor
y una estructura file con informacion relativa a las distintas operaciones que se pueden realizar
con el fichero. Ninguna de estas dos funciones las veremos en profundidad aquı. El kernel lleva
un contador de cuantas veces esta siendo utilizado un driver. El valor para cada driver se puede
ver en la ultima columna numerica del comando lsmod. Cuando se abre un dispositivo para
leer o escribir en el, la cuenta de uso se debe incrementar, tal y como aparece en la funcion
memoria open. Ademas de esta operacion, en la apertura de un fichero, se suelen iniciar las
variables pertinentes al driver y el propio dispositivo en si. En este ejemplo, y debido a su
extrema sencillez, no realizaremos operaciones de este tipo en dicha funcion.
Podemos ver la funcion memoria open a continuacion: como fichero
<<memoria open>>=
int memoria_open(struct inode *inode, struct file *filp)
/* Aumentamos la cuenta de uso */
MOD_INC_USE_COUNT; /* Exito */
return 0;
195
Bibliografıa
[1] Alonso Jose Miguel, “TCP/IP en UNIX Programacion de aplicaciones distribuidas”,
Madrid Espana, Ed. RA-MA, 1999, ISBN 970-15-0368-6.
[2] Xavier Calbet “Breve Tutorial para escribir driver en Linux”, Espana, 2001.
driver en Linux
[3] Gonzalez Morcillo Carlos, Redondo Duque Miguel Angel “Punteros en C”, 2003.
Puntero en C
[4] Francisco M. Marquez: “UNIX Programacion avanzada ”, 3a Edicion, Madrid, 2004, Ed.
RA-MA.
ISBN 84-7897-603-5.
[5] Kurt Wall: “Progrmacion en Linux ”, 2a Edicion, Madrid, 2001, Ed. Prentice Hall.
ISBN:84-205-3014-X.
[6] Tejeda Villela Hector: “Manual de C”,
[7] Santiago Domınguez: “Diapositivas de Programacion Sistemas del Curso Propedeutico de
la Maestrıa”, BUAP, 2000.
196