Programando módulos para el Kernel de Linux. Entendiendo el arranque y la gestión de sistemas básicos.

6 comentarios


Kernel Boot


Ya hemos hecho una introducción al desarrollo de módulos del Kernel para Linux y hemos explicado como configurar un entorno de desarrollo con Eclipse con el que trabajar. Hoy en Programando módulos para el Kernel de Linux vamos a hablar sobre el proceso de arranque del Kernel y los sistemas implicados.

Como todo sistema basado en arquitectura x86, Linux necesita que la BIOS cargue el MBR (Master Boot Record) desde el primer dispositivo de almacenamiento del sistema. El código residente en el MBR procesa la tabla de particiones del dispositivo y carga un gestor de arranque de Linux, generalmente GRUB o SYSLINUX desde la partición activa.

Finalmente, el gestor de arranque carga la imagen comprimida del Kernel en memoria y le pasa el control de la ejecución. El Kernel se descomprime a sí mismo y comienza el proceso de arranque. Esto por supuesto, partiendo del supuesto de que estamos arrancando un sistema simple sin RAIDS ni volúmenes lógicos ni nada por el estilo.

El proceso de arranque

Los procesadores basados en arquitectura x86 disponen de dos modos de operación, el modo real y el modo protegido. En modo real podemos acceder solamente al primer megabyte de memoria sin ningún tipo de protección.

El modo protegido es un modo más sofisticado y nos permite hacer uso de muchas características avanzadas del procesador como la paginación. El procesador tiene que pasar de forma obligada por el modo real en su camino hacia el modo protegido. No se puede pasar de modo protegido otra vez a modo real.

Las primeras inicializaciones del kernel se producen en el modo real. Posteriormente el resto del arranque tiene lugar en modo protegido por la función start_kernel() del archivo init/main.c. Esta función comienza la inicialización del subsistema de la CPU. La gestión de memoria y de procesos tienen lugar un poco después. Los buses periféricos y los dispositivos de E/S son iniciados a continuación.

En último lugar en la secuencia de arranque se inicia el programa init que es el padre de todos los procesos en Linux. Init ejecuta los scripts de espacio de usuario necesarios para levantar diferentes servicios del Kernel y arranca los terminales y muestra el prompt de login en ellos.

Secuencia de ArranqueSecuencia de arranque de Linux en hardware basado en arquitectura x86

Memoria

El Kernel ensambla el mapa de memoria del sistema desde la BIOS utilizando la interrupción int 0x15 con la función número 0xe820 para obtener el mapa de la memoria. El mapa indica los rangos de memoria disponible para su uso y reservada que es usada por el Kernel para crear el memory pool.

La región que puede ser direccionada de forma normal por el Kernel se llama low memory y es de 896MB. El asignador de memoria del kernel, kmalloc(), devuelve memoria desde esa región. La memoria por encima de los 896MB es llamada high memory solo puede ser accedida usando mapeos especiales.

El Kernel organiza la memoria física en páginas. El tamaño de la página depende de la arquitectura, en las máquinas basadas en x86 es de 4096 bytes (4k). Cada página física tiene un struct page (que está definido en include/linux/mm_type.h) asociado a ella.

En sistemas x86 de 32 bits la configuración por defecto del Kernel separa los 4GB disponibles de memoria (232 bytes de direccionamiento sin PAE (Physical Address Extension)) en 3GB de espacio de memoria virtual conocido como espacio de usuario y 1GB de espacio para el Kernel conocido como espacio del Kernel. En sistemas x86 de 64 bits la máxima memoria direccionable por Linux es de 247 bytes o lo que es lo mismo 128 Terabytes (los procesadores AMD64 pueden direccionar hasta 248, 256TB)

En realidad, el límite es 896MB en el Kernel Space porque 128MB de ese espacio de direcciones está ocupado por estructuras de datos del propio Kernel. Esto nos deja las siguientes zonas de memoria del Kernel:

  • ZONE_DMA (<16MB), esta es la zona usada para acceso directo a la memoria (DMA). Esto es necesario para dispositivos ISA que solo pueden acceder a los primeros 16MB de la memoria.
  • ZONE_NORMAL (16MB a 896MB), la región normal de la memoria también conocida como memoria baja
  • ZONE_HIGH (>896MB), espacio al que el Kernel solo puede acceder después de mapear páginas a regiones en la ZONE_NORMAL usando kmap() y kunmap(). Las direcciones correspondientes son virtuales y no lógicas.

La función kmalloc() asigna segmentos contiguos de memoria desde la ZONE_NORMAL. La memoria devuelta por esta función retiene el contenido de llamadas previas, por lo que exponerla al espacio de usuario puede incurrir en un problema de seguridad, para devolver la memoria a cero es necesario usar el método kzalloc()

Jiffies y BogoMIPS, calibración de delays

Durante el arranque, el Kernel calcula el número de veces que el procesador puede ejecutar un bucle de delay interno en un jiffy, que es el intervalo de tiempo entre dos ticks consecutivos del reloj del sistema. La calibración del tiempo de demora se calcula en base a la velocidad de procesamiento de la CPU. El resultado de la calibración se almacena en una variable llamada loops_per_jiffy.

Algunos drivers utilizan loops_per_jiffy para demorar ejecuciones en el orden de los microsegundos. Como se calculan los loops_per_jiffy excede de largo el cometido de este post, si quieres saber más al respecto, puedes leer el código del archivo init/calibrate.c. Los BogoMIPS (millones de instrucciones por segundo) que la CPU puede procesar se calculan con la siguiente fórmula:

loops_per_jiffy * número de jiffies en un segundo (Herzios) * número de instrucciones consumidas por el bucle de delay interno / un millon
En mi caso:
2785450 * 1000 * 2 = 5570900000
5570900000 / 1000000 = 5570,9 BogoMIPS
Que se corresponde con;
$ cat /proc/cpuinfo | grep bogomips | head -n 1
bogomips        : 5570.90

Modo Kernel y Modo Usuario

Algunos sistemas operativos como MS-DOS siempre se ejecutaban en un único modo de la CPU, pero los sistemas operativos basados en UNIX usan modos duales para implementar mecanismos de tiempo compartido de forma efectiva.

En un sistema Linux, la CPU puede estar en un modo Kernel o seguro, o en modo usuario o restringido. Todos los procesos de usuario se ejecutan en modo usuario, mientras que el Kernel en sí se ejecuta en modo Kernel. Con la introducción en el Kernel 2.6 del sistemas de preferencia en el Kernel (kernel preemption) mucho código del modo Kernel pude cambiar de contexto al igual que el código ejecutado en el modo usuario.

A estos dos modos también se les conoce como Ring-0 para el Modo Kernel y Ring-3 para el modo usuario. Si te estás preguntando para que son el Ring-1 y el Ring-2, tan solo decir que son supuéstamente para el código ejecutado por drivers de dispositivos, pero en la implementación, éstos se ejecutan en el Ring-0.

Las CPUs modernas que soportan instrucciones de virtualización por hardware, lo que hacen es precisamente ofrecer instrucciones a un hypervisor para que controle el acceso al hardware del Ring-0. El hypervisor (o host) crea un nuevo Ring-1 donde las máquinas virtuales alojadas pueden ejecutar código del Ring-0 de forma nativa sin afectar a otras máquinas virtuales o al propio hypervisor.

Conclusión

Hoy nos hemos adentrado en las entrañas del funcionamiento del Kernel de Linux en las etapas de arranque del sistema, nos hemos introducido en la gestión de memoria y hemos aprendido como se calibran los delays, por último, hemos indagado en los modos de ejecución de código del Kernel.

Todos estos sistemas son básicos a la hora de comprender como funciona el Kernel y necesitamos conocerlos si pretendemos desarrollar drivers de dispositivos para el mismo. En la próxima entrega hablaremos sobre contextos e interrupciones de procesos, temporizadores en el Kernel, indagaremos un poco más en los Jiffies y los Delays y aprenderemos algo sobre el RTC (Real Time Clock).

Happy Kernel hacking.



Más en Genbeta Dev | Programación a pecho descubierto (Linux Kernel)

Anunciate aquí
Anunciate aquí
Anunciate aquí

¿Quieres saber más?

Artículos

Artículos relacionados que probablemente también te interesen

Ver más

Respuestas

Preguntas sobre este tema que ha contestado la comunidad

+ Deja tu comentario

Comentarios

  • 1

    Avatar de Jose Juan !

    Muy interesante y sencillo de leer (a costa tuya, je, je, ...).

    Ansío el próximo capítulo.

    ¡Gracias!

  • 2

    Avatar de dr_bacterio !

    Buenos días a todos,

    desde que me he fijado en esta série de articulos que me asalta una duda y no estoy seguro si está relacionado o no con este tema. Tengo un teclado usb sin soporte para GNU/Linux,  si quisiera que funcionara en Linux ¿debería programar un módulo ( o driver) para él o para los dispositivos de entrada se utilizan otros mecanismos? Gracias y perdón si es un offtopic. !SALUDOS¡

    -- editado por última vez a las 10:06

  • Respondiendo a #2:
  • 3

    Avatar de Oscar Campos !

    Los teclados normalmente dependen del subsistema de entrada (input kernel subsystem) que unifica en un mismo punto de entrada controladores que manejan diversos tipos de dispositivos de entrada como ratones, teclados, tabletas, joysticks, pantallas táctiles, etc, etc.

    Si el teclado es USB, el subsistema de entrada utilizará un controlador de bajo nivel genérico para él, en este caso el controlador USB Hid, la estructura genérica para eventos de dispositivos de entrada es la llamada input_event y está definida en include/linux/input.h

    Tu dispositivo debería ser reclamado por el controlador usbhid ya que debe identificarse a sí mismo como un dispositivo de clase 0x03 por lo tanto debería de funcionar perfectamente.

    Puedes sacar la información de los dispositivos conectados al bus usb haciendo un cat /proc/bus/usb/devices en mi caso esta es la salida para mi Logitech G15:

    T: Bus=04 Lev=02 Prnt=02 Port=00 Cnt=01 Dev#= 3 Spd=1.5 MxCh= 0 D: Ver= 2.00 Cls=00(>ifc ) Sub=00 Prot=00 MxPS= 8 #Cfgs= 1 P: Vendor=046d ProdID=c226 Rev= 1.00 S: Product=G15 Gaming Keyboard C:* #Ifs= 2 Cfg#= 1 Atr=a0 MxPwr=100mA I:* If#= 0 Alt= 0 #EPs= 1 Cls=03(HID ) Sub=01 Prot=01 Driver=usbhid E: Ad=81(I) Atr=03(Int.) MxPS= 8 Ivl=24ms I:* If#= 1 Alt= 0 #EPs= 1 Cls=03(HID ) Sub=00 Prot=00 Driver=usbhid E: Ad=82(I) Atr=03(Int.) MxPS= 8 Ivl=24ms

    Ten en cuenta que antes de que usbhid pueda tomar el control de cualquier dispositivo pinchado o conectado a un puerto USB, antes debe de pasar por algunos de los controladores HCI (Host Controller Interface), bien sea por UHCI, OHCI o EHCI si el dispositivo está pinchado en un puerto USB 2.0

    Para más información revisa la documentación en Documentation/usb/gadget_hid.txt y Documentation/hid/hiddev.txt.

    En cualquier caso y como he dicho anteriormente, tu teclado debería de funcionar puesto que el controlador de eventos para todos los teclados es el mismo y es universal, si tu teclado se identifica como dispositivo de clase 0x03 no debieras de tener problemas, ahora, si te refieres a que no funcionan teclas especiales, eso ya es cuestión de que mapees el teclado en tu escritorio.

    Si lo que quieres es programar teclas especiales como en el G15, tienes que utilizar el controlador uinput que es el que permite que el dispositivo se comunique con el Kernel a través del espacio de usuario.

    Espero haberte solucionado algo la duda.

  • Respondiendo a #3:
  • 4

    Avatar de dr_bacterio !

    Muchas gracias por tu pronta y detallada respuesta, me has dado una buena base para averiguar porque no funciona mi teclado gaming en GNU/Linux. !SALUDOS¡

  • 5

    !
    | 1 estrellas

    Muy interesante, pero demasiado avanzado para mi u.u aunque entendi muchas cosa gracias a la excelente redacción del articulo :D

  • 6

    !

    El unico camino hacia la libertad, es el software libre!

Escribir un comentario

Para hacer un comentario es necesario que te identifiques: ENTRA o conéctate con Facebook Connect

Anunciate aquí

WSL Weblogs SL