Mente inquieta. Linuxero empedernido.
Hace muchos años, la programación multihilo solo servía para darnos a los usuarios la sensación de multitarea y que el ordenador pudiera respondeer cuando interactuábamos con él. Normalmente sólo teníamos un único núcleo de CPU y solo podía ejecutar una cosa en un momento del tiempo determinado. Aunque el sistema operativo se encargaba de ir cambiando las tareas que iba ejecutando varias veces por segundo, y por eso podíamos estar viendo una página web y al mismo tiempo chateando con otro programa, copiando un archivo, etc. Esta multitarea también nos ayudaba mucho cuando una aplicación estaba esperando un dato del disco o de un dispositivo (ratón, teclado, red...), ya que estos dispositivos son mucho más lentos que la CPU del ordenador. Es decir, mientras la CPU puede procesar millones de operaciones por segundo, pedir un dato a un disco duro, puede tardar lo mismo que tardarían varios miles de operaciones, y si el dato viene de Internet, puede tardar mucho más. Así que los sistemas operativos multitarea, suspenden la ejecución de dicha aplicación hasta que el dato esté listo, y así aprovechan los ciclos de procesador que podían haberse perdido para hacer cosas más productivas como la descompresión de fotogramas de un vídeo de gatitos.
Eso sí, si en una CPU monohilo ejecutamos dos tareas que requieren CPU de forma intensiva como la compresión de vídeo, renderizado 3D o aplicación de filtros sobre imágenes, nos daremos cuenta de que el tiempo total de la ejecución de las tres tareas es aproximadamente el mismo que ejecutar una tarea detrás de otra.
Pero en los tiempos que corren, es muy común encontrar procesadores con varios núcleos, incluso con varios hilos por núcleo. Al tener varios núcleos, podremos ejecutar varios programas y normalmente el sistema operativo se encarga de colocarlos en uno u otro núcleo, de forma que, ya que nuestro procesador puede ejecutar varias cosas a la vez, aprovechemos esa potencia para terminar las tareas antes.
Aunque ahora el problema suele ser que las aplicaciones no están pensadas para ejecutarse en varios hilos. Bueno, actualmente muchas aplicaciones pueden aprovechar la potencia de los procesadores modernos, como compresores de vídeo, software de minería de criptomonedas, tratamiento de imágenes etc. Pero, muchos programas comunes de búsqueda de datos en ficheros, compresión/descompresión, y muchos de nuestros scripts en Bash o Python no están preparados.
Cuando, en Bash queremos ejecutar dos tareas a la vez, siempre podemos ejecutarlo con un & al final, volveremos a nuestro shell para ejecutar más cosas mientras el programa inicial se está ejecutando:
$ programa & 19238 $ … ejecuto más cosas …
O también podemos ejecutarlo normalmente, pulsar Control+Z (^Z). El programa se detendrá, si escribimos en la consola "fg" el programa seguirá en primer plano (foreground), pero si ponemos "bg" el programa seguirá en segundo plano (background), y mientras éste está corriendo, podremos ejecutar más cosas:
$ programa … ^Z [1]+ Stopped programa … ejecuto más cosas … $ fg programa … programa sigue ejecutándose … ^Z [1]+ Stopped programa $ bg [1]+ programa & … puedo ejecutar lo que quiera …
Con esto puede ser suficiente para muchos de nosotros pero, ¿y si queremos utilizar más hilos? Es un poco pesado ejecutar varios programas con sus argumentos y una & al final. ¿O queremos que los argumentos vayan cambiando según la ejecución? ¿O incluso queremos ejecutar cientos de tareas pero que se vayan repartiendo la CPU de 2 en 2, de 4 en 4 o más dependiendo del número de núcleos que tengamos? Para eso, y mucho más, tenemos la utilidad GNU Parallel.
Un ejemplo típico es crear un programa que calcule dígitos del número PI con un hilo de CPU, es muy fácil con bc:
#!/bin/bash echo "scale=4000; a(1)*4" | bc -l
Lo salvamos como pi.sh y le damos permiso de ejecución con:
$ chmod +x pi.sh
Este programa, ejecutado en mi ordenador tarda unos 11 segundos en terminar. Suponiendo que nuestro equipo tenga 2 núcleos, podemos hacer:
$ time ./pi.sh & time ./pi.sh
Veremos que tarda también 11 segundos (más o menos). Esto lo podemos hacer con GNU Parallel de esta forma:
$ time seq 2 | parallel -n0 ./pi.sh
Donde se ejecutará el comando pi.sh dos veces y de forma simultánea. Pero, aunque esta orden pueda parecer complicada, podemos jugar un poco con ella, para ejecutar pi 4 veces de forma simultánea:
$ time seq 4 | parallel -n0 ./pi.sh
Si nuestra CPU tiene 4 núcleos, seguirá tardando 11 segundos, en realizar cuatro tareas, pero si nuestra CPU tiene 2 núcleos, tardará más de 22 segundos. Ya que las tareas utilizan mucho la CPU y es un trabajo extra para el sistema operativo intercalarlas para que se ejecuten a la vez. Pero, imaginémonos que queremos ejecutar 4 veces pi, pero que se repartan de 2 en 2, para ello podemos hacer:
$ time seq 4 | parallel -n0 -j2 ./pi.sh
Si queremos, podemos ejecutar un gestor de tareas o el comando "top" con el que veremos qué tareas están en ejecución cada vez.
Ahora vamos a modificar pi.sh así:
#!/bin/bash DECIMALES=$1 echo "scale=$1; a(1)*4" | bc -l
De modo que tengamos que introducir el número de dígitos que queremos de Pi. Así ejecutaremos pi.sh de forma concurrente pero con parámetros diferentes en cada ejecución. Podemos jacer lo siguiente:
$ time echo "1000 2000 3000 4000" | tr ' ' '\n' | parallel ./pi.sh
O si queremos que solo haya dos ejecuciones de pi.sh simultáneas:
$ time echo "1000 2000 3000 4000" | tr ' ' '\n' | parallel -j2 ./pi.sh
Nota: El uso de tr es porque parallel necesita que los argumentos vayan uno por línea. Si los argumentos los tenemos en un archivo y uno por línea podríamos hacer:
$ cat argumentos | parallel -j2 ./pi.sh
Con GNU Parallel podemos incluso combinar argumentos. En este ejemplo, cambiamos de base varios números binarios a base 10
$ parallel echo -n {1}{2}{3}{4} = \;'echo "ibase=2;{1}{2}{3}{4}" | bc' ::: 0 1 ::: 0 1 ::: 0 1 ::: 0 1 0000 =0 0001 =1 0010 =2 0011 =3 0100 =4 0101 =5 0110 =6 0111 =7 1000 =8 1001 =9 1010 =10 1011 =11 1100 =12 1101 =13 1110 =14 1111 =15
Y como vemos, GNU Parallel, además de proporcionarnos una forma sencilla de ejecutar programas simultáneamente, nos proporciona también una forma de combinar argumentos y generar resultados muy potente. Utilizando {1}, {2},...{n} dependiendo del número de argumento que estemos tratando
Uno de mis usos preferidos, es la conversión automática de archivos, o tratamiento de imágenes con ImageMagick. Además, podemos automatizar esta conversión para realizarla en cientos de archivos y despreocuparnos del ordenador ya que él se encarga de hacer el trabajo. Una tarea muy sencilla es convertir un archivo de JPG a PNG con convert. Pero, ¿si tenemos una carpeta llena de archivos? Podemos ejecutar convert en todos ellos, y hacerlo uno detrás de otro, pero podríamos hacerlo, por ejemplo de 2 en 2 o de 4 en 4 dependiendo dee nuestra CPU, incluso podríamos subir a 3 o 5 porque la conversión de archivos implica lecturas y escrituras y es tiempo que podemos estar procesando datos (los archivos convertidos entrarán en el directorio convertidas):
$ find -maxdepth 1 -name ‘*.jpg’ | parallel --progress -j3 convert -verbose {} convertidas/{/.}.png
Fijaos cómo en el segundo argumento le quito la extensión para llamarla png. Podría variar el número del argumento -j según mis preferencias. Además, he introducido --progress que nos indicará el progreso de la tarea.
Como último ejemplo, nos basaremos en el anterior. Pero esta vez, utilizaremos zenity para presentar una barra de progreso de manera gráfica. ¡Porque, que sea terminal no significa que no sea visual!
$ find -maxdepth 1 -name ‘*.jpg’ | parallel --bar convert {} convertidas/{/.}.png 2> >(zenity --progress --auto-kill)
¡Espero que les haya resultado útil!
Normalmente tenemos en ejecución decenas de servicios, y todos deben estar en ejecución en todo momento. Puede que si somos usuarios de escritorio y tenemos un problema, lo detectemos al momento y reiniciemos el ordenador, o incluso investiguemos qué se ha detenido, lo reiniciamos y continuamos trabajando.
Aunque, si tenemos un VPS, una Raspberry PI o un NAS con un pequeño servidor, o cualquier sistema que tenga que estar todo el día encendido de forma independiente ejecutando un sistema GNU/Linux tenemos que tener en mente que los servicios fallan en algún momento. Ya sea por ejecutar una versión experimental, descubrir un bug, o simplemente quedarte sin memoria o disco. La clave es tener previsto que esos servicios pueden sufrir un problema. En el caso de tener un VPS con una web o un blog, significaría dejar a los usuarios sin poder acceder si por ejemplo se cae el servidor web o el servidor de aplicación. O en el caso de una Raspberry implicaría conectarnos o conectar monitor y teclado para reiniciar el servicio a mano, cuando debería ser algo que funcione de manera independiente.
Y como actualmente muchas distribuciones incluyen Systemd, vamos a hacerlo utilizando este sistema.
NOTA: En este tutorial, estoy utilizando sudo para ejecutar acciones con privilegios de root, pero puede que tu sistema no tenga sudo configurado. Así que tendrás que alcanzar el usuario root para ejecutar los comandos.
Para ello, tenemos que tener claro cómo se llama el servicio. En Systemd todos acaban con ".service", algunos ejemplos de servicios pueden ser: apache, nginx (servidores web), sshd (servidor ssh), ufw (firewall), etc.
Podemos obtener un listado de los servicios de nuestro sistema así
sudo systemctl list-units --type=service
Vamos a probar, por ejemplo con el servicio apache2.service, es un servidor web y será muy fácil hacer pruebas con él.
Es posible que este servicio falle. Y es algo que pasa en servidores del mundo real. Puede ser por algún fallo que se haya descubierto y haya sido explotado por un atacante, falta de recursos en el servidor, o incluso un fallo en un módulo, pero no podemos renunciar a ofrecer el servicio a nuestros usuarios. Así que, vamos a matar el proceso, para que éste finalice repentinamente sin contar con systemd:
sudo killall -9 apache2
Lo ejecutamos varias veces, hasta que nos aseguremos de que el proceso está realmente muerto. Si queremos, podemos intentar acceder al servidor web, y asegurarnos de que no responde.
Ahora, vamos a editar el archivo de descripción del servicio. Lo bueno de systemd es que no tenemos que editar el archivo original que describe el servicio, en mi caso /lib/systemd/system/apache2.service. Editar el archivo original implica que si actualizamos el sistema, una actualización puede deshacer nuestros cambios y nos obligaría a revisarlos cada vez que actualicemos. En su lugar podemos editar el archivo /etc/systemd/system/apache2.service o, en versiones más modernas de systemd, podemos ejecutar:
sudo systemctl edit apache2.service
Esto directamente nos mostrará un editor de texto (vi, nano, etc) con un fichero en blanco desde el que podemos introducir modificaciones al fichero original. Los cambios introducidos aquí se sumarán al fichero original y systemd lo interpretará como un archivo que contiene todas las directivas conjuntas. El comando, crea un directorio llamado /etc/systemd/system/apache2.service.d/ y dentro de ese directorio creará el archivo override.conf.
En dicho editor introduciremos lo siguiente:
[Service] Restart=always
Con esto indicaremos a systemd que debe reiniciar el servicio siempre que se pare. Guardamos el fichero y ejecutaremos ahora:
sudo systemctl daemon-reload sudo systemctl daemon-reload
Con este comando indicaremos a systemd que debe leer de nuevo todos los ficheros de configuración de los servicios. Ya podemos reiniciar el servicio (porque lo dejamos parado antes):
sudo systemctl restart apache2.service
o
sudo service apache2 restart
Este segundo es el antiguo método, aunque systemd lo mantiene. Para comprobar que realmente funciona este método, podemos esperarnos un minuto y pedir el estado del proceso apache2, para ello ejecutamos:
sudo systemctl status apache2.service
Y veremos algo parecido a esto:
● apache2.service - The Apache HTTP Server
Loaded: loaded (/lib/systemd/system/apache2.service; enabled; vendor preset: ena
Drop-In: /lib/systemd/system/apache2.service.d
└─apache2-systemd.conf
/etc/systemd/system/apache2.service.d
└─override.conf
Active: active (running) since Sun 2018-11-18 14:15:51 CET; 1min 19s ago
Process: 13229 ExecStart=/usr/sbin/apachectl start (code=exited, status=0/SUCCESS
Main PID: 13245 (apache2)
Tasks: 55 (limit: 4915)
CGroup: /system.slice/apache2.service
├─13245 /usr/sbin/apache2 -k start
├─13248 /usr/sbin/apache2 -k start
└─13249 /usr/sbin/apache2 -k start
En la línea que comienza por Active, veremos cuánto tiempo lleva el proceso arrancado, en este caso 1 minuto y 19 segundos. Ahora, si matamos el proceso apache2 y pedimos el estado del proceso de nuevo, ahora veremos que el proceso sigue activo, pero se habrá iniciado hace unos segundos nada más. También veremos que los PID de los procesos son diferentes, y veremos una líneas indicando este reinicio.
Si queremos ver el log del proceso para saber el momento en el que se ha reiniciado, o ver directamente si se ha producido este reinicio y diagnosticarlo, porque lo ideal es que esto nunca suceda, podemos ejecutar:
sudo journalctl -u apache2.service
Ya hemos conseguido que los procesos se reinicien automáticamente, ahora vamos a afinar un poco esta configuración. Puede que nuestro servicio necesite unos segundos antes de levantarse, para lo que podremos editar el archivo de descripción del servicio e introducir:
RestartSec=2s
Y así esperaremos 2 segundos antes de reiniciarlo.
Incluso podemos limitar el número de veces que se reiniciará dentro de un intervalo de tiempo, por ejemplo, le podemos decir que si en un intervalo de 10 segundos, el servicio se reinicia 3 veces, no intentemos reiniciarlo más. Imaginemos que el equipo se queda sin disco, y vamos a tener a systemd reiniciando constantemente un servicio, ocupando CPU y memoria y puede que impidiendo la administración remota. Lo podemos hacer introduciendo estas líneas en la descripción del servicio:
StartLimitBurst=3 StartLimitIntervalSec=10
Y por supuesto, podemos probarlo matando el servicio varias veces. Incluso systemd nos indicará en el estado que ha superado el límite de reinicios y por eso el servicio se ha detenido.
Otra buena idea es, por ejemplo, enviar un correo electrónico al administrador en caso de reinicio de determinados servicios. Para ello, en esta misma descripción podemos incluir:
ExecStartPre=/usr/local/bin/correo_error_admin.pl
Luego, podemos crear el script que envía el correo, por ejemplo en Perl, introduciendo lo siguiente en el script /usr/local/bin/correo_error_admin.pl :
#!/usr/bin/perl use strict; use warnings; # first, create your message use Email::MIME; my $message = Email::MIME->create( header_str => [ From => <a href="mailto:Esta dirección de correo electrónico está siendo protegida contra los robots de spam. Necesita tener JavaScript habilitado para poder verlo.'">Esta dirección de correo electrónico está siendo protegida contra los robots de spam. Necesita tener JavaScript habilitado para poder verlo.'</a>, To => <a href="mailto:Esta dirección de correo electrónico está siendo protegida contra los robots de spam. Necesita tener JavaScript habilitado para poder verlo.'">Esta dirección de correo electrónico está siendo protegida contra los robots de spam. Necesita tener JavaScript habilitado para poder verlo.'</a>, Subject => 'Error en tu servidor', ], attributes => { encoding => 'quoted-printable', charset => 'ISO-8859-1', }, body_str => "Lo siento, tu servidor se ha reiniciado\n", ); # send the message use Email::Sender::Simple qw(sendmail); sendmail($message);
En este script podemos incluir cualquier acción que nos interese automatizar cuando se inicie el servicio. Incluso podemos utilizar ExecStartPost= para ejecutar una acción cuando el servicio se haya iniciado y de la misma manera ExecStopPre= y ExecStopPost= para ejecutar acciones cuando paremos el servicio (pero no cuando el servicio se detenga o muera).
¡Espero que les haya sido útil!
Bash nos permite hacer millones de cosas. Es más, como nos permite hacer llamadas a otros programas, colocando las entradas y salidas adecuadas podremos producir cualquier resultado. Nos viene muy bien poder encadenar varios comandos, como en este tutorial en el que la salida de echo la pasamos como entrada de tr. Pero en este tutorial quiero hablar de operaciones que podemos hacer directamente en Bash, sin ningún programa extra o dependencia. Además, su realización será mucho más rápida ya que no tenemos que cargar en memoria un programa. Esto nos resultará de gran ayuda si utilizamos sistemas empotrados como Raspberry PI, o cualquier dispositivo con chips o almacenamiento reducidos.
Eso sí, para muchos ejemplos necesitaremos Bash 4.x (aunque ya tiene unos años, la versión salió en 2009)
¡Manos a la obra!
Aunque existen herramientas como sed o awk que, no estoy diciendo que no las usemos, porque son herramientas muy potentes. Cuando se trata de hacer un simple reemplazo de un texto por otro dentro de un texto más grande. O lo que es lo mismo, reemplazar subcadenas dentro de una cadena, podemos hacer lo siguiente:
texto="Los mejores tutoriales sobre Windows en WindowsCenter" echo ${texto//Windows/Linux}
La clave está en tener el texto en una variable y luego hacer
echo ${VARIABLE//Cadena a buscar/Cadena que reemplaza}
O también podemos hacer lo siguiente:
texto="No hables de Linux en LinuxCenter" echo ${texto/Linux/Windows}
Fijaos que ahora solo hay una barra (/) en lugar de dos. Con las sustituciones podemos hacer muchas cosas, incluso utilizar expresiones regulares.
Es muy común hacer lo siguiente:
texto="cadena de caracteres muy muy larga" echo $texto | wc -m
Pero podemos hacerlo sin llamar a wc (cuidado, también podemos utilizar -c, pero tendremos problemas con caracteres especiales) de la siguiente manera:
texto="cadena de caracteres muy muy larga" echo ${#texto}
Una nota, si comparáis los dos resultados, saldrán distintos, el primer método tendrá un carácter más. Eso es porque contamos el "intro" o mejor dicho, el retorno de carro, cuando utilizamos la primera forma. También podríamos hacerlo así:
texto="cadena de caracteres muy muy larga" echo -n $texto | wc -m
¿Qué ocurre si queremos sacar una subcadena dentro del texto? Por ejemplo, tenemos esta cadena de texto:"Los mejores tutoriales sobre Linux en LinuxCenter"
Y queremos extraer 10 letras desde el carácter número 12, podemos hacer:
texto="Los mejores tutoriales sobre Linux en LinuxCenter" echo ${texto:12:10}
O si queremos sacar desde el carácter 37 hasta el final, podemos hacer:
texto="Los mejores tutoriales sobre Linux en LinuxCenter" echo ${texto:37}
Si tengo este texto: "Esto es un pequeño tutorial de Linux en el que hablo de Bash."
Y quiero saber si una subcadena está presente, puedo hacer lo siguiente:
texto="Esto es un pequeño tutorial de Linux en el que hablo de Bash." if [[ "$texto" == *"Windows"* ]]; then echo "ESTÁ"; else echo "NO ESTÁ"; fi
Si tenemos un nombre de archivo con la ruta completa y queremos extraer el directorio donde está localizado el fichero podemos hacer lo siguiente:
fichero="/usr/share/icons/hicolor/64x64/mimetypes/libreoffice-oasis-data.png" echo "${fichero%/*}"
En este caso obtendremos: /usr/share/icons/hicolor/64x64/mimetypes/ . En teoría estamos eliminando de la cadena desde el final hasta la última / y devolvemos lo que nos queda. Al final obtenemos lo mismo que haciendo:
dirname /usr/share/icons/hicolor/64x64/mimetypes/libreoffice-oasis-data.png
Ahora si lo que queremos es sacar el nombre de archivo libreoffice-oasis-data.png sin la ruta, igual que hacemos con el comando basename. Podemos hacer:
fichero="/usr/share/icons/hicolor/64x64/mimetypes/libreoffice-oasis-data.png" echo "${fichero##*/}"
En este caso, eliminamos todos los caracteres desde el principio hasta la última / que encontramos.
De la misma forma que hicimos para sacar la ruta del archivo, vamos a extraer la extensión de un archivo, de la siguiente manera:
fichero="/home/usuario/backups/2018_11_13.tar.gz" echo "${fichero%%.*}"
Ahora queremos quedarnos con la extensión, para ello, propongo dos casos. En el primero, sacaremos todo lo que hay detrás del primer punto del archivo:
fichero="/home/usuario/backups/2018_11_13.tar.gz" echo ${fichero##*.}
Aunque también puede ser que solo necesitemos la última extensión (desde el último punto hasta el final):
fichero="/home/usuario/backups/2018_11_13.tar.gz" echo ${fichero#*.}
Queremos saber en qué letra se encuentra la palabra Linux dentro de la frase "Esto es un pequeño tutorial de Linux en el que hablo de Bash." así que hacemos lo siguiente:
texto="Esto es un pequeño tutorial de Linux en el que hablo de Bash." tmp="${texto%%Linux*}" echo ${#tmp}
Si os fijáis, es parecido a lo que hacíamos para extraer el nombre de archivo (solo que ahora en lugar de un punto, buscamos la palabra Linux, y luego contamos las letras de la cadena temporal generada. Incluso la palabra a buscar puede ser otra variable. Eso sí, tendremos un problema cuando la palabra no exista, que nos devolverá la posición del final de la cadena, entonces podemos hacer lo siguiente:
busca="Linux"texto="Esto es un pequeño tutorial de Linux en el que hablo de Bash." tmp="${texto%%$busca*}" [[ "$texto" = "$tmp" ]] && echo -1 || echo ${#tmp}
Con lo que si el texto no se encuentra, devuelve -1
Por último, quiero dejaros para copiar y pegar todo esto en forma de funciones para poder incorporar fácilmente a vuestros scripts estas utilidades:
#!/bin/bash function replace() { local text="$1" local busc="$2" local repl="$3" echo ${text//$busc/$repl} } function replace_once() { local text="$1" local busc="$2" local repl="$3" echo ${text/$busc/$repl} } function longitud() { local text="$1" echo ${#text} } function recorta() { local text="$1" local desde=$2 local hasta=$3 [ -z $hasta ] && echo ${text:$desde} || echo ${text:$desde:$hasta} } function encuentra() { local text="$1" local subt="$2" [[ "$texto" == *"Windows"* ]] && true || false } function ruta_archivo() { local fich="$1" echo "${fich%/*}" } function base_archivo() { local fich="$1" echo "${fich##*/}" } function extension_archivo() { local fich="$1" echo ${fich##*.} } function extension_archivo2() { local fich="$1" echo ${fich#*.} } function posicion_cadena() { local text="$1" local subc="$2" tmp="${text%%$subc*}" [[ "$texto" = "$tmp" ]] && echo -1 || echo ${#tmp} }
Ahora Probamos las funciones
replace "I love Windows, forever Windows" "Windows" "Linux"replace "I love Windows, forever Windows" "Windows" "Linux" replace_once "I love Windows. Windows never closes unexpectedly" "Windows" "Linux" longitud "Esta es una cadena muy larga que no se cuantas letras contiene" recorta "Desde que uso GNU/Linux soy más feliz" 14 9 recorta "Desde que uso GNU/Linux soy más feliz" 24 if encuentra "$(uname)" "Linux"; then echo "Buen sistema el tuyo!"; fi ruta_archivo "/home/usuario/proyectos/github/tutoriales/cadenas_bash.txt" base_archivo "/home/usuario/proyectos/github/tutoriales/cadenas_bash.txt" extension_archivo "/home/usuario/proyectos/github/tutoriales/cadenas_bash.tar.bz2" extension_archivo2 "/home/usuario/proyectos/github/tutoriales/cadenas_bash.tar.bz2" posicion_cadena "No sé dónde está Linux en todo este texto" "Linux"
¡Espero que les haya sido útil!