Bienvenido a la wiki dedicada a los usuarios nuevos en GNU/Linux, este sitio esta orientado a ayudar a los usuarios nuevos... Si deseas contribuir, por favor crate una cuenta.

Conceptos de LiveUSB

De PUFs Wiki

Este artículo no pretende ser una guía rápida de como crear un LiveUSB con KDE/GNOME como la mayoría, sino que pretende fijar conceptos de Linux. Para realizar esa tarea tomaremos el camino de crear una pequeña distribución de Linux que permita arrancar mediante un pendrive. Cuando terminen de leer este artículo no tendrán un pendrive para llevar a la casa de sus amigos y mostrarles lo lindo que anda compiz-fusion en KDE mientras escriben documentos en OpenOffice y navegan con Firefox; pero sí tendrán los conocimientos necesarios para hacer todo eso.

Tabla de contenidos

[editar] Introducción

Antes de empezar con la construcción del LiveUSB vamos a pensar cual será nuestro camino a seguir. Como sabemos vamos a necesitar de Linux en sí, es decir, el kernel.

Como las unidades flash USB pueden ubicarse en distintos dispositivos (según cuantos discos SATA/SCSI y otros pendrives tengamos) lo que haremos será arrancar utilizando initrd (un disco en RAM basicamente) de modo que nos independicemos del dispositivo USB, y correremos un script que buscará en que unidad está nuestro pendrive, luego lo montará y seguirá iniciando desde allí.

Para realizar todo esto crearemos distintos directorios de trabajo

# mkdir ~/trabajo
# mkdir ~/trabajo/kernel
# mkdir ~/trabajo/liveusb
# mkdir ~/trabajo/initrd
# mkdir ~/trabajo/mnt

el directorio kernel sirve para compilar el kernel, liveusb para armar el USB en si, el initrd para el initrd y mnt para montar unidades. Tengamos en cuenta que a diferencia de un LiveCD tenemos la ventaja que al pendrive lo podemos escribir, y no es necesario montar sistemas de archivos temporarios en RAM para directorios modificables como /etc o /var y /tmp (aunque sí sería recomendable, de modo que nuestro pendrive no sufra muchas escrituras).

[editar] El Kernel

Vamos a construir el kernel de Linux teniendo en cuenta lo siguiente:

  • Necesitamos soporte initrd.
  • Todo lo que necesitemos al momento del arranque lo incluiremos en el kernel.

Para el soporte initrd tendremos que activar las siguientes opciones:

General Setup-->
   [*] Initial RAM filesystem and RAM disk (initramfs/initrd) support
Device Drivers-->
   Block Devices-->
      <*> RAM disk support

para la segunda parte simplemente nos alcanzará con incluir en el kernel soporte para el sistema de archivos ext2 y para dispositivos USB (incluyendo Almacenamiento Masivo).

Para realizar todos los pasos (configuración, compilación e instalación del kernel) ejecutaremos los siguientes comandos:

make O=~/trabajo/kernel menuconfig
make O=~/trabajo/kernel all
make O=~/trabajo/kernel INSTALL_MOD_PATH=~/trabajo/liveusb modules_install
cp ~/trabajo/kernel/arch/i386/boot/bzImage ~/trabajo/liveusb/kernel

Al finalizar estos pasos tendremos la imagen del kernel en el directorio de trabajo que se convertira en el contenido del pendrive, y también tendremos los módulos (que estarán en ~/trabajo/liveusb/lib/modules/version-kernel).

[editar] Initrd

Cuando queremos iniciar el kernel utilzando initrd, el cargador de arranque (LILO, GRUB, Syslinux, etc) se encarga de poner en memoria el kernel en si mismo y luego pone en memoria el contenido de un archivo; este último archivo es nuestra imágen initrd. Cuando el kernel obtiene el control prepara esta imágen (quizás esté comprimida) y la kernel en un disco en RAM, para ser preciso la carga en /dev/ram0.

Una vez finalizado este proceso el kernel se encarga de ejecutar el archivo /linuxrc (que estaba en la imágen initrd) o si especificamos otro archivo mediante la opción init=... ejecutará ese otro archivo también en la imágen initrd. A diferencia de lo que se podría pensar la diferencia entre utilizar /linuxrc para bootear o especificar otro ejecutable no es una mera cuestión de nombres, el proceso es distinto. Si utilizamos /linuxrc el kernel lo trata como un proceso cualquiera, en cambio si especificamos el archivo con init=... lo trata como si fuera el proceso especial init (el proceso número 1 del sistema).

Para crear una imágen initrd podemos utilizar la utilidad cpio, pero nosotros seguiremos otro método que creo resulta más didáctico; crearemos un archivo de un determinado tamaño al cual formatearemos en ext2 y luego lo montaremos mediante los dispositivos /dev/loop/*

[editar] Preparando la Imágen

Lo primero que haremos será preparar la imágen dentro de ~/trabajo/initrd. Como utilizaremos para nuestro proceso init un shell script (se podría haber hecho un programa en C, un script en Perl, en Python, en lo que más les guste) crearemos el soporte necesario para ejecutar estos lindos bichitos.

En primer lugar necesitamos obviamente la shell en si misma, luego necesitaremos los comandos que utilizaremos dentro de nuestro script, y además necesitaremos todas las bibliotecas que estos usen. Empezemos creando la estructura de directorios:

cd ~/trabajo/initrd
mkdir bin lib dev
cp /bin/cat /bin/chroot /bin/cut /bin/grep /bin/ls /bin/mount /sbin/pivot_root /bin/sh /bin/sleep /bin/umount bin/

al finalizar esta serie de comandos tendremos todos los programas que vamos a usar bajo el directorio bin de nuestra imágen initrd, pero todavía faltan todas las librerías que estos utilizan. Por suerte tenemos el comando ldd el cual nos informa las bibliotecas que utilizan los programas:

ldd /bin/cat
     linux-gate.so.1 =>  (0xb7f4f000)
     libc.so.6 => /lib/libc.so.6 (0xb7de7000)
     /lib/ld-linux.so.2 (0xb7f50000)
cp /lib/ld-linux.so.2 lib/
cp /lib/libc.so.6 lib/

si realizamos este paso con todos los programas y bibliotecas al final tendremos nuestro directorio lib completo; hay que darse cuenta que la biblioteca linux-gate.so.1 en realidad no es un archivo de biblioteca.

Una vez terminado todavía tendremos que llenar el directorio dev de dispositivos para poderlos usar en nuestro shell script. Hay dos dispositivos importantes (pero que no son estrictamente necesarios) /dev/null y /dev/console. Interesantemente no utilizaremos a console, tan solo null para redirigiar la salida de nuestros comandos. También necesitaremos los dispositivos del estilo /dev/sd?1 para poder montar el pendrive; si bien podríamos pedirle a nuestro script que lea el archivo /proc/partitions para crear de manera dinámica estos dispositivos, resulta mucho más sencillo e igualmente didáctico crear de manera manual suficientes de estos dispositivos como para poder montar nuestro pendrive en cualquiera de nuestros sistemas con discos SCSI o SATA. Para ello podemos hacer algo como esto:

cd dev
mknod sda1 b 8 1
mknod sdb1 b 8 17
mknod sdc1 b 8 33
mknod sde1 b 8 65
mknod sdd1 b 8 129
mknod null c 1 3

Listo, ya tenemos nuestra imágen lista, tan solo nos falta crear el shell script que se encargará del proceso de arranque, pero antes crearemos un directorio donde montar el pendrive:

cd ..
mkdir pendrive

[editar] El Script

Al script le podemos dar el nombre que queramos, en particular lo llamaré inicio:

cd ~/trabajo/initrd
touch inicio
chmod +x inicio

El contenido del script será el siguiente:

 
#!/bin/sh
 
# Le indicamos la ruta donde buscar los programas (/bin), también
# le indicamos el número de versión de nuestro sistema y declaramos una variable
# auxiliar listo inicialmente igual a 0
PATH="/bin"
version=1
listo=0
 
# Al utilizar el comando dmesg con el flag -n seguido de un número le estamos indicando
# qué mensajes del kernel queremos que sean logueados por la consola
# con "dmesg -n 1" le pedimos que no muestre ningún mensaje por consola
dmesg -n 1
 
# Nuestro cartel de bienvenida :P
echo -e "Linux LiveUSB \033[1;31mPsicoUSB\033[m v0.1"
 
# Montamos /proc momentaneamente para leer los parámetros que le pasamos al kernel. 
# Los parámetros del kernel se guardan en el archivo /proc/cmdline. Lo que hacemos
# es volcar los contenidos de ese archivo en la variable "comandos" y luego
# desmontamos /proc.
mount -nt proc none /pendrive >/dev/null 2>&1
comandos=`cat /pendrive/cmdline`
umount -n /pendrive >/dev/null 2>&1
 
# Por cada palabra en la variable "comandos" (/proc/cmdline)
# Si la palabra que estamos analizando es del estilo
#  variable=valor
# guardamos el nombre de la variable en la variable "cadena"
# y el valor en la variable "valor".
# Si el nombre de la variable era "espera" entonces imprimimos
# un mensaje indicando lo que estamos haciendo y luego dormimos
# por la cantidad de segundos que le pasamos como parámetro al kernel.
# Por último un mensaje de OK.
for i in ${comandos}; do
	 cadena=`echo ${i} | cut -d= -f1`
	 valor=`echo ${i} | cut -d= -f2`
	 if [ ${cadena} = "espera" ]; then
		 echo -n "Esperando al dispositivo USB..."
		 sleep ${valor}
		 echo -e "\033[75G[\033[1;32m OK \033[m]"
	 fi
done
 
# Una vez que el dispositivo USB está listo para ser utilizado procedemos
# a buscar la unidad del pendrive. Para eso recorremos todos los dispositivos
# del estilo /dev/sdX1, donde X=a,b,c,...,z (en realidad creamos menos
# dispositivos en /dev)
# Por cada una de estas unidades, la montamos en /pendrive como lectura y escritura
# Si dentro de esa unidad existe el archivo psicousb (/pendrive/psicousb) y además
# sus contenidos son igual al número de versión de nuestro sistema, establecemos
# la variable listo en 1 y salimos del ciclo for.
# De lo contrario desmontamos la unidad y probamos con otra.
echo -n "Buscando unidad del pendrive..."
for i in `ls /dev/sd?1`; do
	 mount -n -o rw ${i} /pendrive >/dev/null 2>&1
	 if [ -r /pendrive/psicousb ]; then
		 if [ "`cat /pendrive/psicousb`" = ${version} ]; then
			 listo=1
			 break;
		 fi
	 fi
	 umount -n /pendrive >/dev/null 2>&1
done
 
# Si logramos encontrar la unidad adecuada imprimimos un mensaje de OK.
# De lo contrario un mensaje de ERROR y ejecutamos una shell, quizás el usuario
# sepa montar la partición y seguir manualmente.
if [ $listo = "1" ]; then
	 echo -e "\033[75G[\033[1;32m OK \033[m]"
else
	 echo -e "\033[72G[ \033[1;31mERROR\033[m ]"
	 exec /bin/sh
fi
 
# Ya montado el pendrive en /pendrive, ahora procedo a montarlo como raíz
cd /pendrive
pivot_root . initrd
exec chroot . /sbin/init <dev/console >dev/console 2>&1
while [ true ]; do
/bin/sh
done
 

Esta última parte merece una hermosa explicación. En primer lugar cambiamos el directorio de trabajo a "/pendrive", luego utilizamos el comando pivot_root (que se traduce en la llamada al sistema del mismo nombre) indicándole que queremos que el sistema cambie la unidad raíz al directorio actual, y la unidad raíz vieja pase a estar en el directorio "initrd".

Luego ejecutamos el comando chroot cambiando el directorio raíz del proceso actual al directorio de tabajo actual ( "/pendrive" ) y además procedemos a ejecutar /sbin/init (el script de inicio del pendrive, ya fuera de la imágen initrd; la segunda etapa del inicio). Además redirigimos la entrada estándar de dev/console y redirigimos la salida estándar y la salida de error estándar a dev/console. Y además como el comando está precedido de un exec le estamos pidiendo que reemplaze la imágen en memoria de nuestro proceso con la del nuevo proceso (de esta manera este shell script queda completamente muerto, es reemplazado por la segunda etapa).

En caso de que llegara a fallar presentamos una shell al usuario.

Deben notar que siempre que realizamos el montaje de algún dispositivo utilizamos el flag -n, este flag le dice a mount que no se preocupe por el fichero /etc/mtab (el cual no existe ni remotamente, ni siquiera tenemos un directorio /etc en el initrd)

Notar también que hemos recorrido la lista de parámetros del kernel uno a uno, y por cada uno lo hemos dividido en cadena y valor, es decir, si uno de los parámetros es fichero=imagen.bin la variable "cadena" terminará siendo igual a "fichero" y la variable "valor" será igual a "imagen.bin". Luego, si la cadena es "espera" entonces dormimos la cantidad de segundos indicada en la variable "valor" De este modo podremos pasarle al kernel la cadena espera=5 indicándole que el script tiene que dormir durante 5 segundos antes de intentar montar el pendrive. Esto es así porque ni bien arranca el sistema todavía no está el dispositivo USB listo para usarse, si el valor de 5 segundos resultara poco en nuestro sistema podemos aumentarlo a nuestro gusto.

[editar] pivot_root

Quizás parezca raro realizar el chroot habiendo realizado antes el pivot_root. La razón es que si bien el sistema cambió su directorio raíz, el proceso actual sigue teniendo referencias al antiguo directorio raíz, veamos la ejecución comando a comando:

cd /pendrive (PWD=/pendrive)
pivot_root . initrd (PWD=/pendrive referido al antiguo directorio raíz)
exec chroot . /sbin/init  <dev/console >dev/console 2>&1 (PWD=/sbin referido al nuevo directorio raíz)

vean también como la redirección es <dev/console, es decir, con un PATH relativo en lugar de absoluto, pues si utilzáramos /dev/console estaríamos haciendo referencia a un archivo del viejo directorio raíz.

Nota: PWD es el directorio de trabajo actual (¿¿Process Working Directory??)

[editar] ld-linux.so

Si recuerdan cuando preparábamos la imágen del initrd nos encontramos con este archivo de biblioteca que mágicamente aparece en todos los ejecutables/librerías. Para entender qué es este archivo primero debemos entender ligeramente como carga Linux los archivos ejecutables en la memoria.

Cada archivo ejecutable (archivo en formato elf) tiene un campo llamado intérprete, el cual cumple una función similar al #!/bin/sh del shell script. En un shell script logra que sea la shell quien interprete el archivo de texto (el script); en un archivo elf hace exactamente lo mismo. Veamos qué intérprete usan los programas en Linux:

# readelf -l /bin/cat
 Elf file type is EXEC (Executable file)
 Entry point 0x8048cc0
 There are 8 program headers, starting at offset 52
 
 Program Headers:
   Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
   PHDR           0x000034 0x08048034 0x08048034 0x00100 0x00100 R E 0x4
   INTERP         0x000134 0x08048134 0x08048134 0x00013 0x00013 R   0x1
       [Requesting program interpreter: /lib/ld-linux.so.2]
   LOAD           0x000000 0x08048000 0x08048000 0x03e54 0x03e54 R E 0x1000
   LOAD           0x004000 0x0804c000 0x0804c000 0x001cc 0x00344 RW  0x1000
   DYNAMIC        0x004014 0x0804c014 0x0804c014 0x000c8 0x000c8 RW  0x4
   NOTE           0x000148 0x08048148 0x08048148 0x00020 0x00020 R   0x4
   GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4
   PAX_FLAGS      0x000000 0x00000000 0x00000000 0x00000 0x00000     0x4
 
  Section to Segment mapping:
   Segment Sections...
    00
    01     .interp
    02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame
    03     .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
    04     .dynamic
    05     .note.ABI-tag
    06
    07

Como pueden ver dice [Requesting program interpreter: /lib/ld-linux.so.2]. Ha aparecido nuestro amiguito, de modo que cada vez que se ejecuta un programa es ld-linux.so el que se encarga de "interpretarlo"; su interpretación consiste simplemente en ver que librerías dinámica usa el programa, mapearlas (cargarlas) en memoria, resolver todos los símbolos no resueltos, y todas las demás tareas que tenga que realizar antes de permitir que se ejecute el programa. De modo que en Linux el enlazador dinámico en tiempo de ejecución corre exclusivamente por parte del espacio de usuario; en otro sistemas no Unix (jejeje) esto corre por cuenta del kernel, lo cual puede mejorar levemente el desempeño, ¡¡¡pero un error en ld-linux.so es mucho menos dañino que un error en el kernel del sistema operativo!!! Veamos que pasa si le pedimos a ld-linux.so que nos dé un poco de información:

# readelf -l /lib/ld-linux.so.2
 Elf file type is DYN (Shared object file)
 Entry point 0x8d0
 There are 7 program headers, starting at offset 52
 
 Program Headers:
   Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
   LOAD           0x000000 0x00000000 0x00000000 0x1cf30 0x1cf30 R E 0x1000
   LOAD           0x01dc80 0x0001dc80 0x0001dc80 0x00910 0x009d0 RW  0x1000
   DYNAMIC        0x01defc 0x0001defc 0x0001defc 0x000c0 0x000c0 RW  0x4
   GNU_EH_FRAME   0x01cac0 0x0001cac0 0x0001cac0 0x000e4 0x000e4 R   0x4
   GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4
   GNU_RELRO      0x01dc80 0x0001dc80 0x0001dc80 0x00380 0x00380 R   0x1
   PAX_FLAGS      0x000000 0x00000000 0x00000000 0x00000 0x00000     0x4
 
  Section to Segment mapping:
   Segment Sections...
    00     .hash .dynsym .dynstr .gnu.version .gnu.version_d .rel.dyn .rel.plt .plt .text __libc_freeres_fn .rodata .eh_frame_hdr .eh_frame
    01     .data.rel.ro .dynamic .got .data __libc_subfreeres .bss
    02     .dynamic
    03     .eh_frame_hdr
    04
    05     .data.rel.ro .dynamic .got
    06

como ven no tiene ningún intérprete, lo cual es lógico, pues el es el intérprete de todos los ejecutables del sistema :) Además, si pedimos más información:

# ldd /lib/ld-linux.so.2
  statically linked

lo cual sigue siendo lógico, pues nadie esperaría que el enlazador dinámico fuera a su vez enlazado dinámicamente :)

[editar] linux-gate.so

Otra librería curiosa es linux-gate.so:

# ldd /bin/cat
       linux-gate.so.1 =>  (0xb7fe1000)
       libc.so.6 => /lib/libc.so.6 (0xb7e79000)
       /lib/ld-linux.so.2 (0xb7fe2000)

como podemos ver esta librería figura en el mapa de memoria del proceso, sin embargo no se correspondo con ningún archivo; esto requiere un poco de historia: En un principio cada vez que un programa quería realizar una llamada a un servicio del sistema utilizaba el mecanismo de interrupción por software (el famoso int 0x80), sin embargo con el tiempo se introdujeron otros mecanismos como sysenter/sysexit, syscall. Lo que logró la introducción de estos mecanismos (además de una mejora en el desempeño) fue una serie de incompatibilidades; para solucionarlas se dispuso que el kernel mapearía una página en el mapa de memoria de cada procesos, cada vez que un proceso quisiera realizar una llamada al sistema simplemente tendría que invocar un procedimiento en esta sección de la memoria, el cual se encargaría de utilizar el mecanismo adecuado (int 0x80, sysenter, etc).

Entonces, linux-gate.so no es un archivo, pero sí es parte del mapa de memoria del proceso, pues el kernel se enarga de mapear esa página en la memoria de cada proceso.

[editar] Creando la Imágen

Ahora que tenemos el directorio listo, nos falta crear el archivo initrd en si, para eso primero tenemos que saber que tan grande necesita ser el archivo, veamos como:

# cd ~/trabajo
# du -sh initrd
3,1M	initrd

si el directorio ocupa 3,1 MB entonces vamos a necesitar un archivo de imágen que sea un poco más grande (la sobrecarga del sistema de archivos), vayamos por lo seguro y hagamos uno de 4 MB:

# dd if=/dev/zero of=initrd.bin bs=1k count=4k
4096+0 records in
4096+0 records out
4194304 bytes (4,2 MB) copied, 0,0193849 s, 216 MB/s

Esa instrucción simplemente le dice al programa dd que copie del dispositivo de bloques /dev/zero (un dispositivo que no importa cuanto leas siempre te devuelve bloques lleno de ceros) en el archivo initrd.bin, con un tamaño de bloque de 1 KB (bs=1k, bs=Block Size) y que copie 4.000 unidades (count=4k). Ahora tan solo hay que darle formato:

# mkfs.ext2 initrd.bin
mke2fs 1.40.2 (12-Jul-2007)
initrd.bin no es un dispositivo especial de bloques.
¿Se continúa de todas formas? (s,n) s
Etiqueta del sistema de ficheros=
Tipo de SO: Linux
Tamaño del bloque=1024 (bitácora=0)
Tamaño del fragmento=1024 (bitácora=0)
1024 nodos i, 4096 bloques
204 bloques (4.98%) reservados para el súper usuario
Primer bloque de datos=1
Maximum filesystem blocks=4194304
1 bloque de grupo
8192 bloques por grupo, 8192 fragmentos por grupo
1024 nodos i por grupo

Mientras se escribían las tablas de nodos i: terminado                          
Escribiendo superbloques y la información contable del sistema de ficheros: hecho

Este sistema de ficheros se revisará automáticamente cada 36 meses o
180 dias, lo que suceda primero.  Utilice tune2fs -c o -i para cambiarlo.

Cuando nos pregunte si realmente queremos formatear un archivo que no corresponde con un dispositivo por bloques simplemente le decimos que si. Si quisiéramos utilizar un sistema de archivos para el cual su utilidad de formateo no quiere formatear un archivo que no sea un dispositivo por bloque simplemente podemos utilizar el dispositivo /dev/loopX (dónde X es un número) de la siguiente manera:

# modprobe loop
# losetup /dev/loop/0 initrd.bin
# mkfs.ext2 /dev/loop/0
...
# losetup -d /dev/loop/0

Listo, ahora procedemos a montar la imágen de la siguiente manera:

# mount -o loop -t ext2 initrd.bin ~/trabajo/mnt

Y ahora solamente nos resta copiar todo el contenido:

# cp -R ~/trabajo/initrd/* ~/trabajo/mnt/

el -R le dice que copie recursivamente preservando todos los atributos (por ej. que no trate de copiar los contenidos de /dev/sda1, sino que copie el archivo especial que representa el dispositivo). Luego nos encargamos de desmontar la imágen:

# umount ~/trabajo/mnt

Listo, ahora podemos comprimir la imagen si nos apetece:

# gzip -9 initrd.bin

[editar] El Pendrive

Una vez finalizado nuestro initrd nos tenemos que concentrar en preparar el pendrive, el camino no será muy diferente al del initrd, de modo que tenemos que crear una estructura de directorios que contenga librerías y binarios, en particular queremos todos los binarios que nos ayuden en el script más alguna otra cosa interesante para probar el sistema, en mi caso concreto tengo los siguientes binarios bajo /bin:

bash     chown   df     gunzip    ln     nano        rm      tar     who
bunzip2  chroot  dmesg  gzip      ls     netstat     rmdir   tty
bzip2    clear   du     hostname  mkdir  ping        setsid  umount
cal      cp      env    id        mknod  pivot_root  sleep   uname
cat      cut     find   ldd       mount  ps          stty    uptime
chmod    date    grep   less      mv     pwd         sync    touch

bajo /sbin:

apagar    ifconfig  modprobe   rmmod    udevcontrol  udevtrigger
blockdev  init      parar      route    udevd        halt
hwclock   lsmod     reiniciar  udevadm  udevsettle   reboot

y bajo /usr/bin:

ssh  wget

además las siguientes librerías en /lib (espero recuerden nuestra charla sobre ldd):

ld-linux.so.2      libncurses.so.5.6        libnss_nis.so.2
libacl.so          libncursesw.so.5         libproc-3.2.7.so
libacl.so.1        libnsl-2.6.1.so          libpthread.so.0
libacl.so.1.1.0    libnsl.so.1              libresolv-2.6.1.so
libattr.so.1       libnss_compat-2.6.1.so   libresolv.so.2
libblkid.so.1      libnss_compat.so.2       librt.so.1
libbz2.so.1        libnss_dns-2.6.1.so      libutil-2.6.1.so
libcrypt-2.6.1.so  libnss_dns.so.2          libutil.so.1
libcrypt.so.1      libnss_files-2.6.1.so    libuuid.so.1
libc.so.6          libnss_files.so.2        libz.so
libdl-2.6.1.so     libnss_hesiod-2.6.1.so   libz.so.1
libdl.so.2         libnss_hesiod.so.2       libz.so.1.2.3
libm.so.6          libnss_nis-2.6.1.so      modules
libncurses.so      libnss_nisplus-2.6.1.so  udev
libncurses.so.5    libnss_nisplus.so.2

y estas otras en /usr/lib: libcrypt.a libcrypto.so.0.9.8 libcryptsetup.so.0 libssl.so libcrypt_g.a libcryptsetup.a libcryptsetup.so.0.0.0 libssl.so.0.9.8 libcrypto.a libcryptsetup.la libcrypt.so libcrypto.so libcryptsetup.so libssl.a

Recordemos que toda esta estructura de directorios la crearemos en ~/trabajo/liveusb. A diferencia del initrd nosotros no crearemos un directorio /dev estático, sino que nos valdremos de udev para ello, pero antes de poder utilizar necesitamos todos sus binarios en /bin o /sbin o algún otro directorio dentro del PATH; en nuestro caso los hemos copiado dentro de /bin y son los siguientes archivos:

/sbin/udevadm      /sbin/udevd       /sbin/udevtrigger
/sbin/udevcontrol  /sbin/udevsettle

para copiarlos nos basta con:

# cp -a /sbin/udev* ~/trabajo/liveusb/bin

Utilizá el parámetro -a puesto que le dice a cp que preserve los archivos, es decir, que no desreferencia enlaces simbólicos y ese tipo de cosas, puesto que:

# ls -l /sbin/udev*
-rwxr-xr-x 1 root root 84544 nov 14 10:30 /sbin/udevadm
lrwxrwxrwx 1 root root     7 nov 14 10:30 /sbin/udevcontrol -> udevadm
-rwxr-xr-x 1 root root 80248 nov 14 10:30 /sbin/udevd
lrwxrwxrwx 1 root root     7 nov 14 10:30 /sbin/udevsettle -> udevadm
lrwxrwxrwx 1 root root     7 nov 14 10:30 /sbin/udevtrigger -> udevadm

también necesitaremos la carpeta /lib/udev y /etc/udev, pero supongo que ya saben como copiarla :) Dentro de /dev necesitaremos únicamente el archivo de dispositivo console (pues es la entrada y salida estándar que utilizamos en el script anterior), para ello:

# cd ~/trabajo/liveusb/dev
# mknod console c 5 1

udev se encargará del resto :)

También necesitaremos el archivo "psicousb" que si recuerdan del shell script del initrd este archivo debe contener el número de versión que en este caso es la uno:

# echo 1 > ~/trabajo/liveusb/psicousb

También queremos la base de datos de capacidades de terminal, ya que sin ella "clear", "less", "nano" y otros programas que hagan uso de ncurses o de la base de datos de capacidades de terminal de manera directa no funcionarán bien. Para incluir la base de datos:

# cp -R /usr/share/tabset ~/trabajo/liveusb/usr/share/
# cp -R /usr/share/terminfo ~/trabajo/liveusb/usr/share/
# cp -R /etc/terminfo ~/trabajo/liveusb/etc/

También copiaremos algunos archivos importantes en /etc:

# cd /etc
# cp group passwd host.conf resolv.conf hosts nsswitch.conf timezone localtime ~/trabajo/liveusb/etc/

Los archivos /etc/mtab y /etc/fstab los crearemos durante el arranque. Ahora procedemos a crear algunos archivos exclusivos de nuestra "distribución" (los explicaremos más adelante):

# cd ~/trabajo/liveusb/etc/
# echo -e "Bienvenido a \033[1;31mPsicoUSB\033[m v.01" > msjdia
# mkdir red
# cd red
# echo "10.0.0.254" > gateway
# echo "psicousb" > hostname
# echo "10.0.0.1 netmask 255.255.255.0" > net.eth0

Notar que 10.0.0.254 es mi gateway por defecto, y 10.0.0.1 es la dirección IP de mi placa de red, de esta forma podemos configurar de manera muy sencilla la red; aunque lo lógico sería o bien preguntarle al usuario o bien intentar utilizar DHCP para iniciar la red, pero me parece que esto es didáctico para ver como puede llegar a funcionar una distribución de Linux real.

El flag "-e" le dice a "echo" que interprete los caracteres de escape del estilo "\033", y la secuencia de caracteres "\033[1;31m" logra que se imprima un mensaje en color rojo, mientras que "\033[m" desactiva los colores.

También vamos a crear un archivo que nos indique qué consolas virtuales queremos en el sistema:

# cd ~/trabajo/liveusb/etc
# cat << FIN > consolas
> tty2
> tty3
> tty4
> FIN

No hemos introducido una entrada para tty1 puesto que esta siempre se creará. Ahora procedemos a crear los puntos de montaje para las distintas partes del sistema:

# cd ~/trabajo/liveusb
# mkdir proc sys initrd

En el directorio initrd estará montada la imágen correspondiente inmeditamente antes de ejecutar nuestro script de inicio, esto es así porque la llamada a pivot_root en el script del initrd cambia el directorio raíz al directorio especificado en el primer argumento, y el viejo directorio raíz lo monta en donde indica el segundo argumento.

También vamos a necesitar el kernel en el disco USB, de modo que podemos copiarlo si no lo habíamos hecho antes.

[editar] El Inicio

Ahora que terminamos de forjar la infraestructura del pendrive procedemos a crear nuestro shell script de inicio, para ello:

# cd ~/trabajo/liveusb/sbin
# touch init
# chmod +x init

Y ahora procedemos a codificar el script:

 
#!/bin/bash
# Fijamos los lugares donde buscar ejecutables, también establecemos
# el color de los mensajes de OK y los mensajes de ERROR, y además
# el color normal, además establecemos la variable auxiliar error en cero.
PATH=/bin:/sbin:/usr/bin
COLOROK=\\033[1\;32m
COLORERR=\\033[1\;31m
NOCOLOR=\\033[m
error=0
 
# Aquí definimos una pequeña función, que simplemente imprimirá el mensaje
# que le pasemos como parámetro seguida de tres puntos suspensivos, además
# no imprime el carácter de "nueva línea" al final, y soporte códigos ANSI.
# También establece la variable error en cero.
# Ej: ecartel hola
# Resultado: hola...
function ecartel {
	error=0
	echo -ne $*...
}
 
# Esta función simplemente imprime el mensaje de OK o ERROR en base al valor
# de la variable error.
function fcartel {
	if [ $error -eq 0 ]; then
		echo -e "\033[75G[${COLOROK} OK ${NOCOLOR}]"
	else
		echo -e "\033[72G[${COLORERR} ERROR ${NOCOLOR}]"
	fi
}
 
# La función ejecutar se encarga de evaluar (ejecutar) los comandos pasados
# como argumentos. En caso de existir el archivo /dev/null redirige la entrada
# y salida estándar de esos comandos a /dev/null, de lo contrario no lo hace.
# Esto es así porque temprano durante el proceso de arranque aún no disponemos
# del dispositivo null, el cual crearemos más adelante con una llamada a '''mknod'''.
# Por último actualiza el valor de la variable error con el resultado de la ejecución.
function ejecutar {
	if [ -e /dev/null ]; then
		eval $* &>/dev/null
	else
		eval $*
	fi
 
	error=$(($error | $?))
}
 
# Montamos los sistemas de archivos /proc y /sys. También montamos /dev utilizando
# el sistema de archivos tmpfs, especificándole un tamaño máximo de 5 Megas y el
# modo de acceso para el directorio raíz como 775 (rwxrwxr-x).
# Luego creamos el dispositivo /dev/null :)
# También nos encargamos de crear los directorios /dev/pts y /dev/shm, los cuales
# los montamos de manera adecuada.
ecartel "Montando sistemas de archivos"
ejecutar mount -nt proc none /proc
ejecutar mount -nt sysfs none /sys
ejecutar mount -nt tmpfs -o size=5m,mode=775 none /dev
ejecutar cd /dev
ejecutar mknod -m 666 null c 1 3
ejecutar mkdir pts
ejecutar mkdir shm
ejecutar mount -nt tmpfs -o rw,noexec,nosuid,nodev none /dev/shm
ejecutar mount -nt devpts -o rw,nosuid,noexec none /dev/pts
ejecutar cd /
fcartel
 
# Primero iniciamos udev como un servicio en segundo plano (daemon), luego utilizamos
# el comando '''udevtrigger''' para que todos los dispositivos que estaban presentes
# al momento de arrancar generen los eventos adecuados para que udev pueda crear
# sus archivos de dispositivo bajo /dev.
# Por último esperamos que la cola de eventos de udev se vacíe esperando un máximo
# de 60 segundos.
ecartel "Configurando udev"
ejecutar /sbin/udevd --daemon
ejecutar /sbin/udevtrigger
ejecutar /sbin/udevsettle --timeout=60
fcartel
 
# Aquí pedimos los contenidos de /proc/mounts, le quitamos la entrada rootfs
# y la guardamos en /etc/mtab (construcción de mtab).
# Luego a los nuevos contenidos de /etc/mtab le quitamos la entrada de initrd
# y eso lo guardamos en /etc/fstab.
# Notar que interesantemente la redirección a /etc/mtab y /etc/fstab no es
# nulificada por la redirección a /dev/null dentro de la función ejecutar.
# En caso de que en nuestro sistema no sirva se puede tranquilamente saltear
# la función ejecutar.
ecartel "Actualizando mtab y fstab"
ejecutar "cat /proc/mounts | grep -v rootfs > /etc/mtab"
ejecutar "cat /etc/mtab | grep -v initrd > /etc/fstab"
fcartel
 
# Ahora procedemos a desmontar initrd puesto que ya no lo necesitamos, y luego
# vaciamos los conteidos en ram de /dev/ram0 (el disco en RAM que contenía a initrd)
# para que ya no ocupe más espacio.
ecartel "Desmontando initrd"
ejecutar umount /initrd
ejecutar blockdev --flushbufs /dev/ram0
fcartel
 
# Utilizamos el comando '''hwclock''' para obtener la hora del sistema.
# El flag '''--hctosys''' le indica "Hardware Clock TO SYStem", lo cual le dice que tome
# el horario de la BIOS y lo actualice en el kernel de Linux.
# El flag '''--localtime''' le indica que el tiempo está en formato local (en contraposición
# con UTC, que sería '''--utc''')
ecartel "Actualizando la hora del sistema"
ejecutar hwclock --hctosys --localtime
fcartel
 
# Primero establecemos el nombre de la máquina volcando los contenidos del archivo
# /etc/red/hostname como argumento del comando hostname.
# Luego, por cada archivo del estilo /etc/red/net.* (net.eth0, net.eth1, net.nas0, etc)
# obtengo la segunda parte (eth0, eth1, nas0, etc) separando a partir del punto y tomando
# el segundo fragmento.
# Luego ejecuto el comando ifconfig en la interfaz que obtuve del nombre del archivo
# y le paso los contenidos del archivo, además de especificarle el comando up.
# De modo que si tengo el archivo /etc/red/net.eth0, con los siguientes contenidos:
#   10.0.0.1 netmask 255.255.255.0
# Lo que se termina ejecutando es:
#   ifconfig eth0 10.0.0.1 netmask 255.255.255.0 up
ecartel "Configurando interfaces de red"
ejecutar hostname `cat /etc/red/hostname`
for i in `ls /etc/red/net.*`; do
	interfaz=`echo ${i} | cut -d'.' -f2`
	ejecutar ifconfig ${interfaz} `cat ${i}` up
done
 
# Aquí establezco el gateway por defecto, el cual está listado en /etc/red/gateway.
ejecutar route add default gw `cat /etc/red/gateway`
fcartel
 
# Por cada entrada en /etc/consolas vuelco el contenido de /etc/msjdia en la terminal
# adecuada. Luego escribo el número de terminal y por último ejecuto Bash en esa terminal.
ecartel "Iniciando terminales virtuales"
for i in `cat /etc/consolas`; do
	cat /etc/msjdia > /dev/${i} 2>/dev/null
	echo "Terminal: ${i}" > /dev/${i}
	setsid bash -i </dev/${i} >/dev/${i} 2>&1 &
done
fcartel
 
# Por último ejecuto bash y muestro el mensaje del día en la primer terminal, la cual siempre
# estará presente (por eso no la listamos en /etc/consolas).
cat /etc/msjdia 2>/dev/null
while true; do
	setsid bash -i </dev/tty1 >/dev/tty1 2>&1
	echo -e "${COLORERR}No trates de matar a init!!!${NOCOLOR}"
	sleep 1
done
 

Notemos un par de cosas:

  • Cuando ejecutamos el servicio udev, si en este punto piden un listado de directorio de /dev verán que udev todavía no se tomó el trabajo de crear ningún dispositivo, sin embargo si introducen otro pendrive verán como se crean los dispositivos /dev/sdb y /dev/sdb1 (o los dispositivos correspondientes); esto es así porque todos los dispositivos que estaban ya conectados en el arranque no generan nuevos eventos de modo que udev los reconozca, para que vuelvan a generar todos los eventos ejecutamos udevtrigger. También podríamos haber hecho lo siguiente, de modo que escribamos en cada archivo uevent dentro de /sys para que el dispositivo correspondiente genere el evento adecuado. Pero además de ser una manera primitiva de hacerlo pronto dejará de ser soportada:
 
for i in `find /sys -name 'uevent'`; do
	echo > ${i}
done
 
  • Cuando ejecutamos el comando hwclock simplemente suponemos que la hora está en formato local, se podría pedirle al usuario que lo confirme, o bien utilizar un archivo (ej: /etc/reloj) indicando si es local o no. Ej:
 
function hora_local {
	hwclock --hctosys --localtime
}
 
function hora_utc {
	hwclock -hctosys --utc
}
 
hora=`cat /etc/reloj`
if [ $hora = "UTC" -o $hora = "utc" ]; then
	hora_utc
elif [ $hora = "LOCAL" -o $hora = "local" ]; then
	hora_local
else
	echo "Que hora utiliza Ud.?"
	select eleccion in local utc; do
		if [ $eleccion -a $eleccion = "utc" ]; then
			hora_utc
			break;
		elif [ $eleccion -a $eleccion = "local" ]; then
			hora_local
			break;
		fi
 
		if [ $REPLY = "UTC" -o $REPLY = "utc" ]; then
			hora_utc
			break
		elif [ $REPLY = "local" -o $REPLY = "LOCAL" ]; then
			hora_local
			break
		fi
	done
 
	if [ ! $REPLY ]; then
		hora_local
	fi
fi
 
  • Cuando ejecutamos Bash para las consolas virtuales, le anteponemos el comando setsid, esto es así porque es útil que bash pueda realizar manejo de trabajos (por ej. correr trabajos en el fondo, traerlos al frente, suspenderlos con CTRL+Z, interrumpirlos con CTRL+C, etc) pero para ello debe ser el líder de la sesión actual, entonces con setsid simpemente creamos una sesión nueva y establecemos el ID de grupo del proceso de bash como lider de la nueva sesión. Además le decimos bash que se ejecute de manera interactiva con el argumento "-i" y también redirigimos la entrada, salida y salida de error estándar a /dev/ttyX de modo que funcione en la terminal correspondiente. Además le agregamos al argumento un "&" de modo que se ejecute en el fondo y no tengamos que esperar a que termine.
  • Una vez que terminamos simplemente mostramos el mensaje del día en la terminal actual, y en un ciclo infinito arrancamos bash en una nueva sesión en la terminal 1. Si bash termina en algún momento indicamos con un cartel rojo que no se puede matar a init, dormimos un segundo y volvemos a iniciar bash. Si en lugar de esto hubiésemos ejecutado en una simple línea a bash, al salir de bash se hubiese provocado un KERNEL PANYC pues el script hubiese terminado, y como el script es el proceso 1 (init) este no puede terminar.

[editar] GRUB

Bien, ya tenemos todo listo, tan solo nos falta el gestor de arranque. Pero antes de empezar aclaremos algo: Cuando arrancamos desde un pendrive USB la BIOS por lo general nos ofrece opciones como USB-FDD, USB-ZIP, USB-CDROM o USB-HDD, aquí confiamos en que la BIOS nos deje arrancar como si fuera un disco rígido, si nuestra BIOS no lo permite quizás podamos utilizar USB-CDROM o USB-ZIP realizando los cambios pertinentes (recuerden que ahora la BIOS emula al pendrive como un CDROM o un ZIP) sin embargo si queremos utilzar USB-FDD estamos perdidas, ya nuestra imagen initrd de 4 MB es mucho más grande que un disquette de 1,44 MB. Como suponemos que la BIOS emulará un disco rígido entonces necesitamos que nuestro dispositivo USB tenga tal estructura (por lo general es así pues nos aparece tanto /dev/sda como /dev/sda1, si no tuviese particiones sería tan solo /dev/sda). Podemos asegurarnos de esto de la siguiente manera:

# fdisk -l /dev/sda
Disco /dev/sda: 1027 MB, 1027604480 bytes
32 heads, 62 sectors/track, 1011 cylinders
Units = cilindros of 1984 * 512 = 1015808 bytes
Disk identifier: 0x68bfc462

Disposit. Inicio    Comienzo      Fin      Bloques  Id  Sistema
/dev/sda1   *           1        1011     1002881   83  Linux

Como ven hay una partición y además hay suficiente espacio como para que GRUB embeba su stage 1.5, bueno, enchufemos nuestro pendrive (supondremos que es /dev/sda1) y ejecutemos los siguientes comandos:

# mkfs.ext2 /dev/sda1
# mount -t ext2 /dev/sda1 ~/trabajo/mnt
# cd ~/trabajo
# cp -R liveusb/* mnt/
# cp initrd.bin.gz mnt/
# mkdir -p mnt/grub
# cp -R /boot/grub/* mnt/grub/*
# cat << FIN > mnt/grub/menu.lst
> timeout 30
> default 0
> splashimage=(hd0,0)/grub/splash.xpm.gz
>
> # For booting GNU/Linux
> title   Live USB
> root    (hd0,0)
> kernel  /kernel root=/dev/ram0 ro init=/inicio espera=5
> initrd  /initrd.bin.gz
> FIN
# cd ~/trabajo
# umount mnt/

Vean que estamos utilizando una splashimage que Uds. no tiene porque tener, de modo que esa línea la pueden obviar. Vean como le estamos diciendo que la partición raíz es /dev/ram0 (root=/dev/ram0) esto es así porque initrd aparece reflejado en /dev/ram0 y queremos que este sea nuestro dispositivo raíz (hasta que lo cambiemos con pivot_root). También vean que le decimos donde encontrar el proceso init (dentro del initrd) y además le pasamos el parámetro espera=5 que utilizará nuestro script para saber cuanto tiempo esperar antes de escanear los dispositivos USB en busca del pendrive. Y por último noten que le pasamos la imagen del initrd :)

Ahora nos resta instalar GRUB en el pendrive, para ello ejecutamos lo siguiente:

# grub

    GNU GRUB  version 0.97  (640K lower / 3072K upper memory)

 [ Minimal BASH-like line editing is supported.  For the first word, TAB
   lists possible command completions.  Anywhere else TAB lists the possible
   completions of a device/filename. ]

grub> root (hd2,0)
 Filesystem type is ext2fs, partition type 0x83

grub> setup (hd2)
 Checking if "/boot/grub/stage1" exists... no
 Checking if "/grub/stage1" exists... yes
 Checking if "/grub/stage2" exists... yes
 Checking if "/grub/e2fs_stage1_5" exists... yes
 Running "embed /grub/e2fs_stage1_5 (hd2)"...  15 sectors are embedded. succeeded
 Running "install /grub/stage1 (hd2) (hd2)1+15 p (hd2,0)/grub/stage2 /grub/menu.lst"... succeeded
Done.

grub> quit

Asegúrense que hd2 sea en su caso el disco correcto (el pendrive), de lo contrario pueden utilizar el siguiente comando en grub:

grub> device (hd7) /dev/sda

ahora pueden estar seguros que hd7 es el pendrive (desde luego, en lugar de utilizar /dev/sda utilicen el dispositivo adecuado).

[editar] Tmpfs

Anteriormente dijimos que tmpfs era un disco en RAM, pero la verdad es que era una pequeña mentira para seguir adelante. Un disco en RAM es simplemente un dispositivo por bloques, y como tal no conoce absolutamente nada de archivos, directorios y demás; de modo que necesita una capa de abstracción más, el "sistema de archivo" como por ejemplo ext2, reiserfs, xfs, etc. Sin embargo, estos sistemas de archivos están diseñados para trabajar en discos reales donde siempre se tiene el mismo tamaño, es decir, un disco rígido no puede crecer y achicarse al vuelo (no, un disco rígido no, si me hablan de LVM y demás yerbas quizás si) de modo que un disco en RAM sigue el mismo camino; pero esto es una lástima ya que el disco en RAM está limitado, si nos falta memoria no nos podemos deshacer de un pedazo del disco en RAM, y si el disco en RAM nos queda chico no lo podemos agrandar de manera dinámica (¿¿LVM sobre discos en RAM?? No, no lo creo :P).

Pero aquí llego tmpfs. Este sistema de archivos no requiere un disco en RAM detrás de él, pues utiliza la caché de disco de Linux como dispositivo por bloque; lo interesante de esto es que la caché de Linux está constantemente fluctuando de tamaño y tmpfs es un sistema de archivo diseñado especialmente para manejarse en un medio que cambia de tamaño automáticamente. Supongamos que nos quedamos sin memoria, entonces simplemente podemos borrar un archivo de nuestro directorio tmpfs y automáticamente liberará la memoria; y también podemos decir que inicialmente no consume memoria y a medida que vamos creando archivos el consumo empieza aumentar. Por suerte le podemos establecer un tamaño máximo de modo que el sistema de archivos no crecerá más que eso.

Otra ventaja de tmpfs además de su dinamismo es que reside en la caché de disco de Linux, ¿y qué quiere decir esto? Cuando accedemos a un disco en RAM común y corriente Linux guarda en la caché este acceso como si fuera un disco común y corriente, ¡pero no lo es! sería mucho más eficiente utilizar al mismo disco en RAM como caché. Con tmpfs no hay problemas porque el sistema de archivos utiliza la misma caché, de modo que no hay duplicados y todo se vuelve más eficiente.

[editar] Apagar el Sistema

Si prestaron atención, arriba había unos archivos apagar, reiniciar y parar; esto es así porque si utilizamos los comandos halt, reboot o shutdown que trae nuestro querida distribución no lograremos mcuho. Estos comandos se comunican con el proceso init (a través de la tubería /dev/initctl) para decirle que queremos apagar el sistema; init se encarga luego de cambiar de runlevel, ejecutar los scripts de finalización, quienes a su vez ejecutan halt o reboot con unos flags especiales que le indican que NO se comuniquen con init, sino que simplemente apaguen o reinicien la computadora, puesto que ya está preparada para ser apagada. Como nosotros no tenemos al proceso init tradicional de Linux lo que vamos a hacer es un pequeño script que se encargue de apagar la computadora, el cual será simplemente un envoltorio para reboot y halt.

  • /sbin/apagar:
 
#!/bin/bash
halt -fdihp
 
  • /sbin/reiniciar:
 
#!/bin/bash
reboot -fdhip
 
  • /sbin/parar:
 
#!/bin/bash
halt -fdih
 

Los flags tienen los siguientes significados:

  • -f: Forzar el apagado (sin llamar a shutdown/avisar a init)
  • -d: Sin escribir wtmp (el cual no tenemos en nuestro pequeño sistema)
  • -h: Llevar discos IDE a standby (lo cual hace además que escriban sus caches)
  • -i: Apagar las interfaces de red
  • -p: Power-off (apagar la computadora)

Listo, ya podemos apagar el equipo.

[editar] initramfs

Hasta ahora hemos utilizado initrd, pero esa es la antigua manera de hacer las cosas. Hoy por hoy se utliza initramfs, ¿por qué? pues porque tiene dos grandes ventajas:

  1. No hay que crear una imágen de tamaño fijo para luego formatearla y cargarle archivos, simplemente se crea un archivo comprimido con los contenidos, lo cual se hace en una línea de código.
  2. Initramfs utiliza el sistema de archivos tmpfs, lo cual permite que el espacio ocupado en RAM aumente o disminuya de manera dinámica.

Veamos las diferencias:

  1. No especificaremos quién será init mediante el parámetro init=..., siempre se ejecutará el archivo /init
  2. No podemos utilizar pivot_root.

La diferencia más importante es que no podemos utilizar pivot_root. Cuando utilizábamos initrd, este aparecía bajo el dispositivo /dev/ram0, el cual era montado como dispositivo raíz, al utilizar pivot_root estábamos moviendo /dev/ram0 a otro lugar y montábamos un dispositivo nuevo como raíz.

Hay veces que para facilitar el manejo de estructuras de datos o algoritmos se toman decisiones como por ejemplo que una lista nunca pueda quedar vacía; esto es lo que han hecho los desarrolladores del kernel de Linux al decir que la lista de sistemas de archivos montados nunca puede quedar vacía, siempre existe al menos la entrada rootfs. Cuando utilizábamos initrd en realidad teníamos por lo menos dos sistemas de archivos montados:

  1. rootfs en /
  2. /dev/ram0 en /

el segundo "tapaba" al primero, al utilizar pivot_root, lo que lográbamos era tener algo así:

  1. rootfs en /
  2. /dev/sda1 en /

pero rootfs siempre está. Con initramfs la situación cambia un poco, los contenidos del archivo comprimido se vuelcan directamente sobre rootfs, si quisiéramos utilizar pivot_root le estaríamos diciendo que mueva rootfs a otro lugar, y encima luego lo desmontaríamos; todo eso sería catastrófico puesto que la lista de sistemas de archivos no puede quedar vacía. ¿Qué hace Linux? nos prohíbe utiliar pivot_root.

Para realizar una tarea similar a la que hacía pivot_root haremos lo siguiente:

  1. Borrar el contenido de / (rootfs) sin descender a los otros sistemas de archivos, de esta manera liberamos memoria. Sería el equivalente a liberar la memoria utilizada por /dev/ram0.
  2. Montar el pendrive encima de rootfs, de modo que nos queda la siguiente estructura:
    1. rootfs en /
    2. /dev/sda1 en /

Todo esta funcionalidad y más la podremos encontrar en BusyBox, pero como no todos tenemos ganas de instalarlo lo que haremos será nuestro propio programa que se encargue de esto, es básicamente un estracto modificado de BusyBox (por si quedan dudas, la licencia del código es la GPL, y el programa original se llama switch_root):

 
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/fcntl.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/vfs.h>
 
#ifndef RAMFS_MAGIC
#define RAMFS_MAGIC 0x858458f6
#endif
 
#ifndef TMPFS_MAGIC
#define TMPFS_MAGIC 0x01021994
#endif
 
#ifndef MS_MOVE
#define MS_MOVE 8192
#endif
 
static dev_t raiz;
 
// Esta función simplemente comprueba que la raíz sea válida
int camino_valido ( const char *raiz_nueva )
{
	struct stat s1, s2;
	struct statfs fs1;
 
	// Nos aseguramos que / y la nueva raíz estén en dispositivos diferentes, y además que la nueva raíz
	// sea un directorio.
	errno = 0;
	if ( lstat ( "/", &s1 ) || lstat ( raiz_nueva, &s2 ) || s1.st_dev == s2.st_dev || !S_ISDIR ( s2.st_mode ) )
	{
		if ( errno )
			perror ( "lstat" );
		else
			fprintf ( stderr, "Debe elegir un directorio raíz válido\n" );
		return -1;
	}
 
	// Nos aseguramos que la vieja raíz esté sombre TMPFS o RAMFS, y además que exista el archivo /init
	// lo cual quiere decir que probablemente sea initramfs.
	errno = 0;
	if ( statfs ( "/", &fs1 ) || lstat ( "/init", &s2 ) || !S_ISREG ( s2.st_mode )
	 || ( fs1.f_type != TMPFS_MAGIC && fs1.f_type != RAMFS_MAGIC ) )
	{
		if ( errno )
			perror ( "statfs/lstat" );
		else
			fprintf ( stderr, "Tipo de sistema de archivos incorrecto.\nDebería ser TMPFS\n" );
		return -1;
	}
 
	// Guardamos el dispositivo raíz para después.
	raiz = s1.st_dev;
	return 0;
}
 
// Borramos todos los archivos recursivamente sin descender a otros dispositivos.
int borrar ( const char *camino )
{
	DIR *dir;
	struct dirent *entrada;
	struct stat s;
	char *nombre;
 
	// Si el directorio a borrar está en otro dispositivo, entonces no entramos.
	if ( lstat ( camino, &s ) || s.st_dev != raiz )
		return 0;	
 
	// Abrimos el directorio
	if ( ( dir = opendir ( camino ) ) == NULL )
	{
		perror ( "opendir" );
		return -1;
	}
 
	// Por cada entrada en el directorio...
	errno = 0;
	while ( ( entrada = readdir ( dir ) ) != NULL )
	{
		// Slatamos . y ..
		if ( entrada->d_name [ 0 ] == '.' && ( entrada->d_name [ 1 ] == '\0' ||
			( entrada->d_name [ 1 ] == '.' && entrada->d_name [ 2 ] == '\0' ) ) )
			continue;
 
		// ¿Es un directorio?
		if ( entrada->d_type == DT_DIR )
		{
			// Entonces generamos una cadena del estilo camino_viejo/directorio
			// Espacio para camino, /, d_name y '\0'
			nombre = malloc ( strlen ( entrada->d_name ) + strlen ( camino ) + 2 );
			if ( ! nombre )
			{
				fprintf ( stderr, "Error al pedir memoria\n" );
				errno = 0;
				continue;
			}
			sprintf ( nombre, "%s/%s", camino, entrada->d_name );
			borrar ( nombre ); // Borramos el directorio
			free ( nombre );
		}
		else
			unlink ( entrada->d_name ); // Era un archivo, lo borramos
 
		errno = 0;
	}
 
	if ( errno )
	{
		perror ( "readdir" );
		closedir ( dir );
		return -1;
	}
 
	closedir ( dir );
	rmdir ( camino );
	return 0;
}
 
int main ( int argc, char **argv )
{
	char *raiz_nueva;
 
	if ( argc != 3 )
	{
		fprintf ( stderr, "Uso: switch_root raiz_nueva programa\n" );
		return -1;
	}
 
	// Nos aseguramos que sea init el que ejecute este programa
	if ( getpid () != 1 )
	{
		fprintf ( stderr, "No sos INIT, lo siento :(\n" );
		return -1;
	}
 
	// Comprobamos la validez de la nueva raíz
	argv++;
	raiz_nueva = *argv++;
	if ( camino_valido ( raiz_nueva ) )
		return -1;
 
	// Cambiamos de directorio, ahora estamos en la nueva raíz
	chdir ( raiz_nueva );
 
	// Borramos la raíz vieja
	if ( borrar ( "/" ) )
		fprintf ( stderr, "No se pudo liberar toda la memoria\n" );
 
	// Movemos de manera atómica la raíz nueva a /
	// Luego cambiamos nuestro directorio raíz al directorio de trabajo actual
	// lo cual es necesario para que se actualicen algunos campos del proceso
	// en el kernel de Linux.
	// Luego cambiamos de directorio de trabajo a /, para que ya no sea algo como /pendrive
	// es decir, seguimos actualizando nuestro proceso
	if ( mount ( ".", "/", NULL, MS_MOVE, NULL ) || chroot ( "." ) )
	{
		perror ( "mount/chroot" );
		return -1;
	}
	chdir ( "/" );
 
	// Cerramos la vieja consola y abrimos la nueva.
	close ( 0 );
	close ( 1 );
	close ( 2 );
 
	open ( "/dev/console", O_RDWR );
	dup2 ( 0, 1 );
	dup2 ( 0, 2 );
 
	// Ejecutamos el nuevo init (que pasamos como argumento)
	execve ( *argv, argv, NULL );
	perror ( "execve" );
	return -1;
}
 

Ahora simplemente compilamos:

# gcc cambiar_raiz.c -o cambiar_raiz

ya podemos copiar este ejecutable a /bin dentro de nuestro directorio de trabajo de initrd. Seguidamente procedemos a actualizar el script de inicialización /init:

#cd /pendrive
#pivot_root . initrd
#exec chroot . /sbin/init <dev/console >dev/console 2>&1
#/bin/sh
exec cambiar_raiz /pendrive /sbin/init
exec /bin/sh
/bin/sh

Nuestro programa se encargó de todo. Noten que antes utilizábamos direcciones relativas (dev/console) y ahora utilizamos direcciones absolutas (/dev/console), esto se puede hacer porque nuestro programa ya actualizó todos los datos y referencias del proceso. Por último, generamos initramfs de la siguiente manera:

# cd ~/trabajo/initrd
# find . | cpio -H newc -o | gzip -9 > ../initrd.bin.gz

ahora ya tenemos el archivo ~/trabajo/cpio.gz que podemos guardar en el pendrive. Por último modificamos el archivo menu.lst:

# cd ~/trabajo/liveusb/grub
# cat menu.lst | sed 's/root=\/dev\/ram0 ro init=\/inicio //' > menu2.lst
# mv menu2.lst menu.lst

el que quiera realizar el trabajo a mano, simplemente deberá cambiar la línea

kernel  /kernel root=/dev/ram0 ro init=/inicio espera=5

por la línea

kernel /kernel espera=5

Ya deberíamos tener nuestro initramfs funcionando.

[editar] TIPS

  • En lugar de haber copiado un montón de ejecutables (chown, ls, chmod, ps, etc.) podríamos tan solo haber copiado busybox y luego realizar enlaces simbólicos al mismo para poblar el sistema:
# ln -s busybox ls
# ln -s busybox chown
# ln -s busybox chmod
# ln -s busybox ps
# ln -s busybox lsattr
# ln -s busybox telnet
...
  • Podemos volver nuestro entorno un poco más agradable si le agregamos colores al prompt y si le pedimos a comandos como ls o grep que marquen con colores los distintos archivos o coincidencias, para eso podemos crear el archivo /etc/bash/bashrc con los siguientes contenidos:
export PS1="[\033[1;31m\u\033[1;33m@\033[1;32m\h \033[1;34m\W\033[m]# "
alias ls="ls --color=auto"
alias grep="grep --color=auto"
  • Como habrán notado hemos incluído a ext2 como sistema de archivos en lugar del mejorado ext3, esto es así porque ext3 es un sistema con "journaling" lo que se traduce en escrituras en un lugar dado del pendrive una y otra vez, lo cual puede "gastar" la zona en particular; por eso elegimos un sistema sin "journaling". Además, en general, ni ext2 ni ext3 están diseñados para trabajar con pendrives, una mejor opción es JFFS2 que está diseñado con ese propósito, aquí no lo utilizamos para poder trabajar con herramientas más conocidas.

[editar] Recursos

[editar] Recursos en Inglés

Herramientas personales