Este documento pretende dar un breve repaso a las características del kernel de Linux. El kernel o núcleo en español, es en sí el Sistema Operativo de Linux, las funciones más básicas para trabajar con el ordenador. En Internet se encuentra bastante documentación relacionada con este tema, pero la mayoria está en inglés y ese es el motivo que tengo para crear este documento. Cualquier comentario/sugerencia/ideas serán bien recibidas:
El Kernel puede verse como el corazón del sistema operativo, este reside en la memoria RAM cuando se enciende el ordenador y permanece en funcionamiento hasta que este se apaga. Tiene principalmente dos responsabilidades:
1. Servir a los requerimientos de programación a bajo nivel, por ejemplo tratando las interrupciones hardware (teclados, discos duros, tarjetas de video, etc...).
2. Proveer un entorno a los procesos, que son las instancias en ejecución de los programas o threads.
Se dice que el kernel de Linux es monolítico, que es como un gran ejecutable, que consiste de muchos componentes divididos lógicamente. Pero el kernel es capaz de cargar dinámicamente porciones adicionales de código (módulos) mientras se está ejecutando, para mejorar su funcionalidad. Por ejemplo, hay módulos que pueden añadir soporte para sistemas de ficheros que no es necesario que estén cargados siempre en el kernel. Cuando la funcionalidad proveida por el módulo ya no se necesita más, el módulo puede ser descargado, liberando memoria. Si se compila el kernel con la posibilidad de cargar estos módulos, llamados Linux Kernel Modules (LKM), se dice que está compilado dinámicamente, sino es así se dice que el kernel se ha compilado de manera estática.
El Kernel puede trabajar en dos modos: usuario o kernel. La mayoría de las ejecuciones de programas de los usuarios se hacen en modo usuario o ’user mode’ como se dice en inglés. Este modo de ejecución no tiene acceso directo a las estructuras de datos del kernel o a los equipos hardware. Puede cambiarse a modo kernel de varias formas:
1. Un programa hace una llamada al sistema o ’system call’, por ejemplo cuando una función de una libreria hace una petición al kernel.
2. Una señal de excepción provinente de la CPU, que son condiciones que requieren especial atención, por ejemplo en una división por cero.
3. Una interrupción que se hace hacia la CPU provinente de algún equipo hardware indicando que requiere su atención, como por ejemplo cada vez que apretamos cualquier tecla desde el teclado.
El kernel se pasa la mayoría del tiempo en el modo kernel ejecutándose detrás
de los procesos de usuario. Además hay muchos threads que se están ejecutando
detrás del propio kernel en modo kernel, que se encargan de hacer las actividades
necesarias para mantener en funcionamiento al sistema operativo. Una vez que
todas las operaciones pendientes en modo kernel se han completado y tratado, el
kernel vuelve al modo usuario otra vez.
Un proceso es una instancia de un programa en ejecución. Cada proceso tiene:
1. Un estado, ya sea ’running’ (ejecutándose en el procesador), ’sleeping’ (durmiendo, un proceso que está esperado que un evento se termine), ’runnable’ o ’ready’ (listo para ser ejecutado y esperado en la cola de procesos), ’stopped’ (parado ya sea por una señal de control o porque están haciendole un trace) o zombie (zombie el proceso esta apunto de ser eliminado).
2. Un contexto, una copia con todos los registros de la CPU que indican el estado del proceso (PC, SP, PSW, registros de propósito general, registros de coma flotante y regsitros para el control de memoria)
3. Un descriptor de procesos, es una estructura de datos del tipo task_struct que guarda toda la información relacionada con un proceso.
El kernel se encarga de poner un entorno multiprogramable, lo que indica que se puede tener muchos procesos activos a la misma vez. Cada proceso dispone de los recursos hardware disponibles para él, y es el kernel el encargado de controlar que los recursos se comparten correctamente. Multiprogramming implica que todos los procesos que están en la cola de procesos esperando tendrán su oporturnidad para ejecutarse en la CPU en turnos. El proceso que ’controla’ la CPU en un instante de tiempo se le llama ’current’ porque es el programa que esta corriendo en ese momento. El proceso que se encarga de decidir quien es el que pasa a estado ready o un estado de ejecutandose es el scheduler o ’context switch’, porque se encarga de cambiar el contexto actual. El cometido de este es guardar el contexto actual del proceso ejecutándose (una foto del estado de la CPU en ese momento) y cargar el contexto de algún proceso que este esperando para ser ejecutado, o un proceso con el estado de ready. Los cambios de contexto solo pueden hacerse cuando el kernel está en modo usuario, así que en el modo kernel no pueden hacerse cambios de contexto inmediatamente por eso se le llama non-preemptive.
Cada proceso de usuario se ejecuta en su propio espacio de usuario, una porción
asignada del total de memoria disponible. Espacios de usuario (o partes de él)
pueden compartirse entre procesos si se pide, o bien automáticamente si el kernel
lo cree apropiado. La separación del espacio de direcciones hace que un proceso
no pueda interferir con una operación de otro proceso o del sistema operativo.
Además de los procesos normales de usuario ejecutándose en el sistema, hay
varios threads del kernel que se crean al iniciar el sistema y que corren permanentemente
en modo kernel, encargandose de funciones de mantenimiento del kernel.
El kernel es reentrante, varios procesos pueden estar ejecutándose en modo kernel a la vez. Por supuesto, en un sistema con solo un procesador solo un proceso puede ejecutarse, porque el resto de procesos están bloqueados esperando en una cola. Ejemplo: un proceso pide leer un fichero, el sistema virtual de ficheros el ’Virtual File System’ traduce la petición en una operación de bajo nivel del disco y lo pasa al controlador del disco, por detrás del proceso. En vez de esperar hasta que la operación de escritura a disco se haya completado, miles de ciclos de CPU más tarde, el proceso da voluntariamente a la CPU después de hacer la petición y el kernel permite que otro proceso esperando pueda ejecutarse en la CPU y este a su vez puede entrar en el modo kernel. Cuando una operación a disco se a completado (señalado por una interrupción de hardware), el proceso actual da la CPU al que trata las interrupciones (el interrupt handler) y el proceso original se despierta, siguiendo su estado a donde lo había dejado.
Para poder implementar un kernel reentrante, hay que tener especial cuidado para asegurar la consistencia de las estructuras de datos del kernel. Si un proceso modifica un contador de otro proceso esperando sin que este lo sepa, el resultado puede ser potencialmente desastroso. Hay que seguir los siguientes pasos para preever este tipo de sucesos:
1. Un proceso solo puede reemplazar a otro en modo kernel si ha dejado voluntariamente la CPU, dejando las estructuras de datos en un estado consistente, de ahí que se le llame al kernel ’non-preemptive’.
2. Hay que deshabilitar las interrupciones en regiones críticas, donde el código tiene que completarse sin ninguna interrupción, y asegurarse luego de volver a habilitarlas.
3. El uso de spin locks y semáforos de control para acceder a estructuras de datos.
Los semáforos consisten en un contador inicializado a uno, una lista de procesos esperando para acceder a la estructura de datos y dos métodos atómicos llamados up() y down() que incrementan y decrementean el contador respectivamente. Cuando se accede a una estructura protegida por un semáforo, se llama a la función down(), si el valor del contador es cero o positivo (no negativo vaya), entonces el acceso está garantizado, si no es así y el acceso esta negado significa que está bloqueado y que el proceso se añade a la lista del semáforo de procesos esperando. De forma parecida, cuando un proceso ha terminado de tratar los datos de la estructura, llama a la función up() y el siguiente proceso de la lista consigue acceder.
Hay que tomar precauciones, porque hay que asegurarse que no hay deadlocks
entre varios procesos, en el caso de que controlen varios recursos. Si cada uno está
esperado un recurso controlado por otro proceso y este a su vez esta esperando un
recurso controlado por el primer proceso se dice que existe un deadlock o ’abrazo
de la muerte’ porque se esperan infinitamente hasta que uno de los dos deje el
otro recurso, pero al estar los dos en un estado de espera jamás lo harán. Si se
quiere profundizar en deadlocks buscar por Internet el problema de los filósofos
que cenan o dicho en inglés ’The dining Philosophers Problem’ o en cualquier
libro de sistemas operativos.
Una señal es un mensaje corto, enviado entre dos procesos o entre el kernel y un proceso. Hay dos dipos de señales que se usan para notificar eventos a del sistema a los procesos:
Eventos asíncronos. Por ejemplo SIGTERM, enviado cuando se usa el Ctrl-C del teclado.
Errores o excepciones síncronas. Por ejemplo SIGSEGV cuando un proceso intenta acceder a una dirección ilegal.
Hay cerca de 20 señales diferentes definidas en el estandart POSIX, encapsuladas en estos dos tipos.
Algunas de ellas pueden ser ignoradas, otras no pueden ser ignoradas. Linux usa el sistema V o ’System V’
de comunicación entre procesos llamado en inglés Inter-Process-Comunication (IPC).
Linux usa memoria virtual, un nivel de abstracción entre los pedidos de memoria por parte de los procesos y las direcciones físicas de la memoria. Así hace posible lo siguiente:
1. Permite que muchos procesos corran incluso cuando la suma de toda la memoria exceda la RAM física disponible.
2. Hace posible también un espacio de direcciones contigua, independiente a la organizacioón de la memoria física.
3. Paginado, porciones de datos o código que solo necesitan cargarse en memoria cuando se ejecutan o son accedidos y pueden intercambiarse a disco cuando no son necesarios.
4. Imágenes compartidas de programas y librerias, haciendo un uso más efi- ciente de la memoria.
5. Recolocación de los programas en memoria de forma completamente transparente.
El espacio de direcciones se divide en porciones de 4kBytes llamadas páginas, las cuales forman la unidad básica de todas las transacciones de la memoria virtual. Como la suma total de direcciones de memoria excede a lo que hay en la memoria RAM, solo un grupo de todas las páginas disponibles se guardan en la RAM a la vez. Aún así, un página tiene que estar presente en la RAM para poder acceder a ella ya sea para leer o guardar datos como para ejecutar programas. Como cualquier página puede volver a ponerse en cualquier página física, el kernel tiene que llevar el control de donde estan las páginas usadas. Y además hacer la conversión de direcciones lógicas en direcciones físicas.
En el hardware Intel x86, Linux usa dos nivels de paginación (aunque internamete
usa tres niveles para mejorar la portabilidad) para reducir la cantidad de
memoria usado pora las tablas de paginación. Para convertir una dirección lógica
en una física, las tablas se consultan en este orden: Page Global Directory luego
la Page Table para conseguir el número de página y el offset de la página. Por eso
una dirección lógica se divide en tres partes: Directorio, Tabla y Offset. Como se
puede direccionar un espacio de 4GBytes (usando 32 bits) y se usa una página del
tamaño de 4kB, los 10 bits más significativos de la dirección apuntan al directorio,
los 10 siguientes bits apuntan a la tabla (de ahí que se requiera identificar la página)
y los 12 siguientes bits, los menos significativos, nos marcan el offset dentro
de la página.
El código fuente del kernel está hecho de alrededor de dos millones de líneas de código. A priori intimida, pero no debería ser así ya que muy poca gente entiende todos los subsitemas y la gran mayoría del código está asociado a ellos. A la hora de trabajar es necesario simplemente saber donde buscar el código específico. Por ello es necesario entender qué se puede encontrar en el código fuente y dónde va cada cosa.
Para empezar es necesario bajarse la última versión del kernel de http://www.kernel.org la versión actual es la 2.14.19. Pero es recomendable visitar la página web, ya que seguro que hay nuevas actualizaciones.
$ wget http://www.kernel.org/pub/linux/kernel/v2.4/linux-2.4.19.tar.gzTras ello se descomprime y se accede al directorio:
$tar xvzf linux-2.4.19.tar.gz $cd linux-2.4.19 $dir direcciones total 248 drwxr-xr-x 18 carlosm grp 4096 Aug 3 02:39 arch/ -rw-r--r-- 1 carlosm grp 18691 Aug 3 02:39 COPYING -rw-r--r-- 1 carlosm grp 79410 Aug 3 02:39 CREDITS drwxr-xr-x 28 carlosm grp 4096 Feb 25 19:41 Documentation/ drwxr-xr-x 39 carlosm grp 4096 Feb 25 2002 drivers/ drwxr-xr-x 45 carlosm grp 4096 Feb 25 2002 fs/ drwxr-xr-x 25 carlosm grp 4096 Aug 3 02:39 include/ drwxr-xr-x 2 carlosm grp 4096 Feb 25 2002 init/ drwxr-xr-x 2 carlosm grp 4096 Dec 21 2001 ipc/ drwxr-xr-x 2 carlosm grp 4096 Feb 25 2002 kernel/ drwxr-xr-x 2 carlosm grp 4096 Nov 22 2001 lib/ -rw-r--r-- 1 carlosm grp 41643 Aug 3 02:39 MAINTAINERS -rw-r--r-- 1 carlosm grp 18710 Aug 3 02:39 Makefile drwxr-xr-x 2 carlosm grp 4096 Feb 25 2002 mm/ drwxr-xr-x 28 carlosm grp 4096 Feb 25 2002 net/ -rw-r--r-- 1 carlosm grp 14239 Aug 3 02:39 README -rw-r--r-- 1 carlosm grp 2815 Apr 6 2001 REPORTING-BUGS -rw-r--r-- 1 carlosm grp 9291 Aug 3 02:39 Rules.make drwxr-xr-x 4 carlosm grp 4096 Aug 3 02:39 scripts/Todo este enjambre de ficheros está organizado lógicamente en una estructura de directorios.
Documentation: Información sobre plataformas y devices específicos igual que información general sobre el kernel.
arch: Código específico para cada arquitectura: i386, sparc, alpha, arm, cris, ia64, mips, mips64, parsic, ppc (PowerPC 32bits), ppc64(PowerPC 64bits), s390, s390x, sparc, sparc64.
drivers: Código para cada device específico: tarjeta de sonido, tarjeta ethernet, etc
fs: Código para los sistemas de ficheros o ’Filesystems’: ext2, ext3, vfat, etc
include: Todos los ficheros de cabecera separado en subdirectorios de acuerdo con el tipo fichero y su función.
init: Todo el código asociado con el proceso de arranque e inicialización.
IPC: Código de la comunicación entre procesos o Inter Process Communication: implementación de la memoria compartida, etc
kernel: El código del núcleo del kernel, la parte más importante de Linux son solo 396KBytes y 31 ficheros: scheduling, señales, printk, fork, softirq, contextos, tiempo, cargador de módulos, etc
lib: Librerías relacionadas con el kernel, por ejemplo descompresión de la imagen del kernel, funciones de lock, etc mm: Memory Managment todos los ficheros con el código de trato de memoria.
net: Todo el código relacionado con la red. Donde se encuentra IPv4 e IPv6, trato de paquetes, etc..
scripts: Scripts relacionados con el kernel, como por ejemplo el patch-kernel.
El mejor lugar para empezar leyendo el código depende de las motivaciones que tenga cada uno, yo por mi parte fui directamente al directorio net, al directorio kernel y al directorio include, el resto de directorios casi no los he visitado, pero claro mi intención es construir un firewall, cada uno tendrá sus motivos. Si por ejemplo, se desea escribir un driver para un hardware que todavía no está soportado, en ese caso se empieza leyendo el código para los drivers más parecidos que ya están implementados. Dicen los hackers del kernel que la mejor forma de introducirse a la programación del kernel es escribiendo drivers, ahí queda eso para los interesados.
En cambio si lo que se desea es escribir un nuevo sistema de ficheros la idea es
buscar dentro del directorio fs algo que se parezca a lo que queremos implementar.
Si en cambio se desea jugar con la programación dentro del kernel haced lo
que yo hice que fue probando en la inicialización del sistema, en init/main.c, específicamente
en la función start_kernel() o bien ojeando los ficheros que hay en
el directorio kernel.
Si se desea hacer cambios e introducir código en el kernel para contribuir en el desarrollo, es importante entender el ciclo de desarrollo adoptado por la comunidad del kernel de Linux. Paso a comentar el proceso de desarrollo:
Generalmente, es muy mala idea ejecutar un kernel inestable en un sistema que está en producción. Lo recomendado es tener un ordenador dedicado para hacer el testing con kernels inestables. Parches intermedios
Entre dos releases de kernels estable e inestable, hay varias releases intermedias que intentan testear un pequeño número de cambios a la vez. Estas releases tienen o bien la extensión -preXX o bien la extensión -YYXX, donde XX es el número de la versión incrementada y el YY son las iniciales de quien lo mantiene, por ejemplo -ac12 indica incrementalmente la extensión de Alan Cox. Al parche intermedio actual está numerado como 2.4.20-pre7.
Comentarios: