Ejecución y control de programas desde el shell

El shell constituye la interfaz de usuario (UI) de consola. Es un intérprete de linea de comando (CLI: command line interface) que funciona en modalidad REPL (Read, Execute, Print, Loop). Esto es: el shell espera que ingresemos una orden o comando en la linea de comando, cuando presionamos la tecla <Enter>, el shell interpreta y ejecuta la orden o comando que acabamos de ingresar, el resultado de esa ejecución puede aparecer en la consola (print) y una vez que finaliza el shell vuelve a quedar esperando que ingresemos una nueva orden o comando.

Linux dispone de una gran variedad de shells:

  • bash: Bourne again shell
  • sh: Bourne shell
  • ksh: Korn shell
  • csh: C shell
  • etc.

En la mayoría de las distribuciones de Linux hoy en día, el shell por defecto es bash, por lo que vamos a concentrarnos en la descripción y uso de ese shell.

Modalidad de ejecución de programas

Para correr un programa o ejecutar un comando en la linea de comandos de bash, alcanza con escribir el nombre del programa y presionar la tecla <Enter>

programa <Enter>

donde <Enter> representa la tecla Enter o Intro del teclado. Cuando corremos un programa de esta manera, el shell espera a que el programa termine antes de volver a atendernos. Si el programa demora unos segundos en ejecutar, eso se nota en que el prompt del shell no aparece hasta que el programa termina.

Desde bash hay dos modalidades de ejecución de programas:

  • foreground (primer plano): empleado principalmente para aplicaciones interactivas o ejecución de comandos de linea y/o programas cortos
  • background (segundo plano): empleado para correr programas no interactivos, que pueden estar un buen rato corriendo

Si el programa es interactivo, vale decir, requiere la intervención de usuario, inevitablemente debe usarse la modalidad foreground, que es la que ejemplificamos más arriba. En esta modalidad, el shell no vuelve a atendernos hasta que el programa termine.

Para indicarle al shell que pretendemos correr un programa en background, hay que agregar un & al final de la linea de comandos, antes del <Enter>. Por ejemplo:

programa & <Enter>

En esta modalidad, el programa se desliga del shell, queda ejecutando en segundo plano, reaparece el prompt del shell y no necesitamos esperar a que el programa temine para seguir trabajando en el shell.

Al lanzar un proceso en background el shell nos devuelve dos datos: el índice de proceso del shell, que aparece entre paréntesis rectos, y el PID (Process ID) del proceso. Todo proceso que está corriendo en el sistema tiene un número de proceso o PID.

Por ejemplo:

programa &
[1] 15435

[1] es el índice de proceso y 15435 es su correspondiente PID.

Obviamente que podemos lanzar tantos programas como queramos en background y mientras corren podemos seguir trabajando en el shell. Por eso la modalidad background es tan cómoda y es la preferida para correr programas no interactivos que van a correr por un buen rato.

Para saber cuantas tareas están corriendo en background, podemos correr el comando

jobs

que lista todas las tareas que aún están ejecutando. Ojo: sólo lista las tareas que dependan de ese sesión de shell. Podrían haber otras tares corriendo, pero que fueron lanzadas por otras sesiones de shell y no aparecen con el comando jobs. Para poder ver todos los procesos que están corriendo en el sistema, debemos emplear el comando ps

ps -l
ps aux

Sugerimos consultar el manual para ver todas las opciones del comando ps

man ps

El comando pstree nos muestra el árbol de procesos que están ejecutando en el sistema.

Control de los procesos: señales

Linux dispone de una gran variedad de mecanismos de comunicación entre procesos (IPC: Inter Process communication). Un mecanismo básico es el de las señales. La manera de interrumpir, suspender, terminar o matar un proceso es enviándole una señal apropiada.

Señales en modalidad foreground

Cuando tenemos un programa que está ejecutando en foreground podemos enviarle señales para interrumpirlo o suspenderlo. La manera de enviarle esa señal es con una combinación de teclas:

  • interrumpir: <Ctrl C>
  • suspender: <Ctrl Z>

donde <Ctrl C> coresponde a mantener presionada la tecla Ctrl y presionar la tecla C. Lo mismo para el caso de la suspensión.

¿Qué pasa cuando interrumpimos un programa que está corriendo en foreground? Lo que habitualmente ocurre es que el programa termina, cancela, lo que queda de manifiesto porque reaparece el prompt del shell.

¿Qué pasa cuando lo suspendemos? El programa no cancela, pero queda en suspenso, sin correr y reaparece el prompt.

Si corremos el comando jobs podemos comprobar que no está corriendo.

Podemos volver a activar un programa que fue suspendido, usando uno de dos comandos:

  • fg: el programa se reactiva y vuelve a ejecutar en foreground
  • bg: el programa se reactiva, pero queda corriendo en background

Con jobs podemos verificar que el programa que había sido suspendido, ahora está corriendo en background.

Si queremos traer a foreground uno de los procesos que aparece en el listado que nos da el comando jobs, podemos usar el comando fg

fg %i

donde el valor de i corresponde al índice de tarea que nos lista el comando jobs y que aparece entre paréntesis rectos al principio de la linea.

Señales en modalidad background

Cuando tenemos un proceso corriendo en background, no es posible usar las combinaciones de teclado <Ctrl C> o <Ctrl Z> para interrumpirlo con suspenderlo. Así que ahí recurrimos a otros comandos

Si el proceso sobre el que queremos actuar aparece en el listado de jobs (es decir, es un proceso que fue lanzado desde esa sesión de shell), alcanza con ejecutar el comando kill y el índice de proceso:

kill %i

Eso le manda la señal de terminación y el proceso cancela la ejecución.

Si el proceso no aparece en el listado de jobs, eso quiere decir que es un proceso que fue lanzado desde otro shell, pero de todas maneras le podemos enviar señales empleando el comando kill, usando el PID (Process ID) del proceso.

Para conocer el PID de un proceso, podemos listarlo usando el comando ps. Por ejemplo, si queremos ver todos los procesos que tenemos corriendo bajo nuestro usuario, podemos correr el comando:

ps -u

Ver man ps para más opciones de este comando.

Señales

Para saber qué señales se pueden enviar con el comando kill, sugerimos consultar el manual de signal(7)

man 7 signal

Acá mencionaremos únicamente las señales más empleadas, incluyendo la acción que debería ocurrir con el programa:

 Signal     Value     Action   Keyboard   Comment
 ──────────────────────────────────────────────────────────────────────
 SIGHUP        1       Term               Hangup detected on controlling terminal
                                          or death of controlling process
 SIGINT        2       Term    Ctrl-C     Interrupt from keyboard
 SIGKILL       9       Term               Kill signal
 SIGTERM      15       Term               Termination signal
 SIGSTOP   17,19,23    Stop    Ctrl-Z     Stop process

Por defecto, cuando corremos el comando:

kill <PID>

donde <PID> representa el PID del proceso.

Lo que estamos enviando es la señal 15 (SIGTERM) y por defecto el programa debería terminar.

Si queremos mandar otra señal, debemos explicitarla en el propio comando kill:

kill -15 <PID>
kill -9  <PID>
kill -2  <PID>

kill no es el único comando que puede usarse para enviar señales. Sugerimos que consulten el manual de: killall, pkill, killall5, o corran el comando apropos signal o apropos kill para listar toda la documentación sobre este tema.

Algunas señales son “atrapables” por el programa que está corriendo y se puede programar el comportamiento que el programa vaya a seguir en caso de recibir esa señal. Por ejemplo, las señales 1, 2 y 15 son atrapables por el programa, que podría estar programado para ignorarlas o hacer otra cosa diferente a lo que se espera sea el comportamiento por defecto.

En particular es muy útil que la señal 15 sea atrapable, pues se asume que un programa que reciba esa señal debe terminar. La utilidad de poder atrapar una señal permite al programa realizar tareas de cierre o hosekeeping y terminar de manera ordenada. La señal 15 no mata al proceso, sino que la indica que debe terminar y el programa ocuparse de cerrar archivos, salvar datos a disco o terminar de procesar los datos que tiene en ese momento, antes de cancelar la ejecución.

Entonces el procedimiento previsto para terminar la ejecución de un programa es mandar primero la señal 15, esperar unos segundos y verificar si el programa terminó o sigue corriendo. Si el programa no cancela la ejecución luego de haber recibido la señal 15, pero necesitamos que termine, entonces podemos mandarle la señal 9 (SIGKILL), que no puede ser ingorada y el proceso termina abruptamente. Como eso podría tener consecuencias (archivos de datos mal cerrados, pérdida de información, etc.), es que se recomienda primero mandar la señal 15 y, si no hay más remedio, la señal 9.

La señal SIGSTOP (que corresponde a los números 17, 19 y 23) lo que hace es supender la ejecución del programa (el programa deja de correr, pero no termina, queda esperando por otra señal que lo reactive o cancele). Para reactivar la ejecución del programa, le mandamos la misma señal.

Ejemplos:

kill -19 <PID>  (el programa suspende la ejecución si estaba activo)
kill -19 <PID>  (el programa retoma la ejecución si estaba suspendido)
kill -15 <PID>  (el programa termina de manera ordenada)
kill -9  <PID>  (el programa termina abruptamente)
NOHUP

Si uno deja un programa corriendo y cierra el shell (termina la sesión), dependiendo de bajo qué condiciones ocurra esto, puede pasar que el programa que dejamos corriendo reciba una señal 1 (SIGHUP) y cancele, sin que nosotros nos hayamos enterado. Más tarde, cuando volvemos a abrir un shell en el equipo, nos encontramos con que el programa no continuó corriendo y que proceso quedó trunco.

Para evitar este tipo se situaciones conviene que nuestro programa ignore ese tipo de señales y continúe ejecutando.

Para eso es conveniente lanzar los programas anteponiéndoles el comando nohup:

nohup programa &

El programa nohup se encarga de establecer que la señal SIGHUP sea ignorada. Cómo el programa que mandamos correr es hijo del programa nohup, éste también ignora esa señal. Esa es una de las consecuencias de la manera que los procesos se generan el Linux. El comando nohup también se encarga de redirigir stdout hacia el archivo nohup.out. Si no redirijimos explícitamente el stdout de nuestro programa, la salida que genere irá a parar a nohup.out.

NICE

Los procesos que corren el el sistema pueden tener diferentes propridades. Cuanto mayor sea su prioridad, más tiempo le dedica el sistema a correr ese proceso. Por defecto los procesos de los usuarios tienen priridad 10, mientras que los procesos del sistema tiene mayor prioridad. Eso está pensado de esa manera para que el sistema pueda encargarse de atender todos los procesos y hacer un uso eficiente de sus recursos.

La mejor política es nunca meterse a tocar o cambiar la prioridad de los procesos que se están ejecutando. Pero de todas maneras existen dos comandos: nice y renice, con los cuales podemos elegir la prioridad inicial al ejecutar un programa (nice) o cambiar la prioridad de un proceso que ya está corriendo (renice).

El rango de valores a elegir puede ir desde -19 (máxima prioridad) a 20 (mínima prioridad).

Lo que se recomienda es que uno use el comando nice para bajar la prioridad inicial de un proceso (es decir ser “bueno” (nice) con el sistema.

Por ejemplo:

nice -15 programa

baja a 15 puntos la prioridad del programa que vayamos a ejecutar. No se recomienda subir la prioridad de un programa a ejecutar, pues podemos correr el riesgo de competir con los procesos del propio sistema operativo. Es mejor dejar que sea el propio sistema operativo el que maneje esas cosas. En todo caso, se puede usar el comando renice para bajar la prioridad de un programa que ya está ejecutando. Nunca para subirla… pero si quieren subirla… pueden… la libertad es libre :-)

TIME

Si queremos saber el tiempo que le lleva correr a un programa, podemos anteponerle el comando time:

time programa

Al finalizar el programa, aparece en stdout 3 datos: el tiempo transcurrido (real time), el tiempo de CPU empleado por el usuario (user) y el tiempo de CPU empleado ejecutando llamadas del sistema (sys).

Control de IO (entrada/salida)

En Linux, por defecto, todo comando o programa que es ejecutado desde el shell tiene al menos 3 unidades de entrada/salida:

Unidad Nombre Tipo Dispositivo por defecto
0 stdin Standard Input teclado
1 stdout Standard Output pantalla
2 stderr Standard Error pantalla

Para los programas que se ejecutan desde el shell, stdin corresponde a lectura del teclado, mientras que stdout y stderr corresponden a escritura a pantalla.

El problema es que no siempre resulta práctico leer datos desde el teclado o escribir resultados a la pantalla. Muchos programas requieren leer o escribir grandes cantidades de datos y no tiene sentido ingresar los datos por el teclado o ver los resultados sólo por la pantalla.

El shell de Linux prevé dos mecanismos para controlar la lectura o escritura de datos de las unidades de IO: el redireccionamiento y los pipes.

Redireccionamiento

Para cambiar el origen o destino de los datos que un programa lea o escriba, se pueden usar los redireccionamientos n<, n> y , donde n es la unidad de IO que queramos redireccionar.

Si queremos redireccionar las unidades 0 (stdin) o 1 (stdout), no es obligatorio escribir 0< o 1>, alcanza con poner < o >. Para cualquier otra unidad de IO, es necesario poner el valor numérico de esa unidad.

Así entonces podemos escribir lo siguiente:

programa   <inputfile   >outputfile

o

programa   <inputfile   >>outputfile

donde inputfile es un archivo desde donde el programa va a leer los datos que necesita y outfile es el nombre del archivo adonde el programa va a escribir los resultados.

En el caso de escritura de datos, vimos que hay dos opciones:

  • >: modo sobrescritura. Si outfile existe, se sobrescriben los datos.
  • »: modo “append”. Si outfile existe, la salida que produce el programa se agrega al final del archivo outfile.

Pipes

Puede darse la necesidad de procesar datos con un programa y luego usar otro programa para procesar los datos que generó el primero.

Una posibilidad es escribir los datos del primer programa a un archivo y luego usar ese archivo como datos de entrada para el segundo programa:

prog1 <input1 >output1
prog2 <output1 >output2

Pero Linux ofrece una alternativa mucho más interesante y flexible, que implica conectar la salida estándar (stdout) de un programa con la entrada estándar (stdin) de otro, mediante el uso de un pipe: |

prog1 <input1 | prog2 >output2

De hecho se pueden encadenar varios programas usando pipes:

prog1  |  prog2  |  prog3

Los pipes ofrecen mucha versatilidad, pues permiten encadenar programas sencillos, que hacen una transformación concreta a los datos que reciben y le pasan el resultado de esa transformación a otro programa sencillo que hace otra transformación.

Este mecanismo encierra una idea muy poderosa, pues permite concentrar el esfuerzo en escribir programas sencillos que, encadenados, pueden hacer procesamiento de datos mucho más sofisticados que lo que cada programa puede hacer por separado.

El mecanismo de pipes es muy usado en la programación de scripts, justamente porque es simple y versátil, permitiendo implementar soluciones no previstas.

Resumiendo

¿Cómo se corre un programa en Linux desde el shell?

Así:

nice -15 nohup time programa <inputfile >outputfile &

La Yapa

Desde un shell se pueden ejecutar varios comandos seguidos, simplemente separando cada comando del siguiente con un punto y coma:

prog1 ; prog2; prog3

Los programas también se pueden ejecuta de manera condicional:

prog1 && prog2

prog2 se ejecuta si prog1 termina exitosamente.

¿Cómo sabemos si un programa terminó exitosamente?

Cuando un programa termina, devuelve un exit code. Usualmente un programa que termina bien, devuelve un exit code igual a cero. Si el programa termina con algún error, el exit code será mayor que cero.

Hay que tener en cuenta que en informática un valor igual a cero suele asociarse al valor lógico TRUE, mientras que un valor distinto de cero es considerado FALSE.

El operador && actúa como el AND lógico. Por eso si el primero programa devuelve un código de error distinto de cero, se asume que equivale a un FALSE y sabemos que FALSE AND cualquier otro valor siempre da FALSE y por lo tanto el segundo programa no ejecuta.

El otro operador condicional es el ||, que equivale a un OR:

prog1 || prog2

En este caso prog2 se ejecuta si y sólo si prog1 devuelve un exit code distinto de cero. Así que es un mecanismo que nos permite tomar alguna acción en caso que el primer programa falla.

Vamos a ver más de este tipo de herramientas cuando veamos en más detalle programación shell