El kernel del sistema operativo es el componente más interno después del hardware. Es la parte fundamental del sistema operativo y se encarga de manejar los recursos y permitir que los programas hagan uso de los mismos mediante peticiones de las distintas aplicaciones y procesos que se están ejecutando, siendo éstos recursos principalmente:
CPU.
Memoria.
Dispositivos de entrada/salida (I/O).
Es además el encargado de proporcionar protección mediante diferentes niveles de acceso o rings (asegurando que las aplicaciones únicamente accedan donde deben) y acceso compartido multiplexado, es decir, que las aplicaciones crean que tienen a su disposición todos los recursos, pero en realidad estén siendo compartidos y se compita por los mismos.
Lo más habitual hoy en día es que en los kernel existan al menos 2 niveles para acceder tanto a la CPU como a la memoria:
Kernel Mode: Sin restricciones, es decir, privilegiado. Su ejecución se lleva a cabo de una forma mucho más sencilla, pero con las correspondientes consideraciones en cuanto a seguridad.
User Mode: Restringido, es decir, no privilegiado. Su ejecución no es tan sencilla como el nivel anterior, pero es mucho más segura.
Dependiendo de que algo se ejecute en Kernel Mode o en User Mode, tendrá o no acceso restringido a la CPU, es decir, el acceso a los recursos de la máquina de un proceso que se ejecute en User Mode estará controlado a través del kernel, mientras que si ese mismo proceso se ejecutase en Kernel Mode, no existiría ningún tipo de restricción.
Generalmente, se suelen implementar 4 anillos a nivel de procesador, pero posteriormente los sistemas operativos únicamente suelen usar 2 de ellos: el más interno (Anillo 0) para procesos en Kernel Mode y el más externo (Anillo 4) para procesos en User Mode. En Internet se puede encontrar bastante información al respecto, pues su detallada explicación se saldría del objetivo de esta tarea.
Vamos a dejar de lado la teoría general de los kernel de los sistemas operativos para centrarnos en el kernel Linux. Se trata de un kernelmonolítico que incluye una parte muy importante de los componentes como módulos compilados con enlace dinámico, de manera que la parte que se carga en memoria estáticamente es una parte pequeña, pues también está pensado para pequeños dispositivos cuya cantidad de memoria sea mínima, por lo que debemos intentar que sea lo más pequeño posible, dejando para ello en la parte estática los componentes estrictamente esenciales.
Por definición, al tratarse de un kernel monolítico, implica que todos (o casi todos) sus componentes se ejecutan en Kernel Mode, teniendo por tanto la ventaja de que al acceder sin restricciones, es mucho más sencillo operar con el mismo, con su correspondiente mejora en cuanto a rendimiento, al no tener que realizar llamadas entre los elementos del kernel. Dicha característica implicaría vulnerabilidades en un principio, pero al haber sido desarrollados todos los componentes por el mismo proyecto, no existe dicho problema, pues generalmente, no introducimos nada que no haya sido desarrollado por el propio kernel Linux.
Tenemos la posibilidad de descargar el kernel sin compilar desde kernel.org, aunque lo que generalmente se suele hacer es descargar una distribución, que ya contiene un kernel totalmente funcional y trae consigo una paquetería base, de manera que se convierte en algo muy sencillo de usar por parte de los usuarios, al no requerir grandes conocimientos. Actualmente, el código fuente de rama vanilla del kernel ocupa 1.1 GiB.
Anteriormente he hecho uso de los términos estático y dinámico. Ésto se debe a que los componentes del kernel se pueden compilar (enlazar) de dos formas distintas:
Estáticamente: Todos los componentes forman un único fichero binario que se carga en memoria, denominado vmlinuz o zImage. Compilar todo el kernel de forma estática supondría compilar los 1.1 GiB (o una selección de dichos componentes), quedando un fichero binario de 300, 400, 500 MiB… que se cargaría en memoria. Esto no sería un problema para máquinas de hoy en día, pero para los dispositivos pequeños, sí, pues sus recursos son limitados.
Dinámicamente: Para solventar el problema que acabamos de mencionar existe la parte dinámica, que se encuentra compuesta por ficheros objeto con extensión .ko (kernel object), y dichos componentes (también conocidos como módulos) se cargan a demanda en memoria (se requiere tener el sistema de ficheros donde están contenidos previamente montado). Ésta es la parte más pesada.
Una vez explicado lo necesario para conocer de manera superficial qué es el kernel, más concretamente el kernel Linux, es hora de compilar nuestro propio kernel. Ésto no es algo que se haga frecuentemente hoy en día (aunque existen excepciones, como los sistemas embebidos o dispositivos empotrados), ya que las distribuciones Linux optan por compilar kernel suficientemente genéricos que soporten la mayor cantidad de situaciones posibles, pero nunca está de más ver la estructura de nuestro sistema de una forma más interna e intentar dejarlo lo más pequeño posible (desintegrando para ello los componentes no necesarios y rechazando el uso de otro tipo de funcionalidades no esenciales), para ver dónde se encuentran los límites, y así aprender a realizar ésta técnica.
El primer paso a llevar a cabo será crear un directorio en el que vamos a trabajar, para así mantener una organización en todo momento. En mi caso, he generado uno de nombre compkernel/, ejecutando para ello el comando:
Tras ello, accederemos al directorio que acabamos de crear, haciendo uso del comando:
Para llevar a cabo la compilación, necesitaremos una determinada paquetería, entre la que se encuentra el compilador de C, make, algunas bibliotecas básicas… ésto tiene fácil solución, y es que en lugar de ir instalándolos uno por uno, instalaremos build-essential, que contiene todo lo necesario. Además, posteriormente utilizaremos una aplicación gráfica que nos hará mas amena la tarea de selección de componentes del kernel, que hace uso de las bibliotecas de qt, por lo que tendremos que instalar también el paquete qtbase5-dev (hay otras aplicaciones que hacen uso de otras bibliotecas como las de gtk o ncurses, pero en este caso, no las usaremos). El comando a ejecutar sería:
Ya está todo listo para descargar el código fuente de nuestro kernel, pero primero tendremos que descubrir cuál estamos usando (en caso que no lo sepamos). Para ello, haremos uso de uname:
Donde:
-r: Le pedimos que nos muestre la release en lugar del nombre.
Como se puede apreciar, la versión de núcleo que actualmente estoy utilizando es la 5.7.0. Llegados a este punto, tenemos dos posibilidades para obtener el código fuente de nuestro kernel:
Descargarlo de repositorios: Descargaremos el paquete de nombre linux-image correspondiente (podemos buscarlo con apt search). Si la versión concreta que estamos utilizando se encuentra en los repositorios oficiales, podemos instalarlo con apt install, que realmente lo que hará será ubicar un comprimido .tar.xz dentro de /usr/src, que contendrá el código fuente, para su posterior descompresión en el directorio de trabajo que hemos creado.
Descargarlo de kernel.org: En mi caso, el kernel que actualmente estoy utilizando lo descargué de la rama backports, debido a determinados problemas con el hardware si hacía uso de la versión existente en stable. La cuestión es que dicha versión ya no está disponible, pues actualmente se encuentra disponible la 5.8, por lo que tendré que hacer uso de ésta opción para la correspondiente descarga del código fuente, tal y como veremos a continuación.
Como anteriormente hemos mencionado, en kernel.org tenemos las diferentes versiones de kernel que han sido lanzadas, por lo que nuestra versión concreta (5.7.0) la podremos descargar desde aquí, haciendo para ello uso de wget:
Para verificar que el comprimido se ha descargado correctamente, haremos uso del comando ls -l:
Efectivamente, se ha descargado un paquete de nombre “linux-source-5.7.tar.xz” con un peso total de 107.47 MB (112690468 bytes). Sin embargo, nada podemos hacer hasta que no llevamos a cabo la extracción de los ficheros contenidos. Para ello, ejecutaremos el comando:
Donde:
-J: Utiliza xz para descomprimir el fichero.
-x: Indica a tar que desempaquete el fichero.
-f: Indica a tar que el siguiente argumento es el nombre del fichero .tar.xz.
Para verificar que el fichero se ha descomprimido correctamente, haremos uso del comando:
Como se puede apreciar, el resultado de la descompresión se encuentra ubicado en un directorio de nombre linux-5.7/, al que accederemos ejecutando el comando:
Una vez dentro del mismo, volveremos a listar el contenido, haciendo uso de ls:
Efectivamente, todo el contenido se ha descomprimido, tal y como queríamos. Por curiosidad, vamos a comprobar el tamaño del directorio actual y de todos sus ficheros y directorios de forma recursiva, ejecutando para ello el comando:
Donde:
-s: Le indicamos que muestre el tamaño total, es decir, el tamaño recursivo.
-h: Le indicamos que muestre el tamaño en formato humano, en lugar de mostrarlo en bytes.
Como se puede apreciar, el tamaño total tras la descompresión es de 1.1 GiB, tal y como hemos mencionado con anterioridad.
Si nos fijamos en el contenido anteriormente mostrado, existe un fichero Makefile cuyo contenido es bastante complejo, pues contiene todas las instrucciones necesarias para llevar a cabo dicha compilación, pero la ventaja es que se nos proporciona un comando de ayuda para aprender los primeros pasos y las diferentes opciones y parámetros de los que disponemos a la hora de llevar a cabo la compilación. Dicho comando es make help.
Para las compilaciones se hace uso de un fichero de nombre .config que contiene la información sobre qué componentes se van a enlazar estáticamente, cuáles dinámicamente y cuáles no se van a enlazar. Dicho fichero no se encuentra todavía generado, así que para generarlo tomando como punto de partida nuestra configuración actual del núcleo (existente en /boot/, tal y como veremos más adelante), haremos uso del comando make oldconfig, que a pesar de que preguntará explícitamente si queremos incluir determinados componentes opcionales, en mi caso, dije que no a todos ellos, ya que no serán necesarios:
La salida del comando es muy grande, así que la he recortado para no ensuciar, pero aquí se puede ver al completo. Tras ello, listaremos el contenido del directorio actual, estableciendo un filtro por nombre para así verificar que dicho fichero ha sido correctamente generado:
Efectivamente, el fichero .config ha sido generado, así que vamos a comprobar cuántos componentes del núcleo han sido enlazados estáticamente (y) y cuántos dinámicamente (m) en la configuración actual, leyendo el contenido del fichero, filtrando por líneas y posteriormente, contando las coincidencias para cada uno de los casos:
Como se puede apreciar, en la configuración del núcleo que actualmente estamos utilizando, existen un total de 2097 componentes incluidos dentro del vmlinuz, pues son enlaces estáticos que estarán siempre cargados en memoria, y un total de 3417 componentes dinámicos o módulos del mismo, que se cargarán en memoria bajo demanda.
Sería una tarea bastante larga y tediosa empezar a reducir elementos estáticos y dinámicos partiendo de este punto, ya que existen más de 5000 componentes enlazados en total, de manera que haremos uso de make localmodconfig, que comprobará la lista de componentes que están siendo utilizados en este preciso momento, y en base a dicha información, modificará el fichero .config, descartando los demás, pues se entiende que no están activos porque son prescindibles para nosotros, consiguiendo así reducir considerablemente el número de componentes. Existen algunos componentes que preguntará explícitamente si queremos incluirlos, pero en mi caso, dije que no a todos ellos, ya que no serán necesarios:
La salida del comando es muy grande, así que la he recortado para no ensuciar, pero aquí se puede ver al completo. Una vez más, contaremos el número de componentes tanto estáticos como dinámicos, para así apreciar las diferencias:
Como se puede apreciar, el número de componentes estáticos ha sido reducido de 2097 a 1539, es decir, un total de 558 componentes estáticos han sido eliminados, mientras que en los componentes dinámicos es donde podemos percibir la mayor diferencia, pues el número total ha sido reducido de 3417 a 271, es decir, 3146 componentes dinámicos han sido eliminados.
Tras ello, ya podremos llevar a cabo nuestra primera compilación, que no tendrá ninguna diferencia, pues los componentes son exactamente los mismos que aquellos que están siendo utilizados en este preciso momento, pero así podremos verificar que todo funciona correctamente. Para ello, ejecutaremos el comando:
Donde:
-j: Indicamos el número de subprocesos a generar, que generalmente será igual al número de hilos de nuestro procesador, pues cada uno de dichos subprocesos será acogido por un hilo para su correspondiente procesamiento. Con nproc podemos ver la cantidad de hilos existentes.
bindeb-pkg: Indicamos que produzca el binario en un paquete .deb que posteriormente instalaremos en la máquina haciendo uso de dpkg.
Como se puede apreciar, la ejecución ha finalizado con un error de falta de dependencias, concretamente falta el paquete libelf-dev, así que procederemos a su instalación, ejecutando para ello el comando:
Una vez instalado el paquete, volveremos a tratar de realizar la compilación, ejecutando de nuevo el comando visto con anterioridad:
La salida del comando es muy grande, así que la he recortado para no ensuciar, pero aquí se puede ver al completo. En este caso, la ejecución ha finalizado sin errores, así que el correspondiente fichero .deb ha sido generado, por lo que para verificarlo, listaremos el contenido del directorio padre (pues es donde se genera por defecto):
Como se puede apreciar, existen varios ficheros .deb, pero el que nos interesa es linux-image-5.7.0_5.7.0-1_amd64.deb, cuyo peso actualmente es de 13M. Tras ello, tendremos que instalar el paquete para así poder arrancar con el nuevo núcleo, haciendo uso de dpkg, que desempaquetará, instalará y generará una nueva configuración de GRUB:
Donde:
-i: Indicamos que queremos instalar el paquete introducido.
Una vez realizada la instalación, verificaremos que dicho paquete se encuentra actualmente entre nuestra paquetería instalada, ejecutando para ello dpkg -l, para así listar todos los paquetes existentes, especificando posteriormente un filtro por nombre:
Efectivamente, tal y como se puede apreciar en la salida, tenemos un paquete instalado de nombre linux-image-5.7.0, que es el que acabamos de compilar e instalar.
El siguiente paso consistiría en reiniciar la máquina y cuando nos cargue el gestor de arranque GRUB, acceder a “Opciones avanzadas para Debian GNU/Linux” y una vez ahí, indicar que queremos iniciar con el nuevo kernel. En este caso, arrancará sin problemas ya que no hemos modificado manualmente la configuración, así que tras verificarlo, volveremos a reiniciar, arrancando ésta vez con el kernel original.
Lo primero que tendremos que hacer antes de llevar a cabo una nueva compilación será eliminar los ficheros residuales de la anterior, ejecutando para ello el comando make clean. Tras ello, vamos a hacer una nueva compilación, pero ésta vez, modificando los componentes a mano, para así conseguir reducirlo al máximo. Como anteriormente hemos mencionado, vamos a hacer uso de una aplicación gráfica, así que en esta ocasión, en lugar de hacer uso de make oldconfig o make localmodconfig, vamos a utilizar make xconfig:
Como se puede apreciar, la ejecución ha finalizado con un error de falta de dependencias, concretamente falta el paquete pkg-config, así que procederemos a su instalación, ejecutando para ello el comando:
Una vez instalado el paquete, volveremos a tratar de realizar la apertura de la aplicación gráfica, ejecutando de nuevo el comando visto con anterioridad:
Como se puede apreciar, en esta ocasión no hemos tenido problemas para llevar a cabo la correspondiente apertura de la aplicación gráfica. Su uso es bastante sencillo e intuitivo, por lo que no considero necesario llevar a cabo una explicación al respecto, más allá de mencionar que aquellos componentes que estén marcados con un · son módulos, los marcados con ✓ son estáticos y los que no están marcados, no serán añadidos a la compilación. Cuando hayamos finalizado, pulsaremos en el símbolo del disquete en la parte superior para guardar los cambios.
Una vez más, contaremos el número de componentes tanto estáticos como dinámicos, para así apreciar las diferencias:
Como se puede apreciar, tras desmarcar algunos componentes, el número total se ha visto reducido, aunque esto es una tarea lenta. Tras ello, volveremos a compilar, a instalar y a reiniciar, para verificar que arranca. Llegados a este punto, pueden ocurrir dos cosas:
Que el kernel inicie correctamente, por lo que cuando volvamos a nuestro kernel original, haremos una copia de seguridad del fichero .config, por si acaso necesitásemos hacer uso del mismo posteriormente.
Que el kernel no inicie, de manera que volveremos a nuestro kernel original y restauraremos la copia de seguridad del fichero .config para volver atrás, ya que hemos cometido el error de quitar un componente que era necesario para el correcto funcionamiento.
Como curiosidad, cada vez que compilemos, se generará un fichero .deb con un número secuencial que irá incrementándose, para así no sobreescribir al fichero de la anterior compilación. Sin embargo, a la hora de instalar dicho paquete, sobreescribirá al kernel compilado que hemos instalado con anterioridad (recalco lo de compilado, ya que el nativo no lo toca). Ésto último es modificable (concretamente en el fichero Makefile), pero en mi caso, no lo he considerado necesario, ya que no me apetecía tener un kernel instalado por cada compilación, de manera que he preferido sobreescribir el anterior.
Tras repetir reiteradas veces el procedimiento anteriormente mencionado, he dejado el fichero .config con la siguiente cantidad de componentes enlazados estáticamente y dinámicamente:
Como se puede apreciar, la diferencia es bastante grande, pues tras hacer el make localmodconfig, teníamos 1539 componentes estáticos, que han pasado a ser 513, es decir, una diferencia de 1026 componentes estáticos, y de otro lado, teníamos 271 módulos inicialmente, que han pasado a ser únicamente 24, suponiendo una diferencia de 247 módulos.
Finalmente, vamos a comprobar el tamaño del fichero .deb que se ha generado como consecuencia de ésta última compilación, ejecutando para ello el comando:
En este caso, el peso final es de 3.1M, con respecto a los 13M que pesaba el fichero en la primera compilación, lo que supone una disminución de tamaño de casi 10M, es decir, de alrededor de un 76% del peso inicial, una marca bastante considerable, pero realmente, lo que quiero verificar es el peso del fichero vmlinuz instalado en mi máquina, fichero que contiene todos los componentes enlazados estáticamente, así que listaremos el contenido del directorio /boot/, pues es donde se encuentra ubicado:
En esta ocasión, el peso del fichero vmlinuz correspondiente al kernel que hemos instalado es de 1.9M, a diferencia de los 5.4M que pesa el fichero vmlinuz del kernel original que estoy usando, una diferencia que aunque parezca insignificante, para dispositivos pequeños puede marcar una gran diferencia. Como curiosidad, en el directorio /boot/ también se almacena una copia del fichero .config utilizado durante la compilación de cada uno de los kernel existentes, ficheros de nombre config-*.
Me gustaría dejar por último, en forma de resumen, todos los componentes tanto estáticos como dinámicos que he deshabilitado en la aplicación gráfica para obtener los resultados anteriormente mostrados: