Página siguiente Página anterior Índice general

1. Arrancando

1.1 Construyendo la Imagen del Núcleo Linux

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 arch/i386/boot/zImage o arch/i386/boot/bzImage respectivamente. Aquí está como es construida la imagen:

  1. 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 ar(1).
  2. Usando ld(1), los anteriores .o y .a son enlazados en vmlinux, 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.
  3. System.map es producido por nm vmlinux, donde los símbolos irrelevantes o que no interesan son desechados.
  4. Se entra en el directorio arch/i386/boot.
  5. El código de ensamblador del sector de arranque bootsect.S es preprocesado con o sin -D__BIG_KERNEL__, dependiendo de cuando el objetivo es bzImage o zImage, en bbootsect.s o bootsect.s respectivamente.
  6. bbootsect.s es ensamblado y entonces convertido en la forma 'raw binary' llamada bbootsect (o bootsect.s ensamblado y convertido a raw en bootsect para zImage).
  7. El código de configuración setup.S (setup.S incluye video.S) es preprocesado en bsetup.s para bzImage o setup.s para zImage. De la misma forma que el código del sector de arranque, la diferencia radica en que -D__BIG_KERNEL__ está presente para bzImage. El resultado es entonces convertido en la forma 'raw binary' llamada bsetup.
  8. Se entra en el directorio arch/i386/boot/compressed y se convierte /usr/src/linux/vmlinux a $tmppiggy (nombre temporal) en el formato binario raw, borrando las secciones ELF .note y .comment.
  9. gzip -9 < $tmppiggy > $tmppiggy.gz
  10. Se enlaza $tmppiggy.gz en ELF reasignable (ld -r) piggy.o.
  11. Compila las rutinas de compresión head.S y misc.c (todavía en el directorio arch/i386/boot/compressed) en los objetos ELF head.o y misc.o.
  12. Se enlazan todas ellas: head.o, misc.o y piggy.o en bvmlinux (o vmlinux para zImage, ¡no confundas esto con /usr/src/linux/vmlinux!). Destacar la diferencia entre -Ttext 0x1000 usado para vmlinux y -Ttext 0x100000 para bvmlinux, esto es, el cargador de compresión para bzImage es cargado más arriba.
  13. Se convierte bvmlinux a 'raw binary' bvmlinux.out borrando las secciones ELF .note y .comment.
  14. Se vuelve atrás al directorio arch/i386/boot y, usando el programa tools/build, se concatenan todos ellos: bbootsect, bsetup y compressed/bvmlinux.out en bzImage (borra la 'b' extra anterior para zImage). Esto escribe variables importantes como setup_sects y root_dev al final del sector de arranque.
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 tools/build 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 setup.S.

1.2 Arrancando: Vista General

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:

  1. La BIOS selecciona el dispositivo de arranque.
  2. La BIOS carga el sector de arranque del dispositivo de arranque.
  3. El sector de arranque carga la configuración, las rutinas de descompresión y la imagen del núcleo comprimida.
  4. El núcleo es descomprimido en modo protegido.
  5. La inicialización de bajo nivel es realizada por el código ensamblador.
  6. Inicialización de alto nivel en C.

1.3 Arrancando: BIOS POST

  1. La fuente de alimentación inicia el generador de reloj y aserta la señal #POWERGOOD en el bus.
  2. La linea CPU #RESET es asertada (CPU está ahora en modo real 8086).
  3. %ds=%es=%fs=%gs=%ss=0, %cs=0xFFFF0000,%eip = 0x0000FFF0 (código ROM BIOS POST).
  4. Todos los chequeos POST son realizados con las interrupciones deshabilitadas.
  5. La TVI (Tabla de Vectores de Interrupción) es inicializada en la dirección 0.
  6. La función de la BIOS de cargador de la rutina de arranque es llamada a través de la int0x19, 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).

1.4 Arrancando: sector de arranque y configuración

El sector de arranque usado para arrancar el núcleo Linux puede ser uno de los siguientes:

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:


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 */

(los números a la izquierda son los números de linea del archivo bootsect.S) Los valores de DEF_INITSEG, DEF_SETUPSEG, DEF_SYSSEG y DEF_SYSSIZE son tomados desde include/asm/boot.h:


/* 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

Ahora, consideremos el código actual de bootsect.S:


    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.

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:

  1. establece %ds:%si a $BOOTSEG:0 (0x7C0:0 = 0x7C00)
  2. establece %es:%di a $INITSEG:0 (0x9000:0 = 0x90000)
  3. establece el número de palabras de 16 bits en %cx (256 palabras = 512 bytes = 1 sector)
  4. limpia la bandera DF (dirección) en EFLAGS a direcciones auto-incrementales (cld)
  5. va allí y copia 512 bytes (rep movsw)

El motivo por el que este código no usa rep movsd es intencionado (hint - .code16).

La línea 64 salta a la etiqueta go: 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:


    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)

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:


   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:

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 ok_load_setup:.

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(arch/i386/boot/setup.S). 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 setup.S, el cual prepara las cosas para el modo protegido y salta a 0x1000, que es el comienzo del núcleo comprimido, esto es, arch/386/boot/compressed/{head.S,misc.c}. Esto inicializa la pila y llama a decompress_kernel(), 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 bootsect_kludge en bootsect.S y es definida como bootsect_helper en setup.S. La etiqueta bootsect_kludge en setup.S contiene el valor del segmento de configuración y el desplazamiento del código bootsect_helper en él, por lo que el sector de arranque puede usar la instrucción lcall para saltar a él (salto entre segmentos). El motivo por lo cual esto es realizado en setup.S 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 bootsect.S, 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 bootsect.S no se va fuera de memoria cuando está copiando datos desde disco.

1.5 Usando LILO como cargador de arranque

Existen varias ventajas en usar un cargador de arranque especializado (LILO) sobre un esqueleto desnudo de un sector de arranque:

  1. Habilidad para escoger entre varios núcleos Linux o incluso múltiples Sistemas Operativos.
  2. 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).
  3. Habilidad para cargar núcleos bzImage más grandes - hasta los 2.5M vs 1M.
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 setup.S y entonces las cosas prosiguen de la forma normal.

1.6 Inicialización de Alto Nivel

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 arch/i386/kernel/head.S, que es el comienzo del núcleo descomprimido. Los siguientes pasos son realizados:

  1. Inicializa los valores de segmento (%ds = %es = %fs = %gs = __KERNEL_DS = 0x18).
  2. Inicializa las tablas de páginas.
  3. Habilita el paginamiento estableciendo el bit PG en %cr0.
  4. Limpia a cero BSS (en SMP, sólo la primera CPU realiza esto).
  5. Copia los primeros 2k de los parámetros de arranque (linea de comandos del núcleo).
  6. Chequea el tipo de CPU usando EFLAGS y, si es posible, cpuid, capaz de detectar 386 y superiores.
  7. La primera CPU llama a start_kernel(),y si ready=1 todas las otras llaman a arch/i386/kernel/smpboot.c:initialize_secondary() el cual recarga esp/eip y no retorna.

La función init/main.c:start_kernel() está escrita en C y realiza lo siguiente:

  1. Realiza un cierre global del núcleo (es necesario para que sólo una CPU realice la inicialización).
  2. 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.).
  3. 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 cat /proc/version.
  4. Inicializa las traps.
  5. Inicializa las irqs.
  6. Inicializa los datos requeridos por el planificador (scheduler).
  7. Inicializa el tiempo manteniendo los datos.
  8. Inicializa el subsistema softirq.
  9. Analiza las opciones del arranque de la linea de comandos.
  10. Inicializa la consola.
  11. Si el soporte para módulos ha sido compilado en el núcleo, inicializa la facilidad para la carga dinámica de módulos.
  12. Si la linea de comandos "profile=" ha sido suministrada, inicializa los perfiles de memoria intermedia.
  13. kmem_cache_init(), inicializa la mayoría del asignador slab.
  14. Habilita las interrupciones.
  15. Calcula el valor BogoMips para esta CPU.
  16. Llama a mem_init(), que calcula max_mapnr, totalram_pages y high_memory y muestra la linea "Memory: ...".
  17. kmem_cache_sizes_init(), finaliza la inicialización del asignador slab.
  18. Inicializa las estructuras de datos usadas por procfs.
  19. fork_init(), crea uid_cache, inicializa max_threx_threads basándose en la cantidad de memoria disponible y configura RLIMIT_NPROC para que init_task sea max_threads/2.
  20. Crea varias antememorias slab necesitadas para VFS, VM, la antememoria intermedia, etc.
  21. 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.
  22. Si el soporte de quota ha sido compilado en el núcleo, crea e inicializa una antememoria slab especial para él.
  23. 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.
  24. Establece una bandera para indicar que un planificador debería de ser llamado en la "siguiente oportunidad" y crea un hilo del núcleo init() que ejecuta execute_command si este ha sido suministrado a través del parámetro de inicio "init=", o intenta ejecutar /sbin/init, /etc/init, /bin/init, /bin/sh en este orden; si todos estos fallan, ocurre una situación de pánico con la "sugerencia" de usar el parámetro "init=".
  25. Se va a un bucle vacío, este es un hilo vacío con pid=0.

Una cosa importante que hay que hacer notar aquí es que el hilo del núcleo init() llama a do_basic_setup(), el cual cuando vuelve llama a do_initcalls(), que va a través de la lista de funciones registradas por medio de las macros __initcall o module_init() 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 .initcall.init 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 /sbin/init accidentalmente sobreescrito o para depurar a mano los guiones de inicialización (rc) y /etc/inittab, ejecutándolos de uno en uno.

1.7 Arranque SMP en x86

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 start_kernel(), y entonces sobre smp_init() y especialmente src/i386/kernel/smpboot.c:smp_boot_cpus(). La función smp_boot_cpus() entra en un buche para cada apicid (identificador de cada APIC), hasta NR_CPUS, y llama a do_boot_cpu() en él. Lo que hace do_boot_cpu() es crear (esto es: fork_by_hand) 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 trampoline.S. Entonces genera STARTUP IPI a la cpu de destino la cual hace que este AP (Procesador de Aplicación) ejecute el código en trampoline.S.

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 arch/i386/kernel/head.S.

Ahora, el AP empieza ejecutando head.S y descubriendo que no es un BP, se salta el código que limpia BSS y entonces entra en initialize_secondary(), el cual justamente entra en la tarea vacía para esta CPU - recalcar que init_tasks[cpu] ya había sido inicializada por el BP ejecutando do_boot_cpu(cpu).

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 init_tss[NR_CPUS] es una array.

1.8 Liberando datos y código de inicialización

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:

Estas evalúan al atributo especificador gcc (también conocido como "gcc magic") tal como ha sido definido en include/linux/init.h:


#ifndef MODULE
#define __init        __attribute__ ((__section__ (".text.init")))
#define __initdata    __attribute__ ((__section__ (".data.init")))
#else
#define __init
#define __initdata
#endif

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 .text.init, el cual es declarado en el mapa del enlazado en arch/i386/vmlinux.lds. 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 init/main.c:init()) llama a la función específica de la arquitectura free_initmem() la cual libera todas las páginas entre las direcciones __init_begin e __init_end.

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 module_init() son colocadas en .initcall.init 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 fs/pipe.c. Incluso si un subsistema nunca fuese convertido a módulo, ej. bdflush (ver fs/buffer.c), aún es bonito y arreglado usar la macro module_init() 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 __exit y __exitdata, pero ellas están más directamente conectadas al soporte de módulos por lo que serán explicadas en una sección posterior.

1.9 Procesando la linea de comandos del núcleo

Déjanos recalcar qué es lo que le pasa a la linea de comandos cuando se le pasa al núcleo durante el arranque:

  1. 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.
  2. arch/i386/kernel/head.S 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 arch/i386/kernel/setup.c:parse_mem_cmdline().
  3. arch/i386/kernel/setup.c:parse_mem_cmdline() (llamada por setup_arch(), y esta llamada por start_kernel()) copia 256 bytes de la página cero a saved_command_line la cual es mostrada por /proc/cmdline. Esta misma rutina procesa la opción "mem=" si está presente y realiza los ajustes apropiados en los parámetros de la VM.
  4. Volvemos a la línea de comandos en parse_options() (llamada por start_kernel()) el cual procesa algunos parámetros "dentro del núcleo" (actualmente "init=" y entorno/argumentos para init) y pasa cada palabra a checksetup().
  5. checksetup() va a través del código en la sección ELF .setup.init 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 __setup(), 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.

Por lo tanto, ¿cómo debemos de escribir el código que procesa la linea de comandos del arranque? Nosotros usamos la macro __setup() definida en include/linux/init.h:



/*
 * 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

Por lo tanto, típicamente la usarás en tu código de esta forma (tomado del código del controlador real, BusLogic HBA drivers/scsi/BusLogic.c):


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);

Destacar que __setup() 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.


Página siguiente Página anterior Índice general