Está en la página 1de 31

Suscríbete a DeepL Pro para poder traducir archivos de mayor tamaño.

Más información disponible en www.DeepL.com/pro.

El viaje del
concepto Linux
versión 3.0
septiembre-
2023

Por el Dr. Shlomi Boutnaru

Creado con Craiyon, AI Image Generator


Introducción 3
El vector auxiliar (AUXV) 4
comando no encontrado 5
Out-of-Memory Killer (OOM killer) 6
¿Por qué no funciona "ltrace" en las nuevas versiones de Ubuntu? 7
vDSO (Objeto Virtual Dinámico Compartido) 8
Llamada a syscalls desde Python 10
Regla de denominación de las syscalls: ¿Qué pasa si el nombre de una syscall empieza
por "f"? 11
Regla de denominación de las syscalls: ¿Qué pasa si el nombre de una syscall empieza
por "l"? 12
RCU (Actualización de copia de lectura) 13
cgroups (Grupos de control) 15
Gestores de paquetes 16
¿Qué es un formato ELF (Executable and Linkable Format)? 17
El encabezado ELF (Executable and Linkable Format) 18
Jerarquía del Sistema de Archivos en Linux 19
/boot/config-$(uname-r) 21
/proc/config.gz 22
¿Qué es un inodo? 23
¿Por qué la eliminación de un archivo no depende de los permisos del mismo? 24
VFS (Sistema de archivos virtual) 25
tmpfs (Sistema de archivos temporales) 26
ramfs (Sistema de archivos de memoria de acceso aleatorio) 27
Asignación de memoria a los amigos 28
Introducción
Cuando se empieza a aprender Linux creo que son conceptos básicos que todo el mundo necesita
conocer. Por eso he decidido escribir una serie de breves escritos destinados a proporcionar el
vocabulario básico y la comprensión para conseguirlo.

En general, quería crear algo que mejorará el conocimiento general de Linux en los escritos que
se pueden leer en 1-3 minutos. Espero que disfrutes del viaje.

Por último, puedes seguirme en twitter - @boutnaru (https://twitter.com/boutnaru). También


puedes leer mis otros escritos en medium - https://medium.com/@boutnaru.

¡¡¡¡¡¡Vamos a ir!!!!!!
El vector auxiliar (AUXV)
Hay variables específicas del sistema operativo que un programa probablemente querría
consultar, como el tamaño de una página (parte de la gestión de memoria - para un futuro
artículo). Entonces, ¿cómo se puede hacer?

Cuando el SO ejecuta un programa expone información sobre el entorno en un almacén de datos


clave-valor llamado "vector auxiliar" (abreviado auxv/AUXV). Si queremos comprobar qué
claves están disponibles podemos repasar1 (en versiones anteriores formaba parte de elf.h) y
buscar todas las definiciones que empiecen por "AT_".

Entre la información que se incluye en AUXV podemos encontrar: el uid efectivo del programa,
el uid real del programa, el tamaño de página del sistema, número de cabeceras del programa
(del ELF - más sobre esto en el futuro), tamaño mínimo de pila para la entrega de señales (y hay
más).

Si queremos ver toda la información de AUXV mientras ejecutamos un programa podemos


establecer la variable de entorno LD_SHOW_AUXV a 1 y ejecutar el programa solicitado (ver la
captura de pantalla de abajo, fue tomada de JSLinux ejecutando Fedora 33 basado en una CPU
riscv642 . Podemos ver que el nombre de la variable comienza con "LD_", es porque es
usada/parseada por el enlazador/cargador dinámico (aka ld.so).

Por lo tanto, si enlazamos estáticamente nuestro programa (como usando la bandera -static en
gcc) al establecer la variable no se imprimirán los valores de AUXV. De todas formas, también
podemos acceder a los valores de AUXV usando la función de biblioteca "unsigned long
getauxval(unsigned long type)"3 . Un hecho agradable es que el vector auxiliar se encuentra
junto a las variables de entorno comprueba la siguiente ilustración4 .

1 https://elixir.bootlin.com/linux/latest/source/include/uapi/linux/auxvec.h#L10
2 https://bellard.org/jslinux/
3 https://man7.org/linux/man-pages/man3/getauxval.3.html
4 https://static.lwn.net/images/2012/auxvec.png
comando no encontrado
¿Te has preguntado alguna vez qué pasa cuando ves "command not found" en bash? Este artículo
no va a hablar de eso ni del flujo que determina si un comando se encuentra o no (ese es un tema
para otro artículo ;-).

Voy a centrar mi discusión en lo que ocurre en un entorno basado en bash + Ubuntu (versión
22.04). Supongo que al menos una vez escribiste "sl" en lugar de "ls" y recibiste un mensaje
"Command 'sl' not found, but can be installed with: sudo apt install sl" - ¿cómo supo bash que
existe tal paquete que puede ser instalado? - como se muestra en la siguiente captura de pantalla

En general, la magia ocurre con el script de python "/usr/lib/command-not-found" que se ejecuta


cuando un bash no encuentra un comando - como se muestra en la captura de pantalla de abajo.
Esta característica se basa en una base de datos sqlite que tiene una conexión entre el comando y
los paquetes, se ordena en "/var/lib/command-not-found/commands.db".

Por último, existe un bonito sitio web https://command-not-found.com/ que permite buscar un
comando y obtener una lista de diferentes formas de instalarlo (para diferentes distribuciones de
Linux/Windows/MacOS/Docker/etc).
Out-of-Memory Killer (OOM killer)
El kernel de Linux tiene un mecanismo llamado "out-of-memory killer" (también conocido como
OOM killer) que se utiliza para recuperar memoria en un sistema. El OOM killer permite matar
una única tarea (llamada también oom victim) mientras que esa tarea terminará en un tiempo
razonable y liberará así memoria.

Cuando el OOM killer hace su trabajo podemos encontrar indicaciones sobre ello buscando en los
logs (como
/var/log/messages y buscando "Killed"). Si desea configurar el "OOM killer"5 .

Es importante entender que el OOM killer elige entre procesos basándose en el "oom_score". Si
queremos ver el valor para un proceso específico podemos simplemente leer
"/proc/[PID]/oom_score" - como se muestra en la captura de pantalla de abajo. Si queremos
alterar la puntuación podemos hacerlo usando "/proc/[PID]/oom_score_adj" - como se muestra
también en la captura de pantalla de abajo. El rango válido es de 0 (nunca matar) a 1000 (siempre
matar), cuanto menor sea el valor menor es la probabilidad de que el proceso sea matado6 .

5 https://www.oracle.com/technical-resources/articles/it-infrastructure/dev-oom-killer.html
6 https://man7.org/linux/man-pages/man5/proc.5.html
¿Por qué no funciona "ltrace" en las nuevas
versiones de Ubuntu?
Mucha gente me ha preguntado al respecto, así que he decidido escribir una breve respuesta al
respecto. Dos herramientas de línea de comandos muy conocidas en Linux que pueden ayudar
con el análisis dinámico son "strace" y "ltrace". "strace" permite rastrear llamadas al sistema
("man 2 syscalls") y señales ("man 7 signal"). No voy a centrarme en "strace" en este artículo,
puedes leer más sobre él usando "man strace". Por otro lado, "ltrace" permite rastrear las
llamadas a bibliotecas dinámicas y las señales recibidas por el proceso rastreado ("man 1 ltrace").
Además, también puede rastrear llamadas al sistema (como "strace") si se utiliza la opción "-S".

Si has probado a usar "ltrace" en las nuevas versiones de Ubuntu probablemente habrás visto que
no se muestran las llamadas a librerías (puedes comprobarlo usando "ltrace `which ls`"). Para
demostrarlo he creado un pequeño programa en c - como puedes ver en la captura de abajo
("code.c").

En primer lugar, si compilamos "code.c" y lo ejecutamos con "ltrace" no obtenemos ninguna


información sobre una llamada a biblioteca (ver en la captura de abajo). En segundo lugar, si
compilamos "code.c" con "-z lazy" podemos ver que al ejecutar el ejecutable con "ltrace" sí
obtenemos información sobre las funciones de biblioteca. Entonces, ¿cuál es la diferencia entre
los dos?
"ltrace" (y "strace") funciona insertando un breakpoint7 en el PLT para el símbolo relevante (que
es función de librería) que queremos rastrear. Así que como por defecto los binarios no se
compilan con "lazy loading" de símbolos, se resuelven cuando la aplicación se inicia y por lo
tanto los puntos de interrupción establecidos por "ltrace" no se activan (y no vemos ninguna
llamada a librerías en la salida - como se muestra en la captura de pantalla de abajo). Además,
puedes leer más sobre el funcionamiento interno de "ltrace" aquí -
https://www.kernel.org/doc/ols/2007/ols2007v1-pages-41-52.pdf

7https://medium.com/@boutnaru/have-you-ever-asked-yourself-how-breakpoints-work-c72dd8619538
vDSO (Objeto Virtual Dinámico Compartido )
vDSO es una librería compartida que el kernel mapea en el espacio de direcciones de memoria
de cada aplicación en modo usuario. No es algo en lo que los desarrolladores necesiten pensar
debido al hecho de que es utilizada por la librería C8 .

En general, la razón de tener vDSO es el hecho de que son llamadas al sistema específicas que se
utilizan con frecuencia por las aplicaciones en modo usuario. Debido al tiempo/coste del cambio
de contexto entre el modo usuario y el modo kernel para ejecutar una llamada al sistema, podría
afectar al rendimiento general de una aplicación.

Así, vDSO proporciona "syscalls virtuales" debido a la necesidad de optimizar las


implementaciones de las llamadas al sistema. La solución necesitaba no requerir que libc
rastreara las capacidades de la CPU y/o la versión del kernel. Tomemos por ejemplo x86, que
tiene dos formas de invocar una syscall: "int 0x80" o "sysenter". La opción "sysenter" es más
rápida, debido a que no necesitamos pasar por la IDT (Interrupt Descriptor Table). El problema
es que está soportada para CPUs más nuevas que Pentium II y para versiones del kernel
superiores a 2.69 .

Si vDSO la implementación de la interfaz syscall es definida por el kernel de la siguiente


manera. Un conjunto de instrucciones de CPU formateadas como ELF se mapea al final del
espacio de direcciones en modo usuario de todos los procesos - como se muestra en la captura de
pantalla de abajo. Cuando libc necesita ejecutar una syscall comprueba la presencia de vDSO y si
es relevante para la syscall específica se utilizará la implementación en vDSO - como se muestra
en la captura de pantalla inferior10 .

Además, para el caso de "syscalls virtuales" (que también forman parte de vDSO) hay un marco
mapeado como dos páginas diferentes. Una en el espacio del kernel que es estática/"readable &
writeable" y la segunda en el espacio de usuario que está marcada como "read-only". Dos
ejemplos de esto son los syscalls "getpid()" (que es un ejemplo de datos estáticos) y
"gettimeofday()" (que es un ejemplo dinámico de lectura-escritura).

Además, como parte del proceso de compilación del kernel, el código vDSO se compila y enlaza.
La mayoría de las veces podemos encontrarlo utilizando el siguiente comando "find arch/$ARCH/
-name '*vdso*.so*' -o
-name '*gate*.so*'"11
Si queremos habilitar/deshabilitar vDSO podemos poner "/proc/sys/vm/vdso_enable" a 1/0
respectivamente12 . Por último, a continuación se muestra un benchmark de diferentes syscalls
utilizando diferentes implementaciones.

8 https://man7.org/linux/man-pages/man7/vdso.7.html
9 https://linux-kernel-labs.github.io/refs/heads/master/so2/lec2-syscalls.html
10 https://hackmd.io/@sysprog/linux-vdso
11 https://manpages.ubuntu.com/manpages/xenial/man7/vdso.7.html
12 https://talk.maemo.org/showthread.php?t=32696
https://www.slideshare.net/vh21/twlkhlinuxvsyscallandvdso
Llamada a syscalls desde Python
¿Alguna vez has querido una forma rápida de llamar a una syscall (incluso si no está expuesta
por libc)? Hay una forma rápida de hacerlo usando "ctypes" en Python.

Podemos hacerlo utilizando la función "syscall" exportada por libc (consulta 'man 2 syscall'13
para más información). Llamando a esa función podemos llamar a cualquier syscall pasándole su
número y parámetros.

¿Cómo sabemos cuál e s el número de la llamada al sistema? Podemos comprobarlo en


https://filippo.io/linux-syscall-table/. ¿Y los parámetros? Podemos ir al código fuente que está
apuntado en cualquier entrada de una syscall (del enlace anterior) o podemos usar man (usando
el siguiente patrón - 'man 2 {NameOfSyscall}', por ejemplo 'man 2 getpid').

Veamos un ejemplo, utilizaremos la llamada al sistema getpid(), que no recibe argumentos.


Además, el número de la llamada al sistema es 39 (en Linux x64). Puedes ver el ejemplo
completo en la siguiente captura de pantalla. Por cierto,
el ejemplo fue hecho con
https://www.tutorialspoint.com/linux_terminal_online.php y terminal Linux en línea (kernel
3.10).

13 https://man7.org/linux/man-pages/man2/syscall.2.html
Regla de denominación de las syscalls: ¿Qué pasa
si el nombre de una llamada al sistema empieza por
"f"?
Debido al gran número de syscalls, se utilizan algunas reglas de nomenclatura para ayudar a
entender la operación que realiza cada una de ellas. Permítanme repasar algunas de ellas para dar
más claridad.

Si tenemos una llamada al sistema "<nombre_llamada_sistema>" entonces también podríamos


tener "f<nombre_llamada_sistema>" lo que significa que "f<nombre_llamada_sistema>" hace la
misma operación que "<nombre_llamada_sistema>" pero sobre un fichero referenciado por un fd
(descriptor de fichero). Algunos ejemplos son ("chown", "fchown") y ("stat", "fstat"). Es
importante entender que no todas las syscall que comienzan con "f" son parte de tal par, mire
"fsync()" como ejemplo, sin embargo en este caso el prefijo aún denota que la entrada de la
syscall es un fd. También hay ejemplos en los que el prefijo "f" ni siquiera se refiere a un fd
como en el caso de "fork()", es sólo parte del nombre de la llamada al sistema.
Regla de denominación de las syscalls: ¿Qué pasa
si el nombre de una llamada al sistema empieza por
"l"?
Quiero hablar de esas syscalls que empiezan por "l". Si tenemos una llamada al sistema
"<nombre_llamada_sistema>" también podríamos tener "l<nombre_llamada_sistema>" lo que
significa que "l<nombre_llamada_sistema>" hace la misma operación que
"<nombre_llamada_sistema>" pero en el caso de un enlace simbólico dado como entrada la
información se recupera sobre el propio enlace y no sobre el fichero al que se refiere el enlace
(por ejemplo "getxattr" y "lgetxattr". Además, no todas las llamadas al sistema que empiezan por
"l" entran en esta categoría (piense en "listen").

Creo que la última regla es la más confusa porque hay casos en los que el prefijo "l" no forma
parte del nombre original de la llamada al sistema y no es relevante para ningún tipo de enlace.
Veamos "lseek", la razón de tener el prefijo es para enfatizar que el offset se da como largo a
diferencia de la antigua syscall "seek".
RCU (Copia de lectura Update)
Debido a que hay múltiples hilos del kernel (compruébalo usando 'ps -ef | grep rcu` - la salida del
comando se incluye en la captura de pantalla al final del post) que se basan en RCU (y otras
partes del kernel) . He decidido escribir una breve explicación al respecto.

RCU es un mecanismo de sincronización que evita el uso de primitivas de bloqueo en caso de


múltiples flujos de ejecución que leen y escriben elementos específicos. Esos elementos están la
mayoría de las veces enlazados por punteros y forman parte de una estructura de datos específica
como: tablas has, árboles binarios, listas enlazadas y más.

La idea principal de RCU es dividir la fase de actualización en dos pasos diferentes:


"reclamación" y "eliminación" - vamos a detallar esas fases. En la fase de "eliminación"
eliminamos/desvinculamos/borramos una referencia a un elemento de una estructura de datos
(también puede ser en caso de sustituir un elemento por otro nuevo). Esta fase puede realizarse
de forma concurrente con otros lectores. Es segura debido a que las CPU modernas garantizan
que los lectores verán los datos nuevos o los antiguos, pero no parcialmente actualizados. En el
paso de "reclamación" el objetivo es liberar el elemento de la estructura de datos durante el
proceso de eliminación. Por ello, este paso puede perturbar a un lector que haga referencia a ese
elemento concreto. Por lo tanto, este paso debe comenzar sólo después de que todos los lectores
no tengan una referencia al elemento que queremos eliminar.

Debido a la naturaleza de los dos pasos, un actualizador puede terminar el paso de "eliminación"
inmediatamente y aplazar el de "reclamación" para cuando se completen todos los activos
durante esta fase (puede hacerse de varias formas, como bloqueando o registrando una
devolución de llamada).

RCU se utiliza en casos en los que el rendimiento de lectura es crucial, pero puede soportar la
contrapartida de utilizar más memoria/espacio. Vamos a repasar una secuencia de actualización
de una estructura de datos in situ utilizando RCU. Primero, creamos una nueva estructura de
datos. Segundo, copiamos la estructura de datos antigua en la nueva (no olvides guardar el
puntero a la estructura de datos antigua). Tercero, modificamos la estructura de datos
nueva/copiada. Cuarto, actualizamos el puntero global para que haga referencia a la nueva
estructura de datos. Quinto, dormir hasta que el kernel esté seguro de que no hay más lectores
usando la antigua estructura de datos (llamado también periodo de gracia, en Linux podemos
usar synchronize_rcu()14 .

En resumen, RCU es probablemente la técnica "sin bloqueo" más común para estructuras de
datos compartidas. Es libre de bloqueos para cualquier número de lectores. También hay
implementaciones para un solo escritor e incluso para varios escritores (Sin embargo, está fuera
del alcance por ahora). Por supuesto, RCU también tiene problemas y no está diseñada para
casos en los que sólo hay actualizaciones (es mejor para "mayoritariamente leídas" y "pocas
escrituras").
14 https://elixir.bootlin.com/linux/latest/source/kernel/rcu/tree.c#L3796
rcot 00 :00 :00 [: :.: gp]
FOOt 00 :00 :00 [.' , par gp]
dem 00 :00 :00 [ tareas
asia 00 :00 :00 [. groseras ]
do t 00:00:02 [ . tareas trace ]
ALIME sched)
NTOS
rcot
cgroups (Grupos de control )
"Grupos de Control" (también conocido como cgroups) es una característica del kernel de Linux
que organiza los procesos en grupos jerárquicos. Basándose en esos grupos podemos limitar y
monitorizar diferentes tipos de recursos del sistema operativo. Entre esos recursos están: uso de
E/S de disco, uso de red, uso de memoria, uso de CPU y más (https://man7.org/linux/man-
pages/man7/cgroups.7.html). Los cgroups son uno de los bloques de construcción utilizados para
crear contenedores (que incluyen otras cosas como espacios de nombres, capacidades y sistemas
de archivos superpuestos).

La funcionalidad de los cgroups se ha incorporado al núcleo de Linux desde la versión 2.6.24


(publicada en enero de 2008). En general, los cgroups proporcionan las siguientes características:
limitación de recursos (como se ha explicado anteriormente), priorización (algunos grupos de
procesos pueden tener mayores cuotas de recursos), control (congelación de grupo de procesos) y
contabilidad15 .

Además, existen dos versiones de cgroups. cgroups v1 fue creado por Paul Menage y Rohit Seth.
cgroups v2 fue rediseñado y reescrito por Tejun Heo16 . La documentación de cgroups v2
apareció por primera vez en la versión 4.5 del kernel de Linux en marzo de 201617 .

Voy a escribir sobre las diferencias entre las dos versiones y cómo utilizarlos en los próximos
escritos. Una buena explicación sobre el concepto de cgroups se muestra en la imagen de abajo18
. Por cierto, desde el kernel 4.19 OOM killer19 es consciente de los cgroups, lo que significa que
el SO puede matar un cgroup como una sola unidad.

15 https://docs.kernel.org/admin-guide/cgroup-v1/cgroups.html
16 https://www.wikiwand.com/en/Cgroups
17https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/diff/Documentation/cgroup-v2.txt?id=v4.5&id2=v

4.4
18 https://twitter.com/b0rk/status/1214341831049252870
19 https://medium.com/@boutnaru/linux-fuera-de-memoria-killer-oom-killer-bb2523da15fc
Paquete Gestores
El gestor de paquetes (también conocido como "sistema de gestión de paquetes") es un conjunto
de componentes de software que se encargan de rastrear qué artefactos de software (ejecutables,
scripts, bibliotecas compartidas y más). Los paquetes se definen como un conjunto de artefactos
de software que pueden instalarse/eliminarse como grupo
(https://www.debian.org/doc/manuals/aptitude/pr01s02.en.html).

Así, podemos decir que un gestor de paquetes automatiza la


instalación/actualización/eliminación de programas informáticos de un sistema de forma
coherente. Además, los gestores de paquetes suelen gestionar una base de datos que incluye las
dependencias entre los artefactos de software y la información sobre versiones para evitar
conflictos (https://en.wikipedia.org/wiki/Package_manager).

Básicamente, existen diferentes categorías de gestores de paquetes. Los más comunes son:
Gestores de paquetes OS (como dpkg, apk, rpm, dnf, pacman y más - parte son sólo frontends
como describiremos en el futuro) y gestores de paquetes en tiempo de ejecución centrados en
lenguajes de programación específicos (como maven, npm, PyPi, NuGet, Composer y más).
Cada gestor de paquetes también puede tener su propio formato de archivo de paquete (más
sobre esto en futuros escritos). Además, los gestores de paquetes pueden tener diferentes front-
ends basados en CLI o GUI. Esos gestores de paquetes también pueden soportar la descarga de
artefactos de software desde diferentes repositorios (https://devopedia.org/package-manager).

En general, los gestores de paquetes pueden almacenar diferentes metadatos para cada paquete
como: lista de archivos, versión, autoría, licencias, arquitectura objetivo y sumas de
comprobación. Una visión general de la gestión de paquetes de paquetes es
muestra en el diagrama diagrama siguiente
(https://developerexperience.io/articles/package-management).
¿Qué es un formato ELF (Executable and Linkable
Format) ?
Cada sistema operativo genérico/estándar tiene un formato binario para sus
ejecutables/bibliotecas en modo usuario, código del núcleo y más. Windows tiene PE (Portable
Executable), OSX tiene MachO y Linux tiene ELF. Vamos a empezar con ELF (prometo repasar
los otros también).

En Linux ELF entre los otros (pero no limitado a) para ejecutables, modelos de kernel,
bibliotecas compartidas, volcados de núcleo y archivos de objetos. Aunque Linux no exige una
extensión para los archivos, los archivos ELF pueden tener una extensión *.bin, *.elf, *.ko, *.so,
*.mod, *.o, *.bin y más (también pueden no tener extensión).

Además, hoy en día ELF es un formato ejecutable común para una gran variedad de sistemas
operativos (y no sólo Linux) como: QNX, Android, VxWorks, OpenBSD, NetBSD, FreeBSD,
Fuchsia, BeOS. Además, se utiliza en diferentes plataformas como: Wii, Dreamcast y Playstation
Portable.

ELF, puede incluir 3 tipos de cabeceras: ELF header (que es obligatorio), program headers y
sections header . La aparición de las dos últimas se basa en el objetivo del archivo: ¿Es s ó l o para
enlazar? ¿Es sólo de ejecución? ¿Ambos? (Más información sobre la diferencia entre ambos en los
próximos capítulos). Puedes ver los diferentes diseños de ELF en la imagen de abajo20 .

En las próximas partes repasaremos cada cabecera con más detalle. Por cierto, una gran fuente
para obtener más información sobre ELF es man ("man 5 elf").

20 https://i.stack.imgur.com/RMV0g.png
La cabecera ELF (Executable and Linkable Format)
Ahora vamos a empezar con la cabecera ELF. El tamaño total de la cabecera es de 32 bytes. La
cabecera comienza con la mágica "ELF" (0x7f 0x45 0x4c 0x46).

A partir de la información contenida en la cabecera podemos responder a las siguientes


preguntas: ¿El fichero es de 32 o 64 bits? ¿El fichero almacena datos en big o little endian?
¿Cuál es la versión ELF? ¿Qué tipo d e a r c h i v o e s (ejecutable, reubicable, biblioteca
compartida, etc.)? ¿Cuál es la CPU de destino? ¿Cuál es la dirección del punto de entrada?
¿Cuál es el tamaño de las demás cabeceras (programa/sección)? - y mucho más.

Si queremos analizar la cabecera de un fichero ELF específico podemos usar el comando


"readelf" (lo vamos a usar en todas las siguientes partes para analizar ELFs). Para mostrar la
cabecera de un fichero ELF podemos ejecutar "readelf -h {PATH_TO_ELF_FILE}". En la imagen
de abajo podemos ver la cabecera ELF de "ls". La imagen fue tomada de un Arch Linux en línea
en un navegador (copy.sh).
Jerarquía del sistema de archivos en Linux
Resulta que existe un estándar que es una referencia que describe las convenciones utilizadas por
los sistemas Unix/Linux para la organización y disposición de su sistema de archivos. Este
estándar se creó hace unos 28 años (14 feb 1994) y la última versión (3.0) se publicó hace 7 años
(3 jun 2015). Si quieres repasar la especificación para más detalles utiliza el siguiente enlace -
https://refspecs.linuxfoundation.org/fhs.shtml.

Vamos a dar una breve descripción de cada directorio (la descripción detallada de algunos de
ellos se hará en un artículo específico). Vamos a listar todos los directorios basados en un orden
lexicográfico. Todos los ejemplos que voy a compartir se basan en una máquina virtual que
ejecuta Ubuntu 22.04.1 (abajo hay una captura de pantalla que muestra los directorios de esa
máquina virtual). Que empiece la diversión
;-)

"/", es el directorio raíz de todo el sistema (el inicio de todo).


"/bin", comando básico en su mayoría binarios (también hay scripts como zgrep) que son
necesarios para cada usuario. Algunos ejemplos son: ls, ip e id.
"/boot", contiene los archivos necesarios para el arranque como el kernel (vmlinuz), initrd y la
configuración del gestor de arranque (como para grub). También puede contener información de
metadatos sobre el proceso de construcción del kernel, como la configuración que se utilizó (en
el futuro se compartirá un informe detallado sobre "/boot").
"/dev", archivos de dispositivo, por ahora debes pensar en ello como una interfaz a un
controlador de dispositivo que se encuentra en un sistema de archivos (más sobre esto en el
futuro). Algunos ejemplos son: /dev/null, /dev/cero y
/dev/random.
"/etc", contiene configuración sobre el sistema o una aplicación instalada. Algunos ejemplos son:
/etc/adduser.conf (archivo de configuración para los comandos adduser y addgroup) y
/etc/sudo.conf. "/home", es la ubicación por defecto del directorio personal de los usuarios
(puede modificarse en /etc/passwd por usuario). El directorio puede contener configuraciones
personales del usuario, archivos guardados por el usuario y más. Un ejemplo es .bash_history
que es un archivo oculto que contiene el histórico de comandos introducidos por el usuario
(mientras usa el shell bash).
"/lib", contiene las bibliotecas que necesitan los binarios principalmente (pero sin limitarse a
ellos) en "/bin" y "/sbin". En sistemas de 64 bits también podemos tener "lib64".
"/media", utilizado como punto de montaje para medios extraíbles (como CD-ROM y USB).
"/mnt", puede utilizarse para sistemas de archivos montados temporalmente.
"/opt", debe incluir las aplicaciones instaladas por el usuario como complementos (en realidad
no todos los complementos están instalados ahí).
"/lost+found", este directorio contiene archivos que han sido borrados o perdidos durante una
operación de archivo. Significa que tenemos un inodo para esos archivos, sin embargo no
tenemos un nombre de archivo en disco para ellos (piense en casos de pánico del kernel o un
apagado no planeado). Esto es manejado por herramientas como fsck - más sobre esto en un
futuro escrito.
"/proc", es un pseudo sistema de archivos que permite recuperar información sobre las
estructuras de datos del kernel desde el espacio de usuario usando operaciones de archivo, por
ejemplo "ps" lee información para construir la lista de procesos. Debido al hecho de que es una
parte crucial de Linux voy a dedicarle un artículo entero.
"/root", es el directorio raíz por defecto de la cuenta root.
"/run", se utiliza para datos en tiempo de ejecución como: demonios en ejecución, usuarios
registrados y más. Debe ser borrado/inicializado cada vez que se reinicie.
"/sbin", similar a "/bin" pero contiene binarios del sistema como: lsmod, init (en Ubuntu por
cierto es un enlace a systemd) y dhclient.
"/srv",contiene información que es publicada por el sistema al mundo exterior usando
FTP/servidor web/otro.
"/sys", también un pseudo-sistema de ficheros (similar a /proc) que exporta información sobre
dispositivos hardware, controladores de dispositivos y subsistemas del kernel. También puede
permitir la configuración de diferentes subsistemas (como tracing para ftrace). Lo cubriré por
separado con más detalle en un futuro próximo. "/tmp", el objetivo del directorio es contener
archivos temporales. La mayoría de las veces el contenido no se guarda entre reinicios. Recuerda
que también existe "/var/tmp".
"/usr", recibe varios nombres "Programas de usuario" o "Recursos del sistema de usuario".
Tiene varios subdirectorios que contienen binarios, libs, archivos doc y también puede contener
código fuente. Históricamente, estaba pensado para ser de sólo lectura y compartido entre hosts
compatibles con FHS (https://tldp.org/LDP/Linux-Filesystem-Hierarchy/html/usr.html). Debido
a la naturaleza de su complejidad actual y a la gran cantidad de archivos que contiene, lo
trataremos también en otro artículo.
"/var", también conocido como archivos variables. Contiene archivos que por su diseño van a
cambiar durante el funcionamiento normal del sistema (piense en archivos spool, logs y más).
Más sobre este directorio en el futuro.

Es importante notar que esos no son todos los directorios y subdirectorios incluidos en una
instalación limpia de Linux, sino los principales con los que he decidido empezar (más
información será compartida en el futuro). Hasta pronto ;-)
/boot/config-$(uname- r)
"/boot/config-$(uname-r)" es un archivo de texto que contiene una configuración
(característica/opciones) con la que se compiló el kernel. El "uname -r" se sustituye por la
versión del kernel21 . Es importante entender que el archivo sólo es necesario para la fase de
compilación y no para cargar el núcleo, por lo que puede ser eliminado o incluso alterado por un
usuario root y por lo tanto no reflejar la configuración específica que se utilizó.En general, cada
vez que se realiza uno de los siguientes "make menuconfig"/make xconfig", "make localconfig",
"make oldconfig", "make XXX_defconfig" u otro "make XXXconfig" se crea un archivo
".config". Este archivo no se borra (a menos que se utilice "make mrproper"). Además, muchas
distribuciones copian ese archivo a "/boot"22 .

El sistema de compilación leerá el archivo de configuración y lo utilizará para generar el núcleo


compilando el código fuente correspondiente. Usando el archivo de configuración podemos
personalizar el kernel Linux a nuestras necesidades23 . El archivo de configuración se basa en
valores clave - como se muestra en la siguiente captura de pantalla24 . Usando la configuración
podemos activar/desactivar características como sonido/redes/soporte USB como podemos ver
con "CONFIG_MMU=y" en la captura de pantalla de abajo25 . También, podemos ajustar un
valor específico de características como el "CONFIG_ARCH_MMAP_RND_BITS_MIN=28"26 .

Además, en el caso de los módulos del kernel podemos añadir/eliminar módulos y decidir si
queremos compilarlos dentro del propio kernel o como un fichero ".ko" separado. En caso de que
la configuración sea "y" significa compilar dentro del núcleo, "m" significa como un archivo
separado y "n" significa no compilar27 . Así, si "CONFIG_DRM_TTM=m" entonces el
"subsistema gestor de memoria TTM" va a ser compilado fuera del kernel28 . Si "ttm" está

cargado se mostrará en la salida de "lsmod"29 .


21 https://linux.die.net/man/1/uname
22 https://unix.stackexchange.com/questions/123026/where-kernel-configuration-file-is-stored
23 https://linuxconfig.org/in-depth-howto-on-linux-kernel-configuration
24 https://blog.csdn.net/weixin_43644245/article/details/121578858
25 https://elixir.bootlin.com/linux/v6.4.11/source/arch/um/Kconfig#L36
26 https://elixir.bootlin.com/linux/v6.4.11/source/arch/x86/Kconfig#L322
27h ttps://stackoverflow.com/questions/14587251/understanding-boot-config-file
28 https://github.com/torvalds/linux/blob/master/drivers/gpu/drm/ttm/ttm_module.c
29 https://man7.org/linux/man-pages/man8/lsmod.8.html
/proc/config.gz
Desde la versión 2.6 del kernel las opciones de configuración que se utilizaron para construir el
kernel actual en ejecución están expuestas utilizando procfs en la siguiente ruta
"/proc/config.gz". El formato del contenido es el mismo que el archivo .config que es copiado
por diferentes distribuciones a "/boot"30 .

En general, a diferencia del archivo ".config" los datos de "config.gz" están comprimidos.
Debido a eso, si queremos ver su contenido podemos usar zcat31 o zgrep32 que permiten
leer/buscar dentro de archivos comprimidos. Como se muestra en la siguiente captura de pantalla
(tomada de copy.sh).

Por último, para que "config.gz" sea soportado y exportado por "/proc" es necesario que el kernel
se compile con "CONFIG_IKCONFIG_PROC" habilitado33 - como también se muestra en la
captura de pantalla inferior. También podemos repasar la creación de la entrada "/proc"34 y la
función que devuelve los datos al leer esa entrada35 .

30 https://medium.com/@boutnaru/the-linux-concept-journey-boot-config-uname-r-6a4dd16048c4
31 https://linux.die.net/man/1/zcat
32 https://linux.die.net/man/1/zgrep
33 https://elixir.bootlin.com/linux/v6.5/source/kernel/configs.c#L35
34 https://elixir.bootlin.com/linux/v6.5/source/kernel/configs.c#L60
35 https://elixir.bootlin.com/linux/v6.5/source/kernel/configs.c#L41
¿Qué es un inodo ?
Un inodo (también conocido como nodo índice) es una estructura de datos utilizada por los
sistemas de ficheros tipo Unix/Linux para describir un objeto del sistema de ficheros. Dicho
objeto puede ser un archivo o un directorio. Cada inodo almacena punteros a las ubicaciones de
los bloques de disco de los datos y metadatos del objeto36 . Una ilustración de esto se muestra a
continuación37 .

En general, los metadatos que contiene un inodo son: tipo de archivo (archivo
normal/directorio/enlace simbólico/archivo especial de bloque/archivo especial de carácter/etc),
permisos, id de propietario, id de grupo, tamaño, hora de último acceso, hora de última
modificación, hora de modificación y número de enlaces duros38 .

Mediante el uso de inodos, el sistema de ficheros rastrea todos los ficheros/directorios guardados
en el disco. Además, mediante el uso de inodos podemos leer cualquier byte específico en los
datos de un archivo de manera muy eficaz. Podemos ver el número total de inodos por sistema de
ficheros montado usando el comando "df -i"39 . También podemos ver el inodo de un
archivo/directorio y otros metadatos del archivo utilizando el comando "ls -i"40 o "stat"41 . Por
cierto, el comando "stat" puede utilizar diferentes syscalls (dependiendo del sistema de archivos
y de la versión específica) como "stat"42 , "lstat"43 o "statx"44 .

Por último, puedes consultar "struct inode" en el código fuente del kernel de Linux45 . No todos
los puntos/enlaces están directamente conectados a los bloques de datos, pero lo explicaré en un
próximo artículo.

36h ttps://www.bluematador.com/blog/what-is-an-inode-and-what-are-they-used-for
37 https://www.sobyte.net/post/2022-05/linux-inode/
38 https://www.stackscale.com/blog/inodes-linux/
39 https://linux.die.net/man/1/df
40 https://man7.org/linux/man-pages/man1/ls.1.html
41 https://linux.die.net/man/1/stat
42 https://linux.die.net/man/2/stat
43 https://linux.die.net/man/2/lstat
44 https://man7.org/linux/man-pages/man2/statx.2.html
45 https://elixir.bootlin.com/linux/v6.4.2/source/include/linux/fs.h#L612
¿Por qué la eliminación de un archivo no depende de
los permisos del archivo ?
Algo que no siempre es entendido correctamente por los usuarios de Linux es el hecho de que
eliminar un archivo no depende de los permisos del propio archivo. Como se puede ver en la
captura de pantalla de abajo, incluso si un usuario tiene todos los permisos
(lectura+escritura+ejecución) no puede eliminar un archivo. Por cierto, la eliminación de un
archivo se realiza utilizando la llamada al sistema "unlink"46 o la llamada al sistema "unlinkat"47
.

El motivo es que los datos que indican que un archivo pertenece a un directorio se guardan como
parte del propio directorio. Podemos pensar en un directorio como un "archivo especial" cuyos
datos son el nombre y los números de inodo48 de los archivos que forman parte de ese directorio
específico.

Así, si añadimos permisos de escritura al directorio aunque el usuario no tenga permisos para el
archivo ("chmod 000") el archivo puede ser eliminado (del directorio) - como se muestra en la
captura de pantalla de abajo.

46 https://linux.die.net/man/2/unlink
47 https://linux.die.net/man/2/unlinkat
48 https://medium.com/@boutnaru/linux-que-es-un-inodo-7ba47a519940
VFS (Sistema de archivos virtuales )
VFS (Virtual File System, también conocido como Virtual File Switch) es un componente de
software de Linux responsable de la interfaz del sistema de archivos entre el modo usuario y el
modo kernel. Su uso permite al kernel proporcionar una capa de abstracción que hace muy fácil
la implementación de diferentes sistemas de archivos49 .

En general, VFS enmascara los detalles de implementación de un sistema de archivos específico


detrás de llamadas genéricas al sistema (abrir/leer/escribir/cerrar/etc), que en su mayoría están
expuestas a la aplicación en modo usuario mediante algunas envolturas en libc - como se muestra
en el diagrama siguiente50 .

Además, podemos decir que el objetivo principal de VFS es permitir que las aplicaciones en
modo usuario accedan a diferentes sistemas de archivos (pensemos en NTFS, FAT, etc.) de la
misma manera. Hay cuatro objetos principales en VFS: superbloque, dentries, inodos y archivos51
.

Así, "inode"52 es lo que utiliza el núcleo para llevar la cuenta de los archivos. Dado que un
archivo puede tener varios nombres, existen "dentries" ("entradas de directorio") que representan
los nombres de ruta. Además, debido al hecho de que un par de procesos pueden tener abierto el
mismo fichero (para lectura/escritura) existe una estructura "file" que contiene la información de
cada uno (como la posición del cursor). La estructura "superblock" contiene datos que son
necesarios para realizar acciones en el sistema de ficheros - más detalles sobre todo esto y más
(como el montaje) se publicarán en un futuro próximo.

Por último, también hay otras estructuras de datos relevantes sobre las que publicaré en un futuro
próximo ("filesystem", "vfsmount", "nameidata" y "address_space").

49 https://www.kernel.org/doc/html/next/filesystems/vfs.html
50h ttps://www.starlab.io/blog/introduction-to-the-linux-virtual-filesystem-vfs-part-i-a-high-level-tour
51 https://www.win.tue.nl/~aeb/linux/lk/lk-8.html
52 https://medium.com/@boutnaru/linux-que-es-un-inodo-7ba47a519940
tmpfs (Sistema de archivos temporales )
"tmpfs" es un sistema de archivos que guarda todos sus archivos en la memoria virtual. Al
utilizarlo, ninguno de los archivos creados en él se guarda en el disco duro del sistema. Por lo
tanto, si desmontamos un punto de montaje tmpfs todos los archivos que se almacenan allí se
pierden. tmpfs guarda todos los datos en las cachés internas del kernel53 . Por cierto, antes se
llamaba "shm fs"54 .

Además, tmpfs es capaz de intercambiar espacio si es necesario (también puede aprovechar


"Transparent Huge Pages"), se llenará hasta alcanzar el límite máximo del sistema de ficheros -
como se muestra en la captura de pantalla de abajo. tmpfs soporta tanto POSIX ACLs como
atributos extendidos55 . En general, si queremos utilizar tmpfs podemos utilizar el siguiente
comando: "mount -t tmpfs tmpfs [LOCATION]". También podemos establecer un tamaño
usando "-o size=[REQUESTED_SIZE]" - como se muestra en la siguiente captura de pantalla.

Por último, hay diferentes directorios que se basan en "tmpfs" como: "/run" y "/dev/shm" (más
sobre ellos en futuros escritos). Para añadir soporte para "tmpfs" debemos habilitar
"CONFIG_TMPFS" cuando construyamos el kernel de Linux56 . Podemos ver la implementación
como parte del código fuente del kernel de Linux57 .

53 https://www.kernel.org/doc/html/latest/filesystems/tmpfs.html
54 https://cateee.net/lkddb/web-lkddb/TMPFS.html
55 https://man7.org/linux/man-pages/man5/tmpfs.5.html
56 https://cateee.net/lkddb/web-lkddb/TMPFS.html
57 https://elixir.bootlin.com/linux/v6.6-rc1/source/mm/shmem.c#L133
ramfs (memoria de acceso aleatorio Filesystem)
"ramfs" es un sistema de archivos que exporta el mecanismo de caché de Linux (caché de
páginas/caché de entradas) como un sistema de archivos basado en RAM redimensionable
dinámicamente. Los datos se guardan únicamente en RAM y no existe ningún almacén de
respaldo para ellos58 .

Por lo tanto, si desmontamos un punto de montaje "ramfs" todos los archivos que se almacenan
allí se pierden - como se muestra en la siguiente captura de pantalla. Por cierto, el truco está en
que los archivos escritos en "ramfs" asignan dentries y caché de páginas como es habitual, pero
como no se escriben nunca se marcan como disponibles para liberar59 .

Además, con "ramfs" podemos seguir escribiendo hasta llenar toda la memoria física. Por ello, se
recomienda que sólo los usuarios root puedan escribir en un punto de montaje basado en "ramfs".
Las diferencias entre "ramfs" y "tmpfs"60 es que "tmpfs" tiene un tamaño limitado y también
puede ser intercambiado61 .

Por último, podemos repasar la implementación de "ramfs" como parte del código fuente del
kernel de Linux62 . Hay dos implementaciones, una en caso de una MMU63 y otra en caso de que
no haya MMU64 . Un buen ejemplo para usar "ramfs" es "initramfs".

58 https://docs.kernel.org/filesystems/ramfs-rootfs-initramfs.html
59 https://lwn.net/Articles/157676/
60 https://medium.com/@boutnaru/the-linux-concept-journey-tmpfs-temporary-filesystem-886b61a545a0
61 https://wiki.debian.org/ramfs
62 https://elixir.bootlin.com/linux/v6.5.3/source/fs/ramfs
63 https://elixir.bootlin.com/linux/v6.5.3/source/fs/ramfs/file-mmu.c
64 https://elixir.bootlin.com/linux/v6.5.3/source/fs/ramfs/file-nommu.c
Buddy Memory Asignación
Básicamente, el "sistema amigo" es un algoritmo de asignación de memoria. Funciona
dividiendo la memoria en bloques de un tamaño fijo. Cada bloque de memoria asignado es una
potencia de dos en tamaño. Cada bloque de memoria en este sistema tiene un "orden" (un número
entero que va de 0 a un límite superior especificado). El tamaño de un bloque de orden n es
proporcional a 2n , de modo que los bloques tienen exactamente el doble de tamaño que los
bloques de orden inferior.65

Así, cuando se hace una petición de memoria, el algoritmo encuentra el bloque de memoria más
pequeño disponible (que sea suficiente para satisfacer la petición). Si el bloque es mayor que el
tamaño solicitado, se divide en dos bloques más pequeños de igual tamaño (también conocidos
como "amigos"). Uno de ellos se marca como libre y el segundo como asignado. El algoritmo
continúa recursivamente hasta que encuentra el tamaño exacto de la memoria solicitada o un
bloque del menor tamaño posible66 .

Además, las ventajas de este sistema es que es fácil de implementar y puede manejar una amplia
gama de tamaños de memoria. Las desventajas son que puede llevar a la fragmentación de la
memoria y es ineficiente para asignar pequeñas cantidades de memoria. Por cierto, cuando el
"amigo" usado es liberado, si también está libre pueden fusionarse - un diagrama de dicha
relación se muestra a continuación67 . Por último, la implementación de Linux del "sistema de
amigos" es un poco diferente de lo que se describe aquí, voy a elaborar sobre ello en una
escritura detectada.

65 https://en.wikipedia.org/wiki/Buddy_memory_allocation
66 https://www.geeksforgeeks.org/operating-system-allocating-kernel-memory-buddy-system-slab-system/
67 https://www.expertsmind.com/questions/describe-the-buddy-system-of-memory-allocation-3019462.aspx

También podría gustarte