Lo hice y lo entendí

El blog de Vicente Navarro
19 abr

Sobre la señal SIGHUP: nohup, disown, trap

El nohup es un comando de los históricos de UNIX. Cuando en un terminal físico (terminal serie), virtual (xterm, konsole, putty, telnet, ssh, etc.), o en la consola cerramos la sesión (cerrando la ventana si estamos en un entorno gráfico, con el comando exit, o pulsando Control+D en la shell), el proceso que gestiona dicho terminal (getty, mgetty, sshd, telnetd, etc.) manda una señal SIGHUP a los procesos que cuelgan bajo ellos, normalmente, la shell, y en Linux, típicamente bash. Tal y como podemos leer en la página de man de bash, la shell, a su vez, manda dicha señal a todos los procesos que cuelgan bajo ella y, de hecho, incluso los despierta si están parados:

The shell exits by default upon receipt of a SIGHUP. Before exiting, an interactive shell
resends the SIGHUP to all jobs, running or stopped. Stopped jobs are sent SIGCONT to
ensure that they receive the SIGHUP. To prevent the shell from sending the signal to a
particular job, it should be removed from the jobs table with the disown builtin (see
SHELL BUILTIN COMMANDS below) or marked to not receive SIGHUP using disown -h.

Ante la señal SIGHUP normalmente todos los procesos reaccionan simplemente saliendo, lo cual es un problema si queremos que nuestro programa siga ejecutándose en el sistema una vez que hayamos dejado la shell. Para evitar que esto ocurra, tradicionalmente se ha usado el comando nohup. Adicionalmente, el bash, desde la versión 2.0, también tiene el comando interno disown como podemos leer en el párrafo anterior.

nohup

La ejecución de nohup es muy sencilla. Simplemente ponemos nohup y escribimos el comando exactamente igual que lo hubiéramos ejecutado:

Usage: nohup COMMAND [ARG]...

Es especialmente interesante del nohup la gestión que hace de la entrada estándar y la salida estándar y de error, como leemos en el info coreutils:

If standard input is a terminal, it is redirected from `/dev/null’
so that terminal sessions do not mistakenly consider the terminal to be
used by the command. This is a GNU extension; programs intended to be
portable to non-GNU hosts should use `nohup COMMAND [ARG]… </dev/null’
instead.

If standard output is a terminal, the command’s standard output is
appended to the file `nohup.out’; if that cannot be written to, it is
appended to the file `$HOME/nohup.out’; and if that cannot be written
to, the command is not run. Any `nohup.out’ or `$HOME/nohup.out’ file
created by `nohup’ is made readable and writable only to the user,
regardless of the current umask settings.

If standard error is a terminal, it is redirected to the same file
descriptor as the (possibly-redirected) standard output.

Es decir, si la salida estándar no está ya redirigida a un fichero, es redirigida al fichero nohup.out del directorio actual o, si el usuario no puede escribir en el directorio actual, al fichero $HOME/nohup.out. Si tampoco se puede crear, el comando no se ejecutará. La salida de error se redirigirá al mismo sitio donde vaya la salida estándar, sea al fichero que hayamos elegido, sea al nohup.out que se haya creado.

Pongamos un ejemplo. Sea el script de prueba:

#!/bin/bash
set -x
echo Prueba

Vemos que envía una línea a la salida estándar y otra a la salida de error:

$ ./prueba.sh > /dev/null
+ echo Prueba
$ ./prueba.sh 2> /dev/null                                                             
Prueba

Comprobamos que lo que nos dice la documentación es cierto:

$ nohup ./prueba.sh                                                                    
nohup: appending output to `nohup.out'
$ cat nohup.out                                                                        
+ echo Prueba
Prueba
$ nohup ./prueba.sh > prueba.out
$ cat prueba.out
+ echo Prueba
Prueba

Si no redirigimos la salida del comando, es muy probable que si no somos ordenados nos encontremos el sistema lleno de ficheros nohup.out. Siempre he pensado que estaría bien que el comando nohup tuviera un opción -f outputfile para especificar qué fichero de salida queremos en lugar del nohup.out, así que me he puesto manos a la obra con las coreutils 5.97 de Debian Etch y he creado un parche para el fichero src/nohup.c. El parche (nohup_coreutils_5.97_f_v1.patch) se aplica estando en el directorio src y ejecutando:

patch -p0 < /path_al_parch/nohup_coreutils_5.97_f_v1.patch

Tras aplicar el parche y hacer el ./configure y el ./make de turno en el directorio raíz de las fuentes del coreutils, tendremos un nuevo binario con una opción -f que nos permitirá elegir nuestro opcional sustituto para el nohup.out. Por supuesto, no recomiendo a nadie reemplazar el nohup oficial, así que, ¿por qué no moverlo a /usr/bin con el nombre nohupf?

$ nohupf -f salida.txt ./prueba.sh                                                     
nohupf: appending output to `salida.txt'

$ nohupf -f /tmp/salida.txt ./prueba.sh
nohupf: appending output to `/tmp/salida.txt'

$ nohupf -f /dev/null ./prueba.sh                                                      
nohupf: appending output to `/dev/null'

$ nohupf ./prueba.sh                                                                   
nohupf: appending output to `nohup.out'

Si el usuario no puede crear el fichero aplican las mismas reglas que con el nohup.out, pero ojo si el path es absoluto:

$ nohupf -f salida.txt ./prueba.sh                                         
nohupf: appending output to `/home/vicente/salida.txt'

$ nohupf -f /tmp/salida.txt /tmp/prueba.sh
nohupf: failed to open `/tmp/salida.txt': Permission denied
nohupf: failed to open `/home/vicente/tmp/salida.txt': No such file or directory

El parche no funciona sobre el nohup.c de la última versión disponible de las coreutils, la 6.9, pero por lo que he visto en el código los cambios serían mínimos. Si a alguien le interesara, que lo diga en los comentarios.

disown

El disown es un comando interno de bash que se puede usar sobre los jobs ya existentes:

disown [-ar] [-h] [jobspec ...]
Without options, each jobspec is removed from the table of active jobs. If the -h option is given, each jobspec is not removed from the table, but is marked so that SIGHUP is not sent to the job if the shell receives a SIGHUP. If no jobspec is present, and neither the -a nor the -r option is supplied, the current job is used. If no jobspec is supplied, the -a option means to remove or mark all jobs; the -r option without a jobspec argument restricts operation to running jobs. The return value is 0 unless a jobspec does not specify a valid job.

Las principales diferencias entre nohup y disown son que este último no hace nada con las salidas estándar y de error del comando en cuestión y que se ha de aplicar sobre un job ya existente. Es especialmente interesante cuando queremos cerrar el terminal pero nos damos cuenta de que no queremos que un proceso termine y ya no nos interesa volver a ejecutarlo con nohup.

Veamos un ejemplo con -h:

$ yes > /dev/null &                                                                
[1] 4646
$ jobs                                                                           
[1]+  Running    yes >/dev/null &
$ disown -h %1                                                                         
$ jobs                                                                          
[1]+  Running    yes >/dev/null &

Cerramos el terminal y en otro terminal vemos que el proceso aún está ejecutándose:

$ ps -ef | grep yes
user      4646     1 95 20:02 ?        00:02:02 yes

Sin -h el proceso incluso desaparece completamente de la lista de jobs, tal y como nos anunciaba el manual:

$ yes > /dev/null &                                                                
[1] 4689
$ jobs                                                                           
[1]+  Running    yes >/dev/null &
$ disown -h %1                                                                         
$ jobs

trap

Hace un tiempo, en las listas de distribución de BULMA descubrí una forma muy curiosa de hacer un nohup casero:

$ (trap '' 1 15; comando-a-ejecutar) &

El trap es otro comando interno de bash que nos permite especificar qué hacer ante la llegada de una señal determinada:

trap [-lp] [[arg] sigspec …]
The command arg is to be read and executed when the shell receives signal(s) sigspec. If arg is absent (and there is a single sigspec) or -, each specified signal is reset to its original disposition (the value it had upon entrance to the shell). If arg is the null string the signal specified by each sigspec is ignored by the shell and by the commands it invokes. If arg is not present and -p has been supplied, then the trap commands associated with each sigspec are displayed. If no arguments are supplied or if only -p is given, trap prints the list of commands associated with each signal. The -l option causes the shell to print a list of signal names and their corresponding numbers. Each sigspec is either a signal name defined in <signal.h>, or a signal number.

Para entender este comando, me ha encantado el ejemplo que ponen en el Bash Beginners Guide, capítulo 12: Traps:

#!/bin/bash
trap "echo No me mataras!" SIGINT SIGTERM
echo "pid is $$"
while true
do
   sleep 60
done

Y al ejecutarlo, cada vez que le doy a CONTROL+C (SIGINT), aparece el texto, en lugar de salir. Únicamente un “kill -9 5048” podrá parar este script:

./nomemataras.sh 
pid is 5048
No me mataras!
No me mataras!
No me mataras!
No me mataras!

Entradas relacionadas

15 Comentarios a “Sobre la señal SIGHUP: nohup, disown, trap”

  • Francisco Javier dice:

    Interesante articulo. Yo llevo tiempo utilizando nohup, y me he sorprendido bastante de que hayas tenido que hacer un parche para especificar el fichero de salida.

    La forma en la que utilizo nohup es esta: nohup > fichero.out 2> fichero.err comando &

    Cuando despues cierro la sesion, la salida siguen siendo esos ficheros, con lo que entiendo que consigo lo mismo que tu parche, pero sin necesidad de utilizarlo.

  • Francisco Javier Muchas gracias por tu comentario. Si te fijas, en el artículo comento que sólo redirigiendo la salida estándar logras evitar la creación del nohup.out, pero mira, a mí me hacía ilusión tener una opción -f ;-)

  • Ivan dice:

    Qué pedazo de artículo, me ha encantado!. Mira que llevo tiempo utilizando el nohup, pero el resto de comandos y opciones no los conocía. Eso sí, la mayoría de las veces lo he ejecutado con ksh en Solaris y HP-UX.

    Saludos, Iván.

  • Ivan ¡Muchas gracias! En adelante tendrás que ser un poco menos efusivo con tus halagos o otros visitantes pensarán que te pago para poner los comentarios o algo :oops: Ya hablaremos luego del sobre de este mes ;)

    Por otra parte veo que también eres de la vieja escuela del ksh en UNIXes tochos… ¿Qué tal el ESC-ESC y el ESC-K? :mrgreen:

  • rafa_piltrafa dice:

    Gracias por el artículo Vicente. Muy entretenido y útil como siempre.

  • Iván dice:

    El ESC-ESC bien en hp-ux pero en solaris no funciona si pones de gestor de la línea de comandos el vi (set -o vi); sólo funciona si se pone ese “engendro” de emacs. Así, me acostumbré a utilizar ESC-\ y poder mantener el vi.

    Saludos, Iván.

  • Iván Pues lo he probado en una Solaris y te tengo que dar la razón. En el ksh de Solaris sólo hay ESC-ESC en modo EMacs. En modo vi, como muy bien dices, hay ESC-\. En cualquier caso, yo Solaris que pillo, Solaris a la que le casco el bash. Con los HP-UX no lo he hecho nunca, la verdad… supongo que no es tan típico ponerles utilidades externas como en Solaris.

    Muchas gracias por el apunte. ¡Una cosa más aprendida!

    Y sobre el Emacs, yo uso XEmacs en Windows y Linux para muchas cosas y es farragosillo, pero cuando le pillas el punto está muy bien ;-)

  • miloE dice:

    Hola viejo, está bien bueno el artículo. Ahora..tengo una pregunta.

    Acá en el trabajo tenemos un scripts
    qe es invocado por proceso, de vez en cuando.

    Algunas veces cuando el proceso del script se cuelga, pues el proceso “original” no sigue su flujo normal, lo cual no nos sirve.

    Lo qe necesito saber es: puede hacerse qe el proceso ligado a la invocación del script sea independiente al del proceso qe invoca al script? (es decir qe no sea su hijo) sospecho qe el nohup combinado con el & hace esto, pero me gustaría estar seguro

  • miloE ¡Gracias!

    Si el proceso que lanza el script es un programa hecho por ejemplo en C, hay formas programáticas de conseguir este efecto sin usar el nohup que quedarían fuera del ámbito de este artículo.

    Pero si el programa padre es a su vez un script o si en el programa tienes forma de modificar cómo se lanza el script, sí, la solución sería un simple nohup comando &.

    E incluso también podrías, si estás llamando a script.sh, hacer:

    mv script.sh script_orig.sh

    y crear un nuevo script.sh con las siguientes líneas:

    #!/bin/sh
    nohup /path/script_orig.sh &

  • miloE dice:

    hola…yo de nuevo. mirá lo que hice:

    $ more test.sh
    #!/bin/sh
    nohup test2.sh > /dev/null &

    $ more test2.sh
    #!/bin/sh
    echo “empieza la jugada”
    sleep 600
    echo “se acabo esto”

    peeero..cuando intento correr test.sh…
    [/home/miloe]
    $ ./test.sh

    [/home/miloe]
    $ nohup: test2.sh: Permission denied

    ¿Qué será lo que pasa?

  • miloE Yo diría que el problema es que test2.sh tal vez no tenga permisos de ejecución. ¿Lo has comprobado?

  • Bilbo dice:

    Y relacionado. ¿Conoceís screen (http://gentoo-wiki.com/TIP_Using_screen) ?

    Yo ya no sé vivir sin él. A grosso modo, es una especie de VNC para la terminal. Un poco duro de aprender (tipo vim, con muchos atajos de teclado poco intuitivos) pero una vez que se coje la costumbre, ya no hay stop.
    Entre otras, permite que dos o más personas manipulen simultáneamente una misma terminal, aunque estén conectados por un modem de 14400kbs a 300 kms de distancia, cerrar la terminal sin desconectar la sesión (y los programas que cuelgen de ella), dividir la pantalla física (el xterm en varias subpantallas o regiones en jerga screen, ver una sesión distinta en cada una,…

    Es decir que en vez de seguir el método tradicional de ejecutar el nohup, simplemente creamos una sesión de screen y cuando acabamos, nos desenganchamos y volvemos a enganchar cuando queramos desde donde queramos.

  • @Bilbo Yo sí conozco el screen y me parece extraordinariamente útil. ¡Totalmente de acuerdo en todo lo que dices! ¡Gracias por el apunte!

  • José Manuel dice:

    Excelente tutorial, y por ejemplo si quisieras volver a direccionar la salida estandar a otra terminal sin necesidad de utilizar screen?

  • @José Manuel Definitivamente usaría screen si quisiera hacer tal cosa. Sin screen no se me ocurre cómo hacerlo.

Tema LHYLE09, creado por Vicente Navarro