Dentro del núcleo Linux 2.4 <author>Tigran Aivazian <tt>tigran@veritas.com</tt> <date>23 Agosto 2001 (4 Elul 5761) <abstract> Introducción al núcleo Linux 2.4. La última copia de este documento siempre puede ser descargada desde: <url url="http://www.moses.uklinux.net/patches/lki.sgml"> Esta guía es ahora parte del Proyecto de Documentación de Linux y también puede ser descargada en varios formatos desde: <url url="http://www.linuxdoc.org/guides.html"> o puede ser leída en linea (la última versión en Inglés) en: <url url="http://www.moses.uklinux.net/patches/lki.html"> Esta documentación es software libre; puedes redistribuirla y/o modificarla bajo los términos de la GNU General Public License tal como ha sido publicada por la Free Software Foundation en la versión 2 de la Licencia, o (a tu elección) por cualquier versión posterior. El autor esta trabajando como ingeniero decano del núcleo Linux en VERITAS Software Ltd y escribió este libro con el propósito de dar soporte al pequeño entrenamiento de cursos/charlas que dió sobre este tema, internamente en VERITAS. Gracias a: Juan J. Quintela <tt>(quintela@fi.udc.es)</tt>, Francis Galiegue <tt>(fg@mandrakesoft.com)</tt>, Hakjun Mun <tt>(juniorm@orgio.net)</tt>, Matt Kraai <tt>(kraai@alumni.carnegiemellon.edu)</tt>, Nicholas Dronen <tt>(ndronen@frii.com)</tt>, Samuel S Chessman <tt>(chessman@tux.org)</tt>, Nadeem Hasan <tt>(nhasan@nadmm.com)</tt> por varias correcciones y sugerencias. El capítulo del Caché de Páginas de Linux fue escrito por Christoph Hellwig <tt>(hch@caldera.de)</tt>. El capítulo sobre los mecanismos IPC fue escrito por Russell Weight <tt>(weightr@us.ibm.com)</tt> y Mingming Cao <tt>(mcao@us.ibm.com)</tt> </abstract> <toc> <sect>Arrancando<p> <sect1>Construyendo la Imagen del Núcleo Linux<p> Esta sección explica los pasos dados durante la compilación del núcleo Linux y la salida producida en cada etapa. El proceso de construcción depende de la arquitectura, por lo tanto me gustaría enfatizar que sólo consideraremos la construcción de un núcleo Linux/x86. Cuando el usuario escribe 'make zImage' o 'make bzImage' la imagen inicializable del núcleo resultante es almacenado como <tt>arch/i386/boot/zImage</tt> o <tt>arch/i386/boot/bzImage</tt> respectivamente. Aquí está como es construida la imagen: <enum> <item> Los archivos fuente en C y ensamblador son compilados en formato de objetos reasignables ELF (.o) y algunos de ellos son agrupados lógicamente en archivos (.a) usando <bf>ar(1)</bf>. <item> Usando <bf>ld(1)</bf>, los anteriores .o y .a son enlazados en <tt>vmlinux</tt>, el cual es un fichero ejecutable ELF 32-bits LSB 80386 estáticamente enlazado al que no se le han eliminado los símbolos de depuración. <item> <tt>System.map</tt> es producido por <bf>nm vmlinux</bf>, donde los símbolos irrelevantes o que no interesan son desechados. <item> Se entra en el directorio <tt>arch/i386/boot</tt>. <item> El código de ensamblador del sector de arranque <tt>bootsect.S</tt> es preprocesado con o sin <bf>-D__BIG_KERNEL__</bf>, dependiendo de cuando el objetivo es bzImage o zImage, en <tt>bbootsect.s</tt> o <tt>bootsect.s</tt> respectivamente. <item> <tt>bbootsect.s</tt> es ensamblado y entonces convertido en la forma 'raw binary' llamada <tt>bbootsect</tt> (o <tt>bootsect.s</tt> ensamblado y convertido a raw en <tt>bootsect</tt> para zImage). <item> El código de configuración <tt>setup.S</tt> (<tt>setup.S</tt> incluye <tt>video.S</tt>) es preprocesado en <tt>bsetup.s</tt> para bzImage o <tt>setup.s</tt> para zImage. De la misma forma que el código del sector de arranque, la diferencia radica en que -<bf>D__BIG_KERNEL__</bf> está presente para bzImage. El resultado es entonces convertido en la forma 'raw binary' llamada <tt>bsetup</tt>. <item> Se entra en el directorio <tt>arch/i386/boot/compressed</tt> y se convierte <tt>/usr/src/linux/vmlinux</tt> a $tmppiggy (nombre temporal) en el formato binario raw, borrando las secciones ELF <tt>.note</tt> y <tt>.comment</tt>. <item> <bf>gzip -9 < $tmppiggy > $tmppiggy.gz</bf> <item> Se enlaza $tmppiggy.gz en ELF reasignable (<bf>ld -r</bf>) <tt>piggy.o</tt>. <item> Compila las rutinas de compresión <tt>head.S</tt> y <tt>misc.c</tt> (todavía en el directorio <tt>arch/i386/boot/compressed</tt>) en los objetos ELF <tt>head.o</tt> y <tt>misc.o</tt>. <item> Se enlazan todas ellas: <tt>head.o</tt>, <tt>misc.o</tt> y <tt>piggy.o</tt> en <tt>bvmlinux</tt> (o <tt>vmlinux</tt> para zImage, ¡no confundas esto con <tt>/usr/src/linux/vmlinux</tt>!). Destacar la diferencia entre <bf>-Ttext 0x1000</bf> usado para <tt>vmlinux</tt> y <bf>-Ttext 0x100000</bf> para <tt>bvmlinux</tt>, esto es, el cargador de compresión para bzImage es cargado más arriba. <item> Se convierte <tt>bvmlinux</tt> a 'raw binary' <tt>bvmlinux.out</tt> borrando las secciones ELF <tt>.note</tt> y <tt>.comment</tt>. <item> Se vuelve atrás al directorio <tt>arch/i386/boot</tt> y, usando el programa <bf>tools/build</bf>, se concatenan todos ellos: <tt>bbootsect</tt>, <tt>bsetup</tt> y <tt>compressed/bvmlinux.out</tt> en <tt>bzImage</tt> (borra la 'b' extra anterior para <tt>zImage</tt>). Esto escribe variables importantes como <tt>setup_sects</tt> y <tt>root_dev</tt> al final del sector de arranque. </enum> El tamaño del sector de arranque es siempre de 512 bytes. El tamaño de la configuración debe ser mayor que 4 sectores, pero está limitado superiormente sobre los 12k - la regla es: 0x4000 bytes >= 512 + sectores_configuración * 512 + espacio para la pila mientras está funcionando el sector de arranque/configuración Veremos más tarde de dónde viene esta limitación. El límite superior en el tamaño de bzImage producido en este paso está sobre los 2.5M para arrancarcon LILO y 0xFFFF párrafos ((0xFFFF0 = 1048560 bytes) para arrancar imágenes directamentee, por ejemplo desde un diskette o CD-ROM (con el modo de emulación El-Torito). Destacar que mientras que <bf>tools/build</bf> valida el tamaño del sector de arranque, la imagen del núcleo y el límite inferior del tamaño de la configuración, no chequea el límite *superior* de dicho tamaño de configuración. Entonces, es fácil construir un núcleo defectuoso justamente sumándole algún gran ".espacio" al final de <tt>setup.S</tt>. <sect1>Arrancando: Vista General<p> Los detalles del proceso de arranque son específicos de cada arquitectura, por lo tanto centraremos nuestra atención en la arquitectura IBM PC/IA32. Debido al diseño antiguo y a la compatibilidad hacia atrás, el firmware del PC arranca el sistema operativo a la vieja usanza. Este proceso puede ser separado en las siguientes seis etapas lógicas: <enum> <item> La BIOS selecciona el dispositivo de arranque. <item> La BIOS carga el sector de arranque del dispositivo de arranque. <item> El sector de arranque carga la configuración, las rutinas de descompresión y la imagen del núcleo comprimida. <item> El núcleo es descomprimido en modo protegido. <item> La inicialización de bajo nivel es realizada por el código ensamblador. <item> Inicialización de alto nivel en C. </enum> <sect1>Arrancando: BIOS POST<p> <enum> <item> La fuente de alimentación inicia el generador de reloj y aserta la señal #POWERGOOD en el bus. <item> La linea CPU #RESET es asertada (CPU está ahora en modo real 8086). <item> %ds=%es=%fs=%gs=%ss=0, %cs=0xFFFF0000,%eip = 0x0000FFF0 (código ROM BIOS POST). <item> Todos los chequeos POST son realizados con las interrupciones deshabilitadas. <item> La TVI (Tabla de Vectores de Interrupción) es inicializada en la dirección 0. <item> La función de la BIOS de cargador de la rutina de arranque es llamada a través de la <bf>int0x19</bf>, con %dl conteniendo el dispositivo de arranque 'número de controladora'. Esto carga la pista 0, sector 1 en la dirección física 0x7C00 (0x07C0:0000). </enum> <sect1>Arrancando: sector de arranque y configuración<p> El sector de arranque usado para arrancar el núcleo Linux puede ser uno de los siguientes: <itemize> <item> Sector de arranque de Linux (<tt>arch/i386/boot/bootsect.S</tt>), <item> Sector de arranque de LILO (u otros cargadores de arranque) o <item> sin sector de arranque (loadlin, etc) </itemize> Consideraremos aquí el sector de arranque de Linux en detalle. Las primeras lineas inicializan las macros convenientes para ser usadas por los valores de segmento: <tscreen><code> 29 SETUPSECS = 4 /* tamaño por defecto de los sectores de configuración */ 30 BOOTSEG = 0x07C0 /* dirección original del sector de arranque */ 31 INITSEG = DEF_INITSEG /* movemos el arranque aquí - lejos del camino */ 32 SETUPSEG = DEF_SETUPSEG /* la configuración empieza aquí */ 33 SYSSEG = DEF_SYSSEG /* el sistema es cargado en 0x10000 (65536) */ 34 SYSSIZE = DEF_SYSSIZE /* tamaño del sistema: # de palabras de 16 bits */ </code></tscreen> (los números a la izquierda son los números de linea del archivo bootsect.S) Los valores de <tt>DEF_INITSEG</tt>, <tt>DEF_SETUPSEG</tt>, <tt>DEF_SYSSEG</tt> y <tt>DEF_SYSSIZE</tt> son tomados desde <tt>include/asm/boot.h</tt>: <tscreen><code> /* No toques esto, a menos que realmente sepas lo que estás haciendo. */ #define DEF_INITSEG 0x9000 #define DEF_SYSSEG 0x1000 #define DEF_SETUPSEG 0x9020 #define DEF_SYSSIZE 0x7F00 </code></tscreen> Ahora, consideremos el código actual de <tt>bootsect.S</tt>: <tscreen><code> 54 movw $BOOTSEG, %ax 55 movw %ax, %ds 56 movw $INITSEG, %ax 57 movw %ax, %es 58 movw $256, %cx 59 subw %si, %si 60 subw %di, %di 61 cld 62 rep 63 movsw 64 ljmp $INITSEG, $go 65 # bde - cambiado 0xff00 a 0x4000 para usar el depurador después de 0x6400 (bde). 66 # No tendríamos que preocuparnos por esto si chequeamos el límite superior 67 # de la memoria. También mi BIOS puede ser configurada para poner las tablas 68 # wini de controladoras en la memoria alta en vez de en la tabla de vectores. 69 # La vieja pila quizás tenga que ser insertada en la tabla de controladores. 70 go: movw $0x4000-12, %di # 0x4000 es un valor arbitrario >= 71 # longitud de sector de arranque + longitud de la 72 # configuración + espacio para la pila; 73 # 12 es el tamaño parm del disco. 74 movw %ax, %ds # ax y es ya contienen INITSEG 75 movw %ax, %ss 76 movw %di, %sp # pone la pila en INITSEG:0x4000-12. </code></tscreen> Las lineas 54-63, mueven el código del sector de arranque desde la dirección 0x7C00 a 0x90000. Esto es realizado de la siguiente manera: <enum> <item> establece %ds:%si a $BOOTSEG:0 (0x7C0:0 = 0x7C00) <item> establece %es:%di a $INITSEG:0 (0x9000:0 = 0x90000) <item> establece el número de palabras de 16 bits en %cx (256 palabras = 512 bytes = 1 sector) <item> limpia la bandera DF (dirección) en EFLAGS a direcciones auto-incrementales (cld) <item> va allí y copia 512 bytes (rep movsw) </enum> El motivo por el que este código no usa <tt>rep movsd</tt> es intencionado (hint - .code16). La línea 64 salta a la etiqueta <tt>go:</tt> en la nueva copia hecha del sector de arranque, esto es, en el segmento 0x9000. Esto y las tres instruciones siguientes (lineas 64-76) preparan la pila en $INITSEG:0x4000-0xC, esto es, %ss = $INITSEG (0x9000) y %sp = 0x3FF4 (0x4000-0xC). Aquí es de dónde viene el límite del tamaño de la configuración que mencionamos antes (ver Construyendo la Imagen del Núcleo Linux). Las lineas 77-103 parchean la tabla de parámetros del disco para el primer disco para permitir lecturas multi-sector: <tscreen><code> 77 # Las tablas por defecto de parámetros del disco de muchas BIOS 78 # no reconocerán lecturas multi-sector más allá del número máximo especificado 79 # en las tablas de parámetros del diskette por defecto - esto 80 # quizás signifique 7 sectores en algunos casos. 82 # Como que las lecturas simples de sectores son lentas y fuera de la cuestión 83 # tenemos que tener cuidado con esto creando nuevas tablas de parámetros 84 # (para el primer disco) en la RAM. Estableceremos la cuenta máxima de sectores 85 # a 36 - el máximo que encontraremos en un ED 2.88. 86 # 87 # Lo grande no hace daño. Lo pequeño si. 88 # 89 # Los segmentos son como sigue: ds = es = ss = cs - INITSEG, fs = 0, 90 # y gs queda sin usar. 91 movw %cx, %fs # establece fs a 0 92 movw $0x78, %bx # fs:bx es la dirección de la tabla de parámetros 93 pushw %ds 94 ldsw %fs:(%bx), %si # ds:si es el código 95 movb $6, %cl # copia 12 bytes 96 pushw %di # di = 0x4000-12. 97 rep # no necesita cld -> hecho en la linea 66 98 movsw 99 popw %di 100 popw %ds 101 movb $36, 0x4(%di) # parchea el contador de sectores 102 movw %di, %fs:(%bx) 103 movw %es, %fs:2(%bx) </code></tscreen> El controlador de diskettes es reinicializado usando el servicio de la BIOS int 0x13 función 0 (reinicializa FDC) y los sectores de configuración son cargados inmediatamente después del sector de arranque, esto es, en la dirección física 0x90200 ($INITSEG:0x200), otra vez usando el servicio de la BIOS int 0x13, función 2 (leer sector(es)). Esto sucede durante las lineas 107-124: <tscreen><code> 107 load_setup: 108 xorb %ah, %ah # reinicializa FDC 109 xorb %dl, %dl 110 int $0x13 111 xorw %dx, %dx # controladora 0, cabeza 0 112 movb $0x02, %cl # sector 2, pista 0 113 movw $0x0200, %bx # dirección = 512, en INITSEG 114 movb $0x02, %ah # servicio 2, "leer sector(es)" 115 movb setup_sects, %al # (asume todos en la cabeza 0, pista 0) 116 int $0x13 # los lee 117 jnc ok_load_setup # ok - continua 118 pushw %ax # vuelca el código de error 119 call print_nl 120 movw %sp, %bp 121 call print_hex 122 popw %ax 123 jmp load_setup 124 ok_load_setup: </code></tscreen> Si la carga falla por alguna razón (floppy defectuoso o que alguien quitó el diskette durante la operación), volcamos el código de error y se intenta en un bucle infinito. La única forma de salir de él es reiniciando la máquina, a menos que los reintentos tengan éxito, pero usualmente no lo tienen (si algo está mal sólo se pondrá peor). Si la carga de los sectores setup_sects del código de configuración es realizada con éxito, saltamos a la etiqueta <tt>ok_load_setup:</tt>. Entonces procedemos a cargar la imagen comprimida del núcleo en la dirección física 0x10000. Esto es realizado para preservar las áreas de datos del firmware en la memoria baja (0-64K). Después de que es cargado el núcleo, saltamos a $SETUPSEG:0(<tt>arch/i386/boot/setup.S</tt>). Una vez que los datos no se necesitan mas (ej. no se realizan más llamadas a la BIOS) es sobreescrito moviendo la imagen entera (comprimida) del núcleo desde 0x10000 a 0x1000 (direcciones físicas, por supuesto). Esto es hecho por <tt>setup.S</tt>, el cual prepara las cosas para el modo protegido y salta a 0x1000, que es el comienzo del núcleo comprimido, esto es, <tt>arch/386/boot/compressed/{head.S,misc.c}</tt>. Esto inicializa la pila y llama a <tt>decompress_kernel()</tt>, que descomprime el núcleo en la dirección 0x100000 y salta a ella. Destacar que los viejos cargadores de arranque (viejas versiones de LILO) sólo podían cargar los 4 primeros sectores de la configuración, el cual es el motivo por el que existe código en la configuración para cargar el resto de si mismo si se necesita. También, el código en la configuración tiene que tener cuidado de varias combinaciones de tipo/versión del cargador vs zImage/bzImage y esto es altamente complejo. Examinemos este truco en el código del sector de arranque que nos permite cargar un núcleo grande, también conocido como "bzImage". Los sectores de configuración son cargados usualmente en la dirección 0x90200, pero el núcleo es cargado en fragmentos de 64k cada vez usando una rutina de ayuda especial que llama a la BIOS para mover datos desde la memoria baja a la memoria alta. Esta rutina de ayuda es referida por <tt>bootsect_kludge</tt> en <tt>bootsect.S</tt> y es definida como <tt>bootsect_helper</tt> en <tt>setup.S</tt>. La etiqueta <tt>bootsect_kludge</tt> en <tt>setup.S</tt> contiene el valor del segmento de configuración y el desplazamiento del código <tt>bootsect_helper</tt> en él, por lo que el sector de arranque puede usar la instrucción <tt>lcall</tt> para saltar a él (salto entre segmentos). El motivo por lo cual esto es realizado en <tt>setup.S</tt> es simplemente porque no existe más espacio libre en bootsect.S (lo cual no es estrictamente verdad - hay aproximadamente 4 bytes dispersos y al menos 1 byte disperso en <tt>bootsect.S</tt>, pero que obviamente no es suficiente). Esta rutina usa el servicio de la BIOS int 0x15 (ax=0x8700) para moverlo a la memoria alta y restablecer %es al punto de siempre 0x10000. Esto asegura que el código en <tt>bootsect.S</tt> no se va fuera de memoria cuando está copiando datos desde disco. <sect1> Usando LILO como cargador de arranque <p> Existen varias ventajas en usar un cargador de arranque especializado (LILO) sobre un esqueleto desnudo de un sector de arranque: <enum> <item> Habilidad para escoger entre varios núcleos Linux o incluso múltiples Sistemas Operativos. <item> Habilidad para pasar parámetros a la linea de comandos del núcleo (existe un parche llamado BCP que añade esta habilidad al esqueleto desnudo de sector de arranque + configuración). <item> Habilidad para cargar núcleos bzImage más grandes - hasta los 2.5M vs 1M. </enum> Viejas versiones de LILO (v17 y anteriores) no podían cargar núcleos bzImage. Las versiones más nuevas (como las de hace un par de años y posteriores) usan la misma técnica que el sector de arranque + configuración de mover datos desde la memoria baja a la memoria alta mediante los servicios de la BIOS. Alguna gente (notablemente Peter Anvin) argumentan que el soporte para zImage debería de ser quitado. El motivo principal (de acuerdo con Alan Cox) para que permanezca es que aparentemente existen algunas BIOS defectuosas que hacen imposible arrancar núcleos bzImage, mientras que la carga de núcleos zImage se realiza correctamente. La última cosa que hace LILO es saltar a <tt>setup.S</tt> y entonces las cosas prosiguen de la forma normal. <sect1> Inicialización de Alto Nivel <p> Por "Inicialización de Alto Nivel" consideramos cualquier cosa que no está directamente relacionada con la fase de arranque, incluso aquellas partes del código que están escritas en ensamblador, esto es <tt>arch/i386/kernel/head.S</tt>, que es el comienzo del núcleo descomprimido. Los siguientes pasos son realizados: <enum> <item> Inicializa los valores de segmento (%ds = %es = %fs = %gs = __KERNEL_DS = 0x18). <item> Inicializa las tablas de páginas. <item> Habilita el paginamiento estableciendo el bit PG en %cr0. <item> Limpia a cero BSS (en SMP, sólo la primera CPU realiza esto). <item> Copia los primeros 2k de los parámetros de arranque (linea de comandos del núcleo). <item> Chequea el tipo de CPU usando EFLAGS y, si es posible, cpuid, capaz de detectar 386 y superiores. <item> La primera CPU llama a <tt>start_kernel()</tt>,y si ready=1 todas las otras llaman a <tt>arch/i386/kernel/smpboot.c:initialize_secondary()</tt> el cual recarga esp/eip y no retorna. </enum> La función <tt>init/main.c:start_kernel()</tt> está escrita en C y realiza lo siguiente: <enum> <item> Realiza un cierre global del núcleo (es necesario para que sólo una CPU realice la inicialización). <item> Realiza configuraciones específicas de la arquitectura (análisis de la capa de memoria, copia de la linea de comandos de arranque otra vez, etc.). <item> Muestra el "anuncio" del núcleo Linux conteniendo la versión, el compilador usado para construirlo, etc ..., a la memoria intermedia con forma de anillo del núcleo para los mensajes. Esto es tomado desde la variable linux_banner definida en init/version.c y es la misma cadena mostrada por <bf>cat /proc/version</bf>. <item> Inicializa las traps. <item> Inicializa las irqs. <item> Inicializa los datos requeridos por el planificador (scheduler). <item> Inicializa el tiempo manteniendo los datos. <item> Inicializa el subsistema softirq. <item> Analiza las opciones del arranque de la linea de comandos. <item> Inicializa la consola. <item> Si el soporte para módulos ha sido compilado en el núcleo, inicializa la facilidad para la carga dinámica de módulos. <item> Si la linea de comandos "profile=" ha sido suministrada, inicializa los perfiles de memoria intermedia. <item> <tt>kmem_cache_init()</tt>, inicializa la mayoría del asignador slab. <item> Habilita las interrupciones. <item> Calcula el valor BogoMips para esta CPU. <item> Llama a <tt>mem_init()</tt>, que calcula <tt>max_mapnr</tt>, <tt>totalram_pages</tt> y <tt>high_memory</tt> y muestra la linea "Memory: ...". <item> <tt>kmem_cache_sizes_init()</tt>, finaliza la inicialización del asignador slab. <item> Inicializa las estructuras de datos usadas por procfs. <item> <tt>fork_init()</tt>, crea <tt>uid_cache</tt>, inicializa <tt>max_threx_threads</tt> basándose en la cantidad de memoria disponible y configura <tt>RLIMIT_NPROC</tt> para que <tt>init_task</tt> sea <tt>max_threads/2</tt>. <item> Crea varias antememorias slab necesitadas para VFS, VM, la antememoria intermedia, etc. <item> Si el soporte para System V IPC ha sido compilado en el núcleo, inicializa el subsistema. Nótese que para System V shm, esto incluye el montaje de una instancia (dentro del núcleo) del sistema de archivos shmfs. <item> Si el soporte de quota ha sido compilado en el núcleo, crea e inicializa una antememoria slab especial para él. <item> Realiza "chequeos de fallos" específicos de la arquitectura y, cuando es posible, activa las correcciones para los fallos de procesadores/bus/etc. Comparando varias arquitecturas vemos que "ia64 no tiene fallos" e "ia32 tiene unos pocos". Un buen ejemplo es el "fallo f00f", el cual es sólo chequeado si el núcleo ha sido compilado para menos de un 686 y corregido adecuadamente. <item> Establece una bandera para indicar que un planificador debería de ser llamado en la "siguiente oportunidad" y crea un hilo del núcleo <tt>init()</tt> que ejecuta execute_command si este ha sido suministrado a través del parámetro de inicio "init=", o intenta ejecutar <bf>/sbin/init</bf>, <bf>/etc/init</bf>, <bf>/bin/init</bf>, <bf>/bin/sh</bf> en este orden; si todos estos fallan, ocurre una situación de pánico con la "sugerencia" de usar el parámetro "init=". <item> Se va a un bucle vacío, este es un hilo vacío con pid=0. </enum> Una cosa importante que hay que hacer notar aquí es que el hilo del núcleo <tt>init()</tt> llama a <tt>do_basic_setup()</tt>, el cual cuando vuelve llama a <tt>do_initcalls()</tt>, que va a través de la lista de funciones registradas por medio de las macros <tt>__initcall</tt> o <tt>module_init()</tt> y las invoca. Estas funciones no dependen de otras o sus dependencias han sido manualmente arregladas por el orden de enlazado en los Makefiles. Esto significa que, dependiendo de las posición de los directorios en los árboles y de las estructuras en los Makefiles, el orden en el cual estas funciones de inicialización son llamadas puede cambiar. A veces esto es importante, imagínate dos subsistemas A y B, con B dependiendo de alguna inicialización realizada por A. Si A es compilada estáticamente y B es un módulo entonces el punto de entrada de B está garantizado para ser llamado después de que A prepare todo el entorno necesario. Si A es un módulo, entonces B es también necesariamente un módulo para que no existan problemas. Pero, ¿qué pasa si A y B están estáticamente enlazadas en el núcleo? El orden en el cual son llamadas depende del desplazamiento relativo del punto de entrada en la sección ELF <tt>.initcall.init</tt> de la imagen del núcleo. Rogier Wolff propuso introducir una infraestructura jerárquica de "prioridades" donde los módulos pueden dejar que el enlazador conozca en que orden (relativo) deberían de ser enlazados, pero todavía no existen parches disponibles que implementen esto de una forma suficientemente elegante para ser aceptada en el núcleo. Por consiguiente, asegúrate de que el orden de enlace es correcto, Si, en el ejemplo anterior, A y B trabajan bien cuando han sido compilados estáticamente una vez, trabajarán siempre, tal como han sido listados secuencialmente en el mismo Makefile. Si no trabajan, cambia el orden en el cual sus archivos objetos son listados. Otra cosa de algún valor es la habilidad de Linux de ejecutar un "programa init alternativo" por medio del pase de la linea de comandos "init=". Esto es útil para la recuperación desde un <bf>/sbin/init</bf> accidentalmente sobreescrito o para depurar a mano los guiones de inicialización (rc) y <tt>/etc/inittab</tt>, ejecutándolos de uno en uno. <sect1>Arranque SMP en x86<p> En SMP, el BP (Procesador de arranque) va a través de la secuencia normal del sector de arranque, configuración, etc... hasta que llega a <tt>start_kernel()</tt>, y entonces sobre <tt>smp_init()</tt> y especialmente <tt>src/i386/kernel/smpboot.c:smp_boot_cpus()</tt>. La función <tt>smp_boot_cpus()</tt> entra en un buche para cada apicid (identificador de cada APIC), hasta <tt>NR_CPUS</tt>, y llama a <tt>do_boot_cpu()</tt> en él. Lo que hace <tt>do_boot_cpu()</tt> es crear (esto es: <tt>fork_by_hand</tt>) una tarea vacía para la cpu de destino y escribe en localizaciones bien conocidas definidas por la especificación Intel MP (0x467/0x469) el EIP del código del trampolín encontrado en <tt>trampoline.S</tt>. Entonces genera STARTUP IPI a la cpu de destino la cual hace que este AP (Procesador de Aplicación) ejecute el código en <tt>trampoline.S</tt>. La CPU de arranque crea una copia del código trampolín para cada CPU en la memoria baja. El código del AP escribe un número mágico en su propio código, el cual es verificado por el BP para asegurarse que el AP está ejecutando el código trampolín. El requerimiento de que el código trampolín tenga que estar en la memoria baja es forzado por la especificación Intel MP. El código trampolín simplemente establece el registro %bx a uno, entra en modo protegido y salta a startup_32, que es la entrada principal a <tt>arch/i386/kernel/head.S</tt>. Ahora, el AP empieza ejecutando <tt>head.S</tt> y descubriendo que no es un BP, se salta el código que limpia BSS y entonces entra en <tt>initialize_secondary()</tt>, el cual justamente entra en la tarea vacía para esta CPU - recalcar que <tt>init_tasks[cpu]</tt> ya había sido inicializada por el BP ejecutando <tt>do_boot_cpu(cpu)</tt>. Destacar que init_task puede ser compartido, pero cada hilo vacío debe de tener su propio TSS. Este es el motivo por el que <tt>init_tss[NR_CPUS]</tt> es una array. <sect1>Liberando datos y código de inicialización<p> Cuando el sistema operativo se inicializa a si mismo, la mayoría del código y estructuras de datos no se necesitarán otra vez. La mayoría de los sistemas operativos (BSD, FreeBSD, etc.) no pueden deshacerse de esta información innecesaria, gastando entonces valiosa memoria física del núcleo. El motivo que ellos no lo realizan (ver el libro de McKusick 4.4BSD) es que "el código relevante está propagado a través de varios subsistemas y por lo tanto no es factible liberarlo". Linux, por supuesto, no puede usar tal escusa porque bajo Linux "si en principio algo es posible, entonces ya está implementado o alguien está trabajando en ello". Por lo tanto, como he dicho anteriormente, el núcleo Linux sólo puede ser compilado como un binario ELF, y ahora adivinamos el motivo (o uno de los motivos) para ello. El motivo referente a deshechar el código/datos de inicialización es que Linux suministra dos macros para ser usadas: <itemize> <item> <tt>__init</tt> - para el código de inicialización <item> <tt>__initdata</tt> - para datos </itemize> Estas evalúan al atributo especificador gcc (también conocido como "gcc magic") tal como ha sido definido en <tt>include/linux/init.h</tt>: <tscreen><code> #ifndef MODULE #define __init __attribute__ ((__section__ (".text.init"))) #define __initdata __attribute__ ((__section__ (".data.init"))) #else #define __init #define __initdata #endif </code></tscreen> Lo que esto significa es que si el código es compilado estáticamente en el núcleo (MODULO no está definido), entonces es colocado en la sección especial ELF <tt>.text.init</tt>, el cual es declarado en el mapa del enlazado en <tt>arch/i386/vmlinux.lds</tt>. En caso contrario (si es un módulo) las macros no evalúan nada. Lo que pasa durante el arranque es que el hilo del núcleo "init" (función <tt>init/main.c:init()</tt>) llama a la función específica de la arquitectura <tt>free_initmem()</tt> la cual libera todas las páginas entre las direcciones <tt>__init_begin</tt> e <tt>__init_end</tt>. En un sistema típico (mi estación de trabajo), esto resulta en la liberación de unos 260K de memoria. Las funciones registradas a través de <tt>module_init()</tt> son colocadas en <tt>.initcall.init</tt> el cual es también liberado en el caso estático. La actual tendencia en Linux, cuando se está diseñando un subsistema (no necesariamente un módulo), es suministrar puntos de entrada init/exit desde las etapas tempranas del diseño para que en el futuro, el subsistema en cuestión, pueda ser modularizado si se necesita. Un ejemplo de esto es pipefs, ver <tt>fs/pipe.c</tt>. Incluso si un subsistema nunca fuese convertido a módulo, ej. bdflush (ver <tt>fs/buffer.c</tt>), aún es bonito y arreglado usar la macro <tt>module_init()</tt> contra su función de inicialización, suministrada aunque no haga nada cuando la función es precisamente llamada. Hay dos macros más, las cuales trabajan de una manera similar, llamadas <tt>__exit</tt> y <tt>__exitdata</tt>, pero ellas están más directamente conectadas al soporte de módulos por lo que serán explicadas en una sección posterior. <sect1>Procesando la linea de comandos del núcleo<p> Déjanos recalcar qué es lo que le pasa a la linea de comandos cuando se le pasa al núcleo durante el arranque: <enum> <item> LILO (o BCP) acepta la linea de comandos usando los servicios de teclado de la BIOS y los almacena en una localización bien conocida en la memoria física, también como una firma diciendo que allí existe una linea de comando válida. <item> <tt>arch/i386/kernel/head.S</tt> copia los primeros 2k de ella fuera de la página cero. Nótese que la actual versión de LILO (21) corta la linea de comandos a los 79 bytes. Esto es un fallo no trivial en LILO (cuando el soporte para EBDA grandes está activado) y Werner prometió arreglarlo próximamente. Si realmente necesitas pasarle lineas de comando más grandes de los 79 bytes, entonces puedes usar BCP o codificar tu linea de comandos en la función <tt>arch/i386/kernel/setup.c:parse_mem_cmdline()</tt>. <item> <tt>arch/i386/kernel/setup.c:parse_mem_cmdline()</tt> (llamada por <tt>setup_arch()</tt>, y esta llamada por <tt>start_kernel()</tt>) copia 256 bytes de la página cero a <tt>saved_command_line</tt> la cual es mostrada por <tt>/proc/cmdline</tt>. Esta misma rutina procesa la opción "mem=" si está presente y realiza los ajustes apropiados en los parámetros de la VM. <item> Volvemos a la línea de comandos en <tt>parse_options()</tt> (llamada por <tt>start_kernel()</tt>) el cual procesa algunos parámetros "dentro del núcleo" (actualmente "init=" y entorno/argumentos para init) y pasa cada palabra a <tt>checksetup()</tt>. <item> <tt>checksetup()</tt> va a través del código en la sección ELF <tt>.setup.init</tt> y llama a cada función, pasándole la palabra si corresponde. Nótese que usando el valor de retorno de 0 desde la función registrada a través de <tt>__setup()</tt>, es posible pasarle el mismo "variable=value" a más de una función con el "value" inválido a una y válido a otra. Jeff Garzik comentó: "los hackers que hacen esto son buenos :)" ¿Por qué? Porque esto es claramente específico del orden de enlazado, esto es, el enlazado del núcleo en un orden tendrá a la llamada de la funciónA antes de la funciónB y otro los tendrá en orden inverso, con el resultado dependiendo del orden. </enum> Por lo tanto, ¿cómo debemos de escribir el código que procesa la linea de comandos del arranque? Nosotros usamos la macro <tt>__setup()</tt> definida en <tt>include/linux/init.h</tt>: <tscreen><code> /* * Usado por la configuración de parámetros de la línea de comandos * del núcleo */ struct kernel_param { const char *str; int (*setup_func)(char *); }; extern struct kernel_param __setup_start, __setup_end; #ifndef MODULE #define __setup(str, fn) \ static char __setup_str_##fn[] __initdata = str; \ static struct kernel_param __setup_##fn __initsetup = \ { __setup_str_##fn, fn } #else #define __setup(str,func) /* nada */ endif </code></tscreen> Por lo tanto, típicamente la usarás en tu código de esta forma (tomado del código del controlador real, BusLogic HBA <tt>drivers/scsi/BusLogic.c</tt>): <tscreen><code> static int __init BusLogic_Setup(char *str) { int ints[3]; (void)get_options(str, ARRAY_SIZE(ints), ints); if (ints[0] != 0) { BusLogic_Error("BusLogic: Obsolete Command Line Entry " "Format Ignored\n", NULL); return 0; } if (str == NULL || *str == '\0') return 0; return BusLogic_ParseDriverOptions(str); } __setup("BusLogic=", BusLogic_Setup); </code></tscreen> Destacar que <tt>__setup()</tt> no hace nada por los módulos, por lo tanto el código que quiere procesar la linea de comandos del arranque, que puede ser un módulo o estar estáticamente enlazado, debe de ser llamado pasándole la función manualmente en la rutina de inicialización del módulo. Esto también significa que es posible escribir código que procese los parámetros cuando es compilado como un módulo pero no cuando es estático o viceversa. <sect>Procesos y Manejo de Interrupciones<p> <sect1>Estructura de Tareas y Tabla de Procesos<p> Cada proceso bajo Linux es dinámicamente asignado a una estructura <tt>struct task_struct</tt>. El número máximo de procesos que pueden ser creados bajo Linux está solamente limitado por la cantidad de memoria física presente, y es igual a (ver <tt>kernel/fork.c:fork_init()</tt>): <tscreen><code> /* * El número máximo por defecto de hilos es establecido * a un valor seguro: las estructuras de hilos pueden ocupar al * menos la mitad de la memoria. */ max_threads = mempages / (THREAD_SIZE/PAGE_SIZE) / 2; </code></tscreen> lo cual, en la arquitectura IA32, básicamente significa <tt>num_physpages/4</tt>. Como ejemplo, en una máquina de 512M, puedes crear 32k de hilos. Esto es una mejora considerable sobre el límite de 4k-epsilon para los núcleos viejos (2.2 y anteriores). Es más, esto puede ser cambiado en tiempo de ejecución usando el KERN_MAX_THREADS <bf>sysctl(2)</bf>, o simplemente usando la interfaz procfs para el ajuste del núcleo: <tscreen><code> # cat /proc/sys/kernel/threads-max 32764 # echo 100000 > /proc/sys/kernel/threads-max # cat /proc/sys/kernel/threads-max 100000 # gdb -q vmlinux /proc/kcore Core was generated by `BOOT_IMAGE=240ac18 ro root=306 video=matrox:vesa:0x118'. #0 0x0 in ?? () (gdb) p max_threads $1 = 100000 </code></tscreen> El conjunto de procesos en el sistema Linux está representado como una colección de estructuras <tt>struct task_struct</tt>, las cuales están enlazadas de dos formas: <enum> <item> como una tabla hash, ordenados por el pid, y <item> como una lista circular doblemente enlazada usando los punteros <tt>p->next_task</tt> y <tt>p->prev_task</tt>. </enum> La tabla hash es llamada <tt>pidhash[]</tt> y está definida en <tt>include/linux/sched.h</tt>: <tscreen><code> /* PID hashing. (¿no debería de ser dinámico?) */ #define PIDHASH_SZ (4096 >> 2) extern struct task_struct *pidhash[PIDHASH_SZ]; #define pid_hashfn(x) ((((x) >> 8) ^ (x)) & (PIDHASH_SZ - 1)) </code></tscreen> Las tareas son ordenadas por su valor pid y la posterior función de ordenación se supone que distribuye los elementos uniformemente en sus dominios de (<tt>0</tt> a <tt>PID_MAX-1</tt>). La tabla hash es usada para encontrar rápidamente una tarea por su pid usando <tt>find_task_pid()</tt> dentro de <tt>include/linux/sched.h</tt>: <tscreen><code> static inline struct task_struct *find_task_by_pid(int pid) { struct task_struct *p, **htable = &pidhash[pid_hashfn(pid)]; for(p = *htable; p && p->pid != pid; p = p->pidhash_next) ; return p; } </code></tscreen> Las tareas en cada lista ordenada (esto es, ordenadas por el mismo valor) son enlazadas por <tt>p->pidhash_next/pidhash_pprev</tt> el cual es usado por <tt>hash_pid()</tt> y <tt>unhash_pid()</tt> para insertar y quitar un proceso dado en la tabla hash. Esto es realizado bajo la protección del spinlock read/write (lectura/escritura) llamado <tt>tasklist_lock</tt> tomado para ESCRITURA. La lista circular doblemente enlazada que usa <tt>p->next_task/prev_task</tt> es mantenida para que uno pueda ir fácilmente a través de todas las tareas del sistema. Esto es realizado por la macro <tt>for_each_task()</tt> desde <tt>include/linux/sched.h</tt>: <tscreen><code> #define for_each_task(p) \ for (p = &init_task ; (p = p->next_task) != &init_task ; ) </code></tscreen> Los usuarios de <tt>for_each_task()</tt> deberían de coger la tasklist_lock para LECTURA. Destacar que <tt>for_each_task()</tt> está usando <tt>init_task</tt> para marcar el principio (y el final) de la lista - esto es seguro porque la tarea vacía (pid 0) nunca existe. Los modificadores de los procesos de la tabla hash y/o los enlaces de la tabla de procesos, notablemente <tt>fork()</tt>, <tt>exit()</tt> y <tt>ptrace()</tt>, deben de coger la <tt>tasklist_lock</tt> para ESCRITURA. El motivo por el que esto es interesante es porque los escritores deben de deshabilitar las interrupciones en la CPU local. El motivo para esto no es trivial: la función <tt>send_sigio()</tt> anda por la lista de tareas y entonces coge <tt>tasklist_lock</tt> para ESCRITURA, y esta es llamada desde <tt>kill_fasync()</tt> en el contexto de interrupciones. Este es el motivo por el que los escritores deben de deshabilitar las interrupciones mientras los lectores no lo necesitan. Ahora que entendemos cómo las estructuras <tt>task_struct</tt> son enlazadas entre ellas, déjanos examinar los miembros de <tt>task_struct</tt>. Ellos se corresponden débilmente con los miembros de las estructuras de UNIX 'struct proc' y 'struct user' combinadas entre ellas. Las otras versiones de UNIX separan la información del estado de las tareas en una parte, la cual deberá de ser mantenida en memoria residente durante todo el tiempo (llamada 'proc structure' la cual incluye el estado del proceso, información de planificación, etc.), y otra parte, la cual es solamente necesitada cuando el proceso está funcionando (llamada 'u_area' la cual incluye la tabla de descriptores de archivos, información sobre la cuota de disco etc.). El único motivo para este feo diseño es que la memoria era un recurso muy escaso. Los sistemas operativos modernos (bueno, sólo Linux por el momento, pero otros, como FreeBSD ( que parece que avanzan en esta dirección, hacia Linux) no necesitan tal separación y entonces mantienen el estado de procesos en una estructura de datos del núcleo residente en memoria durante todo el tiempo. La estructura task_struct está declarada en <tt>include/linux/sched.h</tt> y es actualmente de un tamaño de 1680 bytes. El campo de estado es declarado como: <tscreen><code> volatile long state; /* -1 no ejecutable, 0 ejecutable, >0 parado */ #define TASK_RUNNING 0 #define TASK_INTERRUPTIBLE 1 #define TASK_UNINTERRUPTIBLE 2 #define TASK_ZOMBIE 4 #define TASK_STOPPED 8 #define TASK_EXCLUSIVE 32 </code></tscreen> ¿Por qué <tt>TASK_EXCLUSIVE</tt> está definido como 32 y no cómo 16? Porque 16 fue usado por <tt>TASK_SWAPPING</tt> y me olvidé de cambiar <tt>TASK_EXCLUSIVE</tt> cuando quité todas las referencias a <tt>TASK_SWAPPING</tt> (en algún sitio en 2.3.x). La declaración <tt>volatile</tt> en <tt>p->state</tt>significa que puede ser modificada asincrónicamente (desde el manejador de interrupciones); <enum> <item><bf>TASK_RUNNING</bf>: significa que la tarea está "supuestamente" en la cola de ejecución. El motivo por lo que quizás no esté aún en la cola de ejecución es porque marcar una tarea como <tt>TASK_RUNNING</tt> y colocarla en la cola de ejecución no es atómico. Necesitarás mantener el spinlock read/write <tt>runqueue_lock</tt> en lectura para mirar en la cola de ejecución. Si lo haces, verás que cada tarea en la cola de ejecución está en el estado <tt>TASK_RUNNING</tt>. Sin embargo, la conversión no es verdad por los motivos explicados anteriormente. De una forma parecida, los controladores pueden marcarse a ellos mismos (o en realidad, en el contexto del proceso en el que están) como <tt>TASK_INTERRUPTIBLE</tt> (o <tt>TASK_UNINTERRUPTIBLE</tt>) y entonces llaman a <tt>schedule()</tt>, el cual entonces los quita de la cola de ejecución (a memos que exista una señal pendiente, en tal caso permanecen en la cola de ejecución). </item> <item><bf>TASK_INTERRUPTIBLE</bf>: significa que la tarea está durmiendo pero que puede ser despertada por una señal o por la terminación de un cronómetro.</item> <item><bf>TASK_UNINTERRUPTIBLE</bf>: lo mismo que <tt>TASK_INTERRUPTIBLE</tt>, excepto que no puede ser despertado.</item> <item><bf>TASK_ZOMBIE</bf>: tareas que han terminado pero que no tienen su estado reflejado (para <tt>wait()</tt>-ed) por el padre (natural o por adopción).</item> <item><bf>TASK_STOPPED</bf>: tarea que fue parada, por señales de control de trabajos o por <bf>ptrace(2)</bf>.</item> <item><bf>TASK_EXCLUSIVE</bf>: este no es un estado separado pero puede ser uno de <tt>TASK_INTERRUPTIBLE</tt> o <tt>TASK_UNINTERRUPTIBLE</tt>. Esto significa que cuando esta tarea está durmiendo o un una cola de espera con otras tareas, puede ser despertada sóla en vez de causar el problema de "movimiento general" despertando a todos los que están esperando.</item> </enum> Las banderas de las tareas contienen información sobre los estados de los procesos, los cuales no son mutuamente exclusivos: <tscreen><code> unsigned long flags; /* banderas para cada proceso, definidas abajo */ /* * Banderas para cada proceso */ #define PF_ALIGNWARN 0x00000001 /* Imprime mensajes de peligro de alineación */ /* No implementada todavía, solo para 486 */ #define PF_STARTING 0x00000002 /* Durante la creación */ #define PF_EXITING 0x00000004 /* Durante la destrucción */ #define PF_FORKNOEXEC 0x00000040 /* Dividido pero no ejecutado */ #define PF_SUPERPRIV 0x00000100 /* Usados privilegios de super-usuario */ #define PF_DUMPCORE 0x00000200 /* Núcleo volcado */ #define PF_SIGNALED 0x00000400 /* Asesinado por una señal */ #define PF_MEMALLOC 0x00000800 /* Asignando memoria */ #define PF_VFORK 0x00001000 /* Despertar al padre en mm_release */ #define PF_USEDFPU 0x00100000 /* La tarea usó de FPU este quantum (SMP) */ </code></tscreen> Los campos <tt>p->has_cpu</tt>, <tt>p->processor</tt>, <tt>p->counter</tt>, <tt>p->priority</tt>, <tt>p->policy</tt> y <tt>p->rt_priority</tt> son referentes al planificador y serán mirados más tarde. Los campos <tt>p->mm</tt> y <tt>p->active_mm</tt> apuntan respectivamente a la dirección del espacio del proceso descrita por la estructura <tt>mm_struct</tt> y al espacio activo de direcciones, si el proceso no tiene una verdadera (ej, hilos de núcleo). Esto ayuda a minimizar las descargas TLB en los intercambios del espacio de direcciones cuando la tarea es descargada. Por lo tanto, si nosotros estamos planificando el hilo del núcleo (el cual no tiene <tt>p->mm</tt>) entonces su <tt>next->active_mm</tt> será establecido al <tt>prev->active_mm</tt> de la tarea que fue descargada, la cual será la misma que <tt>prev->mm</tt> si <tt>prev->mm != NULL</tt>. El espacio de direcciones puede ser compartido entre hilos si la bandera <tt>CLONE_VM</tt> es pasada a las llamadas al sistema <bf>clone(2)</bf> o <bf>vfork(2)</bf>. Los campos <tt>p->exec_domain</tt> y <tt>p->personality</tt> se refieren a la personalidad de la tarea, esto es, la forma en que ciertas llamadas al sistema se comportan para emular la "personalidad" de tipos externos de UNIX. El campo <tt>p->fs</tt> contiene información sobre el sistema de archivos, lo cual significa bajo Linux tres partes de información: <enum> <item>dentry del directorio raíz y punto de montaje, <item>dentry de un directorio raíz alternativo y punto de montaje, <item>dentry del directorio de trabajo actual y punto de montaje. </enum> Esta estructura también incluye un contador de referencia porque puede ser compartido entre tareas clonadas cuando la bandera <tt>CLONE_FS</tt> es pasada a la llamada al sistema <bf>clone(2)</bf>. El campo <tt>p->files</tt> contiene la tabla de descriptores de ficheros. Esto también puede ser compartido entre tareas, suministrando <tt>CLONE_FILES</tt> el cual es especificado con <bf>clone(2)</bf>. El campo <tt>p->sig</tt> contiene los manejadores de señales y puede ser compartido entre tareas clonadas por medio de <tt>CLONE_SIGHAND</tt>. <sect1>Creación y terminación de tareas e hilos del núcleo<p> Diferentes libros sobre sistemas operativos definen un "proceso" de diferentes formas, empezando por "instancia de un programa en ejecución" y finalizando con "lo que es producido por las llamadas del sistema clone(2) o fork(2)". Bajo Linux, hay tres clases de procesos: <itemize> <item> el/los hilo(s) vacío(s), <item> hilos del núcleo, <item> tareas de usuario. </itemize> El hilo vacío es creado en tiempo de compilación para la primera CPU; es entonces creado "manualmente" para cada CPU por medio de la función específica de la arquitectura <tt>fork_by_hand()</tt> en <tt>arch/i386/kernel/smpboot.c</tt>, el cual desenrolla la llamada al sistema <bf>fork(2)</bf> a mano (en algunas arquitecturas). Las tareas vacías comparten una estructura init_task pero tienen una estructura privada TSS, en la matriz de cada CPU <tt>init_tss</tt>. Todas las tareas vacías tienen pid = 0 y ninguna otra tarea puede compartir el pid, esto es, usar la bandera <tt>CLONE_PID</tt> en <bf>clone(2)</bf>. Los hilos del núcleo son creados usando la función <tt>kernel_thread()</tt> la cual invoca a la llamada al sistema <bf>clone(2)</bf> en modo núcleo. Los hilos del núcleo usualmente no tienen espacio de direcciones de usuario, esto es <tt>p->mm = NULL</tt>, porque ellos explicitamente hacen <tt>exit_mm()</tt>, ej. a través de la función <tt>daemonize()</tt>. Los hilos del núcleo siempre pueden acceder al espacio de direcciones del núcleo directamente. Ellos son asignados a números pid en el rango bajo. Funcionando en el anillo del procesador 0 (en x86) implica que los hilos del núcleo disfrutan de todos los privilegios de E/S y no pueden ser pre-desocupados por el planificador. Las tareas de usuario son creadas por medio de las llamadas al sistema <bf>clone(2)</bf> o <bf>fork(2)</bf>, las cuales internamente invocan a <bf>kernel/fork.c:do_fork()</bf>. Déjenos entender qué pasa cuando un proceso de usuario realiza una llamada al sistema <bf>fork(2)</bf>. Como <bf>fork(2)</bf> es dependiente de la arquitectura debido a las diferentes formas de pasar la pila y registros de usuario, la actual función subyacente <tt>do_fork()</tt> que hace el trabajo es portable y está localizada en <tt>kernel/fork.c</tt>. Los siguientes pasos son realizados: <enum> <item> La variable local <tt>retval</tt> es establecida a <tt>-ENOMEM</tt>, ya que este es el valor al que <tt>errno</tt> debería de ser establecida si <bf>fork(2)</bf> falla al asignar una nueva estructura de tarea. <item> Si <tt>CLONE_PID</tt> es establecido en <tt>clone_flags</tt> entonces devuelve un error (<tt>-EPERM</tt>), a menos que el llamante sea el hilo vacío (sólo durante el arranque). Por lo tanto, un hilo de un usuario normal no puede pasar <tt>CLONE_PID</tt> a <bf>clone(2)</bf> y esperar que tenga éxito. Para <bf>fork(2)</bf>, es irrelevante que <tt>clone_flags</tt> sea establecido a <tt>SIFCHLD</tt> - esto es sólo relevante cuando <tt>do_fork()</tt> es invocado desde <tt>sys_clone()</tt> el cual pasa <tt>clone_flags</tt> desde el valor pedido desde el espacio de usuario. <item> <tt>current->vfork_sem</tt> es inicializado (es más tarde limpiado en el hijo). Esto es usado por <tt>sys_vfork()</tt> (la llamada al sistema <bf>vfork(2)</bf> corresponde a <tt>clone_flags = CLONE_VFORK|CLONE_VM|SIGCHLD</tt>) para hacer que el padre duerma mientras el hijo hace <tt>mm_release()</tt>, por ejemplo como resultado de <tt>exec()</tt> (ejecutar) otro programa o <bf>exit(2)</bf>. <item> Una nueva estructura de tarea es asignada usando la macro dependiente de la arquitectura <tt>alloc_task_struct()</tt>. En x86 es justo un gfp a la prioridad <tt>GFP_KERNEL</tt>. Este es el primer motivo por el que la llamada <bf>fork(2)</bf> quizás duerma. Si la primera asignación falla, devolvemos <tt>-ENOMEM</tt>. <item> Todos los valores de la actual estructura de tareas del proceso son copiadas en la nueva, usando un asignamiento de estructura <tt>*p = *current</tt>. ¿Quizás debería de ser reemplazada por un establecimiento de memoria? Más tarde, los campos que no deberían de ser heredados por el hijo son establecidos a los valores correctos. <item> Un gran cierre del núcleo es tomado durante el resto del código ya que en otro caso sería no reentrante. <item> Si el padre no tiene recursos de usuario (un concepto de UID, Linux es suficientemente flexible para hacer de ellos una cuestión mejor que un hecho), entonces verifica si el límite blando de usuario <tt>RLIMIT_NPROC</tt> ha sido excedido - si lo es, falla con <tt>-EAGAIN</tt>, si no, incrementa la cuenta de procesos con el uid dado <tt>p->user->count</tt>. <item> Si el número de tareas a lo largo del sistema excede el valor de max_threads (recordar que es ajustable), falla con <tt>-EAGAIN</tt>. <item> Si el binario que se ejecuta pertenece a un dominio modularizado de ejecución, incrementa el contador de referencia del correspondiente módulo. <item> Si el binario que se ejecuta pertenece a un formato binario modularizado, incrementa el contador de referencia del módulo correspondiente. <item> El hijo es marcado como 'no tiene que ser ejecutado' (<tt>p->did_exec = 0</tt>) <item> El hijo es marcado como 'no intercambiable' (<tt>p->swappable = 0</tt>) <item> EL hijo es puesto en el estado 'durmiendo no interrumpible', esto es <tt>p->state = TASK_UNINTERRUPTIBLE</tt> (POR HACER: ¿por qué es realizado esto? Creo que no se necesita - librarse de el, Linus confirma que no se necesita) <item> Las <tt>p->flags</tt> del hijo son establecidas de acuerdo a los valores de clone_flags; para <bf>fork(2)</bf> limpias, esto será <tt>p->flags = PF_FORKNOEXEC</tt>. <item> El pid del hijo <tt>p->pid</tt> es establecido usando el algoritmo rápido en <tt>kernel/fork.c:get_pid()</tt> (POR HACER: el spinlock <tt>lastpid_lock</tt> puede ser redundante ya que <tt>get_pid()</tt> siempre es llamado bajo un gran cierre del núcleo desde <tt>do_fork()</tt>, también quita los argumentos bandera de <tt>get_pid()</tt>, parche enviado a Alan el 20/06/2000 - mirar después). <item> El resto del código en <tt>do_fork()</tt> inicializa el resto de la estructura de la tarea del hijo. Muy al final, la estructura de tarea del hijo es ordenada en la tabla hash <tt>pidhash</tt> y el hijo es despertado. (POR HACER: <tt>wake_up_process(p)</tt> establece <tt>p->state = TASK_RUNNING</tt> y añade el proceso a la cola de ejecución, entonces probablemente no necesita establecer <tt>p->state</tt> a <tt>TASK_RUNNING</tt> tempranamente en <tt>do_fork()</tt>). La parte interesante es establecer <tt>p->exit_signal</tt> a <tt>clone_flags & CSIGNAL</tt>, la cual para <bf>fork(2)</bf> significa justamente <tt>SIGCHLD</tt> y establece <tt>p->pdeath_signal</tt> a 0. La <tt>pdeath_signal</tt> es usada cuando un proceso 'olvida' el padre original (durante la muerte) y puede ser establecido/tomado por medio del comando <tt>PR_GET/SET_PDEATHSIG</tt> de la llamada al sistema <bf>prctl(2)</bf> (Tu quizás argumentes que la forma en la que el valor de <tt>pdeath_signal</tt> es devuelto a través de un argumento de un puntero del espacio de usuario en <bf>prctl(2)</bf> es un poco tonto - mea culpa, después de que Andries Brouwer actualizara la página man era muy tarde para arreglarlo ;) </enum> Entonces las tareas son creadas. Hay varias formas para la terminación de tareas: <enum> <item> haciendo la llamada al sistema <bf>exit(2)</bf>; <item> enviando un señal con la disposición por defecto de morir; <item> siendo forzado a morir bajo ciertas excepciones; <item> llamando <bf>bdflush(2)</bf> con <tt>func == 1</tt> (esto es específico de Linux, para compatibilización de viejas distribuciones que todavía tienen la linea 'update' en <tt>/etc/inittab</tt> - hoy en día el trabajo de update es hecho por el hilo del núcleo <tt>kupdate</tt>). </enum> Las funciones implementando llamadas al sistema bajo Linux son prefijadas con <tt>sys_</tt>, pero ellas son usualmente concernientes sólo al chequeo de argumentos o a formas específicas de la arquitectura de pasar alguna información y el trabajo actual es realizado por las funciones <tt>do_</tt>. Por lo tanto, es con <tt>sys_exit()</tt> el cual llama a <tt>do_exit()</tt> para hacer el trabajo. Aunque otras partes del núcleo a veces invocan a <tt>sys_exit()</tt> mientras que deberían realmente de llamar a <tt>do_exit()</tt>. La función <tt>do_exit()</tt> es encontrada en <tt>kernel/exit.c</tt>. Los puntos que destacar sobre <tt>do_exit()</tt> son; <itemize> <item> Usa un cierre global del núcleo (cierra pero no abre). <item> Llama <tt>schedule()</tt> al final, el cual nunca regresa. <item> Establece el estado de tareas a <tt>TASK_ZOMBIE</tt>. <item> Notifica cualquier hijo con <tt>current->pdeath_signal</tt>, si no 0. <item> Notifica al padre con una <tt>current->exit_signal</tt>, el cual es usualmente igual a <tt>SIGCHLD</tt>. <item> Libera los recursos asignador por fork, cierra los archivos abiertos, etc, <item> En arquitecturas que usan FPU lentas (ia64, mips, mips64) (POR HACER: quitar el argumento 'flags' de sparc, sparc64), realiza lo que el hardware requiera para pasar la FPU al dueño (si el dueño es el actual) a "none" (ninguno). </itemize> <sect1>Planificador Linux<p> El trabajo de un planificador es decidir el acceso a la actual CPU entre múltiples procesos. El planificador está implementado en el 'archivo principal del núcleo' <tt>kernel/sched.c</tt>. El archivo de cabeceras correspondiente <tt>include/linux/sched.h</tt> está incluido virtualmente (explícita o implicitamente) en todos los archivos de código fuente del núcleo. Los campos de una estructura de tareas relevante a planificar incluyen: <itemize> <item> <tt>p->need_resched</tt>: este campo es establecido si <tt>schedule()</tt> debería de ser llamado en la 'siguiente oportunidad'. <item> <tt>p->counter</tt>: número de ticks de reloj que quedan en esta porción de tiempo del planificador, decrementada por un cronómetro. Cuando este campo se convierte a un valor menor o igual a cero, es reinicializado a 0 y <tt>p->need_resched</tt> es establecido. Esto también es llamado a veces 'prioridad dinámica' de un proceso porque puede cambiarse a si mismo. <item> <tt>p->priority</tt>: la prioridad estática del proceso, sólo cambiada a través de bien conocidas llamadas al sistema como <bf>nice(2)</bf>, POSIX.1b <bf>sched_setparam(2)</bf> o 4.4BSD/SVR4 <bf>setpriority(2)</bf>. <item> <tt>p->rt_priority</tt>: prioridad en tiempo real. <item> <tt>p->policy</tt>: la política de planificación, específica a la clase de planificación que pertenece la tarea. Las tareas pueden cambiar su clase de planificación usando la llamada al sistema <bf>sched_setscheduler(2)</bf>. Los valores válidos son <tt>SCHED_OTHER</tt> (proceso UNIX tradicional), <tt>SCHED_FIFO</tt> (proceso FIFO en tiempo real POSIX.1b) y <tt>SCHED_RR</tt> (proceso en tiempo real round-robin POSIX). Uno puede también <tt>SCHED_YIELD</tt> a alguno de esos valores para significar que el proceso decidió dejar la CPU, por ejemplo llamando a la llamada al sistema <bf>sched_yield(2)</bf>. Un proceso FIFO en tiempo real funcionará hasta que: a) se bloquee en una E/S, b) explícitamente deje la CPU, o c) es predesocupado por otro proceso de tiempo real con un valor más alto de <tt>p->rt_priority</tt>. <tt>SCHED_RR</tt> es el mismo que <tt>SCHED_FIFO</tt>, excepto que cuando su porción de tiempo acaba vuelve al final de la cola de ejecutables. </itemize> EL algoritmo de planificación es simple, olvídate de la gran complejidad aparente de la función <tt>schedule()</tt>. La función es compleja porque implementa tres algoritmos de planificación en uno y también porque disimula los específicos de SMP. Las aparentemente 'inservibles' etiquetas (gotos) en <tt>schedule()</tt> están allí con el propósito de generar el mejor código optimizado (para i386). También, destacar que el planificador (como la mayoría del núcleo) fue totalmente reescrito para el 2.4, entonces la discusión de más abajo no se aplica a los núcleos 2.2 o anteriores. Déjanos mirar la función en detalle: <enum> <item> Si <tt>current->active_mm == NULL</tt> entonces algo está mal. El actual proceso, incluso un hilo del núcleo (<tt>current->mm == NULL</tt>) debe de tener un <tt>p->active_mm</tt> válido durante todo el tiempo. <item> Si hay algo que hacer en la cola de tareas <tt>tq_scheduler</tt>, entonces se procesa ahora. La cola de tareas suministra al núcleo un mecanismo para planificar la ejecución de las funciones más tarde. Lo miraremos en detalle en otra parte. <item> Se inicializan las variables locales <tt>prev</tt> y <tt>this_cpu</tt> a las tareas y CPUs actuales, respectivamente. <item> Se chequea si <tt>schedule()</tt> fue llamada desde el controlador de interrupciones (debido a un fallo) y provoca un pánico si ha sido así. <item> Se quita el cierre global del núcleo. <item> Si hay algún trabajo que hacer a través del mecanismo de softirq, se hace ahora. <item> Se inicializa el puntero local <tt>struct schedule_data *sched_data</tt> para que apunte a cada CPU (alineado de la linea de antememoria para prevenir que la linea de antememoria salte) planificando el área de datos, el cual contiene el valor TSC de <tt>last_schedule</tt> y el puntero a la última estructura planificada (POR HACER: <tt>sched_data</tt> es usada sólo en SMP, ¿pero porqué inicializa también <tt>init_idle()</tt> en UP (monoprocesadores)? <item> Es tomado el spinlock <tt>runqueue_lock</tt>. Destacar que usamos <tt>spin_lock_irq()</tt> porque en <tt>schedule()</tt> garantizamos que las interrupciones están habilitadas. Por esto, cuando abrimos <tt>runqueue_lock</tt>, podemos rehabilitarlas en vez de salvar/restaurar las eflags (variante <tt>spin_lock_irqsave/restore</tt>). <item> Estado de tareas de la máquina: si la tarea está en el estado <tt>TASK_RUNNING</tt> entonces se deja sólo, si está en el estado <tt>TASK_INTERRUPTIBLE</tt> y hay una señal pendiente, es movido al estado <tt>TASK_RUNNING</tt>. En todos los otros casos es borrado de la cola de ejecución. <item> <tt>next</tt> (mejor candidato para ser planificado) es establecido a la tarea vacía de esta CPU. En todo caso, la virtud de este candidato es establecida a un valor muy bajo (-1000), con la esperanza de que haya otro mejor que él. <item> Si la tarea <tt>prev</tt> (actual) está en el estado <tt>TASK_RUNNING</tt> entonces la actual virtud es establecida a su virtud y es marcado como mejor candidato para ser planificado que la tarea vacía. <item> Ahora la cola de ejecución es examinada y una virtud de cada proceso que puede ser planificado en esta CPU es comparado con el actual valor; el proceso con la virtud más alta gana. Ahora el concepto de "puede ser planificado en esta CPU" debe de ser clarificado: en UP, todos los procesos en la cola de ejecución son elegibles para ser planificados; en SMP, sólo los procesos que no estean corriendo en otra CPU son elegibles para ser planificados en esta CPU. La virtud es calculada por una función llamada <tt>goodness()</tt>, la cual trata los procesos en tiempo real haciendo sus virtudes muy altas (<tt>1000 + p->rt_priority</tt>), siendo mayor que 1000 se garantiza que no puede ganar otro proceso <tt>SCHED_OTHER</tt>; por lo tanto sólo compiten con los otros procesos en tiempo real que quizás tengan un mayor <tt>p->rt_priority</tt>. La función virtud devuelve 0 si la porción de tiempo del proceso (<tt>p->counter</tt>) se acabó. Para procesos que no son en tiempo real, el valor inicial de la virtud es establecido a <tt>p->counter</tt> - por este camino, el proceso tiene menos posibilidades para alcanzar la CPU si ya la tuvo por algún tiempo, esto es, los procesos interactivos son favorecidos más que el límite de impulsos de la CPU. La constante específica de la arquitectura <tt>PROC_CHANGE_PENALTY</tt> intenta implementar la "afinidad de cpu" (esto es, dar ventaja a un proceso en la misma CPU). También da una ligera ventaja a los procesos con mm apuntando al actual <tt>active_mm</tt> o a procesos sin espacio de direcciones (de usuario), esto es, hilos del núcleo. <item> si el actual valor de la virtud es 0 entonces la lista entera de los procesos (¡no sólo los de la lista de ejecutables!) es examinada y sus prioridades dinámicas son recalculadas usando el simple algoritmo: <tscreen><code> recalculate: { struct task_struct *p; spin_unlock_irq(&runqueue_lock); read_lock(&tasklist_lock); for_each_task(p) p->counter = (p->counter >> 1) + p->priority; read_unlock(&tasklist_lock); spin_lock_irq(&runqueue_lock); } </code></tscreen> Destacar que tiramos el <tt>runqueue_lock</tt> antes de recalcular. El motivo para esto es que vamos a través del conjunto entero de procesos; esto puede llevar un gran tiempo, durante el cual el <tt>schedule()</tt> puede ser llamado por otra CPU y seleccionar un proceso con la suficiente virtud para esta CPU, mientras que nosotros en esta CPU seremos obligados a recalcular. Muy bien, admitamos que esto es algo inconsistente porque mientras que nosotros (en esta CPU) estamos seleccionando un proceso con la mejor virtud, <tt>schedule()</tt> corriendo en otra CPU podría estar recalculando las prioridades dinámicas. <item> Desde este punto, es cierto que <tt>next</tt> apunta a la tarea a ser planificada, por lo tanto debemos de inicializar <tt>next->has_cpu</tt> a 1 y <tt>next->processor</tt> a <tt>this_cpu</tt>. La <tt>runqueue_lock</tt> puede ahora ser abierta. <item> Si estamos volviendo a la misma tarea (<tt>next == prev</tt>) entonces podemos simplemente readquirir un cierre global del núcleo y volver, esto es, saltar todos los niveles hardware (registros, pila, etc.) y el grupo relacionado con la VM (Memoria Virtual) (cambiar la página del directorio, recalcular <tt>active_mm</tt> etc.) <item> La macro <tt>switch_to()</tt> es específica de la arquitectura. En i386, es concerniente con: a) manejo de la FPU (Unidad de Punto Flotante), b) manejo de la LDT, c) recargar los registros de segmento, d) manejo de TSS y e) recarga de los registros de depuración. </enum> <sect1>Implementación de la lista enlazada (de) Linux<p> Antes de ir a examinar las implementación de las colas de espera, debemos de informarnos con la implementación estándar de la lista doblemente enlazada Linux. Las colas de espera (igual que todo lo demás en Linux) hacen un uso fuerte de ellas y entonces son llamadas en la jerga "implementación list.h" porque el archivo más relevante es <tt>include/linux/list.h</tt>. La estructura de datos fundamental aquí es <tt>struct list_head</tt>: <tscreen><code> struct list_head { struct list_head *next, *prev; }; #define LIST_HEAD_INIT(name) { &(name), &(name) } #define LIST_HEAD(name) \ struct list_head name = LIST_HEAD_INIT(name) #define INIT_LIST_HEAD(ptr) do { \ (ptr)->next = (ptr); (ptr)->prev = (ptr); \ } while (0) #define list_entry(ptr, type, member) \ ((type *)((char *)(ptr)-(unsigned long)(&((type *)0)->member))) #define list_for_each(pos, head) \ for (pos = (head)->next; pos != (head); pos = pos->next) </code></tscreen> Las tres primeras macros son para inicializar un lista vacía apuntando los punteros <tt>next</tt> y <tt>prev</tt> a ellos mismos. Esto es obvio debido a las restricciones sintácticas de C, las cuales deberían de ser usadas aquí - por ejemplo, <tt>LIST_HEAD_INIT()</tt> puede ser usada para la inicialización de elementos de la estructura en la declaración, la segunda puede ser usada para la inicialización de las declaraciones de variables estáticas y la tercera puede ser usada dentro de la función. La macro <tt>list_entry()</tt> da acceso individual a los elementos de la lista, por ejemplo (desde <tt>fs/file_table.c:fs_may_remount_ro()</tt>): <tscreen><code> struct super_block { ... struct list_head s_files; ... } *sb = &some_super_block; struct file { ... struct list_head f_list; ... } *file; struct list_head *p; for (p = sb->s_files.next; p != &sb->s_files; p = p->next) { struct file *file = list_entry(p, struct file, f_list); haz algo a 'file' } </code></tscreen> Un buen ejemplo del uso de la macro <tt>list_for_each()</tt> está en el planificador, donde andamos a través de la cola de ejecución buscando al proceso con la virtud más alta: <tscreen><code> static LIST_HEAD(runqueue_head); struct list_head *tmp; struct task_struct *p; list_for_each(tmp, &runqueue_head) { p = list_entry(tmp, struct task_struct, run_list); if (can_schedule(p)) { int weight = goodness(p, this_cpu, prev->active_mm); if (weight > c) c = weight, next = p; } } </code></tscreen> Aquí, <tt>p->run_list</tt> es declarada como <tt>struct list_head run_list</tt> dentro de la estructura <tt>task_struct</tt> y sirve como ancla de la lista. Quitando y añadiendo (al principio o al final de la lista) un elemento de la lista es hecho por las macros <tt>list_del()/list_add()/list_add_tail()</tt>. Los ejemplos siguientes están añadiendo y quitando una tarea de la cola de ejecución: <tscreen><code> static inline void del_from_runqueue(struct task_struct * p) { nr_running--; list_del(&p->run_list); p->run_list.next = NULL; } static inline void add_to_runqueue(struct task_struct * p) { list_add(&p->run_list, &runqueue_head); nr_running++; } static inline void move_last_runqueue(struct task_struct * p) { list_del(&p->run_list); list_add_tail(&p->run_list, &runqueue_head); } static inline void move_first_runqueue(struct task_struct * p) { list_del(&p->run_list); list_add(&p->run_list, &runqueue_head); } </code></tscreen> <sect1>Colas de espera<p> Cuando un proceso solicita el núcleo para hacer algo que es actualmente imposible pero que quizás sea posible más tarde, el proceso es puesto a dormir y es despertado cuando la solicitud tiene más probabilidades de ser satisfecha. Uno de los mecanismos del núcleo usados para esto es llamado 'cola de espera'. La implementación de Linux nos permite despertar usando la bandera <tt>TASK_EXCLUSIVE</tt>. Con las colas de espera, también puedes usar una cola bien conocida y entonces simplificar <tt>sleep_on/sleep_on_timeout/interruptible_sleep_on/interruptible_sleep_on_timeout</tt>, o puedes definir tu propia cola de espera y usar <tt>add/remove_wait_queue</tt> para añadir y quitarte desde ella y <tt>wake_up/wake_up_interruptible</tt> para despertar cuando se necesite. Un ejemplo del primer uso de las colas de espera es la interacción entre el asignador de páginas (en <tt>mm/page_alloc.c:__alloc_pages()</tt>) y el demonio del núcleo <tt>kswapd</tt> (en <tt>mm/vmscan.c:kswap()</tt>), por medio de la cola de espera <tt>kswapd_wait,</tt> declarada en <tt>mm/vmscan.c</tt>; el demonio <tt>kswapd</tt> duerme en esta cola, y es despertado cuando el asignador de páginas necesita liberar algunas páginas. Un ejemplo del uso de una cola de espera autónoma es la interacción entre la solicitud de datos de un proceso de usuario a través de la llamada al sistema <bf>read(2)</bf> y el núcleo funcionando en el contexto de interrupción para suministrar los datos. Un manejador de interrupciones quizás se parezca a (<tt>drivers/char/rtc_interrupt()</tt> simplificado): <tscreen><code> static DECLARE_WAIT_QUEUE_HEAD(rtc_wait); void rtc_interrupt(int irq, void *dev_id, struct pt_regs *regs) { spin_lock(&rtc_lock); rtc_irq_data = CMOS_READ(RTC_INTR_FLAGS); spin_unlock(&rtc_lock); wake_up_interruptible(&rtc_wait); } </code></tscreen> Por lo tanto, el manejador de interrupciones obtiene los datos leyendo desde algún puerto de E/S específico del dispositivo (la macro <tt>CMOS_READ()</tt> devuelve un par de <tt>outb/inb</tt>) y entonces despierta a quien esté durmiendo en la cola de espera <tt>rtc_wait</tt>. Ahora, la llamada al sistema <bf>read(2)</bf> puede ser implementada como: <tscreen><code> ssize_t rtc_read(struct file file, char *buf, size_t count, loff_t *ppos) { DECLARE_WAITQUEUE(wait, current); unsigned long data; ssize_t retval; add_wait_queue(&rtc_wait, &wait); current->state = TASK_INTERRUPTIBLE; do { spin_lock_irq(&rtc_lock); data = rtc_irq_data; rtc_irq_data = 0; spin_unlock_irq(&rtc_lock); if (data != 0) break; if (file->f_flags & O_NONBLOCK) { retval = -EAGAIN; goto out; } if (signal_pending(current)) { retval = -ERESTARTSYS; goto out; } schedule(); } while(1); retval = put_user(data, (unsigned long *)buf); if (!retval) retval = sizeof(unsigned long); out: current->state = TASK_RUNNING; remove_wait_queue(&rtc_wait, &wait); return retval; } </code></tscreen> Lo que pasa en <tt>rtc_read()</tt> es esto: <enum> <item> Declaramos un elemento de la cola de espera apuntando al contexto del proceso actual. <item> Añadimos este elemento a la cola de espera <tt>rtc_wait</tt> <item> Marcamos el actual contexto como <tt>TASK_INTERRUPTIBLE</tt> lo que significa que no será replanificado después de la próxima vez que duerma. <item> Chequeamos si no hay datos disponibles; si los hay empezamos, copiamos los datos a la memoria intermedia del usuario, nos marcamos como <tt>TASK_RUNNING</tt>, nos quitamos de la cola de espera y regresamos. <item> Si todavía no hay datos, chequeamos cuando el usuario especificó una E/S no bloqueante, y si es así entonces fallamos con <tt>EAGAIN</tt> (el cual es el mismo que <tt>EWOULDBLOCK</tt>) <item> También chequeamos si hay alguna señal pendiente y si por lo tanto informamos a las "capas superiores" para reinicializar la llamada al sistema si es necesario. Por "si es necesario" yo entiendo los detalles de la disposición de la señal tal como están especificadas en la llamada al sistema <bf>sigaction(2)</bf>. <item> Entonces "salimos", esto es, nos dormimos, hasta que sea despertado por el manejador de interrupciones. Si no nos marcamos como <tt>TASK_INTERRUPTIBLE</tt> entonces el planificador nos podrá planificar tan pronto como los datos estean disponibles, causando así procesamiento no necesario. </enum> Es también valioso apuntar que, usando una cola de espera, es bastante más fácil implementar la llamada al sistema <bf>poll(2)</bf>: <tscreen><code> static unsigned int rtc_poll(struct file *file, poll_table *wait) { unsigned long l; poll_wait(file, &rtc_wait, wait); spin_lock_irq(&rtc_lock); l = rtc_irq_data; spin_unlock_irq(&rtc_lock); if (l != 0) return POLLIN | POLLRDNORM; return 0; } </code></tscreen> Todo el trabajo es realizado por la función independiente del dispositivo <tt>poll_wait()</tt> la cual hace las manipulaciones necesarias en la lista de espera; todo lo que necesitamos hacer es apuntarla a la cola de espera la cual es despertada por nuestro manejador de interrupciones específico del dispositivo. <sect1>Cronómetros del núcleo<p> Ahora déjanos poner nuestra atención en los cronómetros del núcleo. Los cronómetros del núcleo son usados para expedir la ejecución de una función particular (llamada 'manejador de cronómetros') en un tiempo especificado en el futuro. La estructura de datos principal es <tt>struct timer_list</tt> declarada en <tt>include/linux/timer.h</tt>: <tscreen><code> struct timer_list { struct list_head list; unsigned long expires; unsigned long data; void (*function)(unsigned long); volatile int running; }; </code></tscreen> El campo <tt>list</tt> es para enlazar con la lista interna, protegida por el spinlock <tt>timerlist_lock</tt>. El campo <tt>expires</tt> es el valor de <tt>jiffies</tt> cuando el manejador <tt>function</tt> debería de ser invocado con <tt>data</tt> pasado como parámetro. El campo <tt>running</tt> es usado en SMP para probar si el manejador de cronómetros está actualmente funcionando en otra CPU Las funciones <tt>add_timer()</tt> y <tt>del_timer()</tt> añaden y quitan un cronómetro determinado de la lista. Cuando un cronómetro se termina, este es borrado automáticamente. Antes de que el cronómetro sea usado, DEBE de ser inicializado por medio de la función <tt>init_timer()</tt>. Y entonces es añadido, los campos <tt>function</tt> y <tt>expires</tt> deben de ser establecidos. <sect1>Bottom Halves<p> A veces es razonable partir la cantidad de trabajo para ser realizada dentro de un manejador de interrupciones en un trabajo inmediato (ej. agradecer la interrupción, actualizar las estadísticas, etc. ) y el trabajo que puede ser postpuesto para más tarde, cuando las interrupciones están habilitadas (ej, para realizar algún post-procesamiento sobre los datos, despertar a los procesos esperando por estos datos, etc). Los bottom halves son el mecanismo más viejo para posponer la ejecución de una tarea del núcleo y están disponibles desde Linux 1.x. En Linux 2.0, un nuevo mecanismo fue añadido, llamado 'colas de tareas', las cuales serán el título de la siguiente sección. Los bottom halves son serializados por el spinlock <tt>global_bh_lock</tt>, esto es, sólo puede haber un bottom half funcionando en cualquier CPU a la vez. De cualquier modo, cuando se intenta ejecutar el manejador, si no está disponible <tt>global_bh_lock</tt>, el bottom half es marcado (esto es planificado) para ejecución - por lo tanto el procesamiento puede continuar, en opuesto a un bucle ocupado en <tt>global_bh_lock</tt>. Sólo puede haber 32 bottom halves registrados en total. Las funciones requeridas para manipular los bottom halves son las siguientes (todas exportadas a módulos); <itemize> <item> <tt>void init_bh(int nr, void (*routine)(void))</tt>: instala un manejador de bottom half apuntado por el argumento <tt>routine</tt> en el slot <tt>nr</tt>. El slot debe de estar numerado en <tt>include/linux/interrupt.h</tt> en la forma <tt>XXXX_BH</tt>, ej. <tt>TIMER_BH</tt> o <tt>TQUEUE_BH</tt>. Típicamente, una rutina de inicialización del subsistema (<tt>init_module()</tt> para los módulos) instala el bottom half requerido usando esta función. <item> <tt>void remove_bh(int nr)</tt>: hace lo opuesto de <tt>init_bh()</tt>, esto es, desinstala el bottom half instalado en el slot <tt>nr</tt>. No hay chequeos de errores realizados aquí, por lo tanto, como ejemplo <tt>remove_bh(32)</tt> rompe el sistema. Típicamente, una rutina de limpieza del subsistema (<tt>cleanup_module()</tt> para los módulos) usa esta función para liberar el slot, que puede ser reusado por algún otro subsistema. (POR HACER: ¿no sería bonito tener una lista <tt>/proc/bottom_halves</tt> con todos los bottom halves en el sistema? Esto significa que <tt>global_bh_lock</tt> deberían hacer lecturas/escrituras, obviamente). <item> <tt>void mark_bh(int nr)</tt>: marca el bottom half en el slot <tt>nr</tt> para ejecución. Típicamente, un manejador de interrupciones marcará este bottom half (¡de aquí el nombre!) para ejecución en un "tiempo seguro". </itemize> Los bottom halves son tasklets globalmente cerrados, por lo tanto la pregunta "¿cúando es el manejador bottom half ejecutado?" es realmente "¿cuándo son los tasklets ejecutados?". Y la respuesta es, en dos sitios: a) en cada <tt>schedule()</tt> y b) en cada camino de retorno de interrupciones/llamadas al sistema en <tt>entry.S</tt> (POR HACER: entonces, el caso <tt>schedule()</tt> es realmente aburrido - parece añadir todavía otra interrupción muy muy lenta, ¿por qué no desembarazarse de la etiqueta <tt>handle_softirq</tt> de <tt>schedule()</tt> en su conjunto?). <sect1>Colas de Tareas<p> Las colas de tareas pueden ser entendidas como una extensión dinámica de los viejos bottom halves. En realidad, en el código fuente son a veces referidas como los "nuevos" bottom halves. Más específicamente, los viejos bottom halves discutidos en la sección anterior tienen estas limitaciones: <enum> <item> Sólo hay un número fijo de ellos (32). <item> Cada bottom half sólo puede estar asociado con una función de manejador. <item> Los Bottom halves son consumidos con un spinlock mantenido, por lo tanto no pueden bloquear. </enum> Por lo tanto, con las colas de tareas, un número arbitrario de funciones pueden ser encadenadas y procesadas una después de otra en un tiempo posterior. Uno crea una nueva cola de tareas usando la macro <tt>DECLARE_TASK_QUEUE()</tt> y encola la tarea en él usando la función <tt>queue_task()</tt>. La cola de tareas entonces puede ser procesada usando <tt>run_task_queue()</tt>. En vez de crear nuestra propia cola de tareas (y tener que consumirla manualmente) puedes usar una de las colas de tareas predefinidas en Linux las cuales son consumidas en puntos bien conocidos: <enum> <item> <bf>tq_timer</bf>: la cola de tareas de cronómetros, funciona en cada interrupción del cronómetro y cuando se libera un dispositivo tty (cerrando o liberando un dispositivo de terminal medio abierto). Desde que el manejador de cronómetro funciona en el contexto de interrupción, la tarea <tt>tq_timer</tt> también funciona en el contexto de interrupción y de este modo tampoco puede bloquearse. <item> <bf>tq_scheduler</bf>: la cola de tareas del planificador, consumida por el planificador (y también cuando se cierran dispositivos tty, como <tt>tq_timer</tt>). Como el planificador es ejecutado en el contexto de los procesos siendo re-planificados, las tareas <tt>tq_scheduler</tt> pueden hacer todo lo que quieran, esto es bloquear, usar los datos del contexto de los procesos (pero porque ellos quieren), etc . <item> <bf>tq_immediate</bf>: esto es realmente un bottom half <tt>IMMEDIATE_BH</tt>, por lo tanto los controladores pueden <tt>queue_task(task, &tq_immediate)</tt> y entonces <tt>mark_bh(IMMEDIATE_BH)</tt> ser consumido en el contexto de interrupción. <item> <bf>tq_disk</bf>: usado por un acceso de dispositivo de bloqueo de bajo nivel (y RAID) para empezar la actual petición. Esta cola de tareas es exportada a los módulos pero no debería de ser usada excepto para los propósitos especiales para los que fue diseñada. </enum> A menos que un controlador use su propia cola de tareas, no necesita llamar a <tt>run_tasks_queues()</tt> para procesar la cola, excepto bajo ciertas circunstancias explicadas a continuación. El motivo por el que la cola de tareas <tt>tq_timer/tq_scheduler</tt> no es consumida sólo en los sitios usuales sino en otras partes (cerrando un dispositivo tty, pero no el único ejemplo) se aclara si uno recuerda que el controlador puede planificar tareas en la cola, y estas tareas solo tienen sentido mientras una instancia particular del dispositivo sea todavía válida - lo cual usualmente significa hasta que la aplicación la cierre. Por lo tanto, el controlador quizás necesite llamar a <tt>run_task_queue()</tt> para encender las tareas que el (y alguno más) ha puesto en la cola, porque permitiéndoles funcionar en un tiempo posterior quizás no tenga sentido - esto es, las estructuras de datos relevantes quizás no hayan sido liberadas/reusadas por una instancia diferente. Este es el motivo por el que ves <tt>run_task_queue()</tt> en <tt>tq_timer</tt> y <tt>tq_scheduler</tt> en otros lugares más que el cronómetro de interrupciones y <tt>schedule()</tt> respectivamente. <sect1>Tasklets<p> Todavía no, estarán en una revisión futura <sect1>Softirqs<p> Todavía no, estarán en una revisión futura <sect1>¿Cómo son las llamadas al sistema implementadas en la arquitectura i386?<p> Existen dos mecanismos bajo Linux para implementar las llamadas al sistema: <itemize> <item> las llamadas puerta lcall7/lcall27 ; <item> interrupción software int 0x80. </itemize> Los programas nativos de Linux utilizan int 0x80 mientras que los binarios de los distintos tipos de UNIX (Solaris, UnixWare 7 etc.) usan el mecanismo lcall7. El nombre 'lcall7' es históricamente engañoso porque también cubre lcall27 (ej. Solaris/x86), pero la función manejadora es llamada lcall7_func. Cuando el sistema arranca, la función <tt>arch/i386/kernel/traps.c:trap_init()</tt> es llamada, la cual inicializa el IDT, por lo tanto el vector 0x80 (del tipo 15, dpl 3) apunta a la dirección de la entrada system_call desde <tt>arch/i386/kernel/entry.S</tt>. Cuando una aplicación del espacio de usuario realiza una llamada del sistema, los argumentos son pasados a través de los registros y la aplicación ejecuta la instrucción 'int 0x80'. Esto causa un reajuste en el modo núcleo y el procesador salta al punto de entrada system_call en <tt>entry.S</tt>. Lo que esto hace es: <enum> <item> Guarda los registros. <item> Establece %ds y %es a KERNEL_DS, por lo tanto todas las referencias de datos (y segmentos extras) son hechas en el espacio de direcciones del núcleo. <item> Si el valor de %eax es mayor que <tt>NR_syscalls</tt> (actualmente 256) fallará con el error <tt>ENOSYS</tt>. <item> Si la tarea está siendo ptraced (<tt>tsk->ptrace & PF_TRACESYS</tt>), realiza un procesamiento especial. Esto es para soportar programas como strace (análogo a SVR4 <bf>truss(1)</bf>) o depuradores. <item> Llamada <tt>sys_call_table+4*(syscall_number desde %eax)</tt>. Esta tabla es inicializada en el mismo archivo (<tt>arch/i386/kernel/entry.S</tt>) para apuntar a los manejadores individuales de las llamadas al sistema, los cuales bajo Linux son (usualmente) prefijados con <tt>sys_</tt>, ej. <tt>sys_open</tt>, <tt>sys_exit</tt>, etc. Estos manejadores de las llamadas al sistema de C encontrarán sus argumentos en la pila donde <tt>SAVE_ALL</tt> las almacenó. <item> Entra en el 'camino de retorno de la llamada al sistema'. Esta es una etiqueta separada porque es usada no sólo por int 0x80 sino también por lcall7, lcall27. Esto es, relacionado con el manejo de tasklets (incluyendo bottom halves), chequeando si un <tt>schedule()</tt> es necesitado (<tt>tsk->need_resched !=0</tt>), chequeando si hay señales pendientes y por lo tanto manejándolas. </enum> Linux soporta hasta 6 argumentos para las llamadas al sistema. Ellas son pasadas en %ebx, %ecx, %edx, %esi, %edi (y %ebp usado temporalmente, ver <tt>_syscall6()</tt> en <tt>asm-i386/unistd.h</tt>). El número de llamadas al sistema es pasado a través de %eax. <sect1>Operaciones Atómicas<p> Hay dos tipos de operaciones atómicas: bitmaps (mapas de bits) y <tt>atomic_t</tt>. Los bitmaps son muy convenientes para mantener un concepto de unidades "asignadas" o "libres" para alguna colección grande donde cada unidad es identificada por algún número, por ejemplo tres inodos o tres bloques. Son ampliamente usados para un simple cierre, por ejemplo para suministrar acceso exclusivo para abrir un dispositivo. Un ejemplo de esto puede ser encontrado en <tt>arch/i386/kernel/microcode.c</tt>: <tscreen><code> /* * Bits en microcode_status. (31 bits de espacio para una futura expansión) */ #define MICROCODE_IS_OPEN 0 /* establece si el dispositivo está en uso */ static unsigned long microcode_status; </code></tscreen> No hay necesidad de inicializar <tt>microcode_status</tt> a 0 ya que BSS es limpiado a cero explícitamente bajo Linux. <tscreen><code> /* * Forzamos a un sólo usuario a la vez aquí con open/close. */ static int microcode_open(struct inode *inode, struct file *file) { if (!capable(CAP_SYS_RAWIO)) return -EPERM; /* uno de cada vez, por favor */ if (test_and_set_bit(MICROCODE_IS_OPEN, &microcode_status)) return -EBUSY; MOD_INC_USE_COUNT; return 0; } </code></tscreen> Las operaciones en los bitmaps son: <itemize> <item> <bf>void set_bit(int nr, volatile void *addr)</bf>: establece el bit <tt>nr</tt> en el bitmap apuntado por <tt>addr</tt>. <item> <bf>void clear_bit(int nr, volatile void *addr)</bf>: limpia el bit <tt>nr</tt> en el bitmap apuntado por <tt>addr</tt>. <item> <bf>void change_bit(int nr, volatile void *addr)</bf>: cambia el bit <tt>nr</tt> (si está establecido limpia, si está limpio establece) en en el bitmap apuntado por <tt>addr</tt>. <item> <bf>int test_and_set_bit(int nr, volatile void *addr)</bf>: atómicamente establece el bit <tt>nr</tt> y devuelve el viejo valor del bit. <item> <bf>int test_and_clear_bit(int nr, volatile void *addr)</bf>: atómicamente limpia el bit <tt>nr</tt> y devuelve el viejo valor del bit. <item> <bf>int test_and_change_bit(int nr, volatile void *addr)</bf>: atómicamente cambia el bit <tt>nr</tt> y devuelve el viejo valor del bit. </itemize> Estas operaciones usan la macro <tt>LOCK_PREFIX</tt>, la cual en núcleos SMP evalúa la instrucción prefijo de cierre del bus y no hace nada en UP. Esto garantiza la atomicidad del acceso en el entorno SMP. A veces las manipulaciones de bits no son convenientes, pero en cambio necesitamos realizar operaciones aritméticas - suma, resta, incremento decremento. Los casos típicos son cuentas de referencia (ej. para los inodos). Esta facilidad es suministrada por el tipo de datos <tt>atomic_t</tt> y las siguientes operaciones: <itemize> <item> <bf>atomic_read(&v)</bf>: lee el valor de la variable <tt>atomic_t</tt> <tt>v</tt>. <item> <bf>atomic_set(&v, i)</bf>: establece el valor de la variable <tt>atomic_t</tt> <tt>v</tt> al entero <tt>i</tt>. <item> <bf>void atomic_add(int i, volatile atomic_t *v)</bf>: suma un entero <tt>i</tt> al valor de la variable atómica apuntado por <tt>v</tt>. <item> <bf>void atomic_sub(int i, volatile atomic_t *v)</bf>: resta el entero <tt>i</tt> del valor de la variable atómica apuntada por <tt>v</tt>. <item> <bf>int atomic_sub_and_test(int i, volatile atomic_t *v)</bf>: resta el entero <tt>i</tt> del valor de la variable atómica apuntada por <tt>v</tt>; devuelve 1 si el nuevo valor es 0, devuelve 0 en otro caso. <item> <bf>void atomic_inc(volatile atomic_t *v)</bf>: incrementa el valor en 1. <item> <bf>void atomic_dec(volatile atomic_t *v)</bf>: decrementa el valor en 1. <item> <bf>int atomic_dec_and_test(volatile atomic_t *v)</bf>: decrementa el valor; devuelve 1 si el nuevo valor es 0, devuelve 0 en otro caso. <item> <bf>int atomic_inc_and_test(volatile atomic_t *v)</bf>: incrementa el valor; devuelve 1 si el nuevo valor es 0, devuelve 0 en otro caso. <item> <bf>int atomic_add_negative(int i, volatile atomic_t *v)</bf>: suma el valor de <tt>i</tt> a <tt>v</tt> y devuelve 1 si el resultado es negativo. Devuelve 0 si el resultado es mayor o igual a 0. Esta operación es usada para implementar semáforos. </itemize> <sect1>Spinlocks, Spinlocks Read-Write y Spinlocks Big-Reader<p> Desde los primeros días del soporte Linux (al principio de los 90, en el siglo XX), los desarrolladores se encararon con el clásico problema de acceder a datos compartidos entre los diferentes tipos de contexto (procesos de usuario vs interrupciones) y diferentes instancias del mismo contexto para múltiples cpus. El soporte SMP fue añadido a Linux 1.3.42 el 15 de Noviembre de 1995 (el parche original fue hecho para el 1.3.37 en Octubre del mismo año). Si la región crítica del código puede ser ejecutada por el contexto de los procesos y el contexto de las interrupciones, entonces la forma de protegerlo usando las instrucciones <tt>cli/sti</tt> en UP es: <tscreen><code> unsigned long flags; save_flags(flags); cli(); /* código crítico */ restore_flags(flags); </code></tscreen> Mientras que esto está bien en UP, obviamente no lo está usándolo en SMP porque la misma secuencia de código quizás sea ejecutada simultáneamente en otra cpu, y mientras <tt>cli()</tt> suministra protección contra las carreras con el contexto de interrupciones en cada CPU individualmente, no suministra protección contra todas las carreras entre los contextos funcionando en diferentes CPUs. Es aquí donde los spinlocks son útiles. Hay tres tipos de spinlocks: vanilla (básico), read-write y spinlocks big-reader. Los spinlocks read-write deberían de ser usados cuando existe una tendencia natural de 'muchos lectores y pocos escritores'. Un ejemplo de esto es el acceso a las lista de sistemas de archivos registrados (ver <tt>fs/super.c</tt>). La lista es guardada por el spinlock read-write <tt>file_systems_lock</tt> porque uno necesita acceso exclusivo sólo cuando se está registrando/desregistrando un sistema de archivos, pero cualquier proceso puede leer el archivo <tt>/proc/filesystems</tt> o usar la llamada al sistema <bf>sysfs(2)</bf> para forzar un escaneo de sólo lectura de la lista file_systems. Esto lo hace sensible a usar spinlocks read-write. Con los spinlocks read-write, uno puede tener múltiples lectores a la vez pero sólo un escritor y no puede haber lectores mientras hay un escritor. Por el camino, sería bonito si nuevos lectores no isaran un cierre mientras hay un escritor intentando usar un cierre, esto es, si Linux pudiera distribuir correctamente la solución del hambre potencial del escritor por los múltiples lectores. Esto quiere significar que los lectores deben de ser bloqueados mientras exista un escritor intentando usar el cierre. Este no es actualmente el caso y no es obvio cuando debería de ser arreglado - el argumento para lo contrario es - los lectores usualmente ocupan el cierre por un instante de tiempo muy pequeño, por lo tanto, ¿ellos realmente deberían de tener hambre mientras el escritor usa el cierre para periodos potencialmente más largos? Los spinlocks Big-reader son una forma de spinlocks read-write altamente optimizados para accesos de lectura muy ligeros, con una penalización para los escritores. Hay un número limitado de spinlocks big-reader - actualmente sólo existen dos, de los cuales uno es usado sólo en sparc64 (irq global) y el otro es usado para redes. En todos los demás casos donde el patrón de acceso no concuerda con ninguno de estos dos escenarios, se debería utilizar los spinlocks básicos. No puedes bloquear mientras mantienes algún tipo de spinlock. Los Spinlocks vienen en tres tipos: plano, <tt>_irq()</tt> y <tt>_bh()</tt>. <enum> <item> <tt>spin_lock()/spin_unlock()</tt> plano: si conoces que las interrupciones están siempre deshabilitadas o si no compites con el contexto de interrupciones (ej. desde un manejador de interrupciones), entonces puedes utilizar este. No toca el estado de interrupción en la actual CPU. <item> <tt>spin_lock_irq()/spin_unlock_irq()</tt>: si sabes que las interrupciones están siempre habilitadas entonces puedes usar esta versión, la cual simplemente deshabilita (en el cierre) y re-habilita (en el desbloqueo) las interrupciones en la actual CPU. Por ejemplo, <tt>rtc_read()</tt> usa <tt>spin_lock_irq(&rtc_lock)</tt> (las interrupciones están siempre habilitadas dentro de <tt>read()</tt>) mientras que <tt>rtc_interrupt()</tt> usa <tt>spin_lock(&rtc_lock)</tt> (las interrupciones están siempre deshabilitadas dentro del manejador de interrupciones). Nótese que <tt>rtc_read()</tt> usa <tt>spin_lock_irq()</tt> y no el más genérico <tt>spin_lock_irqsave()</tt> porque en la entrada a cualquier llamada al sistema las interrupciones están siempre habilitadas. <item> <tt>spin_lock_irqsave()/spin_unlock_irqrestore()</tt>: la forma más fuerte, es usada cuando el estado de las interrupciones no es conocido, pero sólo si las interrupciones no importan nada, esto es, no hay punteros usándolo si nuestro manejador de interrupciones no ejecuta ningún código crítico. </enum> El motivo por el que no puedes usar el <tt>spin_lock()</tt> plano si compites contra el manejador de interrupciones es porque si lo usas y después un interrupción viene en la misma CPU, el esperará ocupado por el bloqueo para siempre: el que tenga el bloqueo, habiendo sido interrumpido, no continuará hasta el que manejador de interrupciones vuelva. El uso más común de un spinlock es para acceder a estructuras de datos compartidas entre el contexto de proceso de usuario y el manejador de interrupciones: <tscreen><code> spinlock_t my_lock = SPIN_LOCK_UNLOCKED; my_ioctl() { spin_lock_irq(&my_lock); /* sección crítica */ spin_unlock_irq(&my_lock); } my_irq_handler() { spin_lock(&lock); /* sección crítica */ spin_unlock(&lock); } </code></tscreen> Hay un par de cosas que destacar sobre este ejemplo: <enum> <item> El contexto del proceso, representado aquí como un método típico de un controlador - <tt>ioctl()</tt> (argumentos y valores de retorno omitidos para una mayor claridad), deben de usar <tt>spin_lock_irq()</tt> porque conocen que las interrupciones están siempre habilitadas mientras se ejecuta un método de dispositivo <tt>ioctl()</tt>. <item> El contexto de interrupciones, representado aquí por <tt>my_irq_handler()</tt> (otra vez los argumentos son omitidos para una mayor claridad) pueden usar la forma <tt>spin_lock()</tt> plana porque las interrupciones están deshabilitadas dentro del manejador de interrupciones. </enum> <sect1>Semáforos y semáforos read/write<p> A veces, mientras se está accediendo a estructuras de datos compartidas, uno debe realizar operaciones que puedan bloquear, por ejemplo copiar datos al espacio de usuario. La directiva del cierre disponible para tales escenarios bajo Linux es llamada semáforo. Hay dos tipos de semáforos: básicos y semáforos read/write. Dependiendo del valor inicial del semáforo, pueden ser usados para exclusión mutua (valor inicial a 1) o para suministrar un tipo más sofisticado de acceso. Los semáforos read-write difieren de los semáforos básicos de la misma forma que los spinlocks read-write difieren de los spinlocks básicos: uno puede tener múltiples lectores a la vez pero sólo un escritor y no puede haber lectores mientras hay escritores - esto es, el escritor bloquea a todos los lectores, y los nuevos lectores se bloquean mientras un escritor está esperando. También, los semáforos básicos pueden ser interrumpidos - justamente usan las operaciones <tt>down/up_interruptible()</tt> en vez del <tt>down()/up()</tt> plano y chequean el valor devuelto desde <tt>down_interruptible()</tt>: no será cero si la operación fue interrumpida. El uso de semáforos para exclusión mutua es ideal para situaciones donde una sección crítica de código quizás sea llamada por funciones de referencia desconocidas registradas por otros subsistemas/módulos, esto es, el llamante no conoce a priori cuando la función bloquea o no. Un ejemplo simple del uso de semáforos está en la implementación de las llamadas al sistema <bf>gethostname(2)/sethostname(2)</bf> en <tt>kernel/sys.c</tt>. <tscreen><code> asmlinkage long sys_sethostname(char *name, int len) { int errno; if (!capable(CAP_SYS_ADMIN)) return -EPERM; if (len < 0 || len > __NEW_UTS_LEN) return -EINVAL; down_write(&uts_sem); errno = -EFAULT; if (!copy_from_user(system_utsname.nodename, name, len)) { system_utsname.nodename[len] = 0; errno = 0; } up_write(&uts_sem); return errno; } asmlinkage long sys_gethostname(char *name, int len) { int i, errno; if (len < 0) return -EINVAL; down_read(&uts_sem); i = 1 + strlen(system_utsname.nodename); if (i > len) i = len; errno = 0; if (copy_to_user(name, system_utsname.nodename, i)) errno = -EFAULT; up_read(&uts_sem); return errno; } </code></tscreen> Los puntos a destacar en este ejemplo son: <enum> <item> Las funciones quizás bloqueen mientras están copiando datos desde/al espacio de usuario en <tt>copy_from_user()/copy_to_user()</tt>. Entonces no pueden usar ninguna forma de spinlock aquí. <item> El tipo de semáforo escogido es read-write en oposición al básico porque quizás existan un montón de peticiones concurrentes <bf>gethostname(2)</bf> las cuales no tienen que ser mutuamente exclusivas. </enum> Aunque la implementación de Linux de los semáforos y los semáforos de read-write es muy sofisticada, existen posibles escenarios que uno puede pensar en los cuales no están todavía implementados, por ejemplo, no existe el concepto de semáforos read-write interrumpible. Eso es obvio porque no hay situaciones en el mundo real que requieran estos tipos exóticos de directivas. <sect1>Soporte del Núcleo para la Carga de Módulos<p> Linux es un sistema operativo monolítico y olvídate de todos los dichos modernos sobre algunas "ventajas" ofrecidas por los sistemas operativos basados en el diseño micro-núcleo, la verdad permanece (cita de Linus Torvalds): <tscreen> ... el paso de mensajes como la operación fundamental del SO es sólo un ejercicio de masturbación de la ciencia de la computación. Quizás suene bien, pero actualmente no tienes nada HECHO. </tscreen> Entonces, Linux esta y siempre estará basado en un diseño monolítico, lo cual significa que todos los subsistemas funcionan en el mismo modo privilegiado y comparten el mismo espacio de direcciones, la comunicación entre ellos es realizada por medio de las llamadas usuales de funciones de C. De cualquier modo, aunque la separación de la funcionalidad del núcleo en "procesos" separados realizada en los micro-núcleos es definitivamente una mala idea, separándolo en módulos del núcleo dinámicamente cargados bajo demanda es deseable en algunas circunstancias (ej, en máquinas con poca memoria o para núcleos de instalación, los cuales de otra forma pueden contener controladores de dispositivos ISA auto-probables que son mutuamente exclusivos). La decisión de cuando incluir soporte para la carga de módulos es hecha en tiempo de compilación y es determinada por la opción <tt>CONFIG_MODULES</tt>. El soporte para la auto-carga de módulos a través del mecanismo <tt>request_module()</tt> es una opción separada de compilación (<tt>CONFIG_KMOD</tt>). Las siguientes funcionalidades pueden ser implementadas como módulos cargables bajo Linux: <enum> <item> Controladores de dispositivos de bloque y de carácter, incluyendo los controladores de dispositivos misc. <item> Disciplinas de linea de Terminal. <item> Ficheros virtuales (regulares) en <tt>/proc</tt> y en devfs (ej. <tt>/dev/cpu/microcode</tt> vs <tt>/dev/misc/microcode</tt>). <item> Formatos de Ficheros Binarios (ej. ELF, aout, etc). <item> Dominios de Ejecución (ej. Linux, UnixWare7, Solaris, etc). <item> Sistemas de ficheros. <item> System V IPC. </enum> Hay unas pocas cosas que no pueden ser implementadas como módulos bajo Linux (probablemente porque no tienen sentido el ser modularizadas): <enum> <item> Algoritmos de planificación. <item> Políticas de VM (Memoria Virtual). <item> Antememoria intermedia, antememoria de páginas y otras antememoria. </enum> Linux suministra varias llamadas al sistema para asistir en la carga de módulos: <enum> <item><tt>caddr_t create_module(const char *name, size_t size)</tt>: asigna <tt>size</tt> bytes usando <tt>vmalloc()</tt> y mapea una estructura de un módulo al principio de este. Este nuevo módulo es enlazado en la cabecera de la lista por module_list. Sólo un proceso con <tt>CAP_SYS_MODULE</tt> puede llamar a esta llamada al sistema, otros verán como se les retorna <tt>EPERM</tt>. <item><tt>long init_module(const char *name, struct module *image)</tt>: carga la imagen del módulo reasignado y motiva que la rutina de inicialización del módulo sea invocada. Sólo un proceso con <tt>CAP_SYS_MODULE</tt> puede llamar a esta llamada al sistema, otros verán como se les retorna <tt>EPERM</tt>. <item><tt>long delete_module(const char *name)</tt>: intenta descargar el módulo. Si <tt>name == NULL</tt>, el intento es hecho para descargar todos los módulos no utilizados. <item><tt>long query_module(const char *name, int which, void *buf, size_t bufsize, size_t *ret)</tt>: devuelve información sobre un módulo (o sobre todos los módulos). </enum> La interfaz de comandos disponible a los usuarios consiste en: <itemize> <item><bf>insmod</bf>: inserta un módulo simple. <item><bf>modprobe</bf>: inserta un módulo incluyendo todos los otros módulos de los cuales dependa. <item><bf>rmmod</bf>: quita un módulo. <item><bf>modinfo</bf>: imprime alguna información sobre un módulo, ej. autor, descripción, parámetros que acepta el módulo, etc. </itemize> Aparte de ser capaz de cargar un módulo manualmente usando <bf>insmod</bf> o <bf>modprobe</bf>, también es posible tener el módulo insertado automáticamente por el núcleo cuando una funcionalidad particular es requerida. La interface del núcleo para esto es la función llamada <tt>request_module(name)</tt> la cual es exportada a los módulos, por lo tanto los módulos también pueden cargar otros módulos. La <tt>request_module(name)</tt> internamente crea un hilo del núcleo el cual ejecuta el comando del espacio de usuario <bf>modprobe -s -k module_name</bf>, usando la interfaz estándar del núcleo <tt>exec_usermodehelper()</tt> (que es también exportado a los módulos). La función devuelve 0 si es exitosa, de cualquier forma no es usualmente válido chequear el código de retorno desde <tt>request_module()</tt>. En vez de esto, el idioma de programación es: <tscreen><code> if (check_some_feature() == NULL) request_module(module); if (check_some_feature() == NULL) return -ENODEV; </code></tscreen> Por ejemplo, esto es realizado por <tt>fs/block_dev.c:get_blkfops()</tt> para cargar un módulo <tt>block-major-N</tt> cuando el intento es hecho para abrir un dispositivo de bloque con número mayor <tt>N</tt>. Obviamente, no existe tal módulo llamado <tt>block-major-N</tt> (los desarrolladores Linux solo escogen nombres sensibles para sus módulos) pero es mapeado al propio nombre del módulo usando el archivo <tt>/etc/modules.conf</tt>. De cualquier forma, para la mayoría de los números mayores bien conocidos (y otros tipos de módulos) los comandos <bf>modprobe/insmod</bf> conocen qué módulo real cargar sin necesitar una declaración explícita de un alias en <tt>/etc/modules.conf</tt>. Un buen ejemplo de la carga de un módulo está dentro de la llamada del sistema <bf>mount(2)</bf>. La llamada al sistema <bf>mount(2)</bf> acepta el tipo de sistema de archivos como una cadena <tt>fs/super.c:do_mount()</tt> la cual entonces pasa a <tt>fs/super.c:get_fs_type()</tt>: <tscreen><code> static struct file_system_type *get_fs_type(const char *name) { struct file_system_type *fs; read_lock(&file_systems_lock); fs = *(find_filesystem(name)); if (fs && !try_inc_mod_count(fs->owner)) fs = NULL; read_unlock(&file_systems_lock); if (!fs && (request_module(name) == 0)) { read_lock(&file_systems_lock); fs = *(find_filesystem(name)); if (fs && !try_inc_mod_count(fs->owner)) fs = NULL; read_unlock(&file_systems_lock); } return fs; } </code></tscreen> Hay que destacar unas pocas cosas en esta función: <enum> <item> Primero intentamos encontrar el sistema de archivos con el nombre dado entre aquellos ya registrados. Esto es hecho bajo la protección de <tt>file_systems_lock</tt> tomado para lectura (ya que no estamos modificando la lista registrada de sistemas de archivos). <item> Si tal sistema de archivos es encontrado intentamos coger una nueva referencia a él intentando incrementar la cuenta mantenida del módulo. Esto siempre devuelve 1 para sistemas de archivos enlazados dinámicamente o para módulos que actualmente no se han borrados. Si <tt>try_inc_mod_count()</tt> devuelve 0 entonces lo consideraremos un fallo - esto es, si el módulo está allí pero está siendo borrado, es tan bueno como si no estuviera allí en absoluto. <item> Tiramos el <tt>file_systems_lock</tt> porque lo siguiente que vamos a hacer (<tt>request_module()</tt>) es una operación bloqueante, y entonces no podemos mantener un spinlock sobre el. Actualmente, en este caso específico, podríamos tirar <tt>file_systems_lock</tt> de cualquier forma, incluso si <tt>request_module()</tt> fuera garantizada para ser no bloqueante y la carga de módulos fuera ejecutada en el mismo contexto atómicamente. El motivo para esto es que la función de inicialización de módulos intentará llamar a <tt>register_filesystem()</tt>, la cual tomará el mismo spinlock read-write <tt>file_systems_lock</tt> para escritura. <item> Si el intento de carga tiene éxito, entonces cogemos el spinlock <tt>file_systems_lock</tt> e intentamos situar el nuevamente registrado sistema de archivos en la lista. Nótese que esto es ligeramente erróneo porque es posible en un principio que un fallo en el comando modprobe pueda causar un volcado del núcleo después de cargar con éxito el módulo pedido, en tal caso <tt>request_module()</tt> fallará incluso aunque el nuevo sistema de archivos halla sido registrado, y todavía no lo encontrará <tt>get_fs_type()</tt>. <item> Si el sistema de archivos es encontrado y es capaz de obtener una referencia a el, la devolvemos. En otro caso devolvemos NULL. </enum> Cuando un módulo es cargado en el núcleo, puede ser referido por cualquier símbolo que sea exportado como público por el núcleo usando la macro <tt>EXPORT_SYMBOL()</tt> o por otros módulos actualmente cargados. Si el módulo usa símbolos de otro módulo, es marcado como dependiente de ese módulo durante el recálculo de dependencias, realizado funcionando el comando <bf>depmod -a</bf> en el arranque (ej. después de instalar un nuevo núcleo). Usualmente, uno debe comprobar el conjunto de los módulos con la versión de las interfaces del núcleo que usan, lo cual bajo Linux simplemente significa la "versión del núcleo" ya que no hay versionados especiales del mecanismo de interfaces del núcleo en general. De cualquier forma, hay una funcionalidad limitada llamada "versionamiento de módulos" o <tt>CONFIG_MODVERSIONS</tt> la cual nos permite eliminar el recompilamiento de módulos cuando cambiamos a un nuevo núcleo. Lo que pasa aquí es que la tabla de símbolos de núcleo es tratada de forma diferente para el acceso interno y para el acceso de los módulos. Los elementos de la parte pública (exportada) de la tabla de símbolos son construidos en la declaración de C de suma de control 32bit. Por lo tanto, en orden de resolver un símbolo usado por un módulo durante la carga, el cargador debe comprobar la representación total del símbolo que incluye la suma de control; será rechazada para cargar el módulo si estos símbolos difieren. Esto sólo pasa cuando el núcleo y el módulo son compilados con el versionamiento de módulos habilitado. Si ninguno de los dos usa los nombres originales de los símbolos el cargador simplemente intenta comprobar la versión del núcleo declarada por el módulo y el exportado por el núcleo y rechaza cargarlo si difieren. <sect>Sistemas de Archivos Virtuales (VFS)<p> <sect1>Caché de Inodos e Interacción con Dcache<p> En orden para soportar múltiples sistemas de archivos, Linux contiene un nivel especial de interfaces del núcleo llamado VFS (Interruptor de Sistemas de Ficheros Virtuales). Esto es muy similar a la interfaz vnode/vfs encontrada en los derivados de SVR4 (originalmente venían de BSD y de las implementaciones originales de Sun). La antememoria de inodos de Linux es implementada en un simple fichero, <tt>fs/inode.c</tt>, el cual consiste de 977 lineas de código. Es interesante notar que no se han realizado muchos cambios en él durante los últimos 5-7 años: uno todavía puede reconocer algún código comparando la última version con, digamos, 1.3.42. La estructura de la antememoria de inodos Linux es como sigue: <enum> <item> Una tabla global hash, <tt>inode_hashtable</tt>, donde cada inodo es ordenado por el valor del puntero del superbloque y el número de inodo de 32bit. Los inodos sin un superbloque (<tt>inode->i_sb == NULL</tt>) son añadidos a la lista doblemente enlazada encabezada por <tt>anon_hash_chain</tt> en su lugar. Ejemplos de inodos anónimos son los conectores creados por <tt>net/socket.c:sock_alloc()</tt>, llamado por <tt>fs/inode.c:get_empty_inode()</tt>. <item> Una lista global del tipo "en_uso" (<tt>inode_in_use</tt>), la cual contiene los inodos válidos con <tt>i_count>0</tt> y <tt>i_nlink>0</tt>. Los inodos nuevamente asignados por <tt>get_empty_inode()</tt> y <tt>get_new_inode()</tt> son añadidos a la lista <tt>inode_in_use</tt>. <item> Una lista global del tipo "sin_usar" (<tt>inode_unused</tt>), la cual contiene los inodos válidos con <tt>i_count = 0</tt>. <item> Una lista por cada superbloque del tipo "sucia" (<tt>sb->s_dirty</tt>) que contiene los inodos válidos con <tt>i_count>0</tt>, <tt>i_nlink>0</tt> y <tt>i_state & I_DIRTY</tt>. Cuando el inodo es marcado como sucio, es añadido a la lista <tt>sb->s_dirty</tt> si el está también ordenado. Manteniendo una lista sucia por superbloque de inodos nos permite rápidamente sincronizar los inodos. <item> Una antememoria propia de inodos - una antememoria SLAB llamada <tt>inode_cachep</tt>. Tal como los objetos inodos son asignados como libres, ellos son tomados y devueltos a esta antememoria SLAB. </enum> Los tipos de listas son sujetadas desde <tt>inode->i_list</tt>, la tabla hash desde <tt>inode->i_hash</tt>. Cada inodo puede estar en una tabla hash y en uno, y en sólo uno, tipo de lista (en_uso, sin_usar o sucia). Todas estas listas están protegidas por un spinlock simple: <tt>inode_lock</tt>. El subsistema de caché de inodos es inicializado cuando la función <tt>inode_init()</tt> es llamada desde <tt>init/main.c:start_kernel()</tt>. La función es marcada como <tt>__init</tt>, lo que significa que el código será lanzado posteriormente. Se le pasa un argumento simple - el número de páginas físicas en el sistema. Esto es por lo que la antememoria de inodos puede configurarse ella misma dependiendo de cuanta memoria está disponible, esto es, crea una tabla hash más grande si hay suficiente memoria. Las únicas estadísticas de información sobre la antememoria de inodos es el número de inodos sin usar, almacenados en <tt>inodes_stat.nr_unused</tt> y accesibles por los programas de usuario a través de los archivos <tt>/proc/sys/fs/inode-nr</tt> y <tt>/proc/sys/fs/inode-state</tt>. Podemos examinar una de las listas desde <bf>gdb</bf> en un núcleo en funcionamiento de esta forma: <tscreen><code> (gdb) printf "%d\n", (unsigned long)(&((struct inode *)0)->i_list) 8 (gdb) p inode_unused $34 = 0xdfa992a8 (gdb) p (struct list_head)inode_unused $35 = {next = 0xdfa992a8, prev = 0xdfcdd5a8} (gdb) p ((struct list_head)inode_unused).prev $36 = (struct list_head *) 0xdfcdd5a8 (gdb) p (((struct list_head)inode_unused).prev)->prev $37 = (struct list_head *) 0xdfb5a2e8 (gdb) set $i = (struct inode *)0xdfb5a2e0 (gdb) p $i->i_ino $38 = 0x3bec7 (gdb) p $i->i_count $39 = {counter = 0x0} </code></tscreen> Destacar que restamos 8 de la dirección 0xdfb5a2e8 para obtener la dirección de <tt>struct inode</tt> (0xdfb5a2e0) de acuerdo a la definición de la macro <tt>list_entry()</tt> de <tt>include/linux/list.h</tt>. Para entender cómo trabaja la antememoria de inodos, déjanos seguir un tiempo de vida de un inodo de un fichero regular en el sistema de ficheros ext2, el cómo es abierto y cómo es cerrado: <tscreen><code> fd = open("file", O_RDONLY); close(fd); </code></tscreen> La llamada al sistema <bf>open(2)</bf> es implementada en la función <tt>fs/open.c:sys_open</tt> y el trabajo real es realizado por la función <tt>fs/open.c:filp_open()</tt>, la cual está dividida en dos partes: <enum> <item> <tt>open_namei()</tt>: rellena la estructura nameidata conteniendo las estructuras dentry y vfsmount. <item> <tt>dentry_open()</tt>: dado dentry y vfsmount, esta función asigna una nueva <tt>struct file</tt> y las enlaza a todas ellas; también llama al método específico del sistema de ficheros <tt>f_op->open()</tt> el cual fue inicializado en <tt>inode->i_fop</tt> cuando el inodo fue leído en <tt>open_namei()</tt> (el cual suministra el inodo a través de <tt>dentry->d_inode</tt>). </enum> La función <tt>open_namei()</tt> interactúa con la antememoria dentry a través de <tt>path_walk()</tt>, el cual en el regreso llama a <tt>real_lookup()</tt>, el cual llama al método específico del sistema de ficheros <tt>inode_operations->lookup()</tt>. La misión de este método es encontrar la entrada en el directorio padre con el nombre correcto y entonces hace <tt>iget(sb, ino)</tt> para coger el correspondiente inodo - el cual nos trae la antememoria de inodos. Cuando el inodo es leido, el dentry es instanciado por medio de <tt>d_add(dentry, inode)</tt>. Mientras estamos en él, nótese que en los sistemas de ficheros del estilo UNIX que tienen el concepto de número de inodos en disco, el trabajo del método lookup es mapear su bit menos significativo al actual formato de la CPU, ej. si el número de inodos en la entrada del directorio sin formato (específico del sistema de ficheros) está en el formato de 32 bits little-endian uno haría: <tscreen><code> unsigned long ino = le32_to_cpu(de->inode); inode = iget(sb, ino); d_add(dentry, inode); </code></tscreen> Por lo tanto, cuando abrimos un fichero nosotros llamamos a <tt>iget(sb, ino)</tt> el cual es realmente <tt>iget4(sb, ino, NULL, NULL)</tt>, el cual hace: <enum> <item> Intenta encontrar un inodo con el superbloque emparejado y el número de inodo en la tabla hash bajo la protección de <tt>inode_lock</tt>. Si el inodo es encontrado, su cuenta de referencia (<tt>i_count</tt>) es incrementada; si era 0 anteriormente al incremento y el inodo no estaba sucio, es quitado de cualquier tipo de lista (<tt>inode->i_list</tt>) en la que esté (tiene que estar en la lista <tt>inode_unused</tt>, por supuesto) e insertado en la lista del tipo <tt>inode_in_use</tt>; finalmente <tt>inodes_stat.nr_unused</tt> es decrementado. <item> Si el inodo está actualmente bloqueado, esperaremos hasta que se desbloquee, por lo tanto está garantizado que <tt>iget4()</tt> devolverá un inodo desbloqueado. <item> Si el inodo no fue encontrado en la tabla hash entonces es la primera vez que se pide este inodo, por lo tanto llamamos a <tt>get_new_inode()</tt>, pasándole el puntero al sitio de la tabla hash donde debería de ser insertado. <item> <tt>get_new_inode()</tt> asigna un nuevo inodo desde la antememoria SLAB <tt>inode_cachep</tt>, pero esta operación puede bloquear (asignación <tt>GFP_KERNEL</tt>), por lo tanto el spinlock que guarda la tabla hash tiene que ser quitado. Desde que hemos quitado el spinlock, entonces debemos de volver a buscar el inodo en la tabla; si esta vez es encontrado, se devuelve (después de incrementar la referencia por <tt>__iget</tt>) el que se encontró en la tabla hash y se destruye el nuevamente asignado. Si aún no se ha encontrado en la tabla hash, entonces el nuevo inodo que tenemos acaba de ser asignado y es el que va a ser usado; entonces es inicializado a los valores requeridos y el método específico del sistema de ficheros <tt>sb->s_op->read_inode()</tt> es llamado para propagar el resto del inodo. Esto nos proporciona desde la antememoria de inodos la vuelta al código del sistema de archivos - recuerda que venimos de la antememoria de inodos cuando el método específico del sistema de ficheros <tt>lookup()</tt> llama a <tt>iget()</tt>. Mientras el método <tt>s_op->read_inode()</tt> está leyendo el inodo del disco, el inodo está bloqueado (<tt>i_state = I_LOCK</tt>); él es desbloqueado después de que el método <tt>read_inode()</tt> regrese y todos los que están esperando por el hayan sido despertados. </enum> Ahora, veamos que pasa cuando cerramos este descriptor de ficheros. La llamada al sistema <bf>close(2)</bf> está implementada en la función <tt>fs/open.c:sys_close()</tt>, la cual llama a <tt>do_close(fd, 1)</tt> el cual rompe (reemplaza con NULL) el descriptor del descriptor de ficheros de la tabla del proceso y llama a la función <tt>filp_close()</tt>, la cual realiza la mayor parte del trabajo. La parte interesante sucede en <tt>fput()</tt>, la cual chequea si era la última referencia al fichero, y si es así llama a <tt>fs/file_table.c:_fput()</tt> la cual llama a <tt>__fput()</tt> en la cual es donde sucede la interacción con dcache (y entonces con la memoria intermedia de inodos - ¡recuerda que dcache es la memoria intermedia de inodos Maestra!). El <tt>fs/dcache.c:dput()</tt> hace <tt>dentry_iput()</tt> la cual nos brinda la vuelta a la memoria intermedia de inodos a través de <tt>iput(inode)</tt>, por lo tanto déjanos entender <tt>fs/inode.c:iput(inode)</tt>: <enum> <item> Si el parámetro pasado a nosotros es NULL, no hacemos nada y regresamos. <item> Si hay un método específico del sistema de archivos <tt>sb->s_op->put_inode()</tt>, es llamada inmediatamente sin mantener ningún spinlock (por lo tanto puede bloquear). <item> El spinlock <tt>inode_lock</tt> es tomado y <tt>i_count</tt> es decrementado. Si NO era la última referencia a este inodo entonces simplemente chequeamos si hay muchas referencias a el y entonces <tt>i_count</tt> puede urdir sobre los 32 bits asignados a el si por lo tanto podemos imprimir un mensaje de peligro y regresar. Nótese que llamamos a <tt>printk()</tt> mientras mantenemos el spinlock <tt>inode_lock</tt> - esto está bien porque <tt>printk()</tt> nunca bloquea, entonces puede ser llamado absolutamente en cualquier contexto (¡incluso desde el manejador de interrupciones!). <item> Si era la última referencia activa entonces algún trabajo necesita ser realizado. </enum> EL trabajo realizado por <tt>iput()</tt> en la última referencia del inodo es bastante complejo, por lo tanto lo separaremos en una lista de si misma: <enum> <item> Si <tt>i_nlink == 0</tt> (ej. el fichero fué desenlazado mientras lo manteníamos abierto) entonces el inodo es quitado de la tabla hash y de su lista de tipos; si hay alguna página de datos mantenida en la antememoria de páginas para este inodo, son borradas por medio de <tt>truncate_all_inode_pages(&inode->i_data)</tt>. Entonces el método específico del sistema de archivos <tt>s_op->delete_inode()</tt> es llamado, el cual típicamente borra la copia en disco del inodo. Si no hay un método <tt>s_op->delete_inode()</tt> registrado por el sistema de ficheros (ej. ramfs) entonces llamamos a <tt>clear_inode(inode)</tt>, el cual llama <tt>s_op->clear_inode()</tt> si está registrado y si un inodo corresponde a un dispositivo de bloques, esta cuenta de referencia del dispositivo es borrada por <tt>bdput(inode->i_bdev)</tt>. <item> Si <tt>i_nlink != 0</tt> entonces chequeamos si hay otros inodos en el mismo cubo hash y si no hay ninguno, entonces si el inodo no está sucio lo borramos desde su tipo de lista y lo añadimos a la lista <tt>inode_unused</tt> incrementando <tt>inodes_stat.nr_unused</tt>. Si hay inodos en el mismo cubo hash entonces los borramos de la lista de tipo y lo añadimos a la lista <tt>inode_unused</tt>. Si no había ningún inodo (NetApp .snapshot) entonces lo borramos de la lista de tipos y lo limpiamos/destruimos completamente. </enum> <sect1>Registro/Desregistro de sistemas de Ficheros<p> El núcleo Linux suministra un mecanismo para los nuevos sistemas de ficheros para ser escritos con el mínimo esfuerzo. Los motivos históricos para esto son: <enum> <item> En el mundo donde la gente aún usa sistemas operativos no Linux para proteger sus inversiones en el software legado, Linux tiene que suministrar interoperabilidad para soportar una gran multitud de sistemas de ficheros diferentes - la mayoría no merecen existir pero sólo por compatibilidad con los existentes sistemas operativos no Linux. <item> La interfaz para los escritores de sistemas de ficheros tiene que ser muy simple para que la gente pueda intentar hacer ingeniería inversa con los sistemas de ficheros existentes para escribir versiones de sólo lectura de ellos. Entonces el VFS de Linux hace muy fácil implementar sistemas de ficheros de sólo lectura: el 95% del trabajo está por finalizar añadiéndole un soporte total para escritura. Como un ejemplo concreto leí sistemas de ficheros BFS para Linux en modo sólo lectura en unas 10 horas, pero llevó varias semanas completarlo para tener un soporte total de escritura (e incluso hoy algunos puristas dicen que no está completo porque no tiene soporte de compactación). <item> La interfaz VFS es exportada, y entonces todos los sistemas de ficheros Linux pueden ser implementados como módulos. </enum> Déjanos considerar los pasos requeridos para implementar un sistema de ficheros bajo Linux. El código para implementar un sistema de ficheros puede ser un módulo dinámicamente cargado o estár estáticamente enlazado en el núcleo, el camino es realizado por Linux trasparentemente. Todo lo que se necesita es rellenar una estructura <tt>struct file_system_type</tt> y registrarla con el VFS usando la función <tt>register_filesystem()</tt> como en el siguiente ejemplo de <tt>fs/bfs/inode.c</tt>: <tscreen><code> #include <linux/module.h> #include <linux/init.h> static struct super_block *bfs_read_super(struct super_block *, void *, int); static DECLARE_FSTYPE_DEV(bfs_fs_type, "bfs", bfs_read_super); static int __init init_bfs_fs(void) { return register_filesystem(&bfs_fs_type); } static void __exit exit_bfs_fs(void) { unregister_filesystem(&bfs_fs_type); } module_init(init_bfs_fs) module_exit(exit_bfs_fs) </code></tscreen> Las macros <tt>module_init()/module_exit()</tt> aseguran que, cuando BFS es compilado como un módulo, las funciones <tt>init_bfs_fs()</tt> y <tt>exit_bfs_fs()</tt> se convierten en <tt>init_module()</tt> y <tt>cleanup_module()</tt> respectivamente; si BFS está estáticamente enlazado en el núcleo el código <tt>exit_bfs_fs()</tt> lo hace innecesario. La <tt>struct file_system_type</tt> es declarada en <tt>include/linux/fs.h</tt>: <tscreen><code> struct file_system_type { const char *name; int fs_flags; struct super_block *(*read_super) (struct super_block *, void *, int); struct module *owner; struct vfsmount *kern_mnt; /* For kernel mount, if it's FS_SINGLE fs */ struct file_system_type * next; }; </code></tscreen> Los campos anteriores son explicados de esta forma: <itemize> <item><bf>name</bf>: nombre humano leíble, aparece en el fichero <tt>/proc/filesystems</tt> y es usado como clave para encontrar un sistema de ficheros por su nombre; este mismo nombre es usado por el tipo de sistema de ficheros en <bf>mount(2)</bf>, y debería de ser único; (obviamente) sólo puede haber un sistema de ficheros con un nombre dado. Para los módulos, los nombres de los punteros al espacio de direcciones del módulo no son copiados: esto significa que <bf>cat /proc/filesystems</bf> puede fallar si el módulo fue descargado pero el sistema de ficheros aún está registrado. <item><bf>fs_flags</bf>: una o mas (ORed) de las banderas: <tt>FS_REQUIRES_DEV</tt> para sistemas de ficheros que sólo pueden ser montados como dispositivos de bloque, <tt>FS_SINGLE</tt> para sistemas de ficheros que pueden tener sólo un superbloque, <tt>FS_NOMOUNT</tt> para los sistemas de ficheros que no pueden ser montados desde el espacio de usuario por medio de la llamada al sistema <bf>mount(2)</bf>: ellos pueden de todas formas ser montados internamente usando la interfaz <tt>kern_mount()</tt>, ej, pipefs. <item><bf>read_super</bf>: un puntero a la función que lee el superbloque durante la operación de montaje. Esta función es requerida; si no es suministrada, la operación de montaje (desde el espacio de usuario o desde el núcleo) fallará siempre excepto en el caso <tt>FS_SINGLE</tt> donde fallará en <tt>get_sb_single()</tt>, intentando desreferenciar un puntero a NULL en <tt>fs_type->kern_mnt->mnt_sb</tt> con (<tt>fs_type->kern_mnt = NULL</tt>). <item><bf>owner</bf>: puntero al módulo que implementa este sistema de ficheros. Si el sistema de ficheros está enlazado estáticamente en el núcleo entonces esto es NULL. No necesitas establecer esto manualmente puesto que la macro <tt>THIS_MODULE</tt> lo hace automáticamente. <item><bf>kern_mnt</bf>: sólo para sistemas de ficheros <tt>FS_SINGLE</tt>. Esto es establecido por <tt>kern_mount()</tt> (POR HACER: <tt>kern_mount()</tt> debería de rechazar montar sistemas de ficheros si <tt>FS_SINGLE</tt> no está establecido). <item><bf>next</bf>: enlaza a la cabecera de la lista simplemente enlazada <tt>file_systems</tt> (ver <tt>fs/super.c</tt>). La lista está protegida por el spinlock read-write <tt>file_systems_lock</tt> y las funciones <tt>register/unregister_filesystem()</tt> modificada por el enlace y desenlace de la entrada de la lista. </itemize> El trabajo de la función <tt>read_super()</tt> es la de rellenar los campos del superbloque, asignando el inodo raiz e inicializando cualquier información privada del sistema de ficheros asociadas por esta instancia montada del sistema de ficheros. Por lo tanto, tipicamente el <tt>read_super()</tt> hará: <enum> <item> Lee el superbloque desde el dispositivo especificado a través del argumento <tt>sb->s_dev</tt>, usando la función de la antememoria intermedia <tt>bread()</tt>. Si se anticipa a leer unos pocos más bloques de metadatos inmediatamente subsecuentes, entonces tiene sentido usar <tt>breada()</tt> para planificar el leer bloque extra de forma asíncrona. <item> Verifica que el superbloque contiene el número mágico válido y todo "parece" correcto. <item> Inicializa <tt>sb->s_op</tt> para apuntar a la estructura <tt>struct super_block_operations</tt>. Esta estructura contiene las funciones específicas del sistema de ficheros implementando las operaciones como "leer inodo", "borrar inodo", etc. <item> Asigna el inodo y dentry raiz usando <tt>d_alloc_root()</tt>. <item> Si el sistema de ficheros no está montado como sólo lectura entonces establece <tt>sb->s_dirt</tt> a 1 y marca la antememoria conteniendo el superbloque como sucio (POR HACER: ¿porqué hacemos esto? Yo lo hice en BFS porque MINIX lo hizo ...) </enum> <sect1>Administración de Descriptores de Ficheros<p> Bajo Linux hay varios niveles de rodeos entre el descriptor de ficheros del usuario y la estructura de inodos del núcleo. Cuando un proceso realiza la llamada al sistema <bf>open(2)</bf>, el núcleo devuelve un entero pequeño no negativo el cual puede ser usado para operaciones de E/S subsecuentes en este fichero. Cada estructura de fichero apunta a dentry a través de <tt>file->f_dentry</tt>. Y cada dentry apunta a un inodo a través de <tt>dentry->d_inode</tt>. Cada tarea contiene un campo <tt>tsk->files</tt> el cual es un puntero a <tt>struct files_struct</tt> definida en <tt>include/linux/sched.h</tt>: <tscreen><code> /* * Abre la estructura tabla del fichero */ struct files_struct { atomic_t count; rwlock_t file_lock; int max_fds; int max_fdset; int next_fd; struct file ** fd; /* actualmente una matriz de descriptores de ficheros */ fd_set *close_on_exec; fd_set *open_fds; fd_set close_on_exec_init; fd_set open_fds_init; struct file * fd_array[NR_OPEN_DEFAULT]; }; </code></tscreen> El <tt>file->count</tt> es una cuenta de referencia, incrementada por <tt>get_file()</tt> (usualmente llamada por <tt>fget()</tt>) y decrementada por <tt>fput()</tt> y por <tt>put_filp()</tt>. La diferencia entre <tt>fput()</tt> y <tt>put_filp()</tt> es que <tt>fput()</tt> hace más trabajo usualmente necesitado para ficheros regulares, como la liberación de conjuntos de bloqueos, liberación de dentry, etc, mientras que <tt>put_filp()</tt> es sólo para manipular las estructuras de tablas de ficheros, esto es, decrementa la cuenta, quita el fichero desde <tt>anon_list</tt> y lo añade a la <tt>free_list</tt>, bajo la protección del spinlock <tt>files_lock</tt>. El <tt>tsk->files</tt> puede ser compartido entre padre e hijo si el hilo hijo fue creado usando la llamada al sistema <tt>clone()</tt> con la bandera <tt>CLONE_FILES</tt> establecida en los argumentos de las banderas de clone. Esto puede ser visto en <tt>kernel/fork.c:copy_files()</tt> (llamada por <tt>do_fork()</tt>) el cual sólo incrementa el <tt>file->count</tt> si <tt>CLONE_FILES</tt> está establecido, en vez de la copia usual de la tabla de descriptores de ficheros en la tradición respetable en el tiempo de los clásicos <bf>fork(2)</bf> de UNIX. Cuando un fichero es abierto, la estructura del fichero asignada para él es instalada en el slot <tt>current->files->fd[fd]</tt> y un bit <tt>fd</tt> es establecido en el bitmap <tt>current->files->open_fds</tt>. Todo esto es realizado bajo la protección de escritura del spinlock read-write <tt>current->files->file_lock</tt>. Cuando el descriptor es cerrado un bit <tt>fd</tt> es limpiado en <tt>current->files->open_fds</tt> y <tt>current->files->next_fd</tt> es establecido igual a <tt>fd</tt> como una indicación para encontrar el primer descriptor sin usar la próxima vez que este proceso quiera abrir un fichero. <sect1>Administración de estructuras de ficheros<p> La estructura de ficheros es declarada en <tt>include/linux/fs.h</tt>: <tscreen><code> struct fown_struct { int pid; /* pid o -pgrp donde SIGIO debería de ser enviado */ uid_t uid, euid; /* uid/euid del proceso estableciendo el dueño */ int signum; /* posix.1b rt señal para ser enviada en ES */ }; struct file { struct list_head f_list; struct dentry *f_dentry; struct vfsmount *f_vfsmnt; struct file_operations *f_op; atomic_t f_count; unsigned int f_flags; mode_t f_mode; loff_t f_pos; unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin; struct fown_struct f_owner; unsigned int f_uid, f_gid; int f_error; unsigned long f_version; /* necesitado para este controlador tty, y quizás por otros */ void *private_data; }; </code></tscreen> Déjanos mirar varios de los campos de <tt>struct file</tt>: <enum> <item><bf>f_list</bf>: este campo enlaza la estructura del fichero con una (y sólo una) de las listas: a) <tt>sb->s_files</tt> lista de todos los ficheros abiertos en este sistema de ficheros, si el correspondiente inodo no es anónimo, entonces <tt>dentry_open()</tt> (llamado por <tt>filp_open()</tt>) enlaza el fichero en esta lista; b) <tt>fs/file_table.c:free_list</tt>, conteniendo las estructuras de ficheros sin utilizar; c) <tt>fs/file_table.c:anon_list</tt>, cuando una nueva estructura de ficheros es creada por <tt>get_empty_filp()</tt> es colocada en esta lista. Todas estas listas son protegidas por el spinlock <tt>files_lock</tt>. <item><bf>f_dentry</bf>: la dentry (entrada de directorio) correspondiente a este fichero. La dentry es creada en tiempo de búsqueda de nombre y datos (nameidata) por <tt>open_namei()</tt> (o más bien <tt>path_walk()</tt> la cual lo llama a él) pero el campo actual <tt>file->f_dentry</tt> es establecido por <tt>dentry_open()</tt> para contener la dentry de esta forma encontrada. <item><bf>f_vfsmnt</bf>: el puntero a la estructura <tt>vfsmount</tt> del sistema de ficheros conteniendo el fichero. Esto es establecido por <tt>dentry_open()</tt>, pero es encontrado como una parte de la búsqueda de nameidata por <tt>open_namei()</tt> (o más bien <tt>path_init()</tt> la cual lo llama a él). <item><bf>f_op</bf>: el puntero a <tt>file_operations</tt>, el cual contiene varios métodos que pueden ser llamados desde el fichero. Esto es copiado desde <tt>inode->i_fop</tt> que es colocado aquí durante la búsqueda nameidata. Miraremos los métodos <tt>file_operations</tt> en detalle más tarde en esta sección. <item><bf>f_count</bf>: cuenta de referencia manipulada por <tt>get_file/put_filp/fput</tt>. <item><bf>f_flags</bf>: banderas <tt>O_XXX</tt> desde la llamada al sistema <bf>open(2)</bf> copiadas allí (con ligeras modificaciones de <tt>filp_open()</tt>) por <tt>dentry_open()</tt> y después de limpiar <tt>O_CREAT</tt>, <tt>O_EXCL</tt>, <tt>O_NOCTTY</tt>, <tt>O_TRUNC</tt> - no hay sitio para almacenar estas banderas permanentemente ya que no pueden ser modificadas por las llamadas <bf>fcntl(2)</bf> <tt>F_SETFL</tt> (o consultadas por <tt>F_GETFL</tt>). <item><bf>f_mode</bf>: una combinación de banderas del espacio de usuario y modos, establecido por <tt>dentry_open()</tt>. El punto de conversión es almacenar los accesos de lectura y escritura en bits separados, por lo tanto uno los chequearía fácilmente como <tt>(f_mode & FMODE_WRITE)</tt> y <tt>(f_mode & FMODE_READ)</tt>. <item><bf>f_pos</bf>: la actual posición en el fichero para la siguiente lectura o escritura. Bajo i386 es del tipo <tt>long long</tt>, esto es un valor de 64 bits. <item><bf>f_reada, f_ramax, f_raend, f_ralen, f_rawin</bf>: para soportar readahead - muy complejo para ser discutido por mortales ;) <item><bf>f_owner</bf>: dueño del archivo de E/S a recibir las modificaciones de E/S asíncronas a través del mecanismo <tt>SIGIO</tt> (ver <tt>fs/fcntl.c:kill_fasync()</tt>). <item><bf>f_uid, f_gid</bf> - establece el identificador del usuario y el identificador del grupo del proceso que abrió el fichero, cuando la estructura del fichero es creada por <tt>get_empty_filp()</tt>. Si el fichero es un conector, usado por netfilter ipv4. <item><bf>f_error</bf>: usado por el cliente NFS para devolver errores de escritura. Esto es establecido en <tt>fs/nfs/file.c</tt> y chequeado en <tt>mm/filemap.c:generic_file_write()</tt>. <item><bf>f_version</bf> - mecanismo de versionado para la invalidación de antememorias, incrementado (usando un <tt>event</tt> global) cuando cambia <tt>f_pos</tt>. <item><bf>private_data</bf>: datos privados para cada fichero, los cuales pueden ser usados por los sistemas de ficheros (ej. coda almacena las credenciales aquí) o por otros controladores de dispositivos. Los controladores de dispositivos (en la presencia de devfs) pueden usar este campo para diferenciar entre múltiples instancias, en vez del clásico número menor codificado en <tt>file->f_dentry->d_inode->i_rdev</tt>. </enum> Ahora déjanos mirar en la estructura <tt>file_operations</tt> la cual contiene los métodos que serán llamados en los archivos. Déjanos recalcar que es copiado desde <tt>inode->i_fop</tt> donde es establecido por el método <tt>s_op->read_inode()</tt>. Se declara en <tt>include/linux/fs.h</tt>: <tscreen><code> struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char *, size_t, loff_t *); ssize_t (*write) (struct file *, const char *, size_t, loff_t *); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); }; </code></tscreen> <enum> <item><bf>owner</bf>: un puntero al módulo que es dueño del subsistema en cuestión. Sólo los controladores necesitan establecerlo a <tt>THIS_MODULE</tt>, los sistemas de ficheros puede felizmente ignorarlos porque sus cuentas de módulos son controladas en el tiempo de montaje/desmontaje, en cambio los controladores necesitan controlarlo en tiempo de apertura/liberación. <item><bf>llseek</bf>: implementa la llamada al sistema <bf>lseek(2)</bf>. Usualmente es omitida y es usada <tt>fs/read_write.c:default_llseek()</tt>, la cual hace lo correcto (POR HACER: fuerza a todos aquellos que establecen a NULL actualmente a usar default_llseek - que es el camino por el que salvamos una <tt>if()</tt> en <tt>llseek()</tt>). <item><bf>read</bf>: implementa la llamada al sistema <tt>read(2)</tt>. Los sistemas de ficheros pueden usar <tt>mm/filemap.c:generic_file_read()</tt> para ficheros regulares y <tt>fs/read_write.c:generic_read_dir()</tt> (la cual simplemente devuelve <tt>-EISDIR</tt>) para directorios aquí. <item><bf>write</bf>: implementa la llamada al sistema <bf>write(2)</bf>. Los sistemas de ficheros pueden usar <tt>mm/filemap.c:generic_file_write()</tt> para ficheros regulares e ignorarlo para directorios aquí. <item><bf>readdir</bf>: usado por los sistema de ficheros. Ignorado por los ficheros regulares e implementa las llamadas al sistema <bf>readdir(2)</bf> y <bf>getdents(2)</bf> para directorios. <item><bf>poll</bf>: implementa las llamadas al sistema <bf>poll(2)</bf> y <bf>select(2)</bf>. <item><bf>ioctl</bf>: implementa el controlador o los ioctls específicos del sistema de ficheros. Nótese que los ioctls genéricos de los ficheros como <tt>FIBMAP</tt>, <tt>FIGETBSZ</tt>, <tt>FIONREAD</tt> son implementados por niveles más altos y por lo tanto nunca leerán el método <tt>f_op->ioctl()</tt>. <item><bf>mmap</bf>: implementa la llamada al sistema <bf>mmap(2)</bf>. Los sistemas de ficheros pueden usar aquí <bf>generic_file_mmap</bf> para ficheros regulares e ignorarlo en los directorios. <item><bf>open</bf>: llamado en tiempo de <bf>open(2)</bf> por <tt>dentry_open()</tt>. Los sistemas de ficheros raramente usan esto, ej. coda intenta almacenar el fichero localmente en tiempo de apertura. <item><bf>flush</bf>: llamada en cada <bf>close(2)</bf> de este fichero, no necesariamente el último (ver el método <tt>release()</tt> a continuación). El único sistema de ficheros que lo utiliza es en un cliente NFS para pasar a disco todas las páginas sucias. Nótese que esto puede devolver un error el cual será retornado al espacio de usuario que realizó la llamada al sistema <bf>close(2)</bf>. <item><bf>release</bf>:llamado por la última <bf>close(2)</bf> de este fichero, esto es cuando <tt>file->f_count</tt> llega a 0. Aunque definido como un entero de retorno, el valor de retorno es ignorado por VFS (ver <tt>fs/file_table.c:__fput()</tt>). <item><bf>fsync</bf>: mapea directamente a las llamadas al sistema <bf>fsync(2)/fdatasync(2)</bf>, con el último argumento especificando cuando es fsync o fdatasync. Por lo menos no se realiza trabajo por VFS sobre esto, excepto el mapear el descriptor del fichero a una estructura de fichero (<tt>file = fget(fd)</tt>) y bajar/subir el semáforo <tt>inode->i_sem</tt>. El sistema de ficheros Ext2 ignora el último argumento y realiza lo mismo para <bf>fsync(2)</bf> y <bf>fdatasync(2)</bf>. <item><bf>fasync</bf>: este método es llamado cuando cambia <tt>file->f_flags & FASYNC</tt>. <item><bf>lock</bf>: parte del mecanismo de bloqueo de la región del <bf>fcntl(2)</bf> POSIX de la porción específica del sistema de ficheros. El único fallo aquí es porque es llamado antes por una porción independiente del sistema de ficheros (<tt>posix_lock_file()</tt>), si tiene éxito pero el código de bloqueo estandart POSIX falla, entonces nunca será desbloqueado en un nivel dependiente del sistema de ficheros... <item><bf>readv</bf>: implementa la llamada al sistema <bf>readv(2)</bf>. <item><bf>writev</bf>: implementa la llamada al sistema <bf>writev(2)</bf>. </enum> <sect1>Administración de Puntos de Montaje y Superbloque<p> Bajo Linux, la información sobre los sistemas de ficheros montados es mantenida en dos estructuras separadas - <tt>super_block</tt> y <tt>vfsmount</tt>. El motivo para esto es que Linux permite montar el mismo sistema de ficheros (dispositivo de bloque) bajo múltiples puntos de montaje, lo cual significa que el mismo <tt>super_block</tt> puede corresponder a múltiples estructuras <tt>vfsmount</tt>. Déjanos mirar primero en <tt>struct super_block</tt>, declarado en <tt>include/linux/fs.h</tt>: <tscreen><code> struct super_block { struct list_head s_list; /* Mantiene esto primero */ kdev_t s_dev; unsigned long s_blocksize; unsigned char s_blocksize_bits; unsigned char s_lock; unsigned char s_dirt; struct file_system_type *s_type; struct super_operations *s_op; struct dquot_operations *dq_op; unsigned long s_flags; unsigned long s_magic; struct dentry *s_root; wait_queue_head_t s_wait; struct list_head s_dirty; /* inodos sucios */ struct list_head s_files; struct block_device *s_bdev; struct list_head s_mounts; /* vfsmount(s) de este */ struct quota_mount_options s_dquot; /* Opciones Específicas de Diskquota */ union { struct minix_sb_info minix_sb; struct ext2_sb_info ext2_sb; ..... todos los sistemas de archivos necesitan sb-private ... void *generic_sbp; } u; /* * El siguiente campo es *sólo* para VFS. Los sistemas de ficheros * no tienen trabajo alguno mirando en él. Has sido avisado. */ struct semaphore s_vfs_rename_sem; /* Truco */ /* El siguiente campo es usado por knfsd cuando convierte un * manejador de ficheros (basado en el número de inodo) en * una dentry. Tal como construye un camino en el árbol dcache * desde el fondo hasta arriba, quizás exista durante algún * tiempo un subcamino de dentrys que no están conectados al * árbol principal. Este semáforo asegura que hay sólo * siempre un camino libre por sistema de ficheros. Nótese que * los ficheros no conectados (o otros no directorios) son * permitidos, pero no los directorios no conectados. */ struct semaphore s_nfsd_free_path_sem; }; </code></tscreen> Las diversos campos en la estructura <tt>super_block</tt> son: <enum> <item><bf>s_list</bf>: una lista doblemente enlazada de todos los superbloques activos; nótese que no he dicho "de todos los sistemas de ficheros montados" porque bajo Linux uno puede tener múltiples instancias de un sistema de ficheros montados correspondientes a un superbloque simple. <item><bf>s_dev</bf>: para sistemas de ficheros que requieren un bloque para ser montado en él. Esto es para los sistemas de ficheros <tt>FS_REQUIRES_DEV</tt>, esto es la <tt>i_dev</tt> del dispositivo de bloques. Para otros (llamados sistemas de ficheros anónimos) esto es un entero <tt>MKDEV(UNNAMED_MAJOR, i)</tt> donde <tt>i</tt> es el primer bit no establecido en la matriz <tt>unnamed_dev_in_use</tt>, entre 1 y 255 incluidos. Ver <tt>fs/super.c:get_unnamed_dev()/put_unnamed_dev()</tt>. Ha sido sugerido muchas veces que los sistemas de ficheros anónimos no deberían de usar el campo <tt>s_dev</tt>. <item><bf>s_blocksize, s_blocksize_bits</bf>: tamaño del bloque y log2(tamaño del bloque). <item><bf>s_lock</bf>: indica cuando un superbloque está actualmente bloqueado por <tt>lock_super()/unlock_super()</tt>. <item><bf>s_dirt</bf>: establece cuando el superbloque está modificado, y limpiado cuando es vuelto a ser escrito a disco. <item><bf>s_type</bf>: puntero a <tt>struct file_system_type</tt> del sistema de ficheros correspondiente. El método <tt>read_super()</tt> del sistema de ficheros no necesita ser establecido como VFS <tt>fs/super.c:read_super()</tt>, lo establece para ti si el <tt>read_super()</tt> que es específico del sistema de ficheros tiene éxito, y se reinicializa a NULL si es que falla. <item><bf>s_op</bf>: puntero a la estructura <tt>super_operations</tt>, la cual contiene métodos específicos del sistema de ficheros para leer/escribir inodos, etc. Es el trabajo del método <tt>read_super()</tt> del sistema de ficheros inicializar <tt>s_op</tt> correctamente. <item><bf>dq_op</bf>: operaciones de cuota de disco. <item><bf>s_flags</bf>: banderas de superbloque. <item><bf>s_magic</bf>: número mágico del sistema de ficheros. Usado por el sistema de ficheros de minix para diferenciar entre múltiples tipos del mismo. <item><bf>s_root</bf>: dentry de la raiz del sistema de ficheros. Es trabajo de <tt>read_super()</tt> leer el inodo raiz desde el disco y pasárselo a <tt>d_alloc_root()</tt> para asignar la dentry e instanciarlo. Algunos sistemas de ficheros dicen "raiz" mejor que "/" y por lo tanto usamos la función más genérica <tt>d_alloc()</tt> para unir la dentry a un nombre, ej. pipefs se monta a si mismo en "pipe:" como su raiz en vez de "/". <item><bf>s_wait</bf>: cola de espera de los procesos esperando para que el superbloque sea desbloqueado. <item><bf>s_dirty</bf>: una lista de todos los inodos sucios. Recalcar que si un inodo está sucio (<tt>inode->i_state & I_DIRTY</tt>) entonces su lista sucia específica del superbloque es enlazada a través de <tt>inode->i_list</tt>. <item><bf>s_files</bf>: una lista de todos los ficheros abiertos en este superbloque. Util para decidir cuándo los sistemas de archivos pueden ser remontados como de sólo lectura, ver <tt>fs/file_table.c:fs_may_remount_ro()</tt> el cual va a través de la lista <tt>sb->s_files</tt> y deniega el remontar si hay ficheros abiertos para escritura (<tt>file->f_mode & FMODE_WRITE</tt>) o ficheros con desenlaces pendientes (<tt>inode->i_nlink == 0</tt>). <item><bf>s_bdev</bf>: para <tt>FS_REQUIRES_DEV</tt>, esto apunta a la estructura block_device describiendo el dispositivo en el que el sistema de ficheros está montado. <item><bf>s_mounts</bf>: una lista de todas las estructuras <tt>vfsmount</tt>, una por cada instancia montada de este superbloque. <item><bf>s_dquot</bf>: más miembros de diskquota. </enum> Las operaciones de superbloque son descritas en la estructura <tt>super_operations</tt> declarada en <tt>include/linux/fs.h</tt>: <tscreen><code> struct super_operations { void (*read_inode) (struct inode *); void (*write_inode) (struct inode *, int); void (*put_inode) (struct inode *); void (*delete_inode) (struct inode *); void (*put_super) (struct super_block *); void (*write_super) (struct super_block *); int (*statfs) (struct super_block *, struct statfs *); int (*remount_fs) (struct super_block *, int *, char *); void (*clear_inode) (struct inode *); void (*umount_begin) (struct super_block *); }; </code></tscreen> <enum> <item><bf>read_inode</bf>: lee el inodo desde el sistema de archivos. Es sólo llamado desde <tt>fs/inode.c:get_new_inode()</tt>, y desde <tt>iget4()</tt> (y por consiguiente <tt>iget()</tt>). Si un sistema de ficheros quiere usar <tt>iget()</tt> entonces <tt>read_inode()</tt> debe de ser implementado - en otro caso <tt>get_new_inode()</tt> fallará. Mientras el inodo está siendo leido está bloqueado (<tt>inode->i_state = I_LOCK</tt>). Cuando la función regresa, todos los que están esperando en <tt>inode->i_wait</tt> son despertados. El trabajo del método <tt>read_inode()</tt> del sistema de ficheros es localizar el bloque del disco que contiene el inodo a ser leído y usar la funcion de la antememoria intermedia <tt>bread()</tt> para leerlo e inicializar varios campos de la estructura de inodos, por ejemplo el <tt>inode->i_op</tt> y <tt>inode->i_fop</tt> para que los niveles VFS conozcan qué operaciones pueden ser efectuadas en el inodo o fichero correspondiente. Los sistemas de ficheros que no implementan <tt>read_inode()</tt> son ramfs y pipefs. Por ejemplo, ramfs tiene su propia función de generación de inodos <tt>ramfs_get_inode()</tt> con todas las operaciones de inodos llamándola cuando se necesita. <item><bf>write_inode</bf>: escribe el inodo de vuelta al disco. Similar a <tt>read_inode()</tt> en que necesita localizar el bloque relevante en el disco e interactuar con la antememoria intermedia llamando a <tt>mark_buffer_dirty(bh)</tt>. Este método es llamado en los inodos sucios (aquellos marcados como sucios por <tt>mark_inode_dirty()</tt>) cuando el inodo necesita ser sincronizado individualmente o como parte de la actualizacion entera del sistema de ficheros. <item><bf>put_inode</bf>: llamado cuando la cuenta de referencia es decrementada. <item><bf>delete_inode</bf>: llamado cuando <tt>inode->i_count</tt> y <tt>inode->i_nlink</tt> llegan a 0. El sistema de ficheros borra la copia en disco del inodo y llama a <tt>clear_inode()</tt> en el inodo VFS para "terminar con él con el perjuicio extremo". <item><bf>put_super</bf>: llamado en las últimas etapas de la llamada al sistema <bf>umount(2)</bf> para notificar al sistema de ficheros que cualquier información mantenida por el sistema de ficheros sobre esa instancia tiene que ser liberada. Típicamente esto <tt>brelse()</tt> el bloque conteniendo el superbloque y <tt>kfree()</tt> cualesquiera bitmaps asignados para bloques libres, inodos, etc. <item><bf>write_super</bf>: llamado cuando el superbloque necesita ser vuelto a escribir en el disco. Debería de encontrar el bloque conteniendo el superbloque (usualmente mantenido en el área <tt>sb-private</tt>) y <tt>mark_buffer_dirty(bh)</tt>. También debería de limpiar la bandera <tt>sb->s_dirt</tt>. <item><bf>statfs</bf>: implementa las llamadas al sistema <bf>fstatfs(2)/statfs(2)</bf>. Nótese que el puntero a <tt>struct statfs</tt> pasado como argumento, es el puntero del núcleo, no un puntero del usuario, por lo tanto no necesitamos hacer ninguna E/S al espacio de usuario. Si no está implementada entonces <tt>statfs(2)</tt> fallará con <tt>ENOSYS</tt>. <item><bf>remount_fs</bf>: llamado cuando el sistema de ficheros está siendo remontado. <item><bf>clear_inode</bf>: llamado desde el nivel VFS <tt>clear_inode()</tt>. Los sistemas que atacan datos privados a la estructura del inodo (a través del campo <tt>generic_ip</tt>) deben liberarse aquí. <item><bf>umount_begin</bf>: llamado durante el desmontaje forzado para notificarlo al sistema de ficheros de antemano, por lo tanto puede ser lo mejor para asegurarse que nada mantiene al sistema de ficheros ocupado. Actualmente usado sólo por NFS. Esto no tiene nada que hacer con la idea del soporte de desmontaje forzado del nivel genérico de VFS </enum> Por lo tanto, déjanos mirar qué pasa cuando montamos un sistema de ficheros en disco (<tt>FS_REQUIRES_DEV</tt>). La implementación de la llamada al sistema <bf>mount(2)</bf> está en <tt>fs/super.c:sys_mount()</tt> que es justo un envoltorio que copia las opciones, el tipo del sistema de ficheros y el nombre del dispositivo para la función <tt>do_mount()</tt>, la cual realiza el trabajo real: <enum> <item>El controlador del sistema de ficheros es cargado si se necesita y la cuenta de referencia del módulo es incrementada. Nótese que durante la operación de montaje, la cuenta del sistema de ficheros es incrementada dos veces - una vez por <tt>do_mount()</tt> llamando a <tt>get_fs_type()</tt> y otra vez por <tt>get_sb_dev()</tt> llamando a <tt>get_filesystem()</tt> si <tt>read_super()</tt> tuvo éxito. El primer incremento es para prevenir la descarga del módulo mientras estamos dentro del método <tt>read_super()</tt>, y el segundo incremento es para indicar que el módulo está en uso por esta instancia montada. Obviamente, <tt>do_mount()</tt> decrementa la cuenta antes de regresar, por lo tanto, después de todo, la cuenta sólo crece en 1 después de cada montaje. <item>Desde, que en nuestro caso, <tt>fs_type->fs_flags & FS_REQUIRES_DEV</tt> es verdad, el superbloque es inicializado por una llamada a <tt>get_sb_bdev()</tt>, la cual obtiene la referencia al dispositivo de bloques e interactúa con el método <tt>read_super()</tt> del sistema de ficheros para rellenar el superbloque. Si todo va vien, la estructura <tt>super_block</tt> es inicializada y tenemos una referencia extra al módulo del sistema de ficheros y una referencia al dispositivo de bloques subyacente. <item>Una nueva estructura <tt>vfsmount</tt> es asignada y enlazada a la lista <tt>sb->s_mounts</tt> y a la lista global <tt>vfsmntlist</tt>. El campo <tt>vfsmount</tt> de <tt>mnt_instances</tt> nos permite encontrar todas las instancias montadas en el mismo superbloque que este. El campo <tt>mnt_list</tt> nos permite encontrar todas las instancias para todos los superbloques a lo largo del sistema. El campo <tt>mnt_sb</tt> apunta a este superbloque y <tt>mnt_root</tt> tiene una nueva referencia a la dentry <tt>sb->s_root</tt>. </enum> <sect1>Ejemplo de un Sistema de Ficheros Virtual: pipefs<p> Como un ejemplo simple del sistema de ficheros de Linux que no requiere un dispositivo de bloque para montar, déjanos considerar pipefs desde <tt>fs/pipe.c</tt>. El preámbulo del sistema de ficheros es bastante directo y requiere una pequeña explicación: <tscreen><code> static DECLARE_FSTYPE(pipe_fs_type, "pipefs", pipefs_read_super, FS_NOMOUNT|FS_SINGLE); static int __init init_pipe_fs(void) { int err = register_filesystem(&pipe_fs_type); if (!err) { pipe_mnt = kern_mount(&pipe_fs_type); err = PTR_ERR(pipe_mnt); if (!IS_ERR(pipe_mnt)) err = 0; } return err; } static void __exit exit_pipe_fs(void) { unregister_filesystem(&pipe_fs_type); kern_umount(pipe_mnt); } module_init(init_pipe_fs) module_exit(exit_pipe_fs) </code></tscreen> El sistema de ficheros es del tipo <tt>FS_NOMOUNT|FS_SINGLE</tt>, lo que significa que no puede ser montado desde el espacio de usuario y sólo puede haber uno en el sistema. El fichero <tt>FS_SINGLE</tt> también significa que debe de ser montado a través de <tt>kern_mount()</tt> después de que haya sido registrado con éxito a través de <tt>register_filesystem()</tt>, lo cual es exactamente lo que pasa en <tt>init_pipe_fs()</tt>. El único fallo en esta función es que si <tt>kern_mount()</tt> falla (ej. porque <tt>kmalloc()</tt> falló en <tt>add_vfsmnt()</tt>) entonces el sistema de ficheros es dejado como registrado pero la inicialización del módulo falla. Esto causará que <bf>cat /proc/filesystems</bf> falle (justamente acabo de enviar un parche a Linus mencionándole que esto no es un fallo real hoy en día porque pipefs no puede ser compilado como módulo, debería de ser escrito desde el punto de vista de que en el futuro pudiera ser modularizado). El resultado de <tt>register_filesystem()</tt> es que <tt>pipe_fs_type</tt> es enlazado en la lista <tt>file_systems</tt>, por lo tanto uno puede leer <tt>/proc/filesystems</tt> y encontrar la entrada "pipefs" allí con la bandera "nodev" indicando que <tt>FS_REQUIRES_DEV</tt> no fue establecida. El archivo <tt>/proc/filesystems</tt> debería realmente de ser capaz de soportar todas las nuevas banderas <tt>FS_</tt> (y yo he hecho un parche que lo hace) pero no puede ser realizado porque hará fallar a todas las aplicaciones de usuario que lo utilicen. A pesar de que los interfaces del núcleo Linux cambian cada minuto (sólo para mejor) cuando se refiere a la compatibilidad del espacio de usuario, Linux es un sistema operativo muy conservador que permite que muchas aplicaciones sean usadas durante un largo periodo de tiempo sin ser recompiladas. El resultado de <tt>kern_mount()</tt> es que: <enum> <item>Un nuevo número de dispositvo sin nombre (anónimo) es asignado estableciendo un bit en el bitmap <tt>unnamed_dev_in_use</tt>; si no hay más bits entonces <tt>kern_mount()</tt> fallará con <tt>EMFILE</tt>. <item>Una nueva estructura superbloque es asignada por medio de <tt>get_empty_super()</tt>. La función <tt>get_empty_super()</tt> camina a través de las cabeceras de las lista de superbloques por <tt>super_block</tt> y busca una entrada vacía, esto es <tt>s->s_dev == 0</tt>. Si no se encuentra dicho superbloque vacío entonces uno nuevo es asignado usando <tt>kmalloc()</tt> con la prioridad <tt>GFP_USER</tt>. El número máximo de superbloques en el sistema es chequeado en <tt>get_empty_super()</tt>, por lo tanto empieza fallando, uno puede modificar el parámetro ajustable <tt>/proc/sys/fs/super-max</tt>. <item>Un método específico del sistema de ficheros <tt>pipe_fs_type->read_super()</tt>, esto es <tt>pipefs_read_super()</tt>, es invocada, la cual asigna el inodo y la dentry raiz <tt>sb->s_root</tt>, y establece <tt>sb->s_op</tt> para ser <tt>&pipefs_ops</tt>. <item> Entonces <tt>kern_mount()</tt> llama a <tt>add_vfsmnt(NULL, sb->s_root, "none")</tt> la cual asigna una nueva estructura <tt>vfsmount</tt> y la enlaza en <tt>vfsmntlist</tt> y <tt>sb->s_mounts</tt>. <item>El <tt>pipe_fs_type->kern_mnt</tt> es establecido a esta nueva estructura <tt>vfsmount</tt> y es devuelta. El motivo por el que el valor de retorno de <tt>kern_mount()</tt> es una estructura <tt>vfsmount</tt> es porque incluso los sistemas de ficheros <tt>FS_SINGLE</tt> pueden ser montados múltiples veces y por lo tanto sus <tt>mnt->mnt_sb</tt> deberían apuntar a la misma cosa, que sería tonto devolverla desde múltiples llamadas a <tt>kern_mount()</tt>. </enum> Ahora que el sistema de ficheros está registrado y montado dentro del núcleo podemos usarlo. El punto de entrada en el sistema de ficheros pipefs es la llamada al sistema <bf>pipe(2)</bf>, implementada por una función dependiente de la arquitectura <tt>sys_pipe()</tt>, pero el trabajo real es realizado por un función portable <tt>fs/pipe.c:do_pipe()</tt>. Déjanos mirar entonces en <tt>do_pipe()</tt>. La interacción con pipefs sucede cuando <tt>do_pipe()</tt> llama a <tt>get_pipe_inode()</tt> para asignar un nuevo inodo pipefs. Para este inodo, <tt>inode->i_sb</tt> es establecido al superbloque de pipefs <tt>pipe_mnt->mnt_sb</tt>, las operaciones del archivo <tt>i_fop</tt> son establecidas a <tt>rdwr_pipe_fops</tt> y el número de lectores y escritores (mantenidos en <tt>inode->i_pipe</tt>) es establecido a 1. El motivo por el que hay un campo de inodos separado <tt>i_pipe</tt> en vez de mantenerlo en la unión <tt>fs-private</tt> es que pipes y FIFOs comparten el mismo código y los FIFOs puede existir en otros sistemas de ficheros los cuales usan otros caminos de acceso con la misma unión, lo cual en C es muy malo y puede trabajar sólo con pura suerte. Por lo tanto, sí, los núcleos 2.2.x trabajan por pura suerte y pararán de trabajar tan pronto como tu retoques ligeramente los campos en el inodo. Cada llamada al sistema <bf>pipe(2)</bf> incrementa una cuenta de referencia en la instancia de montaje <tt>pipe_mnt</tt>. Bajo Linux, los pipes no son simétricos (pipes STREAM o bidireccionales) esto es, dos caras del mismo fichero tienes diferentes operaciones <tt>file->f_op</tt> - la <tt>read_pipe_fops</tt> y <tt>write_pipe_fops</tt> respectivamente. La escritura en la cara de lectura devuelve un <tt>EBADF</tt> y lo mismo si se lee en la cara de escritura. <sect1>Ejemplo de Sistema de Ficheros de Disco: BFS<p> Como un simple ejemplo de un sistema de ficheros Linux en disco, déjanos considerar BFS. El preámbulo del módulo de BFS está en <tt>fs/bfs/inode.c</tt>: <tscreen><code> static DECLARE_FSTYPE_DEV(bfs_fs_type, "bfs", bfs_read_super); static int __init init_bfs_fs(void) { return register_filesystem(&bfs_fs_type); } static void __exit exit_bfs_fs(void) { unregister_filesystem(&bfs_fs_type); } module_init(init_bfs_fs) module_exit(exit_bfs_fs) </code></tscreen> Una declaracion especial de la macro del sistema de ficheros <tt>DECLARE_FSTYPE_DEV()</tt> es usada, la cual establece el <tt>fs_type->flags</tt> a <tt>FS_REQUIRES_DEV</tt> para significar que BFS requiere un dispositivo de bloque real para ser montado. La función de inicialización del módulo registra el sistema de ficheros con VFS y la función de limpieza (sólo presente cuando BFS está configurada para ser un módulo) lo desregistra. Con el sistema de ficheros registrado, podemos proceder a montarlo, lo cual invocará al método <tt>fs_type->read_super()</tt> que es implementado en <tt>fs/bfs/inode.c:bfs_read_super().</tt>. El realiza lo siguiente: <enum> <item><tt>set_blocksize(s->s_dev, BFS_BSIZE)</tt>: desde que nosotros vamos a interactuar con la capa de dispositivos de bloque a través de la antememoria intermedia, debemos inicializar unas pocas cosas, esto es, establecer el tamaño del bloque y también informar a VFS a través de los campos <tt>s->s_blocksize</tt> y <tt>s->s_blocksize_bits</tt>. <item><tt>bh = bread(dev, 0, BFS_BSIZE)</tt>: leemos el bloque 0 del dispositivo a través de <tt>s->s_dev</tt>. Este bloque es el superbloque del sistema. <item>El superbloque es validado contra el número <tt>BFS_MAGIC</tt> y, si es válido, es almacenado en el campo sb-private <tt>s->su_sbh</tt> (el cual es realmente <tt>s->u.bfs_sb.si_sbh</tt>). <item>Entonces asignamos el bitmap del inodo <tt>kmalloc(GFP_KERNEL)</tt> y limpiamos todos sus bits a 0, excepto los dos primeros, los cuales estableceremos a 1 para indicar que nunca deberemos asignar los inodos 0 y 1. El inodo 2 es la raiz y el correspondiente bit será establecido a 1 unas pocas lineas después de cualquier forma - ¡el sistema de ficheros debe de tener un inodo raiz válido en tiempo de montaje! <item>Entonces inicializamos <tt>s->s_op</tt>, lo cual significa que podemos desde este punto llamar a la memoria intermedia de inodos a través de <tt>iget()</tt>, lo cual resulta en la invocación de <tt>s_op->read_inode()</tt>. Esto encuentra el bloque que contiene el inodo especificado (por <tt>inode->i_ino</tt> y <tt>inode->i_dev</tt>) y lo lee. Si fallamos al obtener el inodo raiz entonces liberamos el bitmap de inodos y descargaremos la antememoria de superbloque a la antememoria intermedia y devolveremos NULL. Si el inodo raiz fue leido correctamente, entonces asignamos una dentry con el nombre <tt>/</tt> (como convirtiéndolo en raiz) y lo instanciamos con este inodo. <item>Ahora vamos a través de todos los inodos del sistema de ficheros y los leemos en orden a establecer los bits correspondientes en nuestro bitmap interno de inodos y también calculamos otros parámetros internos como el desplazamiento del último inodo y el comienzo/final del último fichero. Cada inodo que leemos es devuelto atrás a la memoria intermedia de inodos a través de <tt>iput()</tt> - no mantenemos una referencia a él más tiempo del necesario. <item>Si el sistema de ficheros no fue montado como de sólo lectura, marcamos la memoria intermedia del superbloque como sucio y establecemos la bandera <tt>s->s_dirt</tt> (POR HACER: ¿Porqué hago esto? Originalmente, lo hice porque lo hacía <tt>minix_read_super()</tt> pero ni minix ni BFS parecen modificar el superbloque en el <tt>read_super()</tt>). <item>Todo está bién, por lo tanto regresamos atrás a este superbloque inicializado para el llamante en el nivel VFS, esto es, <tt>fs/super.c:read_super()</tt>. </enum> Después de que la función <tt>read_super()</tt> regrese con éxito, VFS obtiene la referencia al módulo del sistema de ficheros a través de la llamada a <tt>get_filesystem(fs_type)</tt> en <tt>fs/super.c:get_sb_bdev()</tt> y una referencia al dispositivo de bloques. Ahora, déjanos examinar qué pasa cuando hacemos una E/S en el sistema de ficheros. Ya hemos examinado cómo los inodos son leidos cuando <tt>iget()</tt> es llamado y cómo son quitados en <tt>iput().</tt> Leyendo inodos, configura entre otras cosas, <tt>inode->i_op</tt> y <tt>inode->i_fop</tt>; abriendo un fichero propagará <tt>inode->i_fop</tt> en <tt>file->f_op</tt>. Déjanos examinar el camino de código de la llamada al sistema <bf>link(2)</bf>. La implementación de la llamada al sistema está en <tt>fs/namei.c:sys_link()</tt>: <enum> <item>Los nombres del espacio de usuario son copiados en el espacio del núcleo por medio de la función <tt>getname()</tt> la cual realiza el chequeo de errores. <item>Estos nombres son convertidos a datos usando <tt>path_init()/path_walk()</tt> interactuando con dcache. El resultado es almacenado en las estructuras <tt>old_nd</tt> y <tt>nd</tt>. <item>Si <tt>old_nd.mnt != nd.mnt</tt> entonces "enlace a través de dispositvos" <tt>EXDEV</tt> es devuelto - uno no puede enlazar entre sistemas de ficheros, en Linux esto se traduce en - uno no puede enlazar entre múltiples instancias de un sistema de ficheros (o, en particular entre sistemas de ficheros). <item>Una nueva dentry es creada correspondiente a <tt>nd</tt> por <tt>lookup_create()</tt> . <item>Una función genérica <tt>vfs_link()</tt> es llamada, la cual chequea si podemos crear una nueva entrada en el directorio e invoca el método <tt>dir->i_op->link()</tt>, que nos trae atrás a la función específica del sistema de ficheros <tt>fs/bfs/dir.c:bfs_link()</tt>. <item>Dentro de <tt>bfs_link()</tt>, chequeamos si estamos intentando enlazar un directorio, y si es así, lo rechazamos con un error <tt>EPERM</tt>. Este es el mismo comportamiento que el estándar (ext2). <item>Intentamos añadir una nueva entrada de directorio al directorio especificado por la función de ayuda <tt>bfs_add_entry()</tt> la cual va a través de todas las entradas buscando un slot sin usar (<tt>de->ino == 0</tt>) y, cuando lo encuentra, escribe en el par nombre/inodo en el bloque correspondiente y lo marca como sucio (a una prioridad no-superbloque). <item>Si hemos añadido con éxito la entrada de directorio entonces no hay forma de fallar la operación y por lo tanto incrementamos <tt>inode->i_nlink</tt>, actualizamos <tt>inode->i_ctime</tt> y marcamos este inodo como sucio a la vez que instanciamos la nueva dentry con el inodo. </enum> Otras operaciones de inodos relacionadas como <tt>unlink()/rename()</tt> etc. trabajan en una forma similar, por lo tanto no se gana mucho examinándolas a todas ellas en detalle. <sect1>Dominios de Ejecución y Formatos Binarios<p> Linux soporta la carga de aplicaciones binarias de usuario desde disco. Más interesantemente, los binarios pueden ser almacenados en formatos diferentes y la respuesta del sistema operativo a los programas a través de las llamadas al sistema pueden desviarla de la norma (la norma es el comportamiento de Linux) tal como es requerido, en orden a emular los formatos encontrados en otros tipos de UNIX (COFF, etc) y también emular el comportamiento de las llamadas al sistema de otros tipos (Solaris, UnixWare, etc). Esto es para lo que son los dominios de ejecución y los formatos binarios. Cada tarea Linux tiene una personalidad almacenada en su <tt>task_struct</tt> (<tt>p->personality</tt>). Las personalidades actualmente existentes (en el núcleo oficial o en el parche añadido) incluyen soporte para FreeBSD, Solaris, UnixWare, OpenServer y algunos otros sistemas operativos populares. El valor de <tt>current->personality</tt> es dividido en dos partes: <enum> <item>tres bytes altos - emulación de fallos: <tt>STICKY_TIMEOUTS</tt>, <tt>WHOLE_SECONDS</tt>, etc. <item>byte bajo - personalidad propia, un número único. </enum> Cambiando la personalidad, podemos cambiar la forma en la que el sistema operativo trata ciertas llamadas al sistema, por ejemplo añadiendo una <tt>STICKY_TIMEOUT</tt> a <tt>current->personality</tt> hacemos que la llamada al sistema <bf>select(2)</bf> preserve el valor del último argumento (timeout) en vez de almacenar el tiempo no dormido. Algunos programas defectuosos confían en sistemas operativos defectuosos (no Linux) y por lo tanto suministra una forma para emular fallos en casos donde el código fuente no está disponible y por lo tanto los fallos no pueden ser arreglados. El dominio de ejecución es un rango contiguo de personalidades implementadas por un módulo simple. Usualmente un dominio de ejecución simple implementa una personalidad simple, pero a veces es posible implementar personalidades "cerradas" en un módulo simple sin muchos condicionantes. Los dominios de ejecución son implementados en <tt>kernel/exec_domain.c</tt> y fueron completamente reescritos para el núcleo 2.4, comparado con el 2.2.x. La lista de dominios de ejecución actualmente soportada por el núcleo, a lo largo del rango de personalidades que soportan, está disponible leyendo el archivo <tt>/proc/execdomains</tt>. Los dominios de ejecución, excepto el <tt>PER_LINUX</tt>, pueden ser implementados como módulos dinámicamente cargados. La interfaz de usuario es a través de la llamada al sistema <bf>personality(2)</bf>, la cual establece la actual personalidad del proceso o devuelve el valor de <tt>current->personality</tt> si el argumento es establecido a una personalidad imposible. Obviamente, el comportamiento de esta llamada al sistema no depende de la personalidad. La interfaz del núcleo para el registro de dominios de ejecución consiste en dos funciones: <itemize> <item><tt>int register_exec_domain(struct exec_domain *)</tt>: registra el dominio de ejecución enlazándolo en una lista simplemente enlazada <tt>exec_domains</tt> bajo la protección de escritura del spinlock read-write <tt>exec_domains_lock</tt>. Devuelve 0 si tiene éxito, distinto de cero en caso de fallo. <item><tt>int unregister_exec_domain(struct exec_domain *)</tt>: desregistra el dominio de ejecución desenlazándolo desde la lista <tt>exec_domains</tt>, otra vez usando el spinlock <tt>exec_domains_lock</tt> en modo de escritura. Retorna 0 si tiene éxito. </itemize> El motivo por el que <tt>exec_domains_lock</tt> es read-write es que sólo las peticiones de registro y desregistro modifican la lista, mientras haciendo <bf>cat /proc/filesystems</bf> llama <tt>fs/exec_domain.c:get_exec_domain_list()</tt>, el cual necesita sólo acceso de lectura a la lista. Registrando un nuevo dominio de ejecución define un "manejador lcall7" y un mapa de conversión de número de señales. Actualmente, el parche ABI extiende este concepto a los dominios de ejecución para incluir información extra (como opciones de conector, tipos de conector, familia de direcciones y mapas de números de errores). Los formatos binarios son implementados de una forma similar, esto es, una lista simplemente enlazada de formatos es definida en <tt>fs/exec.c</tt> y es protegida por un cierre read-write <tt>binfmt_lock</tt>. Tal como con <tt>exec_domains_lock</tt>, el <tt>binfmt_lock</tt> es tomado para leer en la mayoría de las ocasiones excepto para el registro/desregistro de los formatos binarios. Registrando un nuevo formato binario intensifica la llamada al sistema <bf>execve(2)</bf> con nuevas funciones <tt>load_binary()/load_shlib()</tt>. Al igual que la habilidad para <tt>core_dump()</tt>. El método <tt>load_shlib()</tt> es usado sólo por la vieja llamada al sistema <bf>uselib(2)</bf> mientras que el método <tt>load_binary()</tt> es llamada por el <tt>search_binary_handler()</tt> desde <tt>do_execve()</tt> el cual implementa la llamada al sistema <bf>execve(2)</bf>. La personalidad del proceso está determinada por el formato binario cargado por el método del correspondiente formato <tt>load_binary()</tt> usando algunas heurísticas. Por ejemplo, para determinar los binarios UnixWare7, uno primero marca el binario usando la utilidad <bf>elfmark(1)</bf>, la cual establece la cabecera de ELF <tt>e_flags</tt> al valor mágico 0x314B4455, el cual es detectado en tiempo de carga del ELF y <tt>current->personality</tt> es establecido a PER_SVR4. Si esta heurística falla entonces una más genérica como el tratamiento de los caminos del intérprete ELF como <tt>/usr/lib/ld.so.1</tt> o <tt>/usr/lib/libc.so.1</tt> para indicar un binario SVR4, es usado y la personalidad es establecida a PER_SVR4. Uno podría escribir un pequeño programa de utilidad que usara las capacidades del <bf>ptrace(2)</bf> de Linux para, en un simple paso, códificar y forzar a un programa funcionando a cualquier personalidad. Una vez que la personalidad (y entonces <tt>current->exec_domain</tt>) es conocida, las llamadas al sistema son manejadas como sigue. Déjanos asumir que un proceso realiza una llamada al sistema por medio de la instrucción puerta lcall7. Esto transfiere el control a <tt>ENTRY(lcall7)</tt> de <tt>arch/i386/kernel/entry.S</tt> porque fue preparado en <tt>arch/i386/kernel/traps.c:trap_init()</tt>. Después de la apropiada conversión de la pila, <tt>entry.S:lcall7</tt> obtiene el puntero a <tt>exec_domain</tt> desde <tt>current</tt> y entonces un desplazamiento del manejador lcall7 con el <tt>exec_domain</tt> (el cual es codificado fuertemente como 4 en código ensamblador, por lo tanto no puedes desplazar el campo <tt>handler</tt> a través de la declaración en C de <tt>struct exec_domain</tt>) y salta a él. Por lo tanto, en C, se parecería a esto: <tscreen><code> static void UW7_lcall7(int segment, struct pt_regs * regs) { abi_dispatch(regs, &uw7_funcs[regs->eax & 0xff], 1); } </code></tscreen> donde <tt>abi_dispatch()</tt> es un envoltorio sobre la tabla de punteros de función que implementa las llamadas al sistema de esta personalidad <tt>uw7_funcs</tt>. <sect>Memoria Intermedia de Páginas Linux<p> En este capítulo describimos la memoria intermedia de páginas de Linux 2.4. La memoria intermedia de páginas es - como sugiere el nombre - una memoria intermedia de páginas físicas. En el mundo UNIX el concepto de memoria intermedia de páginas se convirtió popular con la introdución de SVR4 UNIX, donde reemplazó a la antememoria intermedia para las operaciones de E/S. Mientras la memoria intermedia de páginas de SVR4 es sólamente usada como memoria intermedia de datos del sistema de ficheros y estos usan la estructura vnode y un desplazamiento dentro del fichero como parámetros hash, la memoria intermedia de páginas de Linux está diseñada para ser más genérica, y entonces usa una estructura address_space (explicada posteriormente) como primer parámetro. Porque la memoria intermedia de páginas Linux está cerradamente emparejada a la notación del espacio de direcciones, necesitarás como mínimo un conocimiento previo del adress_spaces para entender la forma en la que trabaja la memoria intermedia de páginas. Un address_space es algún tipo de software MMU que mapea todas las páginas de un objeto (ej. inodo) a otro concurrentemente (tipicamente bloques físicos de disco). La estructura address_space está definida en <tt>include/linux/fs.h</tt> como: <tscreen><code> struct address_space { struct list_head clean_pages; struct list_head dirty_pages; struct list_head locked_pages; unsigned long nrpages; struct address_space_operations *a_ops; struct inode *host; struct vm_area_struct *i_mmap; struct vm_area_struct *i_mmap_shared; spinlock_t i_shared_lock; }; </code></tscreen> Para entender la forma en la que address_spaces trabaja, sólo necesitamos mirar unos pocos de estos campos: <tt>clean_pages</tt>, <tt>dirty_pages</tt> y <tt>locked_pages</tt> son listas doblemente enlazadas de páginas limpias, sucias y bloqueadas pertenecientes a este address_space, <tt>nrpages</tt> es el número total de páginas en este address_space. <tt>a_ops</tt> define los métodos de este objeto y <tt>host</tt> es un puntero perteneciente a este inodo address_space - quizás sea NULL, ej. en el caso del swapper (intercambiador) address_space (<tt>mm/swap_state.c,</tt>). El uso de <tt>clean_pages</tt>, <tt>dirty_pages</tt>, <tt>locked_pages</tt> y <tt>nrpages</tt> es obvio, por lo tanto hecharemos un severo vistazo a la estructura <tt>address_space_operations</tt>, definida en la misma cabecera: <tscreen><code> struct address_space_operations { int (*writepage)(struct page *); int (*readpage)(struct file *, struct page *); int (*sync_page)(struct page *); int (*prepare_write)(struct file *, struct page *, unsigned, unsigned); int (*commit_write)(struct file *, struct page *, unsigned, unsigned); int (*bmap)(struct address_space *, long); }; </code></tscreen> Para una vista básica del principio de address_spaces (y de la memoria intermedia de páginas) necesitamos hechar una mirada a -><tt>writepage</tt> y -><tt>readpage</tt>, pero en la práctica necesitamos también mirar en -><tt>prepare_write</tt> y -><tt>commit_write</tt>. Probablemente supongas que los métodos address_space_operations lo hacen en virtud de sus nombres solamente; no obstante, requieren hacer alguna explicación. Su uso en el camino de las E/S de los datos de los sistemas de ficheros, por lo lejos del más común camino a través de la memoria intermedia de páginas, suministra una buena forma para entenderlas. Como la mayoría de otros sistemas operativos del estilo UNIX, Linux tiene unas operaciones genéricas de ficheros (un subconjunto de las operaciones vnode SYSVish) para los datos E/S a través de la memoria intermedia de páginas. Esto significa que los datos no interactúan directamente con el sistema de ficheros en read/write/mmap, pero serán leidos/escritos desde/a la memoria intermedia de páginas cuando sea posible. La memoria intermedia de páginas tiene que obtener datos desde el sistema de ficheros actual de bajo nivel en el caso de que el usuario quiera leer desde una página que todavía no está en memoria, o escribir datos al disco en el caso de que la memoria sea insuficiente. En el camino de lectura, los métodos genéricos primero intentarán encontrar una página que corresponda con la pareja buscada de inodo/índice. <tscreen> hash = page_hash(inode->i_mapping, index); </tscreen> Entonces testeamos cuando la página actualmente existe. <tscreen> hash = page_hash(inode->i_mapping, index); page = __find_page_nolock(inode->i_mapping, index, *hash); </tscreen> Cuando no existe, asignamos una nueva página, y la añadimos al hash de la memoria intermedia de páginas. <tscreen> page = page_cache_alloc(); __add_to_page_cache(page, mapping, index, hash); </tscreen> Después de que la página haya sido hashed (ordenada) utilizamos la operación -><tt>readpage</tt> address_space para en este instante rellenar la página con datos (el fichero es una instancia abierta del inodo). <tscreen> error = mapping->a_ops->readpage(file, page); </tscreen> Finalmente podemos copiar los datos al espacio de usuario. Para escribir en el sistema de archivos existen dos formas: una para mapeos escribibles (mmap) y otra para la familia de llamadas al sistema write(2). El caso mmap es muy simple, por lo tanto será el que primero discutamos. Cuando un usuario modifica los mapas, el subsistema VM marca la página como sucia. <tscreen> SetPageDirty(page); </tscreen> El hilo del núcleo bdflush que está intentando liberar páginas, como actividad en segundo plano o porque no hay suficiente memoria, intentará llamar a -><tt>writepage</tt> en las páginas que están explicitamente marcadas como sucias. El método -><tt>writepage</tt> tiene ahora que escribir el contenido de las páginas de vuelta al disco y liberar la página. El segundo camino de escritura es _mucho_ más complicado. Para cada página que el usuario escribe, tenemos básicamente que hacer lo siguiente: (para el código completo ver <tt>mm/filemap.c:generic_file_write()</tt>). <tscreen> page = __grab_cache_page(mapping, index, &cached_page); mapping->a_ops->prepare_write(file, page, offset, offset+bytes); copy_from_user(kaddr+offset, buf, bytes); mapping->a_ops->commit_write(file, page, offset, offset+bytes); </tscreen> Por lo tanto intentamos encontrar la página ordenada o asignar una nueva, entonces llamamos al método -><tt>prepare_write</tt> address_space, copiamos la antememoria del usuario a la memoria del núcleo y finalmente llamamos al método -><tt>commit_write</tt>. Tal como probablemente has visto ->prepare_write y -><tt>commit_write</tt> son fundamentalmente diferentes de -><tt>readpage</tt> y -><tt>writepage</tt>, porque ellas no sólo son llamadas cuando la E/S física se quiere actualizar sino que son llamadas cada vez que el usuario modifica el fichero. Hay dos (¿o más?) formas para manejar esto, la primero es usar la antememoria intermedia de Linux para retrasar la E/S física, rellenando un puntero <tt>page->buffers</tt> con buffer_heads, que será usado en try_to_free_buffers (<tt>fs/buffers.c</tt>) para pedir E/S una vez que no haya suficientemente memoria, y es usada de forma muy difundida en el actual núcleo. La otra forma justamente establece la página como sucia y confía en que -><tt>writepage</tt> realice todo el trabajo. Debido a la carencia de un bitmap de validez en la página de estructuras, esto no realiza todo el trabajo que tiene una granularidad más pequeña que <tt>PAGE_SIZE</tt>. <sect>Mecanismos IPC<p> Este capítulo describe los mecanismos IPC semáforo, memoria compartida y cola de mensajes tal como han sido implementados en el núcleo Linux 2.4. Está organizado en 4 secciones. Las tres primeras secciones cubren las interfaces y las funciones de soporte para <ref id="semaphores" name="semaphores">, <ref id="message" name="message queues">, y <ref id="sharedmem" name="shared memory"> respectivamente. La sección <ref id="ipc_primitives" name="last"> describe un conjunto de funciones comunes y estructuras de datos que son compartidas por los tres mecanismos. <sect1>Semáforos<label id="semaphores"><p> Las funciones descritas en esta sección implementan el nivel de usuario de los mecanismos de los semáforos. Nótese que esta implementación ayuda en el uso de los spinlocks y semáforos del núcleo. Para eliminar esta confusión el término "semáforo del núcleo" será usado en referencia a los semáforos del núcleo. Todos los otros usos de la palabra "semáforo" será una referencia a los semáforos del nivel de usuario. <sect2>Interfaces de la Llamada al sistema de los Semáforos<label id="sem_apis"><p> <sect3>sys_semget()<label id="sys_semget"><p> La llamada entera a sys_semget() es protegida por el semáforo global del núcleo <ref id="struct_ipc_ids" name="sem_ids.sem"> En el caso donde un nuevo conjunto de semáforos deben de ser creados, la función <ref id="newary" name="newary()"> es llamada para crear e inicializar un nuevo conjunto de semáforos. La ID del nuevo conjunto es retornada al llamante. En el caso donde un valor de llave es suministrado por un conjunto de semáforos existentes, <ref id="ipc_findkey" name="ipc_findkey()"> es llamado para buscar el correspondiente descriptor del semáforo en el índice de la matriz. Los parámetros y los permisos del llamante son verificados antes de devolver la ID del conjunto de semáforos. </sect3> <sect3>sys_semctl()<label id="sys_semctl"><p> Para los comandos <ref id="IPC_INFO_and_SEM_INFO" name="IPC_INFO">, <ref id="IPC_INFO_and_SEM_INFO" name="SEM_INFO">, y <ref id="SEM_STAT" name="SEM_STAT">, <ref id="semctl_nolock" name="semctl_nolock()"> es llamado para realizar las funciones necesarias. Para los comandos <ref id="GETALL" name="GETALL">, <ref id="GETVAL" name="GETVAL">, <ref id="GETPID" name="GETPID">, <ref id="GETNCNT" name="GETNCNT">, <ref id="GETZCNT" name="GETZCNT">, <ref id="IPC_STAT" name="IPC_STAT">, <ref id="SETVAL" name="SETVAL">, y <ref id="SETALL" name="SETALL">, <ref id="semctl_main" name="semctl_main()"> es llamado para realizar las funciones necesarias. Para los comandos <ref id="semctl_ipc_rmid" name="IPC_RMID"> y <ref id="semctl_ipc_set" name="IPC_SET">, <ref id="semctl_down" name="semctl_down()"> es llamada para realizar las funciones necesarias. Durante todas estas operaciones, es mantenido el semáforo global del núcleo <ref id="struct_ipc_ids" name="sem_ids.sem">. </sect3> <sect3>sys_semop()<label id="sys_semop"><p> Después de validar los parámetros de la llamada, los datos de las operaciones de los semáforos son copiados desde el espacio de usuario a una antememoria temporal. Si una pequeña antememoria temporal es suficiente, entonces es usada una antememoria de pila. En otro caso, es asignad una antememoria más grande. Después de copiar los datos de las operaciones de los semáforos, el spinlock global de los semáforos es cerrado, y la ID del conjunto de semáforos especificado por el usuario es validado. Los permisos de acceso para el conjunto de semáforos también son validados. Todas las operaciones de los semáforos especificadas por el usuario son analizadas. Durante este proceso, es mantenida una cuenta para todas las operaciones que tienen la bandera SEM_UNDO establecida. Una bandera <tt>decrease</tt> es establecida si alguna de las operaciones quitan de un valor del semáforo, y una bandera <tt>alter</tt> es establecida si alguno de los valores de los semáforos es modificado (esto es, incrementados o decrementados). El número de cada semáforos a ser modificado es validado. Si SEM_UNDO estaba asertado para alguna de las operaciones del semáforo, entonces la lista para deshacer la actual tarea es buscada por una estructura deshacer asociada con este conjunto de semáforos. Durante esta búsqueda, si la ID del conjunto de semáforos de alguna de las estructuras deshacer es encontrada será -1, entonces <ref id="freeundos" name="freeundos()"> es llamada para liberar la estructura deshacer y quitarla de la lista. Si no se encuentra ninguna estructura deshacer para este conjunto de semáforos entonces <ref id="alloc_undo" name="alloc_undo()"> es llamada para asignar e inicializar una. La función <ref id="try_atomic_semop" name="try_atomic_semop()"> es llamada con el parámetro <tt>do_undo</tt> igual a 0 en orden de ejecutar la secuencia de operaciones. El valor de retorno indica si ambas operaciones han tenido éxito, han sido fallidas, o que no han sido ejecutadas porque necesitaban bloquear. Cada uno de estos casos son más ampliamente descritos a continuación: <sect4>Operaciones de semáforos no bloqueantes<label id="Non-blocking_Semaphore_Operations"><p> La función <ref id="try_atomic_semop" name="try_atomic_semop()"> devuelve cero para indicar que todas las operaciones en la secuencia han sido realizadas con éxito. Es este caso, <ref id="update_queue" name="update_queue()"> es llamada para recorrer la cola de las operaciones pendientes del semáforo para el conjunto del semáforo y despertar cualquier tarea dormida que no necesite bloquear más. Esto completa la ejecución de la llamada al sistema sys_semop() para este caso. </sect4> <sect4>Operaciones de Semáforo con fallos<label id="Failing_Semaphore_Operations"><p> Si <ref id="try_atomic_semop" name="try_atomic_semop()"> devuelve un valor negativo, entonces ha sido encontrada una condición de fallo. En este caso, ninguna de las operaciones han sido ejecutadas. Esto ocurre cuando una operación de un semáforo causaría un valor inválido del semáforo, o un operación marcada como IPC_NOWAIT es incapaz de completarse. La condición de error es retornada al llamante de sys_semop(). Antes de que sys_semop() retorne, es hecha una llamada a <ref id="update_queue" name="update_queue()"> para recorrer la cola de operaciones pendientes del semáforo para el conjunto del semáforo y despierta cualquier tarea dormida que no necesite más bloqueos. </sect4> <sect4>Operaciones de Semáforo bloqueantes<label id="Blocking_Semaphore_Operations"><p> La función <ref id="try_atomic_semop" name="try_atomic_semop()"> devuelve un 1 para indicar que la secuencia de operaciones del semáforo no fue ejecutada porque uno de los semáforos bloquearía. Para este caso, un nuevo elemento <ref id="struct_sem_queue" name="sem_queue"> es inicializado conteniendo estas operaciones del semáforo. Si alguna de estas operaciones afectaran al estado del semáforo, entonces un nuevo elemento de cola es añadido al final de la cola. En otro caso, el nuevo elemento es añadido al principio de la cola. El elemento <tt>semsleeping</tt> de la tarea actual está establecido para indicar que la tarea está durmiendo en este elemento <ref id="struct_sem_queue" name="sem_queue">. La tarea actual es marcada como TASK_INTERRUPTIBLE, y el elemento <tt>sleeper</tt> del <ref id="struct_sem_queue" name="sem_queue"> es establecido para identificar esta tarea por el durmiente. El spinlock global del semáforo es entonces desbloqueado, y schedule() es llamado para poner la tarea actual a dormir. Cuando es despertada, la tarea vuelve a cerrar el spinlock global del semáforo, determina por qué fue despertada, y cómo debería de responder. Los siguientes casos son manejados: <itemize> <item> Si el conjunto de semáforos ha sido borrado, entonces la llamada al sistema falla con EIDRM. <item> Si el elemento <tt>status</tt> de la estructura <ref id="struct_sem_queue" name="sem_queue"> está establecido a 1, entonces la tarea es despertada en orden a reintentar las operaciones del semáforo, Otra llamada a <ref id="try_atomic_semop" name="try_atomic_semop()"> es realizada para ejecutar la secuencia de las operaciones del semáforo. Si try_atomic_sweep() devuelve 1, entonces la tarea debe de bloquearse otra vez como se describió anteriormente. En otro caso, se devuelve 0 en caso de éxito, o un código de error apropiado en caso de fallo. Antes de que sys_semop() regrese, current->semsleeping es limpiado, y <ref id="struct_sem_queue" name="sem_queue"> es borrado de la cola. Si alguna de las operaciones del semáforo especificada eran operaciones alteradoras (incremento o decremento), entonces <ref id="update_queue" name="update_queue()"> es llamado para recorrer la cola de operaciones pendientes del semáforo para el conjunto del semáforo y despertar cualquier tarea dormida que no necesite bloquear más. <item> Si el elemento <tt>status</tt> de la estructura <ref id="struct_sem_queue" name="sem_queue"> NO está establecida a 1, y el elemento <ref id="struct_sem_queue" name="sem_queue"> no ha sido quitado de la cola, entonces la tarea ha sido despertada por una interrupción. Es este caso, la llamada al sistema falla con EINTR. Antes de regresar, current->semsleeping es limpiado, y <ref id="struct_sem_queue" name="sem_queue"> es borrado de la cola. También <ref id="update_queue" name="update_queue()"> es llamado si alguna de las operaciones eran operaciones alterantes. <item> Si el elemento <tt>status</tt> de la estructura <ref id="struct_sem_queue" name="sem_queue"> NO está establecido a 1, y el elemento <ref id="struct_sem_queue" name="sem_queue"> ha sido quitado de la cola, entonces las operaciones del semáforo ya han sido ejecutadas por <ref id="update_queue" name="update_queue()">. La cola <tt>status</tt>, la cual será 0 si se tiene éxito o un código negativo de error en caso de fallo, se convertirá en el valor de retorno de la llamada al sistema. </itemize> </sect4> </sect3> </sect2> <sect2>Estructuras Específicas de Soporte de Semáforos<label id="sem_structures"><p> Las siguientes estructuras son usadas específicamente para el soporte de semáforos: <sect3>struct sem_array<label id="struct_sem_array"><p> <tscreen><code> /* Una estructura de datos sem_array para cada conjunto de semáforos en el sistema. */ struct sem_array { struct kern_ipc_perm sem_perm; /* permisos .. ver ipc.h */ time_t sem_otime; /* último tiempo de la operación con el semáforo */ time_t sem_ctime; /* último tiempo de cambio */ struct sem *sem_base; /* puntero al primer semáforo en el array */ struct sem_queue *sem_pending; /* operaciones pendientes para ser procesadas */ struct sem_queue **sem_pending_last; /* última operación pendiente */ struct sem_undo *undo; /* peticiones deshechas en este array * / unsigned long sem_nsems; /* número de semáforos en el array */ }; </code></tscreen> </sect3> <sect3>struct sem<label id="struct_sem"><p> <tscreen><code> /* Una estructura de semáforo para cada semáforo en el sistema. */ struct sem { int semval; /* valor actual */ int sempid; /* pid de la última operación */ }; </code></tscreen> </sect3> <sect3>struct seminfo<label id="struct_seminfo"><p> <tscreen><code> struct seminfo { int semmap; int semmni; int semmns; int semmnu; int semmsl; int semopm; int semume; int semusz; int semvmx; int semaem; }; </code></tscreen> </sect3> <sect3>struct semid64_ds<label id="struct_semid64_ds"><p> <tscreen><code> struct semid64_ds { struct ipc64_perm sem_perm; /* permisos.. ver ipc.h */ __kernel_time_t sem_otime; /* último tiempo de operación con el semáforo */ unsigned long __unused1; __kernel_time_t sem_ctime; /* último tiempo de cambio */ unsigned long __unused2; unsigned long sem_nsems; /* número de semáforos en la matriz */ unsigned long __unused3; unsigned long __unused4; }; </code></tscreen> </sect3> <sect3>struct sem_queue<label id="struct_sem_queue"><p> <tscreen><code> /* Una cola para cada proceso durmiendo en el sistema. */ struct sem_queue { struct sem_queue * next; /* siguiente entrada en la cola */ struct sem_queue ** prev; /* entrada anterior en la cola, *(q->prev) == q */ struct task_struct* sleeper; /* este proceso */ struct sem_undo * undo; /* estructura deshacer */ int pid; /* id del proceso del proceso pedido */ int status; /* status de terminación de la operación */ struct sem_array * sma; /* matriz de semáforos para las operaciones */ int id; /* id interna del semáforo */ struct sembuf * sops; /* matriz de operaciones pendientes*/ int nsops; /* número de operaciones */ int alter; /* operaciones que alterarán el semáforo */ }; </code></tscreen> </sect3> <sect3>struct sembuf<label id="struct_sembuf"><p> <tscreen><code> /* las llamadas al sistema cogen una matriz de estas. */ struct sembuf { unsigned short sem_num; /* indice del semáforo en la matriz */ short sem_op; /* operación del semáforo */ short sem_flg; /* banderas de la operación */ }; </code></tscreen> </sect3> <sect3>struct sem_undo<label id="struct_sem_undo"><p> <tscreen><code> /* Cada tarea tiene una lista de peticiones de deshacer. Ellas son * ejecutadas cuando el proceso sale. */ struct sem_undo { struct sem_undo * proc_next; /* siguiente entrada en este proceso */ struct sem_undo * id_next; /* siguiente entrada en este conjunto de semáforos */ int semid; /* identificador del conjunto de semáforos */ short * semadj; /* matriz de ajustes, una por semáforo */ }; </code></tscreen> </sect3> </sect2> <sect2>Funciones de Soporte de Semáforos<label id="sem_primitives"><p> Las siguientes funciones son usadas específicamente para soportar los semáforos: <sect3>newary()<label id="newary"><p> newary() confía en la función <ref id="ipc_alloc" name="ipc_alloc()"> para asignar la memoria requerida para el nuevo conjunto de semáforos. El asigna suficiente memoria para el conjunto de descriptores del semáforo y para cada uno de los semáforos en el conjunto. La memoria asignada es limpiada, y la dirección del primer elemento del conjunto de descriptores del semáforo es pasada a <ref id="ipc_addid" name="ipc_addid()">. <ref id="ipc_addid" name="ipc_addid()"> reserva una entrada de la matriz para el conjunto de descriptores del semáforo e inicializa los datos (<ref id="struct_kern_ipc_perm" name="struct kern_ipc_perm">) para el conjunto. La variavle global <tt>used_sems</tt> es actualizada por el número de semáforos en el nuevo conjunto y la inicialización de los datos (<ref id="struct_kern_ipc_perm" name="struct kern_ipc_perm">) para el nuevo conjunto es completada. Otras inicializaciones realizadas para este conjunto son listadas a continuación: <itemize> <item> El elemento <tt>sem_base</tt> para el conjunto es inicializado a la dirección inmediatamente siguiente siguiendo la porción (<ref id="struct_sem_array" name="struct sem_array">) de los nuevos segmentos asignados. Esto corresponde a la localización del primer semáforon en el conjunto. <item> La cola <tt>sem_pending</tt> es inicializada como vacía. </itemize> Todas las operaciones siguiendo la llamada a <ref id="ipc_addid" name="ipc_addid()"> son realizadas mientras se mantiene el spinlock global de los semáforos. Después de desbloquear el spinlock global de los semáforos, newary() llama a <ref id="ipc_buildid" name="ipc_buildid()"> (a través de sem_buildid()). Esta función usa el índice del conjunto de descriptores del semáforo para crear una única ID, que es entonces devuelta al llamador de newary(). </sect3> <sect3>freeary()<label id="freeary"><p> freeary() es llamada por <ref id="semctl_down" name="semctl_down()"> para realizar las funciones listadas a continuación. Es llamada con el spinlock global de los semáforos bloqueado y regresa con el spinlock desbloqueado. <itemize> <item> La función <ref id="func_ipc_rmid" name="ipc_rmid()"> es llamada (a través del envoltorio sem_rmid()) para borrar la ID del conjunto de semáforos y para recuperar un puntero al conjunto de semáforos. <item> La lista de deshacer para el conjunto de semáforos es invalidada. <item> Todos los procesos pendientes son despertados y son obligados a fallar con EIDRM. <item> EL número de semáforos usados es reducido con el número de semáforos en el conjunto borrado. <item> La memoria asociada con el conjunto de semáforos es liberada. </itemize> </sect3> <sect3>semctl_down()<label id="semctl_down"><p> semctcl_down() suministra las operaciones <ref id="semctl_ipc_rmid" name="IPC_RMID"> y <ref id="semctl_ipc_set" name="IPC_SET"> de la llamada al sistema semctl(). La ID del conjunto de semáforos y los permisos de acceso son verificadas en ambas operaciones, y en ambos casos, el spinlock global del semáforo es mantenido a lo largo de la operación. <sect4>IPC_RMID<label id="semctl_ipc_rmid"><p> La operación IPC_RMID llama a <ref id="freeary" name="freeary()"> para borrar el conjunto del semáforo. </sect4> <sect4>IPC_SET<label id="semctl_ipc_set"><p> La operación IPC_SET actualiza los elementos <tt>uid</tt>, <tt>gid</tt>, <tt>mode</tt>, y <tt>ctime</tt> del conjunto de semáforos. </sect4> </sect3> <sect3>semctl_nolock()<label id="semctl_nolock"><p> semctl_nolock() es llamada por <ref id="sys_semctl" name="sys_semctl()"> para realizar las operaciones IPC_INFO, SEM_INFO y SEM_STAT. <sect4>IPC_INFO y SEM_INFO<label id="IPC_INFO_and_SEM_INFO"><p> IPC_INFO y SEM_INFO causan una antememoria temporal <ref id="struct_seminfo" name="seminfo"> para que sea inicializada y cargada con los datos estadísticos sin cambiar del semáforo, los elementos <tt>semusz</tt> y <tt>semaem</tt> de la estructura <ref id="struct_seminfo" name="seminfo"> son actualizados de acuerdo con el comando dado (IPC_INFO o SEM_INFO). El valor de retorno de las llamadas al sistema es establecido al conjunto máximo de IDs del conjunto de semáforos. </sect4> <sect4>SEM_STAT<label id="SEM_STAT"><p> SEM_STAT causa la inicialización de la antememoria temporal <ref id="struct_semid64_ds" name="semid64_ds">. El spinlock global del semáforo es entonces mantenido mientras se copian los valores <tt>sem_otime</tt>, <tt>sem_ctime</tt>, y <tt>sem_nsems</tt> en la antememoria. Estos datos son entonces copiados al espacio de usuario. </sect4> </sect3> <sect3>semctl_main()<label id="semctl_main"><p> semctl_main() es llamado por <ref id="sys_semctl" name="sys_semctl()"> para realizar muchas de las funciones soportadas, tal como se describe en la sección posterior. Anteriormente a realizar alguna de las siguientes operaciones, semctl_main() cierra el spinlock global del semáforo y valida la ID del conjunto de semáforos y los permisos. El spinlock es liberado antes de retornar. <sect4>GETALL<label id="GETALL"><p> La operación GETALL carga los actuales valores del semáforo en una antememoria temporal del núcleo y entonces los copia fuera del espacio de usuario. La pequeña pila de antememoria es usada si el conjunto del semáforo es pequeño. En otro caso, el spinlock es temporalmente deshechado en orden de asignar una antememoria más grande. El spinlock es mantenido mientras se copian los valores del semáforo en la antememoria temporal. </sect4> <sect4>SETALL<label id="SETALL"><p> La operación SETALL copia los valores del semáforo desde el espacio de usuario en una antememoria temporal, y entonces en el conjunto del semáforo. El spinlock es quitado mientras se copian los valores desde el espacio de usuario a la antememoria temporal, y mientras se verifican valores razonables. Si el conjunto del semáforo es pequeño, entonces una pila de antememoria es usada, en otro caso una antememoria más grande es asignado. El spinlock es recuperado y mantenido mientras las siguientes operaciones son realizadas en el conjunto del semáforo: <itemize> <item> Los valores del semáforo son copiados en el conjunto del semáforo. <item> Los ajustes del semáforon de la cola de deshacer para el conjunto del semáforo son limpiados. <item> El valor <tt>sem_ctime</tt> para el conjunto de semáforos es establecido. <item> La función <ref id="update_queue" name="update_queue()"> es llamada para recorrer la cola de semops (operaciones del semáforo) pendientes y mirar por alguna tarea que pueda ser completada como un resultado de la operación SETALL. Cualquier tarea pendiente que no sea más bloqueada es despertada. </itemize> </sect4> <sect4>IPC_STAT<label id="IPC_STAT"><p> En la operación IPC_STAT, los valores <tt>sem_otime</tt>, <tt>sem_ctime</tt>, y <tt>sem_nsems</tt> son copiados en una pila de antememoria. Los datos son entonces copiados al espacio de usuario después de tirar con el spinlock. </sect4> <sect4>GETVAL<label id="GETVAL"><p> Para GETVALL en el caso de no error, el valor de retorno para la llamada al sistema es establecido al valor del semáforo especificado. </sect4> <sect4>GETPID<label id="GETPID"><p> Para GETPID en el caso de no error, el valor de retorno para la llamada al sistema es establecido al <tt>pid</tt> asociado con las última operación del semáforo. </sect4> <sect4>GETNCNT<label id="GETNCNT"><p> Para GETNCNT en el caso de no error, el valor de retorno para la llamada al sistema de establecido al número de procesos esperando en el semáforo siendo menor que cero. Este número es calculado por la función <ref id="count_semncnt" name="count_semncnt()">. </sect4> <sect4>GETZCNT<label id="GETZCNT"><p> Para GETZCNT en la caso de no error, el valor de retorno para la llamada al sistema es establecido al número de procesos esperando en el semáforo estando establecido a cero. Este número es calculado por la función <ref id="count_semzcnt" name="count_semzcnt()">. </sect4> <sect4>SETVAL<label id="SETVAL"><p> Después de validar el nuevo valor del semáforo, las siguientes funciones son realizadas: <itemize> <item> La cola de deshacer es buscada para cualquier ajuste en este semáforo. Cualquier ajuste que sea encontrado es reinicializado a cero. <item> El valor del semáforo es establecido al valor suministrado. <item> El valor del semáforo <tt>sem_ctime</tt> para el conjunto del semáforo es actualizado. <item> La función <ref id="update_queue" name="update_queue()"> es llamada para recorrer la cola de semops (operaciones del semáforo) pendientes y buscar a cualquier tarea que pueda ser completada como resultado de la operación <ref id="SETALL" name="SETALL">. Cualquier tarea que no vaya a ser más bloqueada es despertada. </itemize> </sect4> </sect3> <sect3>count_semncnt()<label id="count_semncnt"><p> count_semncnt() cuenta el número de tareas esperando por el valor del semáforo para que sea menor que cero. </sect3> <sect3>count_semzcnt()<label id="count_semzcnt"><p> count_semzcnt() cuenta el número de tareas esperando por el valor del semáforo para que sea cero. </sect3> <sect3>update_queue()<label id="update_queue"><p> update_queue() recorre la cola de semops pendientes para un conjunto de un semáforo y llama a <ref id="try_atomic_semop" name="try_atomic_semop()"> para determinar qué secuencias de las operaciones de los semáforos serán realizadas. Si el estado de la cola de elementos indica que las tareas bloqueadas ya han sido despertadas, entonces la cola de elementos es pasada por alto. Para los otros elementos de la cola, la bandera <tt>q-alter</tt> es pasada como el parámetro deshacer a <ref id="try_atomic_semop" name="try_atomic_semop()">, indicando que cualquier operación alterante debería de ser deshecha antes de retornar. Si la secuencia de operaciones bloquearan, entonces update_queue() retornará sin hacer ningún cambio. Una secuencia de operaciones puede fallar si una de las operaciones de los semáforos puede causar un valor inválido del semáforo, o una operación marcada como IPC_NOWAIT es incapaz de completarse. En este caso, la tarea que es bloqueada en la secuencia de las operaciones del semáforo es despertada, y la cola de status es establecida con un código de error apropiado. El elemento de la cola es también quitado de la cola. Si la secuencia de las operaciones no es alterante, entonces ellas deberían de pasar un valor cero como parámetro deshacer a <ref id="try_atomic_semop" name="try_atomic_semop()">. Si estas operaciones tienen éxito, entonces son consideradas completas y son borradas de la cola. La tarea bloqueada es despertada, y el elemento de la cola <tt>status</tt> es establecido para indicar el éxito. Si la secuencia de las operaciones pueden alterar los valores del semáforo, pero puede tener éxito, entonces las tareas durmiendo que no necesiten ser más bloqueadas tienen que ser despertadas. La cola status es establecida a 1 para indicar que la tarea bloqueada ha sido despertada. Las operaciones no han sido realizadas, por lo tanto el elemento de la cola no es quitado de la cola. Las operaciones del semáforo serán ejecutadas por la tarea despertada. </sect3> <sect3>try_atomic_semop()<label id="try_atomic_semop"><p> try_atomic_semop() es llamada por <ref id="sys_semop" name="sys_semop()"> y <ref id="update_queue" name="update_queue()"> para determinar si una secuencia de operaciones del semáforo tendrán éxito. El determina esto intentando realizar cada una de las operaciones. Si una operación bloqueante es encontrada, entonces el proceso es abortado y todas los operaciones son deshechas. -EAGAIN es devuelto si IPC_NOWAIT es establecido. En otro caso, es devuelto 1 para indicar que la secuencia de las operaciones del semáforo está bloqueada. Si un valor del semáforo es ajustado más alla de los límites del sistema, entonces todas las operaciones son deshechas, y -ERANGE es retornado. Si todas las operaciones de la secuencia tienen éxito, y el parámetro <tt>do_undo</tt> no es cero, entonces todas las operaciones son deshechas, y 0 es devuelto. Si el parámetro <tt>do_undo</tt> es cero, entonces todas las operaciones tienen éxito y continúan obligadas, y el <tt>sem_otime</tt>, campo del conjunto de semáforos es actualizado. </sect3> <sect3>sem_revalidate()<label id="sem_revalidate"><p> sem_revalidate() es llamado cuando el spinlock global del semáforo ha sido temporalmente tirado y necesita ser bloqueado otra vez. Es llamado por <ref id="semctl_main" name="semctl_main()"> y <ref id="alloc_undo" name="alloc_undo()">. Valida la ID del semáforo y los permisos, y si tiene éxito retorna con el spinlock global de los semáforos bloqueado. </sect3> <sect3>freeundos()<label id="freeundos"><p> freeundos() recorre las lista de procesos por deshacer en busca de la estructura deshacer deseada. Si es encontrada, la estructura deshacer es quitada de la lista y liberada. Un puntero a la siguiente estructura deshacer en la lista de procesos es devuelta. </sect3> <sect3>alloc_undo()<label id="alloc_undo"><p> alloc_undo() espera ser llamada con el spinlock global de los semáforos cerrado. En el caso de un error, regresa con él desbloqueado. El spinlock global de los semáforos es desbloqueado, y kmallock() es llamado para asignar suficiente memoria para la estructura <ref id="struct_sem_undo" name="sem_undo">, y también para un array de uno de los valores de ajuste para cada semáforo en el conjunto. Si tiene éxito, el spinlock es recuperado con una llamada a <ref id="sem_revalidate" name="sem_revalidate()">. La nueva estructura semundo es entonces inicializada, y la dirección de esta estructura es colocada en la dirección suministrada por el llamante. La nueva estructura deshacer es entonces colocada en la cabeza de la lista deshacer para la actual tarea. </sect3> <sect3>sem_exit()<label id="sem_exit"><p> sem_exit() es llamada por do_exit(), y es la responsable de ejecutar todos los ajustes deshacer para la tarea saliente. Si el actual proceso fue bloqueado en un semáforo, entonces es borrado desde la lista <ref id="struct_sem_queue" name="sem_queue"> mientras se mantiene el spinlock global de los semáforos. La lista deshacer para la actual tarea es entonces recorrida, y las siguientes operaciones son realizadas mientras se mantienen y liberan los spinlocks globales de los semáforos a lo largo del procesamiento de cada elemento de la lista. Las siguientes operaciones son realizadas para cada uno de los elementos deshacer: <itemize> <item> La estructura deshacer y la ID del conjunto del semáforo son validadas. <item> La lista deshacer del correspondiente conjunto de semáforos es buscada para encontrar una referencia a la misma estructura deshacer y para quitarla de esa lista. <item> Los ajustes indicadores en la estructura deshacer son aplicados al conjunto de semáforos. <item> El parámetro <tt>sem_otime</tt> del conjunto de semáforos es actualizado. <item> <ref id="update_queue" name="update_queue()"> es llamado para recorrer la cola de las semops pendientes y despertar cualquier tarea durmiento que no necesite ser bloqueada como resultado de la operación deshacer. <item> La estructura deshacer es liberada. </itemize> Cuando el procesamiento de la lista está completo, el valor current->semundo es limpiado. </sect3> </sect2> </sect1> <sect1>Colas de Mensajes<label id= "message"><p> <sect2>Interfaces de las llamadas al sistema de Colas<label id="Message_System_Call_Interfaces"><p> <sect3>sys_msgget()<label id="sys_msgget"><p> La llamada entera a sys_msgget() es protegida por el semáforo global de la cola de mensajes (<ref id="struct_ipc_ids" name="msg_ids.sem">). En el caso donde una nueva cola de mensajes tiene que ser creada, la función <ref id="newque" name="newque()"> es llamada para crear e inicializar una nueva cola de mensajes, y la nueva ID de la cola es devuelta al llamante. Si un valor llave es suministrado para una cola de mensajes existente, entonces <ref id="ipc_findkey" name="ipc_findkey()"> es llamada para mirar el índice correspondiente en la matriz de colas globales de descriptores de mensajes (msg_ids.entries). Los parámetros y los permisos del llamante son verificados antes de devolver la ID de la cola de mensajes. Las operaciónes de búsqueda y verificación son realizadas mientras el spinlock global de la cola de mensajes (msg_ids.ary) es mantenido. </sect3> <sect3>sys_msgctl()<label id="sys_msgctl"><p> Los parámetros pasados a sys_msgctl() son: una ID de una cola de mensajes (<tt>msqid</tt>), la operación (<tt>cmd</tt>), y un puntero al espacio de la antememoria <ref id="struct_msqid_ds" name="msgid_ds"> del tipo (<tt>buf</tt>). Seis operaciones son suministradas en esta función: IPC_INFO, MSG_INFO,IPC_STAT, MSG_STAT, IPC_SET y IPC_RMID. La ID de la cola de mensajes y los parámetros de la operación son validados; entonces, la operación (cmd) es realizada como sigue: <sect4>IPC_INFO (o MSG_INFO)<label id="msgctl_IPCINFO"><p> La información de la cola global de mensajes es copiada al espacio de usuario. </sect4> <sect4>IPC_STAT (o MSG_STAT)<label id="msgctl_IPCSTAT"><p> Una antememoria temporal del tipo <ref id="struct_msqid64_ds" name="struct msqid64_ds"> es inicializado y el spinlock de la cola de mensajes global es cerrado. Después de verificar los permisos de acceso del proceso llamante, la información de la cola de mensajes asociada con la ID de la cola de mensajes es cargada en una antememoria temporal, el spinlock de la cola de mensajes global es abierto, y los contenidos de la antememoria temporal son copiados fuera del espacio de usuario por <ref id="copy_msqid_to_user" name="copy_msqid_to_user()">. </sect4> <sect4>IPC_SET<label id="msgctl_IPCSET"><p> Los datos del usuario son copiados a través de <ref id="copy_msqid_to_user" name="copy_msqid_to_user()">. El semáforo de la cola de mensajes global y el spinlock son obtenidos y liberados al final. Después de que la ID de la cola de mensajes y los permisos de acceso del actual proceso hayan sido validados, la información de la cola de mensajes es actualizada con los datos suministrados por el usuario, Después, <ref id="expunge_all" name="expunge_all()"> y <ref id="ss_wakeup" name="ss_wakeup()"> son llamadas para despertar todos los procesos durmiendo en las colas de espera del emisor y del receptor de las colas de mensajes. Esto es el motivo por el que algunos receptores quizás sean ahora excluidos por permisos de acceso estrictos y alguno de los emisores sean capaces ahora de enviar el mensaje debido a un incremento del tamaño de la cola. </sect4> <sect4>IPC_RMID<label id="msgctl_IPCRMID"><p> El semáforo de la cola de mensajes global es obtenido y el spinlock global de la cola de mensajes es cerrado. Después de validar la ID de la cola de mensajes y los permisos de acceso de la actual tarea, <ref id="freeque" name="freeque()"> es llamado para liberar los recursos relacionados con la ID de la cola de mensajes. El semáforo de la cola de mensajes global y el spinlock son liberados. </sect4> </sect3> <sect3>sys_msgsnd()<label id="sys_msgsnd"><p> sys_msgsnd() recibe como parámetros una ID de una cola de mensajes (<tt>msqid</tt>), un puntero a la antememoria del tipo <ref id="struct_msg_msg" name="struct msg_msg"> (<tt>msgp</tt>), el tamaño del mensaje a ser enviado (<tt>msgsz</tt>), y una bandera indicando esperar o no esperar (<tt>msgflg</tt>). Hay dos tareas esperando las colas y un mensaje esperando la cola asociada con la ID de la cola de mensajes. Si hay una nueva tarea en la cola de espera del receptor que está esperando por este mensaje, entonces el mensaje es enviado directamente al receptor. En otro caso, si hay suficiente espacio disponible en la cola de espera de mensajes, el mensaje es guardado en esta cola. Como último recurso, la tarea emisora se encola a si misma en la cola de espera del emisor. Una discusión con más profundidad de las operaciones realizadas por sys_msgsnd() es la siguiente: <enum> <item> Valida la dirección de la antememoria del usuario y el tipo de mensaje, entonces invoca a <ref id="load_msg" name="load_msg()"> para cargar el contenido del mensaje del usuario en un objeto temporal <tt<label id="msg">msg</tt> del tipo <ref id="struct_msg_msg" name="struct msg_msg">. El tipo de mensaje y los campos del tamaño del mensaje de <tt>msg</tt> también son inicializados. <item> Cierra el spinlock global de la cola de mensajes y coge el descriptor asociado con la cola de mensajes con la ID de la cola de mensajes. Si dicha cola de mensajes no existe, retorna EINVAL. <item><label id="sndretry"> Invoca a <ref id="ipc_checkid" name="ipc_checkid()"> (a través de msg_checkid()) para verificar que la ID de la cola de mensajes es válida y llama a <ref id="ipcperms" name="ipcperms()"> para chequear los permisos de acceso de proceso llamante. <item> Chequea el tamaño del mensaje y el espacio que sobra en la cola de espera de mensajes para ver si hay suficiente espacio para almacenar el mensaje. Si no, los siguiens subpasos son realizados: <enum> <item> Si IPC_NOWAIT es especificado en <tt>msgflg</tt> el spinlock global de la cola de mensajes es abierto, los recursos de memoria para el mensaje son liberados, y EAGAIN es retornado. <item> Invoca a <ref id="ss_add" name="ss_add()"> para encolar la actual tarea en la cola de espera del emisor. También abre al spinlock global de la cola de mensajes e invoca a schedule() para poner la actual tarea a dormir. <item> Cuando es despertado, obtiene el spinlock global otra vez y verifica que la ID de la cola de mensajes es todavía válida. Si la ID de la cola de mensajes no es valida, ERMID es retornado. <item> Invoca <ref id="ss_del" name="ss_del()"> para quitar la tarea emisora de la cola de espera del emisor. Si hay alguna señal pendiente para la tarea, sys_msgsnd() abre el spinlock global, invoca a <ref id="free_msg" name="free_msg()"> para liberar la antememoria del mensaje, y retorna EINTR. En otro caso, la función va a <ref id="sndretry" name="back"> para chequear otra vez cuando hay otra vez suficiente sitio en la cola de espera de mensajes. </enum> <item> Invoca <ref id="pipelined_send" name="pipelined_send()"> para intentar enviar el mensaje a la cola receptora directamente. <item> Si no hay receptores esperando por este mensaje, desencola <tt>msg</tt> en la cola de mensajes esperando (msq->q_messages). Actualiza los campos <tt>q_cbytes</tt> y <tt>q_qnum</tt> del descriptor de la cola de mensajes, al igual que las variables globales <tt>msg_bytes</tt> y <tt>msg_hdrs</tt>, las cuales indican el número de bytes total usados por los mensajes y el número total de mensajes a lo largo del sistema. <item> Si el mensaje ha sido enviado con éxito o encolado, actualiza los campos <tt>q_lspid</tt> y <tt>q_stime</tt> del descriptor de la cola de mensajes y abre el spinlock de colas de mensajes global. </enum> </sect3> <sect3>sys_msgrcv()<label id="sys_msgrcv"><p> La función sys_msgrcv() recibe como parámetro una ID de una cola de mensajes (<tt>msqid</tt>), un puntero a una antememoria del tipo <ref id="struct_msg_msg" name="msg_msg"> (<tt>msgp</tt>), el tamaño del mensaje deseado (<tt>msgsz</tt>), el tipo de mensaje (<tt>msgtyp</tt>), y las banderas (<tt>msgflg</tt>). Busca las colas de mensajes esperando asociadas con la ID de la cola de mensajes, encuentra el primer mensaje en la cola, la cual comprueba el tipo pedido y lo copia en la antememoria del usuario dado. Si tal mensaje no es encontrado en la cola de mensajes esperando, la tarea pedida es encolada en la cola de receptores esperando hasta que el mensaje deseado está disponible. Una discución más profunda de las operaciones realizadas por sys_msgrcv() es la que sigue: <enum> <item> Primero, invoca a <ref id="convert_mode" name="convert_mode()"> para derivar el modo de búsqueda desde <tt>msgtyp</tt>. sys_msgrcv() entonces cierra el spinlock global de la cola de mensajes y obtiene el descriptor de la cola de mensajes asociado con la ID de la cola de mensajes. Si tal cola de mensajes no existe, retorna EINVAL. <item> Chequea cuando la tarea actual tiene los permisos correctos para acceder a la cola de mensajes. <item><label id="rcvretry"> Empezando desde el primer mensaje en la cola de mensajes de espera, invoca a <ref id="testmsg" name="testmsg()"> para chequear cuando el tipo del mensaje se empareja con el tipo requerido. sys_msgrcv() continúa buscando hasta que un mensaje emparejado es encontrado o la cola de espera entera está exausta. Si el modo de búsqueda es SEARCH_LESSEQUAL, entonces es buscado el primer mensaje en la cola con un tipo más bajo o igual a <tt>msgtyp</tt>. <item> Si un mensaje en encontrado, sys_msgrcv() realiza los siguientes subpasos: <enum> <item> Si el tamaño del mensaje es más grande que el tamaño deseado y <tt>msgflg</tt> indica que no se permiten errores, abre el spinlock de cola de mensajes global y retorna E2BIG. <item> Quita el mensaje de la cola de mensajes esperando y actualiza las estadísticas de la cola de mensajes. <item> Despierta todas las tareas durmiendo en las colas de espera de los emisores. El borrado de un mensaje de la cola en los pasos previos hace posible que uno de los emisores progresen. Va a través de <ref id="laststep" name="last step">. </enum> <item> Si no hay mensajes emparejados el criterio del receptor es encontrado en la cola de mensajes esperando, entonces <tt>msgflg</tt> es chequeado. Si IPC_NOWAIT está establecido, entonces el spinlock global de la cola de mensajes es abierto y ENOMSG es retornado. En otro caso, el receptor es encolado en la cola de espera del receptor como sigue: <enum> <item> Una estructura de datos <ref id="struct_msg_receiver" name="msg_receiver"> <tt>msr</tt> es asignada y es añadida en la cabeza de la cola de espera. <item> El campo <tt>r_tsk</tt> de <tt>msr</tt> es establecido a la tarea actual. <item> Los campos <tt>r_msgtype</tt> y <tt>r_mode</tt> son inicializados con el tipo y modo del mensaje deseado respectivamente. <item> Si <tt>msgflg</tt> indica MSG_NOERROR, entonces el campo r_maxsize de <tt>msr</tt> es establecido para ser el valor de <tt>msgsz</tt>. En otro caso es establecido para ser INT_MAX. <item> El campo <tt>r_msg</tt> es inicializado para indicar que todavía no ha sido recibido el mensaje. <item> Después de que la inicialización está completa, el estado de la tarea receptora es establecido a TASK_INTERRUPTIBLE, el spinlock global de colas de mensajes es abierto, y schedule() es invocado. </enum> <item> Después de que el receptor es despertado, el campo <tt>r_msg</tt> de <tt>msr</tt> es chequeado. Este campo es usado para almacenar el mensaje entubado o, en el caso de un error, almacenar el estado del error. Si el campo <tt>r_msg</tt> es rellenado con el mensaje deseado, entonces va a <ref id="laststep" name="last step">. En otro caso, el spinlock global de colas de mensajes es cerrado otra vez. <item> Después de obtener el spinlock, el campo <tt>r_msg</tt> es re-chequeado para ver si el mensaje fue recibido mientras se estaba esperando por el spinlock. Si el mensaje ha sido recibido, ocurre el <ref id="laststep" name="last step">. <item> Si el campo <tt>r_msg</tt> todavía está sin cambiar, entonces la tarea tiene que ser despertada en orden de reintentarlo. En este caso, <tt>msr</tt> es quitado de la cola. Si hay una señal pendiente para la tarea, entonces el spinlock de la cola de mensajes global es abierto y EINTR es retornado. En otro caso, la función necesita ir a <ref id="rcvretry" name="back"> y reintentarlo. <item> Si el campo <tt>r_msg</tt> muestra que ha ocurrido un error mientras estaba durmiendo, el spinlock de la cola de mensajes global es abierto y el error es devuelto. <item><label id="laststep"> Después de validar que la dirección de la antememoria del usuario <tt>msp</tt> es válida, el tipo de mensaje es cargado en el campo <tt>mtype</tt> de <tt>msp</tt>, y <ref id="store_msg" name="store_msg()"> es invocado para copiar el contenido del mensaje al campo de <tt>msp</tt>. Finalmente la memoria para el mensaje es liberada por la función <ref id="free_msg" name="free_msg()">. </enum> </sect3> </sect2> <sect2>Estructuras Específicas de Mensajes<label id="datastructs"><p> Las estructuras de datos para las colas de mensajes están definidas en msg.c. <sect3>struct msg_queue<label id="struct_msg_queue"><p> <tscreen><code> /* una estructura msq_queue para cada cola presente en el sistema */ struct msg_queue { struct kern_ipc_perm q_perm; time_t q_stime; /* último tiempo del mensaje enviado */ time_t q_rtime; /* último tiempo del mensaje recibido */ time_t q_ctime; /* útimo tiempo de cambio */ unsigned long q_cbytes; /* número actual de bytes en la cola */ unsigned long q_qnum; /* número de mensajes en la cola */ unsigned long q_qbytes; /* máximo número de bytes en la cola */ pid_t q_lspid; /* último pid del mensaje recibido */ pid_t q_lrpid; /* último pid del mensaje recibido */ struct list_head q_messages; struct list_head q_receivers; struct list_head q_senders; }; </code></tscreen> </sect3> <sect3>struct msg_msg<label id="struct_msg_msg"><p> <tscreen><code> /* una estructura msg_msg para cada mensaje */ struct msg_msg { struct list_head m_list; long m_type; int m_ts; /* tamaño del mensaje de texto */ struct msg_msgseg* next; /* el mensaje actual sigue inmediatamente */ }; </code></tscreen> </sect3> <sect3>struct msg_msgseg<label id="struct_msg_msgseg"><p> <tscreen><code> /* segmento de mensaje para cada mensaje */ struct msg_msgseg { struct msg_msgseg* next; /* la siguiente parte del mensaje sigue inmediatamente */ }; </code></tscreen> </sect3> <sect3>struct msg_sender<label id="struct_msg_sender"><p> <tscreen><code> /* un msg_sender para cada emisor durmiendo */ struct msg_sender { struct list_head list; struct task_struct* tsk; }; </code></tscreen> </sect3> <sect3>struct msg_receiver<label id="struct_msg_receiver"><p> <tscreen><code> /* una estructura msg_receiver para cada receptor durmiendo */ struct msg_receiver { struct list_head r_list; struct task_struct* r_tsk; int r_mode; long r_msgtype; long r_maxsize; struct msg_msg* volatile r_msg; }; </code></tscreen> </sect3> <sect3>struct msqid64_ds<label id="struct_msqid64_ds"><p> <tscreen><code> struct msqid64_ds { struct ipc64_perm msg_perm; __kernel_time_t msg_stime; /* último tiempo del mensaje enviado */ unsigned long __unused1; __kernel_time_t msg_rtime; /* último tiempo del mensaje recibido */ unsigned long __unused2; __kernel_time_t msg_ctime; /* último tiempo de cambio */ unsigned long __unused3; unsigned long msg_cbytes; /* número actual de bytes en la cola */ unsigned long msg_qnum; /* número de mensajes en la cola */ unsigned long msg_qbytes; /* número máximo de bytes en la cola */ __kernel_pid_t msg_lspid; /* pid del último mensaje enviado */ __kernel_pid_t msg_lrpid; /* pid del último mensaje recibido */ unsigned long __unused4; unsigned long __unused5; }; </code></tscreen> </sect3> <sect3>struct msqid_ds<label id="struct_msqid_ds"><p> <tscreen><code> struct msqid_ds { struct ipc_perm msg_perm; struct msg *msg_first; /* primer mensaje en la cola, no usado */ struct msg *msg_last; /* último mensaje en la cola, no usado */ __kernel_time_t msg_stime; /* último tiempo del mensaje enviado */ __kernel_time_t msg_rtime; /* último tiempo del mensaje recibido */ __kernel_time_t msg_ctime; /* último tiempo de cambio */ unsigned long msg_lcbytes; /* reusar los campos borrados para 32 bit */ unsigned long msg_lqbytes; /* idem */ unsigned short msg_cbytes; /* número actual de bytes en la cola */ unsigned short msg_qnum; /* número de mensajes en la cola */ unsigned short msg_qbytes; /* número máximo de bytes en la cola */ __kernel_ipc_pid_t msg_lspid; /* último pid del mensaje enviado */ __kernel_ipc_pid_t msg_lrpid; /* último pid del mensaje recibido */ }; </code></tscreen> </sect3> <sect3>msg_setbuf<label id="msg_setbuf"><p> <tscreen><code> struct msq_setbuf { unsigned long qbytes; uid_t uid; gid_t gid; mode_t mode; }; </code></tscreen> </sect3> </sect2> <sect2>Funciones de Soporte de Mensajes<label id="msgfuncs"><p> <sect3>newque()<label id="newque"><p> newqueue() asigna la memoria para un nuevo descriptor de una cola de mensajes (<ref id="struct_msg_queue" name="struct msg_queue">) y entonces llama a <ref id="ipc_addid" name="ipc_addid()">, la cual reserva una entrada de la matriz de colas de mensaje para el nuevo descriptor de cola de mensajes. El descriptor de cola de mensajes es inicializado como sigue: <itemize> <item> La estructura <ref id="struct_kern_ipc_perm" name="kern_ipc_perm"> es inicializada. <item> Los campos <tt>q_stime</tt> y <tt>q_rtime</tt> del descriptor de cola de mensajes son inicializados a 0. El campo <tt>q_ctime</tt> es establecido a CURRENT_TIME. <item> El número máximo de bytes permitidos en esta cola de mensajes (<tt>q_qbytes</tt>) es establecida a MSGMNB, y el número de bytes actualmente usados por la cola (<tt>q_cbytes</tt>) es inicializada a 0. <item> La cola de mensajes esperando (<tt>q_messages</tt>), la cola de receptores esperando (<tt>q_receivers</tt>), y la cola de emisores esperando (<tt>q_senders</tt>) son inicializadas como vacías. </itemize> Todas las operaciones siguiendo la llamada a <ref id="ipc_addid" name="ipc_addid()"> son realizadas mientras se mantiente el spinlock global de cola de mensajes. Después de abrir el spinlock, newque() llama a msg_buildid(), que mapea directamente a <ref id="ipc_buildid" name="ipc_buildid()">. <ref id="ipc_buildid" name="ipc_buildid()"> usa el índice del descriptor de cola de mensajes para crear una única ID de cola de mensaje que es entonces retornada al llamante de newque(). </sect3> <sect3>freeque()<label id="freeque"><p> Cuando una cola de mensajes va a ser borrada, la función freeque() es llamada. Esta función asume que el spinlock global de la cola de mensajes ya está cerrado por la función llamante. Libera todos los recursos del núcleo asociados con esta cola de mensajes. Primero, llama a <ref id="func_ipc_rmid" name="ipc_rmid()"> (a través de msg_rmid()) para borrar el descriptor de cola de mensajes del array de descriptores de cola de mensajes global. Entonces llama a <ref id="expunge_all" name="expunge_all"> para despertar a todos los receptores durmiendo en esta cola de mensajes. Posteriormente el spinlock global de la cola de mensajes es liberado. Todos los mensajes almacenados en esta cola de mensajes son liberados y la memoria para los descriptores de cola son liberados. </sect3> <sect3>ss_wakeup()<label id="ss_wakeup"><p> ss_wakeup() despierta todas las tareas en la cola de mensajes del emisor dado. Si esta función es llamada por <ref id="freeque" name="freeque()">, entonces todos los emisores en la cola son quitados de ella. </sect3> <sect3>ss_add()<label id="ss_add"><p> ss_add() recibe como parámetro un descriptor de cola de mensajes y un mensaje de estructura de datos del emisor. Rellena el campo <tt>tsk</tt> del mensaje de la estructura de datos del emisor con el proceso actual, cambia el estado del proceso actual a TASK_INTERRUPTIBLE, entonces inserta el mensaje de la estructura de datos del emisor a la cabeza de la cola de emisores esperando la cola de mensajes dada. </sect3> <sect3>ss_del()<label id="ss_del"><p> Si el mensaje de la estrutura de datos del emisor dado (<tt>mss</tt>) aún está en la cola de espera del emisor asociado, entonces ss_del() quita <tt>mss</tt> de la cola. </sect3> <sect3>expunge_all()<label id="expunge_all"><p> expunge_all() recibe como parámetros un descriptor de la cola de mensajes (<tt>msq</tt>) y un valor entero (<tt>res</tt>) indicando el motivo para despertar a los receptores. Para cada receptor durmiendo asociado con <tt>msq</tt>, el campo <tt>r_msg</tt> es establecido para indicar el motivo para despertar (<tt>res</tt>), y la tarea asociada recibiendo es despertada. Esta función es llamada cuando una cola de mensajes es quitada o un operación de control de mensajes ha sido realizada. </sect3> <sect3>load_msg()<label id="load_msg"><p> Cuando un proceso envía un mensaje, la función <ref id="sys_msgsnd" name="sys_msgsnd()"> primero invoca a la función load_msg() para cargar el mensaje desde al espacio de usuario al espacio del núcleo. El mensaje es representado en la memoria del núcleo como una lista enlazada de bloques de datos. Asociado con el primer bloque de datos está una estructura <ref id="struct_msg_msg" name="msg_msg"> que describe el mensaje completo. El bloque de datos asociado con la estructura msg_msg está limitado por el tamaño de DATA_MSG_LEN. El bloque de datos y la estructura son asignados en un bloque contiguo de memoria que puede ser tan grande como un página en memoria. Si el mensaje total no se ajusta en este primer bloque de datos, entonces bloques de datos adicionales son asignados y son reorganizados en una lista enlazada. Estos bloques de datos están limitados por un tamaño de DATA_SEG_LEN, y cada uno incluye una estructura <ref id="struct_msg_msgseg" name="msg_msgseg)">. La estructura msg_msgseg y los bloques de datos asociados son asignados en un bloque de memoria contigua que puede ser tan grande como una página en memoria. Esta función retorna la dirección de la nueva estructura <ref id="struct_msg_msg" name="msg_msg"> si es que tiene éxito. </sect3> <sect3>store_msg()<label id="store_msg"><p> La función store_msg() es llamada por <ref id="sys_msgrcv" name="sys_msgrcv()"> para reensamblar un mensaje recibido en la antememoria del espacio de usuario suministrado por el llamante. Los datos descritos por la estructura <ref id="struct_msg_msg" name="msg_msg"> y cualquier estructura <ref id="struct_msg_msgseg" name="msg_msgseg"> son secuencialmente copiados a la antememoria del espacio de usuario. </sect3> <sect3>free_msg()<label id="free_msg"><p> La función free_msg() libera la memoria para una estructura de datos de mensaje <ref id="struct_msg_msg" name="msg_msg">, y los segmentos del mensaje. </sect3> <sect3>convert_mode()<label id="convert_mode"><p> convert_mode() es llamada por <ref id="sys_msgrcv" name="sys_msgrcv()">. Recibe como parámetros las direcciones del tipo del mensaje especificado (<tt>msgtyp</tt>) y una bandera (<tt>msgflg</tt>). Devuelve el modo de búsqueda al llamante basado en el valor de <tt>msgtyp</tt> y <tt>msgflg</tt>. Si <tt>msgtyp</tt> es null (cero), entonces SEARCH_ANY es devuelto, Si <tt>msgtyp</tt> es menor que 0, entonces <tt>msgtyp</tt> es establecido a su valor absoluto y SEARCH_LESSEQUAL es retornado. Si MSG_EXCEPT es especificado en <tt>msgflg</tt>, entonces SEARCH_NOTEQUAL es retornado. En otro caso SEARCH_EQUAL es retornado. </sect3> <sect3>testmsg()<label id="testmsg"><p> La función testmsg() chequea cuando un mensaje conoce el criterio especificado por el receptor. Devuelve 1 si una de las siguientes condiciones es verdad: <itemize> <item> El modo de búsqueda indica buscar cualquier mensaje (SEARCH_ANY). <item> El modo de búsqueda es SEARCH_LESSEQUAL y el tipo de mensaje es menor o igual que el tipo deseado. <item> El modo de búsqueda es SEARCH_EQUAL y el tipo de mensaje es el mismo que el tipo deseado. <item> El modo de búsqueda es SEARCH_NOTEQUAL y el tipo de mensajes no es igual al tipo especificado. </itemize> </sect3> <sect3>pipelined_send()<label id="pipelined_send"><p> pipelined_send() permite a un proceso enviar directamente un mensaje a la cola de receptores mejor que depositar el mensaje en la cola asociada de mensajes esperando. La función <ref id="testmsg" name="testmsg()"> es invocada para encontrar el primer receptor que está esperando por el mensaje dado. Si lo encuentra, el receptor esperando es quitado de la cola de receptores esperando, y la tarea receptora asociada es despertada. El mensaje es almacenado en el campo <tt>r_msg</tt> del receptor, y 1 es retornado. En el caso donde no hay un receptor esperando por el mensaje, 0 es devuelto. En el proceso de búsqueda de un receptor, los receptores potenciales quizás encuentren que han solicitado un tamaño que es muy pequeño para el mensaje dado. Tales receptores son quitados de la cola, y son despertados con un status de error de E2BIG, el cual es almacenado en el campo <tt>r_msg</tt>. La búsqueda entonces continúa hasta que alguno de los receptores válidos es encontrado, o la cola está exausta. </sect3> <sect3>copy_msqid_to_user()<label id="copy_msqid_to_user"><p> copy_msqid_to_user() copia el contenido de una antememoria del núcleo a la antememoria del usuario. Recibe como parámetros una antememoria del usuario, una antememoria del núcleo del tipo <ref id="struct_msqid64_ds" name="msqid64_ds">, y una bandera versión indicando la nueva versión IPC vs. la vieja versión IPC. Si la bandera de la versión es igual a IPC_64, entonces copy_to_user() es llamado para copiar desde la antememoria del núcleo a la antememoria del usuario directamente. En otro caso una antememoria temporal del tipo struct msqid_ds es inicializada, y los datos del núcleo son trasladados a esta antememoria temporal. Posteriormente copy_to_user() es llamado para copiar el contenido de la antememoria temporal a la antememoria del usuario. </sect3> <sect3>copy_msqid_from_user()<label id="copy_msqid_from_user"><p> La función copy_msqid_from_user() recibe como parámetros un mensaje de la antememoria del núcleo del tipo struct msq_setbuf, una antememoria de usuario y una bandera de la versión indicando la nueva versión IPC vs. la vieja versión IPC. En la caso de la nueva versión IPC, copy_from_user() es llamada para copiar el contenido de la antememoria del usuario a una antememoria temporal del tipo <ref id="struct_msqid64_ds" name="msqid64_ds">. Entonces, los campos <tt>qbytes</tt>,<tt>uid</tt>, <tt>gid</tt>, y <tt>mode</tt> de la antememoria del núcleo son rellenados con los valores de los campos correspondientes desde la antememoria temporal. En el caso de la vieja versión IPC, una antememoria temporal del tipo struct <ref id="struct_msqid_ds" name="msqid_ds"> es usado en su vez. </sect3> </sect2> </sect1> <sect1>Memoria Compartida<label id="sharedmem"><p> <sect2>Interfaces de las llamadas al sistema de la Memoria Compartida<label id="Shared_Memory_System_Call_Interfaces"> <p> <sect3>sys_shmget()<label id="sys_shmget"><p> La llamada entera a sys_shmget() es protegida por el semáforo global de memoria compartida. En el caso donde un valor de llave es suministrado para un segmento existente de memoria compartida, el correspondiente índice es buscado en la matriz de descriptores de memoria compartida, y los parámetros y los permisos del llamante son verificados antes de devolver la ID del segmento de memoria compartida. Las operaciones de búsqueda y verificación son realizadas mientras es mantenido el spinlock global de memoria compartida. </sect3> <sect3>sys_shmctl()<label id="sys_shmctl"><p> <sect4>IPC_INFO<label id="IPC_INFO"><p> Una antememoria temporal <ref id="struct_shminfo64" name="shminfo64"> es cargada con los parámetros del sistema de memoria compartida y es copiada fuera del espacio de usuario para el acceso de la aplicación llamante. </sect4> <sect4>SHM_INFO<label id="SHM_INFO"><p> El semáforo global de memoria compartida y el spinlock global de memoria compartida son mantenidos mientras se obtienen estadísticas de la información del sistema para la memoria compartida. La función <ref id="shm_get_stat" name="shm_get_stat()"> es llamada para calcular el número de páginas de memoria compartidas que están residentes en memoria y el número de páginas de memoria compartida que han sido intercambiadas (swapped out). Otras estadísticas incluyen el número total de páginas de memoria compartida y el número de segmentos de memoria compartida en uso. Las cuentas de <tt>swap_attempts</tt> y <tt>swap_successes</tt> son codificadas fuertemente a cero. Estas estadísticas son almacenadas en una antememoria temporal <ref id="struct_shm_info" name="shm_info"> y copiadas fuera del espacio de usuario para la aplicación llamante. </sect4> <sect4>SHM_STAT, IPC_STAT<label id="SHM_STAT_IPC_STAT"><p> Para SHM_STAT y IPC_STATA, una antememoria temporal del tipo <ref id="struct_shmid64_ds" name="struct shmid64_ds"> es inicializada, y el spinlock global de memoria compartida es cerrado. Para el caso SHM_STAT, el parámetro ID del segmento de memoria compartida se espera que sea un índice exacto (esto es, 0 a n donde n es el número de IDs de memoria compartida en el sistema). Después de validar el índice, <ref id="ipc_buildid" name="ipc_buildid()"> es llamado (a través de shm_buildid()) para convertir el índice en una ID de memoria compartida. En el caso transitorio de SHM_STAT, la ID de la memoria compartida será el valor de retorno. Notar que esta es una característica no documentada, pero es mantenida para el programa ipcs(8). Para el caso IPC_STAT, el parámetro ID del segmento de memoria compartida se espera que sea una ID que ha sido generada por una llamada a <ref id="sys_shmget" name="shmget()">. La ID es validada antes de proceder. En el caso transitorio de IPC_STAT, el valor de retorno será 0. Para SHM_STAT y IPC_STAT, los permisos de acceso del llamante son verificados. Las estadísticas deseadas son cargadas en la antememoria temporal y entonces copiadas fuera de la aplicación llamante. </sect4> <sect4>SHM_LOCK, SHM_UNLOCK<label id="SHM_LOCK_SHM_UNLOCK"><p> Después de validar los permisos de acceso, el spinlock global de memoria compartida es cerrado, y la ID del segmento de memoria compartida es validado. Para SHM_LOCK y SHM_UNLOCK, <ref id="shmem_lock" name="shmem_lock()"> es llamada para realizar la función. Los parámetros para <ref id="shmem_lock" name="shmem_lock()"> identifican la función a realizar. </sect4> <sect4>IPC_RMID<label id="IPC_RMID"><p> Durante el IPC_RMID el semáforo global de memoria compartida y el spinlock global de memoria compartida son mantenidos a través de esta función. La ID de la Memoria Compartida es validada, y entonces si no hay conexiones actuales, <ref id="shm_destroy" name="shm_destroy()"> es llamada para destruir el segmento de memoria compartida. En otro caso, la bandera SHM_DEST es establecida para marcarlo para destrucción, y la bandera IPC_PRIVATE es establecida para prevenir que otro proceso sea capaz de referenciar la ID de la memoria compartida. </sect4> <sect4>IPC_SET<label id="IPC_SET"><p> Después de validar la ID del segmento de memoria compartida y los permisos de acceso del usuario, las banderas <tt>uid</tt>, <tt>gid</tt>, y <tt>mode</tt> del segmento de la memoria compartida son actualizadas con los datos del usuario. El campo <tt>shm_ctime</tt> también es actualizado. Estos cambios son realizados mientras se mantiene el semáforo global de memoria compartida global y el spinlock global de memoria compartida. </sect4> </sect3> <sect3>sys_shmat()<label id="sys_shmat"><p> sys_shmat() toma como parámetro, una ID de segmento de memoria compartida, una dirección en la cual el segmento de memoria compartida debería de ser conectada (<tt>shmaddr</tt>), y las banderas que serán descritas más adelante. Si <tt>shmaddr</tt> no es cero, y la bandera SHM_RND es especificada, entonces <tt>shmaddr</tt> es redondeado por abajo a un múltiplo de SHMLBA. Si <tt>shmaddr</tt> no es un múltiplo de SHMLBA y SHM_RND no es especificado, entonces EINVAL es devuelto. Los permisos de acceso del llamante son validados y el campo <tt>shm_nattch</tt> del segmento de memoria compartida es incrementado. Nótese que este incremento garantiza que la cuenta de enlaces no es cero y previene que el segmento de memoria compartida sea destruido durante el proceso de enlazamiento al segmento. Estas operaciones son realizadas mientras se mantiene el spinlock global de memoria compartida. La función do_mmap() es llamada para crear un mapeo de memoria virtual de las páginas del segmento de memoria compartida. Esto es realizado mientras se mantiene el semáforo <tt>mmap_sem</tt> de la tarea actual. La bandera MAP_SHARED es pasada a do_mmap(). Si una dirección fue suministrada por el llamante, entonces la bandera MAP_FIXED también es pasada a do_mmap(). En otro caso, do_mmap() seleccionará la dirección virtual en la cual mapear el segmento de memoria compartida. NÓTESE que <ref id="shm_inc" name="shm_inc()"> será invocado con la llamada a la función do_mmap() a través de la estructura <tt>shm_file_operations</tt>. Esta función es llamada para establecer el PID, para establecer el tiempo actual, y para incrementar el número de enlaces a este segmento de memoria compartida. Después de la llamada a do_mmap(), son obtenidos el semáforo global de memoria compartida y el spinlock global de la memoria compartida. La cuenta de enlaces es entonces decrementada. El siguiente cambio en la cuenta de enlaces es 1 para una llamada a shmat() por culpa de la llamada a <ref id="shm_inc" name="shm_inc()">. Si, después de decrementar la cuenta de enlaces, la cuenta resultante que se encuentra es cero, y el segmento se marca para la destrucciónn (SHM_DEST), entonces <ref id="shm_destroy" name="shm_destroy()"> es llamado para liberar los recursos del segmento de memoria compartida. Finalmente, la dirección virtual en la cual la memoria compartida es mapeada es devuelta al llamante en la dirección especificada por el usuario. Si un código de error ha sido retornado por do_mmap(), entonces este código de fallo es pasado en el valor de retorno para la llamada al sistema. </sect3> <sect3>sys_shmdt()<label id="sys_shmdt"><p> El semáforo global de la memoria compartida es mantenido mientras se realiza sys_shmdt(). La <tt>mm_struct</tt> del actual proceso es buscado para la <tt>vm_area_struct</tt> asociada con la dirección de memoria compartida. Cuando es encontrada, do_munmap() es llamado para deshacer el mapeo de direcciones virtuales para el segmento de la memoria compartida. Nótese también que do_munmap() realiza una llamada atrás a <ref id="shm_close" name="shm_close()">, la cual realiza las funciones manteniendo el libro de memoria compartida, y libera los recursos del segmento de memoria compartida si no hay más enlaces. sys_shmdt() incondicionalmente devuelve 0. </sect3> </sect2> <sect2>Estructuras de Soporte de Memoria Compartida<label id="shm_structures"><p> <sect3>struct shminfo64<label id="struct_shminfo64"><p> <tscreen><code> struct shminfo64 { unsigned long shmmax; unsigned long shmmin; unsigned long shmmni; unsigned long shmseg; unsigned long shmall; unsigned long __unused1; unsigned long __unused2; unsigned long __unused3; unsigned long __unused4; }; </code></tscreen> </sect3> <sect3>struct shm_info<label id="struct_shm_info"><p> <tscreen><code> struct shm_info { int used_ids; unsigned long shm_tot; /* shm asignada total */ unsigned long shm_rss; /* shm residente total */ unsigned long shm_swp; /* shm intercambiada total */ unsigned long swap_attempts; unsigned long swap_successes; }; </code></tscreen> </sect3> <sect3>struct shmid_kernel<label id="struct_shmid_kernel"><p> <tscreen><code> struct shmid_kernel /* privadas del núcleo */ { struct kern_ipc_perm shm_perm; struct file * shm_file; int id; unsigned long shm_nattch; unsigned long shm_segsz; time_t shm_atim; time_t shm_dtim; time_t shm_ctim; pid_t shm_cprid; pid_t shm_lprid; }; </code></tscreen> </sect3> <sect3>struct shmid64_ds<label id="struct_shmid64_ds"><p> <tscreen><code> struct shmid64_ds { struct ipc64_perm shm_perm; /* permisos de la operación */ size_t shm_segsz; /* tamaño del segmento (bytes) */ __kernel_time_t shm_atime; /* último tiempo de enlace */ unsigned long __unused1; __kernel_time_t shm_dtime; /* último tiempo de desenlace */ unsigned long __unused2; __kernel_time_t shm_ctime; /* último tiempo de cambio */ unsigned long __unused3; __kernel_pid_t shm_cpid; /* pid del creador */ __kernel_pid_t shm_lpid; /* pid del último operador */ unsigned long shm_nattch; /* número de enlaces actuales */ unsigned long __unused4; unsigned long __unused5; }; </code></tscreen> </sect3> <sect3>struct shmem_inode_info<label id="struct_shmem_inode_info"><p> <tscreen><code> struct shmem_inode_info { spinlock_t lock; unsigned long max_index; swp_entry_t i_direct[SHMEM_NR_DIRECT]; /* para el primer bloque */ swp_entry_t **i_indirect; /* doble bloque indirecto */ unsigned long swapped; int locked; /* en la memoria */ struct list_head list; }; </code></tscreen> </sect3> </sect2> <sect2>Funciones de Soporte De Memoria Compartida<label id="shm_primitives"><p> <sect3>newseg()<label id="newseg"><p> La función newseg() es llamada cuando un nuevo segmento de memoria compartida tiene que ser creado. Actúa con tres parámetros para el nuevo segmento: la llave, la bandera y el tamaño, Después de validar que el tamaño del segmento de la memoria compartida que va a ser creado está entre SHMMIN y SHMMAX y que el número total de segmentos de memoria compartida no excede de SHMALL, asigna un nuevo descriptor de memoria compartida. La función <ref id="shmem_file_setup" name="shmem_file_setup()"> es invocada posteriormente a crear un archivo no enlazado del tipo tmpfs. El puntero del archivo retornado es guardado en el campo <tt>shm_file</tt> del descriptor del segmento de memoria compartida asociado. El nuevo descriptor de memoria compartida es inicializado e insertado en la matriz global de IPC de descriptores de segmentos de memoria compartida. La ID del segmento de memoria compartida es creado por shm_buildid() (a través de <ref id="ipc_buildid" name="ipc_buildid()">). La ID de este segmento es guardada en el campo <tt>id</tt> del descriptor de memoria compartida, al igual que en el campo <tt>i_ino</tt> del inodo asociado. En adicción, la dirección de las operaciones de memoria compartida definidas en la estructura <tt>shm_file_operation</tt> son almacenadas en el fichero asociado. El valor de la variable global <tt>shm_tot</tt>, que indica el número total de segmentos de memoria compartida a lo largo del sistema, es también incrementado para reflejar este cambio. Si tiene éxito, la ID del segmento es retornada a la aplicación llamante. </sect3> <sect3>shm_get_stat()<label id="shm_get_stat"><p> Los ciclos de shm_get_stat() van a través de todas las estructuras de memoria compartida, y calcula el número total de páginas de memoria en uso por la memoria compartida y el número total de páginas de memoria compartida que están intercambiadas. Hay una estructura de fichero y una estructura de inodo para cada segmento de memoria compartida. Como los datos requeridos son obtenidos a través del inodo, el spinlock para cada estructura inodo que es accedido es cerrado y abierto en secuencia. </sect3> <sect3>shmem_lock()<label id="shmem_lock"><p> shmem_lock() recibe como parámetros un puntero al descriptor del segmento de memoria compartida y una bandera indicando cerrado vs. abierto. El estado de bloqueo del segmento de memoria compartida es almacenado en el inodo asociado. Este estado es comparado con el estado de bloqueo deseado: shmem_lock() simplemente retorna si ellos se corresponden. Mientras se está manteniendo el semáforo del inodo asociado, el estado de bloqueo del inodo es establecido. La siguiente lista de puntos ocurren en cada página en el segmento de memoria compartida: <itemize> <item> find_lock_page() es llamado para cerrar la página (estableciendo PG_locked) y para incrementar la cuenta de referencia de la página. Incrementando la cuenta de referencia se asegura que el segmento de memoria compartida permanece bloqueado en memoria durante esta operación. <item> Si el estado deseado es cerrado, entonces PG_locked es limpiado, pero la cuenta de referencia permanece incrementada. <item> Si el estado deseado es abierto, entonces la cuenta de referencia es decrementada dos veces durante la actual referencia, y una vez para la referencia existente que causó que la página permanezca bloqueada en memoria. Entonces PG_locked es limpiado. </itemize> </sect3> <sect3>shm_destroy()<label id="shm_destroy"><p> Durante shm_destroy() el número total de páginas de memoria compartida es ajustada para que cuente el borrado del segmento de memoria compartida. <ref id="func_ipc_rmid" name="ipc_rmid()"> es llamado (a través de shm_rmid()) para borrar la ID de Memoria Compartida. <ref id="shmem_lock" name="shmem_lock"> es llamado para abrir las páginas de memoria compartida, efectivamente, decrementando la cuenta de referencia a cero para cada página. fput() es llamado para decrementar el contador de uso para <tt>f_count</tt> para el objeto fichero deseado, y si es necesario, para liberar los recursos del objeto fichero. kfree() es llamado para liberar el descriptor de segmento de memoria compartida. </sect3> <sect3>shm_inc()<label id="shm_inc"><p> shm_inc() establece el PID, establece el tiempo actual, e incrementa el número de enlaces para el segmento de memoria compartida dado. Estas operaciones son realizadas mientras se mantiene el spinlock global de memoria compartida. </sect3> <sect3>shm_close()<label id="shm_close"><p> shm_close() actualiza los campos <tt>shm_lprid</tt> y <tt>shm_dtim</tt> y decrementa el número de segmentos enlazados de memoria compartida. Si no hay otros enlaces al segmento de memoria compartida, entonces <ref id="shm_destroy" name="shm_destroy()"> es llamado para liberar los recursos de la memoria compartida. Estas operaciones son todas realizadas mientras se mantienen el semáforo global de memoria compartida y el spinlock global de memoria compartida. </sect3> <sect3>shmem_file_setup()<label id="shmem_file_setup"><p> La función shmem_file_setup() configura un archivo sin enlazar que vive en el sistema de ficheros tmpfs con el nombre y tamaño dados. Si hay suficientes recursos de memoria para este fichero, crea una nueva dentry bajo la raiz montada de tmpfs, y asigna un nuevo descriptor de fichero y un nuevo objeto inodo del tipo tmpfs. Entonces asocia el nuevo objeto dentry con el nuevo objeto inodo llamando a d_instantiate() y guarda la dirección del objeto dentry en el descriptor de fichero. El campo <tt>i_size</tt> del objeto inodo es establecido para ser del tamaño del fichero y el campo <tt>i_nlink</tt> es establecido para ser 0 en orden a marcar el inodo no enlazado. También, shmem_file_setup() almacena la dirección de la estructura <tt>shmem_file_operations</tt> en el campo <tt>f_op</tt>, e inicializa los campos <tt>f_mode</tt> y <tt>f_vfsmnt</tt> del descriptor de fichero propio. La función shmem_truncate() es llamada para completar la inicialización del objeto inodo. Si tiene éxito, shmem_file_setup() devuelve el nuevo descriptor de fichero. </sect3> </sect2> </sect1> <sect1>Primitivas IPC de Linux<label id="ipc_primitives"><p> <sect2>Primitivas IPC de Linux Genéricas usadas con Semáforos, Mensajes y Memoria Compartida <label id="Generic_Linux_IPC_Primitives_used_with_Semaphores_Messages_and_Shared_Memory"> <p> Los semáforos, mensajes, y mecanismos de memoria compartida de Linux son construidos con un conjunto de primitivas comunes. Estas primitivas son descritas en las secciones posteriores. <sect3>ipc_alloc()<label id="ipc_alloc"><p> Si el asignamiento de memoria es mayor que PAGE_SIZE, entonces vmalloc() es usado para asignar memoria. En otro caso, kmalloc() es llamado con GFP_KERNEL para asignar la memoria. </sect3> <sect3>ipc_addid()<label id="ipc_addid"><p> Cuando un nuevo conjunto de semáforos, cola de mensajes, o segmento de memoria compartido es añadido, ipc_addid() primero llama a <ref id="grow_ary" name="grow_ary()"> para asegurarse que el tamaño de la correspondiente matriz de descriptores es suficientemente grande para el máximo del sistema. La matriz de descriptores es buscada para el primer elemento sin usar. Si un elemento sin usar es encontrado, la cuenta de descriptores que están en uso es incrementada. La estructura <ref id="struct_kern_ipc_perm" name="kern_ipc_perm"> para el nuevo recurso descriptor es entonces inicializado, y el índice de la matriz para el nuevo descriptor es devuelto. Cuando ipc_addid() tiene éxito, retorna con el spinlock global cerrado para el tipo IPC dado. </sect3> <sect3>ipc_rmid()<label id="func_ipc_rmid"><p> ipc_rmid() borra el descriptor IPC desde la matriz de descriptores global del tipo IPC, actualiza la cuenta de IDs que están en uso, y ajusta la máxima ID en la matriz de descriptores correspondiente si es necesario. Un puntero al descriptor asociado IPC con la ID del IPC dado es devuelto. </sect3> <sect3>ipc_buildid()<label id="ipc_buildid"><p> ipc_buildid() crea una única ID para ser asociada con cada descriptor con el tipo IPC dado. Esta ID es creada a la vez que el nuevo elemento IPC es añadido (ej. un nuevo segmento de memoria compartido o un nuevo conjunto de semáforos). La ID del IPC se convierte fácilmente en el índice de la correspondiente matriz de descriptores. Cada tipo IPC mantiene una secuencia de números la cual es incrementada cada vez que un descriptor es añadido. Una ID es creada multiplicando el número de secuencia con SEQ_MULTIPLIER y añadiendo el producto al índice de la matriz de descriptores. La secuencia de números usados en crear una ID de un IPC particular es entonces almacenada en el descriptor correspondiente. La existencia de una secuencia de números hace posible detectar el uso de una ID de IPC sin uso. </sect3> <sect3>ipc_checkid()<label id="ipc_checkid"><p> ipc_checkid() divide la ID del IPC dado por el SEQ_MULTIPLIER y compara el cociente con el valor seq guardado en el descriptor correspondiente. Si son iguales, entonces la ID del IPC se considera válida y 1 es devuelto. En otro caso, 0 es devuelto. </sect3> <sect3>grow_ary()<label id="grow_ary"><p> grow_ary() maneja la posibilidad de que el número máximo (ajustable) de IDs para un tipo IPC dado pueda ser dinámicamente cambiado. Fuerza al actual límite máximo para que no sea mayor que el limite del sistema permanente (IPCMNI) y lo baja si es necesario. También se asegura de que la matriz de descriptores existente es suficientemente grande. Si el tamaño de la matriz existente es suficientemente grande, entonces el límite máximo actual es devuelto. En otro caso, una nueva matriz más grande es asignada, la matriz vieja es copiada en la nueva, y la vieja es liberada. El correspondiente spinlock global es mantenido mientras se actualiza la matriz de descriptores para el tipo IPC dado. </sect3> <sect3>ipc_findkey()<label id="ipc_findkey"><p> ipc_findkey() busca a través de la matriz de descriptores del objeto especificado <ref id="struct_ipc_ids" name="ipc_ids">, y busca la llave especificada. Una vez encontrada, el índice del descriptor correspondiente es devuelto. Si la llave no es encontrada, entonces es devuelto -1. </sect3> <sect3>ipcperms()<label id="ipcperms"><p> ipcperms() chequa el usuario, grupo, y otros permisos para el acceso de los recursos IPC. Devuelve 0 si el permiso está garantizado y -1 en otro caso. </sect3> <sect3>ipc_lock()<label id="ipc_lock"><p> ipc_lock() coge una ID de IPC como uno de sus parámetros. Cierra el spinlock global para el tipo IPC dado, y devuelve un puntero al descriptor correspondiente a la ID IPC especificada. </sect3> <sect3>ipc_unlock()<label id="ipc_unlock"><p> ipc_unlock() libera el spinlock global para el tipo IPC indicado. </sect3> <sect3>ipc_lockall()<label id="ipc_lockall"><p> ipc_lockall() cierra el spinlock global para el mecanismo IPC dado (esto es: memoria compartida, semáforos, y mensajes). </sect3> <sect3>ipc_unlockall()<label id="ipc_unlockall"><p> ipc_unlockall() abre el spinlock global para el mecanismo IPC dado (esto es: memoria compartida, semáforos, y mensajes). </sect3> <sect3>ipc_get()<label id="ipc_get"><p> ipc_get() coge un puntero al tipo particular IPC (memoria compartida, semáforos o colas de mensajes) y una ID de un descriptor, y devuelve un puntero al descriptor IPC correspondiente. Nótese que aunque los descriptores para cada tipo IPC son tipos de datos diferentes, el tipo de estructura común <ref id="struct_kern_ipc_perm" name="kern_ipc_perm"> está embebida como la primera entidad en todos los casos. La función ipc_get() devuelve este tipo de datos común. El modelo esperado es que ipc_get() es llamado a través de la función envoltorio (ej. shm_get()) la cual arroja el tipo de datos al tipo de datos correcto del descriptor. </sect3> <sect3>ipc_parse_version()<label id="ipc_parse_version"><p> ipc_parse_version() borra la bandera IPC_64 desde el comando si está presente y devuelve IPC_64 o IPC_OLD. </sect3> </sect2> <sect2>Estructuras Genéricas IPC usadas con Semáforos, Mensajes, y Memoria Compartida<label id="ipc_structures"><p> Todos los semáforos, mensajes, y mecanismos de memoria compartida hacen un uso de las siguientes estructuras comunes: <sect3>struct kern_ipc_perm<label id="struct_kern_ipc_perm"><p> Cada descriptor IPC tiene un objeto de datos de este tipo como primer elemento. Esto hace posible acceder a cualquier descriptor desde cualquier función genérica IPC usando un puntero de este tipo de datos. <tscreen><code> /* usados por la estructuras de datos en el núcleo */ struct kern_ipc_perm { key_t key; uid_t uid; gid_t gid; uid_t cuid; gid_t cgid; mode_t mode; unsigned long seq; }; </code></tscreen> </sect3> <sect3>struct ipc_ids<label id="struct_ipc_ids"><p> La estructura ipc_ids describe los datos comunes para los semáforos, colas de mensajes, y memoria compartida. Hay tres instancias globales de esta estructura de datos --<tt>semid_ds</tt>, <tt>msgid_ds</tt> y <tt>shmid_ds</tt>-- para los semáforos, mensajes y memoria compartida respectivemente. En cada instancia, el semáforo <tt>sem</tt> es usado para proteger el acceso a la estructura. El campo <tt>entries</tt> apunta a una matriz de descriptores de IPC, y el spinlock <tt>ary</tt> protege el acceso a esta matriz. El campo <tt>seq</tt> es una secuencia de números global la cual será incrementada cuando un nuevo recurso IPC es creado. <tscreen><code> struct ipc_ids { int size; int in_use; int max_id; unsigned short seq; unsigned short seq_max; struct semaphore sem; spinlock_t ary; struct ipc_id* entries; }; </code></tscreen> </sect3> <sect3>struct ipc_id<label id="struct_ipc_id"><p> Una matriz de estructuras ipc_id existe en cada instancia de la estructura <ref id="struct_ipc_ids" name="ipc_ids">. La matriz es dinámicamente asignada y quizás sea reemplazada con una matriz más grande por <ref id="grow_ary" name="grow_ary()"> tal como requiere. La matriz es a veces referida por la matriz de descriptores, desde que el tipo de datos <ref id="struct_kern_ipc_perm" name="kern_ipc_perm"> es usado como tipo de datos de descriptores comunes por las funciones genéricas IPC. <tscreen><code> struct ipc_id { struct kern_ipc_perm* p; }; </code></tscreen> </sect3> </sect2> </sect1> </sect> <sect>Sobre la traducción <p> Este libro ha sido traducido por Rubén Melcón <melkon@terra.es>. </p> <p> Revisión de la traducción: Beta 0.03 (2 de Marzo de 2002). </p> <p> Revisado el 27 de Octubre de 2002 por Manuel Canales Esparcia <macana@macana-es.con> </p> <p> Publicado por <htmlurl url="http://es.tldp.org" name="TLDP-ES"> </p> </sect> </article>