Aprendiendo_Korn_Shell/Secciones/Capitulo8.tex

934 lines
81 KiB
TeX

El sistema operativo Unix construyó su reputación sobre un pequeño número de conceptos, todos los cuales son simples pero poderosos. Ya hemos visto la mayoría de ellos: entrada/salida estándar, tuberías, utilidades de filtrado de texto, el sistema de archivos estructurado en árbol, y así sucesivamente. Unix también ganó notoriedad como el primer sistema operativo para computadoras pequeñas \footnote{Los sistemas PDP-11 en los que Unix se volvió popular inicialmente se consideraban pequeños para esa época.}
que le daba a cada usuario control sobre más de un proceso. Llamamos a esta \emph{capacidad multitarea controlada} por el usuario.
Si Unix es el único sistema operativo que te resulta familiar, podrías sorprenderte al descubrir que varios otros sistemas operativos importantes han carecido lamentablemente en esta área. Por ejemplo, el MS-DOS de Microsoft, para compatibles IBM PC, no tiene multitarea en absoluto, y mucho menos multitarea controlada por el usuario. El sistema VM/CMS de IBM para mainframes grandes maneja múltiples usuarios pero les otorga solo un proceso cada uno. OpenVMS de Compaq tiene multitarea controlada por el usuario, pero es limitada y difícil de usar. La última generación de sistemas operativos para computadoras pequeñas, como el Macintosh OS X de Apple (que se basa en BSD) y el Windows de Microsoft (Windows 95 y posteriores), finalmente incluyen la multitarea controlada por el usuario a nivel del sistema operativo.
Pero si has llegado tan lejos en este libro, probablemente no pienses que la multitarea sea algo importante. Probablemente estés acostumbrado a la idea de ejecutar un proceso en segundo plano colocando un ampersand (\&) al final de la línea de comandos. También has visto la idea de un subproceso de shell en el \hyperref[sec:Chapter4]{Capítulo 4}, cuando mostramos cómo se ejecutan los scripts de shell.
En este capítulo, cubrimos la mayoría de las características del shell Korn que se relacionan con la multitarea y el manejo de procesos en general. Decimos <<la mayoría>> porque algunas de estas características son, al igual que los descriptores de archivos que vimos en el \hyperref[sec:Chapter7]{Capítulo 7}, de interés solo para programadores de sistemas de bajo nivel.
Comenzamos mirando ciertos primitivos importantes para identificar procesos y controlarlos durante las sesiones de inicio y dentro de los scripts de shell. Luego nos movemos a una perspectiva de nivel superior, examinando formas de hacer que los procesos se comuniquen entre sí. La facilidad de las corrutinas del shell Korn es el esquema de comunicación interprocesos más sofisticado que examinaremos; también analizamos en más detalle conceptos que ya hemos visto, como las tuberías y los subprocesos de shell.
No te preocupes por atascarte en detalles técnicos de bajo nivel sobre Unix. Proporcionamos solo la información técnica necesaria para explicar las características de nivel superior, además de algunos otros detalles diseñados para despertar tu curiosidad. Si estás interesado en obtener más información sobre estas áreas, consulta tu Manual del programador de Unix o un libro sobre internos de Unix que sea relevante para tu versión de Unix.
Recomendamos encarecidamente que pruebes los ejemplos en este capítulo. El comportamiento del código que involucra múltiples procesos no es tan fácil de entender en papel como la mayoría de los otros ejemplos en este libro.
\section{ID de proceso y números de trabajo}
Unix asigna números, llamados \emph{identificadores de proceso} (PID, por sus siglas en inglés), a todos los procesos cuando se crean. Notarás que, cuando ejecutas un comando en segundo plano agregándole un ampersand (\&), el shell responde con una línea que se ve así:
\begin{lstlisting}[language=bash]
$ fred &
[1] 2349
\end{lstlisting}
En este ejemplo, 2349 es el PID del proceso \emph{fred}. El \texttt{[1]} es un \emph{número de trabajo} asignado por el shell (no el sistema operativo). ¿Cuál es la diferencia? Los números de trabajo se refieren a procesos en segundo plano que se están ejecutando actualmente bajo tu shell, mientras que los PID se refieren a todos los procesos que se están ejecutando actualmente en todo el sistema, para todos los usuarios. El término \emph{job} básicamente se refiere a una línea de comando que se invocó desde tu shell de inicio de sesión.
Si inicias trabajos adicionales en segundo plano mientras el primero aún se está ejecutando, el shell los numera como 2, 3, etc. Por ejemplo:
\begin{lstlisting}[language=bash]
$ bob &
[2] 2367
$ dave | george &
[3] 2382
\end{lstlisting}
Claramente, 1, 2 y 3 son más fáciles de recordar que 2349, 2367 y 2382.
El shell incluye números de trabajo en mensajes que imprime cuando se completa un trabajo en segundo plano, como este:
\begin{lstlisting}[language=bash]
[1] + Done fred &
\end{lstlisting}
Explicaremos lo que significa el signo más pronto. Si el trabajo sale con un estado distinto de cero (ver \hyperref[sec:Chapter5]{Capítulo 5}), el shell incluye el estado de salida entre paréntesis:
\begin{lstlisting}[language=bash]
[1] + Done(1) fred &
\end{lstlisting}
El shell imprime otros tipos de mensajes cuando suceden ciertas cosas anormales con los trabajos en segundo plano; veremos estos más adelante en este capítulo.
\section{Control de trabajo}
¿Por qué deberías preocuparte por los identificadores de proceso o los números de trabajo? En realidad, podrías manejarte bien en tu vida de Unix sin referirte nunca a los identificadores de proceso (a menos que uses una estación de trabajo con ventanas, como veremos pronto). Sin embargo, los números de trabajo son más importantes: puedes usarlos con los comandos del shell para el \emph{control de trabajos}.
Ya conoces la forma más obvia de controlar un trabajo: puedes crear uno en segundo plano con \&. Una vez que un trabajo se está ejecutando en segundo plano, puedes dejar que se complete, llevarlo al \emph{primer plano} o enviarle un mensaje llamado \emph{signal}.
\subsection{Primer plano y segundo plano}
El comando integrado \emph{fg} lleva un trabajo en segundo plano al primer plano. Normalmente, esto significa que el trabajo tiene control de tu terminal o ventana y, por lo tanto, puede aceptar tu entrada. En otras palabras, el trabajo comienza a actuar como si hubieras escrito su comando sin el \&.
Si solo tienes un trabajo en segundo plano, puedes usar \emph{fg} sin argumentos, y el shell lleva ese trabajo al primer plano. Pero si tienes varios trabajos en segundo plano, el shell elige el que pusiste en segundo plano más recientemente. Si deseas que se lleve al primer plano un trabajo diferente, debes usar el nombre del comando del trabajo, precedido por un signo de porcentaje (\%), o puedes usar su número de trabajo, también precedido por \%, o su identificador de proceso sin un signo de porcentaje. Si no recuerdas qué trabajos se están ejecutando, puedes usar el comando \emph{jobs} para listarlos.
Algunos ejemplos deberían aclarar esto. Digamos que creaste tres trabajos en segundo plano como se muestra arriba. Si escribes \emph{jobs}, verás esto:
\begin{lstlisting}[language=bash]
[1] Running fred &
[2] - Running bob &
[3] + Running dave | george &
\end{lstlisting}
\emph{jobs} tiene algunas opciones interesantes. Además del estado del trabajo, \texttt{jobs -l} también lista los identificadores de grupo de procesos:
\begin{lstlisting}[language=bash]
[1] 2349 Running fred &
[2] - 2367 Running bob &
[3] + 2382 Running dave | george &
\end{lstlisting}
¿Cómo funciona todo esto? Cada vez que ejecutas un trabajo, el o los procesos en el trabajo se colocan en un nuevo \emph{grupo de procesos}. Cada proceso en un grupo de procesos, además de su número único de identificación de proceso, también tiene un \emph{identificador de grupo de procesos(ID)}. El identificador de grupo de procesos es igual al identificador de proceso del \emph{líder} del grupo de procesos, que es uno de los procesos invocados como parte del trabajo. (De hecho, el último en la tubería). Los números que imprime el shell son, de hecho, los identificadores de grupo de procesos. (Observa que para el trabajo 3, hay dos procesos, pero solo un número).
Ahora bien, tu dispositivo terminal, ya sea un puerto serie real o un seudoterminal como el que obtienes en un sistema de ventanas o una \emph{sesión de telnet}, también tiene un número de identificación de grupo de procesos. Los procesos cuyo identificador de grupo de procesos coincide con el del terminal <<poseen>> el terminal, en el sentido de que se les permite leer la entrada desde él. En resumen, el control de trabajos funciona configurando el grupo de procesos del terminal para que sea el mismo que el grupo de procesos del trabajo actual. (Hay muchos más detalles técnicos, incluida la idea de una <<sesión>> introducida por POSIX, pero esos detalles no son necesarios para entender el uso cotidiano del control de trabajos).
La opción \texttt{-p} le dice a \emph{jobs} que liste \emph{solo} los identificadores de grupo de procesos:
\begin{lstlisting}[language=bash]
$ jobs -p
2349
2367
2382
\end{lstlisting}
Esto podría ser útil con la sustitución de comandos; consulta la \hyperref[box:8-1]{Tarea 8-1} más adelante en este capítulo. Finalmente, la opción \texttt{-n} lista solo aquellos trabajos cuyo estado ha cambiado desde la última vez que el shell lo informó, ya sea con un comando \emph{jobs} o de otra manera.
Si escribes \emph{fg} sin un argumento, el shell coloca a \texttt{dave | george} en primer plano, porque fue colocado en segundo plano más recientemente. Pero si escribes \texttt{fg \%bob} (o \texttt{fg \%2}), bob irá al primer plano.
También puedes referirte al trabajo colocado más recientemente en segundo plano con \texttt{\%+}. De manera similar, \texttt{\%-} se refiere al trabajo en segundo plano invocado a continuación más recientemente (\emph{bob} en este caso). Eso explica los signos más y menos en lo anterior: el signo más muestra el trabajo invocado más recientemente; el signo menos muestra el trabajo invocado a continuación más recientemente. \footnote{Esto es análogo a \~{}+ y \~{}- como referencias al directorio actual y anterior; consulta la nota al pie en el C\hyperref[sec:Chapter7]{Capítulo 7}. Además: \%\% es un sinónimo de \%+.}
Si más de un trabajo en segundo plano tiene el mismo comando, entonces \texttt{\%command} desambiguará eligiendo el trabajo invocado más recientemente (como cabría esperar). Si esto no es lo que deseas, debes usar el número del trabajo en lugar del nombre del comando. Sin embargo, si los comandos tienen argumentos diferentes, puedes usar \texttt{\%?string} en lugar de \texttt{\%command. \%?string} se refiere al trabajo cuyo comando contiene la cadena. Por ejemplo, supongamos que iniciaste estos trabajos en segundo plano:
\begin{lstlisting}[language=bash]
$ bob pete &
[1] 189
$ bob ralph &
[2] 190
$
\end{lstlisting}
Luego puedes usar \texttt{\%?pete} y \texttt{\%?ralph} para referirte a cada uno de ellos, aunque en realidad \texttt{\%?pe} y \texttt{\%?ra} son suficientes para desambiguar.
La Tabla \ref{Tab:8-1} enumera todas las formas de referirse a trabajos en segundo plano. Hemos descubierto que, dado lo infrecuentemente que las personas utilizan comandos de control de trabajos, los números de trabajo o los nombres de comando son suficientes, y las demás formas son superfluas.
\begin{longtable}[h]{|m{3cm}|m{12cm}|}
\caption{Formas de referirse a trabajos en segundo plano}
\label{Tab:8-1}\\
\hline
\textbf{Referencia} & \textbf{Trabajo en segundo plano} \\ \hline
\endfirsthead
\hline
\textbf{Referencia} & \textbf{Trabajo en segundo plano} \\ \hline
\endhead
N & Identificación de proceso N \\\hline
-N & Identificación de grupo de procesos N \\\hline
\%N & Número de trabajo N \\\hline
\%\emph{string} & Trabajo cuyo comando comienza con \emph{string} \\\hline
\%?\emph{string} & Trabajo cuyo comando contiene \emph{string} \\\hline
\%+, \%\% & Trabajo en segundo plano invocado más recientemente \\\hline
\%- & Segundo trabajo en segundo plano invocado más recientemente \\\hline
\end{longtable}
\subsection{Suspendiendo un trabajo}
Al igual que puedes llevar trabajos en segundo plano al primer plano con \emph{fg}, también puedes enviar un trabajo en primer plano al segundo plano. Esto implica suspender el trabajo para que el shell recupere el control de tu terminal.
Para suspender un trabajo, escribe CTRL-Z \footnote{Esto asume que la tecla CTRL-Z está configurada como tu tecla de suspensión; al igual que con CTRL-C e interrupciones, esto es convencional pero no obligatorio.}
mientras se está ejecutando. Esto es análogo a escribir CTRL-C (o la tecla de interrupción que tengas configurada), con la diferencia de que puedes reanudar el trabajo después de detenerlo. Cuando escribes CTRL-Z, el shell responde con un mensaje como este:
\begin{lstlisting}[language=bash, escapeinside={(*}{*)}]]
[1] + Detenido (* \emph{comando} *)
\end{lstlisting}
Luego te devuelve tu indicador de comando. También coloca el trabajo suspendido en la parte superior de la lista de trabajos, como indica el signo +.
Para reanudar un trabajo suspendido para que continúe ejecutándose en primer plano, simplemente escribe \emph{fg}. Si, por alguna razón, pusiste otros trabajos en segundo plano después de escribir CTRL-Z, usa \emph{fg} con un nombre o número de trabajo. Por ejemplo:
\begin{lstlisting}[language=bash, escapeinside={(*}{*)}]]
(* \emph{fred se está ejecutando...} *)
(* \textbf{CTRL-Z} *)
[1] + Detenido (* \emph{fred} *)
(* \textbf{\$ bob \&} *)
[2] bob &
(* \textbf{\$ fg \%fred} *)
(* \emph{fred se reanuda en primer plano...} *)
\end{lstlisting}
La capacidad de suspender trabajos y reanudarlos en primer plano resulta muy útil cuando solo tienes una conexión a tu sistema, \footnote{Como cuando te conectas desde casa a tu oficina, o estás conectado a un sistema remoto a través de Internet mediante \emph{telnet} o \emph{ssh}.}
y estás utilizando un editor de texto como vi en un archivo que necesita ser procesado. Por ejemplo, si estás editando un archivo HTML para tu servidor web, puedes hacer lo siguiente:
\begin{lstlisting}[language=bash]
$ vi myfile.html
Edita el archivo... CTRL-Z
[1] + Detenido vi myfile.html
$ lynx myfile.html Previsualiza los resultados con un navegador de solo texto
Ves que cometiste un error
$ fg
vi vuelve a abrir en el mismo lugar de tu archivo
\end{lstlisting}
Los programadores a menudo utilizan la misma técnica al depurar código fuente.
También es probable que encuentres útil suspender un trabajo y luego reanudarlo en segundo plano en lugar de en primer plano. Puedes iniciar un comando en primer plano (es decir, normalmente) y descubrir que tarda mucho más de lo esperado, por ejemplo, una búsqueda con \emph{grep}, \emph{sort} o una consulta a una base de datos. Necesitas que el comando finalice, pero también te gustaría recuperar el control de tu terminal para poder hacer otras tareas. Si escribes CTRL-Z seguido de \emph{bg}, mueves el trabajo al segundo plano. \footnote{Sin embargo, ten cuidado, ya que no todos los comandos se comportan de manera <<adecuada>> cuando haces esto. Ten especial precaución con los comandos que se ejecutan sobre una red en una máquina remota; podrías <<confundir>> al programa remoto.}
\subsection{Desvinculando un trabajo}
Normalmente, cuando cierras la sesión, el shell envía la señal \emph{HUP} (consulta la siguiente sección) a cualquier trabajo en segundo plano. Si has iniciado un trabajo de larga duración en segundo plano y deseas que se complete sin importar qué, debes indicárselo al shell mediante el comando \emph{disown} con uno o más números de identificación de trabajo como argumentos. Sin argumentos, se desvinculan \emph{todos} los trabajos en segundo plano.
\section{Señales}
Dijimos anteriormente que escribir CTRL-Z para suspender un trabajo es similar a escribir CTRL-C para detener un trabajo, excepto que puedes reanudar el trabajo más tarde. De hecho, son similares de una manera más profunda: ambos son casos particulares del acto de enviar una \emph{señal} a un proceso.
Una señal es un mensaje que un proceso envía a otro cuando ocurre algún evento anormal o cuando quiere que el otro proceso haga algo. La mayoría de las veces, un proceso envía una señal a un subproceso que creó. Sin duda, ya te sientes cómodo con la idea de que un proceso puede comunicarse con otro a través de una tubería de entrada/salida; piensa en una señal como otra forma para que los procesos se comuniquen entre sí. (De hecho, cualquier libro de sistemas operativos te dirá que ambos son ejemplos del concepto general de \emph{comunicación entre procesos}, o IPC por sus siglas en inglés). \footnote{Las tuberías (pipes) y las señales eran los únicos mecanismos IPC en las primeras versiones de Unix. Las versiones más modernas tienen mecanismos adicionales, como sockets, tuberías con nombre y memoria compartida. Las tuberías con nombre son accesibles para los programadores de shell a través del comando \emph{mkfifo(1)}, lo cual está más allá del alcance de este libro.}
Dependiendo de la versión de Unix, hay entre dos y tres docenas de tipos de señales, incluyendo algunas que un programador puede usar para cualquier propósito. Las señales tienen números (del 1 al número de señales que el sistema admite) y nombres; nosotros usaremos los últimos. Puedes obtener una lista de todas las señales en tu sistema escribiendo \texttt{kill -l}. Ten en cuenta que, al escribir código de shell que involucre señales, los nombres de las señales son más portátiles a otras versiones de Unix que los números de señal.
\subsection{Señales con Control-Key}
Cuando escribes CTRL-C, le estás indicando al shell que envíe la señal INT (de <<interrupción>>) al trabajo actual; CTRL-Z envía la señal TSTP (de <<parada de terminal>>). También puedes enviar al trabajo actual una señal QUIT escribiendo CTRL-\textbackslash{} (control barra invertida); esto es como una versión <<más fuerte>> de CTRL-C. \footnote{CTRL-\textbackslash{} también puede hacer que el programa en ejecución deje un archivo llamado core en el directorio actual del programa. Este archivo contiene una imagen del proceso al cual le enviaste la señal; un programador podría usarlo para ayudar a depurar el programa que se estaba ejecutando. El nombre del archivo es un término (muy) anticuado para la memoria de una computadora. Otras señales también dejan estos <<volcados de núcleo>>; puedes eliminarlos a menos que un programador del sistema te diga lo contrario.}
Normalmente usarías CTRL-\textbackslash{} cuando (y solo cuando) CTRL-C no funciona.
Como veremos pronto, también hay una señal de <<pánico>> llamada KILL que puedes enviar a un proceso cuando ni siquiera CTRL-\textbackslash{} funciona. Pero no está asociada a ninguna tecla de control, lo que significa que no puedes usarla para detener el proceso que se está ejecutando actualmente. INT, TSTP y QUIT son las únicas señales que puedes usar con teclas de control (aunque algunos sistemas tienen señales de teclas de control adicionales).
Puedes personalizar las teclas de control que se utilizan para enviar señales con opciones del comando \emph{stty(1)}. Estas opciones varían de un sistema a otro; consulta la página de manual del comando para obtener información, pero la sintaxis habitual es \texttt{stty signame char}. \emph{signame} es un nombre para la señal que, lamentablemente, a menudo no es el mismo que los nombres que usamos aquí. La Tabla \ref{Tab:1-7} en el \hyperref[sec:Chapter1]{Capítulo 1} enumera los nombres \emph{stty} para señales y las acciones del controlador de terminal que se encuentran en todas las versiones modernas de Unix. \emph{char} es el carácter de control, que puedes dar en la misma notación que usamos. Por ejemplo, para configurar tu tecla INT en CTRL-X en la mayoría de los sistemas, usa:
\begin{lstlisting}[language=bash]
stty intr ^X
\end{lstlisting}
Ahora que te hemos dicho cómo hacer esto, deberíamos agregar que no lo recomendamos. Cambiar las teclas de señal podría causar problemas si alguien más tiene que detener un proceso descontrolado en tu máquina.
La mayoría de las otras señales son utilizadas por el sistema operativo para informar a los procesos sobre condiciones de error, como una instrucción de código de máquina incorrecta, una dirección de memoria incorrecta, división por cero u otros eventos como la disponibilidad de entrada en un descriptor de archivo o el temporizador (<<alarma>> en la terminología de Unix) que se activa. Las señales restantes se utilizan para condiciones de error esotéricas que solo interesan a los programadores de sistemas de bajo nivel; las versiones más nuevas de Unix tienen tipos de señales cada vez más arcanos.
\subsection{kill}
Puedes utilizar el comando incorporado del shell, \emph{kill}, para enviar una señal a cualquier proceso que hayas creado, no solo al trabajo que se está ejecutando actualmente. \emph{kill} toma como argumento el ID del proceso, el número de trabajo o el nombre del comando del proceso al cual deseas enviar la señal. Por defecto, \emph{kill} envía la señal TERM (<<terminar>>), que generalmente tiene el mismo efecto que la señal INT que envías con CTRL-C. Pero puedes especificar una señal diferente utilizando la opción -s y el nombre de la señal, o la opción -n y un número de señal.
\emph{kill} recibe su nombre debido a la naturaleza de la señal TERM predeterminada, pero hay otra razón, que tiene que ver con la forma en que Unix maneja las señales en general. Los detalles completos son demasiado complejos para entrar en ellos aquí, pero la siguiente explicación debería ser suficiente.
La mayoría de las señales hacen que un proceso que las recibe se detenga y muera; por lo tanto, si envías cualquiera de estas señales, <<matas>> el proceso que la recibe. Sin embargo, los programas pueden configurarse para <<atrapar>> señales específicas y tomar alguna otra acción. Por ejemplo, un editor de texto haría bien en guardar el archivo que se está editando antes de terminar cuando recibe una señal como INT, TERM o QUIT. Determinar qué hacer cuando llegan diversas señales es parte de la diversión de la programación de sistemas Unix.
Aquí tienes un ejemplo de \emph{kill}. Supongamos que tienes un proceso \emph{fred} en segundo plano, con un ID de proceso 480 y un número de trabajo 1, que necesita detenerse. Comenzarías con este comando:
\begin{lstlisting}[language=bash]
kill %1
\end{lstlisting}
Si tuviste éxito, verías un mensaje como este:
\begin{lstlisting}[language=bash]
[1] + Terminated fred &
\end{lstlisting}
Si no ves esto, entonces la señal TERM no logró detener el trabajo. El siguiente paso sería intentar con QUIT:
\begin{lstlisting}[language=bash]
kill -s QUIT %1
\end{lstlisting}
Si eso funcionó, verías este mensaje:
\begin{lstlisting}[language=bash]
[1] + Quit(coredump) fred &
\end{lstlisting}
El shell indica la señal que mató al programa (\emph{Quit}) y el hecho de que produjo un archivo de núcleo. Cuando un programa sale de manera normal, el estado de salida que devuelve al shell es un valor entre 0 y 255. Cuando un programa muere al recibir una señal, sale no con un valor de estado de su elección, sino con el estado 256+\emph{N}, donde \emph{N} es el número de la señal que recibió. (Con \emph{ksh88} y la mayoría de las otras shells, los estados de salida normales están entre 0 y 127, y el estado de <<muerte por señal>> es 128+\emph{N}. Caveat emptor.)
Si ni siquiera \texttt{QUIT} funciona, el método de último recurso sería usar \texttt{KILL}:
\begin{lstlisting}[language=bash]
kill -s KILL %1
\end{lstlisting}
(Observa cómo esto tiene el sabor de <<gritarle>> al proceso fuera de control). Esto produce el mensaje:
\begin{lstlisting}[language=bash]
[1] + Killed fred &
\end{lstlisting}
Es imposible que un proceso atrape una señal KILL: el sistema operativo debería terminar el proceso de inmediato e incondicionalmente. Si no lo hace, entonces tu proceso está en uno de los <<estados divertidos>> que veremos más adelante en este capítulo, o (mucho menos probable) hay un error en tu versión de Unix.
En sistemas de control de trabajos, hay una señal adicional e inatrapable: STOP. Esto es similar a TSTP, ya que suspende el trabajo objetivo. Pero a diferencia de TSTP, no se puede capturar ni ignorar. Es una señal más drástica que TSTP, pero menos que QUIT o TERM, ya que un trabajo detenido aún se puede continuar con \emph{fg} o \emph{bg}. El shell Korn proporciona el alias predefinido \texttt{stop='kill -s STOP'} para facilitar la detención de trabajos.
La \hyperref[box:8-1]{Tarea 8-1} es otro ejemplo de cómo usar el comando \emph{kill}.
\begin{mybox}[Tarea 8-1]\label{box:8-1}
Escribe una función llamada \emph{killalljobs} que mate todos los trabajos en segundo plano.\footnote{Para probar tu comprensión de cómo funciona el shell, responde a esta pregunta: ¿por qué esto no se puede hacer como un script separado?}
\end{mybox}
La solución para esta tarea es simple, utilizando \texttt{jobs -p}:
\begin{lstlisting}[language=bash]
function killalljobs {
kill "$@" $(jobs -p)
}
\end{lstlisting}
Es posible que te sientas tentado a usar la señal KILL inmediatamente, en lugar de probar primero TERM (la predeterminada) y QUIT. No lo hagas. TERM y QUIT están diseñadas para darle al proceso la oportunidad de limpiar antes de salir, mientras que KILL detendrá el proceso, sin importar en qué parte de su cálculo se encuentre. \emph{Usa KILL solo como último recurso}.
Puedes usar el comando \emph{kill} con cualquier proceso que crees, no solo trabajos en segundo plano de tu shell actual. Por ejemplo, si usas un sistema de ventanas, es posible que tengas varias ventanas de terminal, cada una de las cuales ejecuta su propia shell. Si una shell está ejecutando un proceso que deseas detener, puedes matarlo desde otra ventana, pero no puedes referirte a él con un número de trabajo porque se está ejecutando bajo una shell diferente. Debes usar su ID de proceso en su lugar.
\subsection{ps}
Esta es probablemente la única situación en la que un usuario casual necesitaría conocer el ID de un proceso. El comando \emph{ps(1)} te brinda esta información; sin embargo, también puede darte mucha información adicional por la cual debes navegar.
\emph{ps} es un comando complejo. Tiene muchas opciones, algunas de las cuales difieren de una versión de Unix a otra. Para agregar a la confusión, es posible que necesites opciones diferentes en diferentes versiones de Unix para obtener la misma información. Usaremos opciones disponibles en los dos principales tipos de sistemas Unix, los derivados de System V (como la mayoría de las versiones para Intel x86, así como Solaris, AIX de IBM y HP-UX de Hewlett-Packard) y BSD (Ultrix de Compaq, SunOS 4.x y también GNU/Linux). Si no estás seguro de qué tipo de versión de Unix tienes, prueba primero con las opciones de System V.
Puedes invocar \emph{ps} en su forma más simple sin ninguna opción. En este caso, imprime una línea de información sobre el shell de inicio de sesión actual y cualquier proceso que se esté ejecutando bajo ella (es decir, trabajos en segundo plano). Por ejemplo, si invocaste tres trabajos en segundo plano, como vimos anteriormente en el capítulo, \emph{ps} en versiones derivadas de System V de Unix produciría una salida que se parece a esto:
\begin{lstlisting}[language=bash]
PID TTY TIME COMD
146 pts/10 0:03 ksh
2349 pts/10 0:03 fred
2367 pts/10 0:17 bob
2387 pts/10 0:06 george
2389 pts/10 0:09 dave
2390 pts/10 0:00 ps
\end{lstlisting}
La salida en sistemas derivados de BSD se vería así:
\begin{lstlisting}[language=bash]
PID TT STAT TIME COMMAND
146 10 S 0:03 /bin/ksh -i
2349 10 R 0:03 fred
2367 10 D 0:17 bob
2387 10 S 0:06 george
2389 10 R 0:09 dave
2390 10 R 0:00 ps
\end{lstlisting}
(Puedes ignorar la columna STAT). Esto es un poco como el comando \emph{jobs}. PID es el ID del proceso; TTY (o TT) es el terminal (o pseudo-terminal, si estás utilizando un sistema de ventanas) desde el cual se invocó el proceso; TIME es la cantidad de tiempo del procesador (no el tiempo real o <<de reloj>>) que el proceso ha utilizado hasta ahora; COMD (o COMMAND) es el comando. Observa que la versión BSD incluye los argumentos del comando, si los hay; también observa que la primera línea informa sobre el proceso del shell principal, y en la última línea, \emph{ps} informa sobre sí mismo.
\emph{ps} sin argumentos lista todos los procesos iniciados desde el terminal o pseudo-terminal actual. Pero como \emph{ps} no es un comando de shell, no correlaciona los ID de procesos con los números de trabajo del shell. Tampoco te ayuda a encontrar el ID del proceso fuera de control en otra ventana del shell.
Para obtener esta información, usa \texttt{ps -a} (para <<todo>>); esto lista información sobre un conjunto diferente de procesos, dependiendo de tu versión de Unix.
\subsubsection{System V}
En lugar de enumerar todos los procesos que se iniciaron bajo un terminal específico, \texttt{ps -a} en sistemas derivados de System V lista todos los procesos asociados con cualquier terminal que no sean líderes de grupo. Para nuestros propósitos, un <<líder de grupo>> es el shell principal de un terminal o ventana. Por lo tanto, si estás utilizando un sistema de ventanas, \texttt{ps -a} lista todos los trabajos iniciados en todas las ventanas (por todos los usuarios), pero no sus shells principales.
Supongamos que, en el ejemplo anterior, tienes solo un terminal o ventana. Entonces, \texttt{ps -a} imprime la misma salida que \emph{ps} simple, excepto por la primera línea, ya que esa es el shell principal. Esto no parece ser muy útil.
Pero considera lo que sucede cuando tienes varias ventanas abiertas. Digamos que tienes tres ventanas, todas ejecutando emuladores de terminal como \emph{xterm} para el Sistema de Ventanas X. Inicias trabajos en segundo plano \emph{fred, dave} y \emph{bob} en ventanas con números de pseudo-terminal 1, 2 y 3, respectivamente. Esta situación se muestra en la Figura \ref{fig:8-1}.
\begin{figure}[h!]
\centering
\includegraphics[scale=0.5]{fig_09}
\caption{\small{Trabajos en segundo plano en múltiples ventanas}}
\label{fig:8-1}
\end{figure}
Supongamos que estás en la ventana superior. Si escribes \emph{ps}, verás algo como esto:
\begin{lstlisting}[language=bash]
PID TTY TIME COMD
146 pts/1 0:03 ksh
2349 pts/1 0:03 fred
2390 pts/1 0:00 ps
\end{lstlisting}
Pero si escribes \emph{ps -a}, verás esto:
\begin{lstlisting}[language=bash]
PID TTY TIME COMD
2349 pts/1 0:03 fred
2367 pts/2 0:17 bob
2389 pts/3 0:09 dave
2390 pts/1 0:00 ps
\end{lstlisting}
Ahora deberías ver cómo \texttt{ps -a} puede ayudarte a rastrear un proceso fuera de control. Si es \emph{dave}, puedes escribir \texttt{kill 2389}. Si eso no funciona, intenta \texttt{kill -s QUIT 2389}, o en el peor caso, \texttt{kill -s KILL 2389}.
\subsubsection{BSD}
En sistemas derivados de BSD, \footnote{En sistemas GNU/Linux, \emph{ps} actúa como la versión de BSD.}
\emph{ps -a} lista todos los trabajos que se iniciaron en cualquier terminal; en otras palabras, es un poco como concatenar los resultados de \emph{ps} simple para cada usuario en el sistema. Dado el escenario anterior, \texttt{ps -a} te mostrará todos los procesos que la versión de System V muestra, además de los líderes de grupo (shells principales).
Desafortunadamente, \texttt{ps -a} (en cualquier versión de Unix) no informará sobre procesos que se encuentren en ciertas condiciones patológicas donde <<olvidan>> cosas como qué shell los invocó y a qué terminal pertenecen. Estos procesos tienen nombres coloridos (zombies, huérfanos) que realmente se utilizan en la literatura técnica de Unix, no solo de manera informal por programadores de sistemas profesionales. Si tienes un problema grave con un proceso fuera de control, es posible que el proceso haya ingresado a uno de estos estados.
No nos preocupemos por el porqué o cómo un proceso llega a este estado. Todo lo que necesitas entender es que el proceso no aparecerá cuando escribas \texttt{ps -a}. Necesitas otra opción para \emph{ps} para verlo: en System V, es \texttt{ps -e} (<<everything>>), mientras que en BSD, es \texttt{ps -ax}.
Estas opciones indican a \emph{ps} que liste procesos que no se iniciaron desde terminales o que <<olvidaron>> desde qué terminal se iniciaron. La primera categoría incluye muchos procesos que probablemente ni siquiera sabías que existían: estos incluyen procesos básicos que ejecutan el sistema y los llamados \emph{daemons} (pronunciado <demonios>>) que manejan servicios del sistema como correo, impresión, sistemas de archivos en red, etc.
De hecho, la salida de \texttt{ps -e} o \texttt{ps -ax} es una excelente fuente de educación sobre los entresijos internos del sistema Unix, si tienes curiosidad por conocerlos. Ejecuta el comando en tu sistema y, para cada línea de la lista que parezca interesante, invoca `man` en el nombre del proceso o búscalo en el Manual del Programador de Unix para tu sistema.
Las shells de usuario y los procesos se enumeran en la parte inferior de la salida de \texttt{ps -e} o \texttt{ps -ax}; aquí es donde debes buscar procesos fuera de control. Observa que muchos procesos en la lista tienen un ? en lugar de un terminal. Estos bien no deberían tener uno (como los \emph{daemons} básicos) o son procesos fuera de control. Por lo tanto, es probable que si \texttt{ps -a} no encuentra un proceso que estás intentando eliminar, \texttt{ps -e} (o \texttt{ps -ax}) lo mostrará con ? en la columna TTY (o TT). Puedes determinar qué proceso deseas observando la columna COMD (o COMMAND).
\subsection{kill: La historia completa}
El comando \emph{kill} tiene un nombre bastante equívoco. Debería haberse llamado \emph{sendsignal} o algo similar, ya que envía señales a los procesos. (De hecho, el nombre se deriva de la llamada al sistema \emph{kill(2)}, que el comando \emph{kill} utiliza para enviar señales, y que está igualmente mal nombrada).
Como vimos anteriormente, \texttt{kill -l} te proporciona la lista completa de nombres de señales disponibles en tu sistema. El comportamiento de la versión incorporada de \emph{kill} se ha racionalizado considerablemente en \emph{ksh93}. Las opciones y lo que hacen se resumen en la Tabla \ref{Tab:8-2}.
\begin{table}[h]
\center
\caption{Opciones para kill}
\label{Tab:8-2}
\begin{tabular}{|m{4cm}|m{11cm}|} \hline
\textbf{Opción} & \textbf{Significado} \\ \hline
kill \emph{job}... & Envía la señal TERM a cada \emph{trabajo} nombrado. Este es el uso normal. \\\hline
kill -l & Enumera los nombres de todas las señales compatibles. \\\hline
kill -l \emph{signal}... & Si \emph{signal} es un número, imprime su nombre. Si es un nombre, imprime su número. Si \emph{signal} es un número mayor que 256, se trata como un estado de salida. El shell resta 256 e imprime la señal correspondiente. \\\hline
kill -s \emph{signal-name job}... & Envía la señal nombrada por \emph{signal-name} a cada trabajo dado. \\\hline
kill -n \emph{signal-number job}... & Envía la señal numérica dada por \emph{signal-number} a cada trabajo dado. \\\hline
kill \emph{-signal job}... & Envía la señal especificada por \emph{signal} a cada \emph{trabajo} dado. \emph{signal} puede ser un número o un nombre de señal. Esta forma se considera obsoleta; se proporciona por compatibilidad con \emph{ksh88} y la orden externa \emph{kill(1)}. \\\hline
\end{tabular}
\end{table}
Un lugar para aprovechar la capacidad de \emph{kill} de convertir un número en un nombre es al emitir diagnósticos. Cuando un trabajo muere debido a una señal, el estado de salida es 256 más el número de señal. Por lo tanto, podrías usar un código como este para producir un diagnóstico significativo desde un script:
\begin{lstlisting}[language=bash]
es=$? # guarda el estado de salida
if ((es >= 256)); then
print "el trabajo recibió la señal $(kill -l $((es - 256)))"
fi
\end{lstlisting}
\section{trap}\label{sec:84}
Hemos estado discutiendo cómo las señales afectan al usuario casual; ahora hablemos un poco sobre cómo los programadores de shell pueden usarlas. No profundizaremos demasiado en esto, porque realmente es el ámbito de los programadores de sistemas.
Mencionamos anteriormente que los programas en general pueden configurarse para <<atrapar>> señales específicas y procesarlas a su manera. El comando integrado \emph{trap} te permite hacer esto desde un script de shell. \emph{trap} es crucial para <<blindar>> programas de shell extensos para que reaccionen adecuadamente a eventos anómalos, al igual que los programas en cualquier lenguaje deberían protegerse contra una entrada no válida. También es importante para ciertas tareas de programación de sistemas, como veremos en el próximo capítulo.
La sintaxis de trap es:
\begin{lstlisting}[language=bash]
trap cmd sig1 sig2 ...
\end{lstlisting}
Es decir, cuando se recibe cualquiera de \emph{sig1, sig2}, etc., ejecuta \emph{cmd} y luego continúa la ejecución. Después de que \emph{cmd} finaliza, el script continúa la ejecución justo después del comando que fue interrumpido. \footnote{Esto es lo que \emph{suele} suceder. A veces, el comando en ejecución actual se aborta (\emph{sleep} actúa así, como veremos pronto); otras veces, termina su ejecución. Los detalles adicionales están más allá del alcance de este libro.}
Por supuesto, \emph{cmd} puede ser un script o una función. Las señales pueden especificarse por nombre o por número. También puedes invocar \emph{trap} sin argumentos, en cuyo caso el shell imprime una lista de cualquier trampa que se haya establecido, utilizando nombres simbólicos para las señales. Si usas \texttt{trap -p}, el shell imprime la configuración de la trampa de una manera que se puede guardar y volver a leer más tarde mediante una invocación diferente del shell.
El shell escanea el texto de \emph{cmd} dos veces. La primera vez es mientras se prepara para ejecutar el comando \emph{trap}; todas las sustituciones como se describen en el C\hyperref[sec:Chapter7]{apítulo 7} se realizan antes de ejecutar el comando \emph{trap}. La segunda vez es cuando el shell ejecuta realmente la trampa. Por esta razón, es mejor usar comillas simples alrededor de \emph{cmd} en el texto del programa de shell. Cuando el shell ejecuta el comando de la trampa, \texttt{\$?} es siempre el estado de salida del último comando ejecutado antes de que comenzara la trampa. Esto es importante para diagnósticos.
Aquí tienes un ejemplo simple que muestra cómo funciona \emph{trap}. Supongamos que tenemos un script de shell llamado loop con este código:
\begin{lstlisting}[language=bash]
while true; do
sleep 60
done
\end{lstlisting}
Esto simplemente pausa durante 60 segundos (el comando \emph{sleep(1)}) y se repite indefinidamente. \emph{true} es un comando <<sin hacer nada>> cuyo estado de salida siempre es 0. Por eficiencia, está integrado en el shell. (El comando \emph{false} es un comando similar <<sin hacer nada>> cuyo estado de salida siempre es 1. También está integrado en el shell.) Como sucede, \emph{sleep} también está integrado en el shell. Intenta escribir este script. Invócalo, déjalo correr un rato y luego escribe CTRL-C (asumiendo que esa es tu tecla de interrupción). Debería detenerse y deberías recuperar tu indicador de shell.
Ahora inserta la siguiente línea al principio del script:
\begin{lstlisting}[language=bash]
trap 'print "¡Presionaste control-C!"' INT
\end{lstlisting}
Invoca el script de nuevo. Ahora presiona CTRL-C. Las probabilidades son abrumadoras de que estés interrumpiendo el comando \emph{sleep} (en lugar de \emph{true}). Deberías ver el mensaje <<¡Presionaste control-C!>>, y el script no se detendrá; en cambio, el comando \emph{sleep} se abortará y volverá a empezar otro \emph{sleep}. Presiona CTRL-\textbackslash{} para detenerlo. Escribe \texttt{rm core} para deshacerte del archivo de volcado de núcleo resultante.
A continuación, ejecuta el script en segundo plano escribiendo \texttt{loop \&}. Escribe \texttt{kill \%loop} (es decir, envíale la señal TERM); el script se detendrá. Agrega TERM al comando de trampa, de modo que se vea así:
\begin{lstlisting}[language=bash]
trap 'print "¡Presionaste control-C!"' INT TERM
\end{lstlisting}
Ahora repite el proceso: ejecútalo en segundo plano y escribe \texttt{kill \%loop}. Como antes, verás el mensaje y el proceso seguirá ejecutándose. Escribe \texttt{kill -KILL \%loop} para detenerlo.
Observa que el mensaje no es realmente apropiado cuando usas \emph{kill}. Cambiaremos el script para que imprima un mensaje mejor en el caso de \emph{kill}:
\begin{lstlisting}[language=bash]
trap 'print "¡Presionaste control-C!"' INT
trap 'print "¡Intentaste matarme!"' TERM
while true; do
sleep 60
done
\end{lstlisting}
Ahora pruébalo de ambas maneras: en primer plano con CTRL-C y en segundo plano con \emph{kill}. Verás mensajes diferentes.
\subsection{Trampasy funciones}
La relación entre las trampas y las funciones del shell es directa, pero tiene ciertos matices que vale la pena discutir. Lo más importante que debes entender es que las funciones del shell Korn (aquellas creadas con la palabra clave \texttt{function}; consulta el C\hyperref[sec:Chapter4]{Capítulo 4}) tienen sus propias trampas locales; estas no son conocidas fuera de la función. Las funciones de estilo antiguo de POSIX (aquellas creadas con la sintaxis \texttt{nombre()}) comparten trampas con el script principal.
Comencemos con las funciones de estilo \texttt{function}, donde las trampas son locales. En particular, el script circundante no las conoce. Considera este código:
\begin{lstlisting}[language=bash]
function settrap {
trap 'print "You hit control-C!"' INT
}
settrap
while true; do
sleep 60
done
\end{lstlisting}
Si invocas este script y presionas tu tecla de interrupción, simplemente se cierra. La trampa en INT en la función solo es conocida dentro de esa función. Por otro lado:
\begin{lstlisting}[language=bash]
function loop {
trap 'print "How dare you!"' INT
while true; do
sleep 60
done
}
trap 'print "You hit control-C!"' INT
loop
\end{lstlisting}
Cuando ejecutas este script y presionas tu tecla de interrupción, imprime <<¡Cómo te atreves!>> Pero, ¿qué tal esto?
\begin{lstlisting}[language=bash]
function loop {
while true; do
sleep 60
done
}
trap 'print "You hit control-C!"' INT
loop
print 'exiting ...'
\end{lstlisting}
Esta vez, el código de bucle está dentro de una función, y la trampa se establece en el script circundante. Si presionas tu tecla de interrupción, imprime el mensaje y luego imprime <<saliendo...>>. No repite el bucle como antes.
¿Por qué? Recuerda que cuando la señal entra, el shell aborta el comando actual, que en este caso es una llamada a una función. Toda la función se aborta, y la ejecución se reanuda en la siguiente declaración después de la llamada a la función.
La ventaja de las trampas locales a las funciones es que te permiten controlar el comportamiento de una función por separado del código circundante.
Sin embargo, es posible que desees definir trampas globales dentro de funciones. Existe una forma bastante chapucera de hacerlo; depende de una función que presentamos en el \hyperref[sec:Chapter9]{Capítulo 9}, que llamamos <<falsa señal>>. Aquí hay una manera de configurar trapcode como una trampa global para la señal SIG dentro de una función:
\begin{lstlisting}[language=bash]
trap "trap trapcode sig" EXIT
\end{lstlisting}
Esto configura el comando \texttt{trap trapcode SIG} para ejecutarse justo después de que la función salga, momento en el cual el script del shell circundante está en el ámbito (es decir, está <<a cargo>>). Cuando se ejecuta ese comando, \emph{trapcode} se configura para manejar la señal SIG.
Por ejemplo, es posible que desees restablecer la trampa en la señal que acabas de recibir, así:
\begin{lstlisting}[language=bash]
function trap_handler {
trap "trap second_handler INT" EXIT
print 'Interrupt: one more to abort.'
}
function second_handler {
print 'Aborted.' exit
}
trap trap_handler INT
\end{lstlisting}
Este código actúa como la utilidad de correo Unix: cuando estás escribiendo un mensaje, debes presionar tu tecla de interrupción dos veces para abortar el proceso.
Hay una forma menos chapucera de hacer esto, aprovechando el hecho de que las funciones de estilo POSIX comparten trampas con el script principal:
\begin{lstlisting}[language=bash]
# POSIX style function, trap is global
trap_handler () {
trap second_handler INT
print 'Interrupt: one more to abort.'
}
function second_handler {
print 'Aborted.' exit
}
trap trap_handler INT
while true ; do
sleep 60
done
\end{lstlisting}
Si escribes esto y lo ejecutas, obtendrás los mismos resultados que en el ejemplo anterior, sin la artimaña adicional de usar la falsa señal EXIT.
Hablando de \emph{mail}, en la \hyperref[box:8-2]{Tarea 8-2} mostraremos un ejemplo más práctico de trampas.
\begin{mybox}[Tarea 8-2]\label{box:8-2}
Como parte de un sistema de correo electrónico, escribe el código de shell que permite a un usuario componer un mensaje.
\end{mybox}
La idea básica es usar \emph{cat} para crear el mensaje en un archivo temporal y luego pasar el nombre del archivo a un programa que realmente envía el mensaje a su destino. El código para crear el archivo es muy simple:
\begin{lstlisting}[language=bash]
msgfile=/tmp/msg$$
cat > $msgfile
\end{lstlisting}
Dado que \emph{cat} sin un argumento lee desde la entrada estándar, esto simplemente espera a que el usuario escriba un mensaje y lo termine con el carácter de fin de archivo CTRL-D
\subsection{Variables de ID de proceso y ficheros temporales}
Lo único nuevo aquí es \$\$ en la expresión del nombre de archivo. Esta es una variable especial del shell cuyo valor es el ID de proceso del shell actual.
Para ver cómo funciona \$\$, escribe \texttt{ps} y toma nota del ID de proceso de tu proceso de shell (\emph{ksh}). Luego, escribe \texttt{print "\$\$"}; el shell responderá con ese mismo número. Ahora escribe \texttt{ksh} para iniciar un subproceso del shell y, cuando obtengas un indicador, repite el proceso. Deberías ver un número diferente, probablemente ligeramente superior al último.
Puedes examinar la relación entre padres e hijos con más detalle utilizando la variable \texttt{PPID} (ID de proceso padre). \emph{ksh} establece esto como el ID de proceso del proceso padre. Cada vez que inicias una nueva instancia de \emph{ksh}, si escribes \texttt{print \$PPID}, deberías ver un número que es igual a \$\$ del shell anterior.
Otra variable del shell relacionada es \emph{!} (es decir, su valor es \texttt{\$!}), que contiene el ID de proceso del trabajo en segundo plano más recientemente invocado. Para ver cómo funciona esto, inicia cualquier trabajo en segundo plano y toma nota del ID de proceso impreso por el shell junto a \texttt{[1]}. Luego escribe \texttt{print "\$!"}; deberías ver el mismo número.
Volviendo a nuestro ejemplo de correo: dado que todos los procesos en el sistema deben tener ID de procesos únicos, \$\$ es excelente para construir nombres de archivos temporales. Vimos un ejemplo de esto en el \hyperref[sec:Chapter7]{Capítulo 7}, cuando discutíamos los pasos de evaluación de la línea de comandos, y también hay ejemplos en el \hyperref[sec:Chapter9]{Capítulo 9}. \footnote{En la práctica, los nombres de archivos temporales basados solo en \$\$ pueden llevar a sistemas inseguros. Si tienes el programa \emph{mktemp(1)} en tu sistema, deberías usarlo en tus aplicaciones para generar nombres únicos para tus archivos temporales.}
El directorio \texttt{/tmp} se utiliza convencionalmente para archivos temporales. Por lo general, los archivos en este directorio se borran cada vez que se reinicia la computadora.
Sin embargo, un programa debería limpiar esos archivos antes de salir para evitar ocupar espacio en disco innecesario. Podríamos hacer esto en nuestro código muy fácilmente agregando la línea \texttt{rm \$msgfile} después del código que realmente envía el mensaje. Pero, ¿qué pasa si el programa recibe una señal durante la ejecución? Por ejemplo, ¿qué pasa si un usuario cambia de opinión sobre el envío del mensaje y presiona CTRL-C para detener el proceso? Necesitaríamos limpiar antes de salir. Emularemos el sistema Unix real guardando el mensaje que se está escribiendo en un archivo llamado \emph{dead.letter} en el directorio actual. Podemos hacer esto usando \emph{trap} con una cadena de comandos que incluye un comando de salida:
\begin{lstlisting}[language=bash]
trap 'mv $msgfile dead.letter; exit' INT TERM
msgfile=/tmp/msg$$
cat > $msgfile
# send the contents of $msgfile to the specified mail address ...
rm $msgfile
\end{lstlisting}
Cuando el script recibe una señal INT o TERM, guarda el archivo temporal y luego sale. Ten en cuenta que la cadena de comandos no se evalúa hasta que necesita ejecutarse, por lo que \texttt{\$msgfile} contendrá el valor correcto; es por eso que rodeamos la cadena con comillas simples.
Pero, ¿qué pasa si el script recibe una señal antes de que se cree \texttt{msgfile} - aunque sea poco probable? En ese caso, \emph{mv} intentará cambiar el nombre de un archivo que no existe. Para solucionar esto, necesitamos verificar la existencia del archivo \texttt{\$msgfile} antes de intentar guardarlo. El código para esto es un poco incómodo para ponerlo en una sola cadena de comandos, así que usaremos una función en su lugar:
\begin{lstlisting}[language=bash]
function cleanup {
if [[ -e $msgfile ]]; then
mv $msgfile dead.letter
fi
exit
}
trap cleanup INT TERM
msgfile=/tmp/msg$$
cat > $msgfile
# send the contents of $msgfile to the specified mail address ...
rm $msgfile
\end{lstlisting}
\subsection{Ignorando señales}
A veces, llega una señal que no deseas manejar. Si proporcionas la cadena nula (\texttt{""} o \texttt{''}) como argumento de comando para \emph{trap}, el shell efectivamente ignora esa señal. El ejemplo clásico de una señal que es posible que desees ignorar es \emph{HUP} (hangup), la señal que reciben todos tus procesos en segundo plano cuando cierras sesión. (Si tu conexión realmente se cae, Unix envía la señal \emph{HUP} al shell. El shell reenvía la señal a todos tus procesos en segundo plano o la envía por iniciativa propia si cierras sesión de forma normal).
\emph{HUP} tiene el comportamiento predeterminado habitual: mata al proceso que la recibe. Pero seguramente habrá momentos en los que no querrás que un trabajo en segundo plano finalice cuando cierras sesión. Por ejemplo, podrías iniciar una compilación larga o un trabajo de formato de texto; quieres cerrar sesión y volver más tarde cuando esperas que el trabajo esté terminado. En circunstancias normales, tu trabajo en segundo plano terminaría cuando cierras sesión. Pero si lo ejecutas en un entorno de shell donde la señal \emph{HUP} se ignora, el trabajo se completa.
Para hacer esto, podrías escribir una función simple que se ve así:
\begin{lstlisting}[language=bash]
function ignorehup {
trap "" HUP
eval "$@"
}
\end{lstlisting}
Escribimos esto como una función en lugar de un script por razones que se harán más claras cuando examinemos detenidamente los subprocesos al final de este capítulo.
En realidad, hay un comando Unix llamado \emph{nohup} que hace precisamente esto. La función \emph{start} del último capítulo podría incluir \emph{nohup}:
\begin{lstlisting}[language=bash]
function start {
eval nohup "$@" > logfile 2>&1 &
}
\end{lstlisting}
Esto evita que \emph{HUP} termine tu comando y guarda su salida estándar y de error en un archivo. De hecho, lo siguiente es igualmente bueno:
\begin{lstlisting}[language=bash]
function start {
nohup "$@" > logfile 2>&1 &
}
\end{lstlisting}
Si comprendes por qué \emph{eval} es prácticamente redundante cuando usas \emph{nohup} en este caso, entonces tienes un sólido entendimiento del material en el \hyperref[sec:Chapter7]{Capítulo 7}.
\subsection{Restablecimiento de trampas}
Otro <<caso especial>> del comando \emph{trap} ocurre cuando proporcionas un guion (-) como argumento de comando. Esto restablece la acción tomada cuando se recibe la señal al valor predeterminado, que generalmente es la terminación del proceso.
Como ejemplo de esto, volvamos a la \hyperref[box:8-2]{Tarea 8-2}, nuestro programa de correo. Después de que el usuario ha terminado de enviar el mensaje, el archivo temporal se borra. En ese momento, ya que ya no hay necesidad de <<limpiar>>, podemos restablecer la trampa de señal a su estado predeterminado. El código para esto, aparte de las definiciones de funciones, es:
\begin{lstlisting}[language=bash]
trap cleanup INT TERM
msgfile=/tmp/msg$$
cat > $msgfile
# enviar el contenido de $msgfile a la dirección de correo especificada ...
rm $msgfile
trap - INT TERM
\end{lstlisting}
La última línea de este código restablece los manejadores para las señales INT y TERM.
En este punto, es posible que estés pensando que uno podría involucrarse seriamente con el manejo de señales en un script de shell. Es cierto que los programas de alta resistencia dedican cantidades considerable de código al manejo de señales. Pero estos programas son casi siempre lo suficientemente grandes como para que el código de manejo de señales sea una fracción muy pequeña del conjunto. Por ejemplo, puedes apostar a que el sistema de correo real de Unix es bastante resistente.
Sin embargo, es probable que nunca escribas un script de shell lo suficientemente complejo y que necesite ser lo suficientemente robusto como para justificar mucho manejo de señales. Puedes escribir un prototipo para un programa tan grande como el correo en código de shell, pero los prototipos, por definición, no necesitan ser a prueba de balas.
Por lo tanto, no deberías preocuparte por poner código de manejo de señales en cada script de shell de 20 líneas que escribas. Nuestro consejo es determinar si hay situaciones en las que una señal podría hacer que tu programa haga algo seriamente malo y agregar código para manejar esas contingencias. ¿Qué es <<seriamente malo>>? Bueno, con respecto a los ejemplos anteriores, diríamos que el caso en el que \emph{HUP} hace que tu trabajo termine al cerrar la sesión es seriamente malo, mientras que la situación del archivo temporal en nuestro programa de correo no lo es.
El shell Korn tiene varias opciones nuevas para \emph{trap} (con respecto al mismo comando en la mayoría de los shell de Bourne) que lo hacen útil como ayuda para depurar scripts de shell. Las cubrimos en el \hyperref[sec:Chapter9]{Capítulo 9}.
\section{Corrutinas}
Hemos dedicado las últimas páginas a detalles casi microscópicos del comportamiento de los procesos. En lugar de continuar nuestra inmersión en las profundidades turbias, volveremos a una vista de más alto nivel de los procesos.
Anteriormente en este capítulo, cubrimos formas de controlar múltiples trabajos simultáneos dentro de una sesión interactiva de inicio de sesión; ahora consideramos el control de múltiples procesos dentro de programas de shell. Cuando dos (o más) procesos están programados explícitamente para ejecutarse simultáneamente y posiblemente comunicarse entre sí, los llamamos corrutinas.
Esto no es realmente nuevo: una \emph{tubería} es un ejemplo de corrutinas. El constructo de una tubería del shell encapsula un conjunto bastante sofisticado de reglas sobre cómo interactúan los procesos entre sí. Si observamos más de cerca estas reglas, podremos entender mejor otras formas de manejar corrutinas, la mayoría de las cuales resultan ser más simples que las tuberías.
Cuando invocas una tubería simple, por ejemplo, \texttt{ls | more}, el shell invoca una serie de operaciones primitivas de Unix, también conocidas como \emph{llamadas al sistema}. En efecto, el shell le indica a Unix que haga lo siguiente; en caso de que estés interesado, incluimos entre paréntesis la llamada al sistema real utilizada en cada paso:
\begin{enumerate}
\item Crear la tubería que manejará la entrada/salida entre los procesos (llamada al sistema \emph{pipe}).
\item Crear dos subprocesos, que llamaremos P1 y P2 (\emph{fork}).
\item Configurar la entrada/salida entre los procesos para que la salida estándar de P1 alimente la entrada estándar de P2 (\emph{dup}, \emph{close}).
\item Iniciar \texttt{/bin/ls} en el proceso P1 (\emph{exec}).
\item Iniciar \texttt{/bin/more} en el proceso P2 (\emph{exec}).
\item Esperar a que ambos procesos terminen (\emph{wait}).
\end{enumerate}
Probablemente puedas imaginar cómo cambian los pasos anteriores cuando la tubería involucra más de dos procesos.
Ahora simplifiquemos las cosas. Veremos cómo hacer que varios procesos se ejecuten al mismo tiempo si los procesos no necesitan comunicarse. Por ejemplo, queremos que los procesos \emph{dave} y \emph{bob} se ejecuten como corrutinas, sin comunicación, en un script de shell. Ambos deben ejecutarse por completo antes de que el script finalice. Nuestra solución inicial sería esta:
\begin{lstlisting}[language=bash]
dave &
bob
\end{lstlisting}
Supongamos por un momento que \emph{bob} es el último comando en el script. Lo anterior funciona, pero solo si \emph{dave} termina primero. Si \emph{dave} aún se está ejecutando cuando el script termina, se convierte en un proceso \emph{huérfano}, es decir, entra en uno de los <<estados extraños>> que mencionamos anteriormente en este capítulo. No importa los detalles de ser un huérfano; simplemente cree que no quieres que esto suceda, y si ocurre, es posible que necesites usar el método de <<proceso fugitivo>> para detenerlo, discutido anteriormente en este capítulo. (Por ejemplo, considera el caso en el que \emph{dave} se lanza a un consumo excesivo de recursos, ralentizando tu sistema, y es mucho más difícil detenerlo si el script principal ya ha salido).
\subsection{wait}
Hay una manera de asegurarse de que el script no termine antes que \emph{dave}: el comando integrado \emph{wait}. Sin argumentos, \emph{wait} simplemente espera hasta que todos los trabajos en segundo plano hayan terminado. Entonces, para asegurarse de que el código anterior se comporte correctamente, agregaríamos \emph{wait} de la siguiente manera:
\begin{lstlisting}[language=bash]
dave &
bob
wait
\end{lstlisting}
Aquí, si \emph{bob} termina primero, el shell principal espera a que \emph{dave} termine antes de finalizar.
Si tu script tiene más de un trabajo en segundo plano y necesitas esperar a que terminen trabajos específicos, puedes darle a \emph{wait} el mismo tipo de argumento de trabajo (con un signo de porcentaje) que usarías con \emph{kill, fg} o \emph{bg}.
Sin embargo, es probable que encuentres que \emph{wait} sin argumentos es suficiente para todas las corrutinas que programarás. Las situaciones en las que necesitarías esperar trabajos en segundo plano específicos son bastante complejas y están fuera del alcance de este libro.
\subsection{Ventajas y desventajas de las corrutinas}
De hecho, puede que te preguntes por qué necesitarías programar corrutinas que no se comunican entre sí. Por ejemplo, ¿por qué no simplemente ejecutar \emph{bob} después de \emph{dave} de la manera habitual? ¿Qué ventaja hay en ejecutar los dos trabajos simultáneamente?
Si estás ejecutando un equipo con un solo procesador (CPU), hay una ventaja de rendimiento, pero solo si tienes desactivada la opción \emph{bgnice} (ver \hyperref[sec:Chapter3]{Capítulo 3}), y aún así solo en ciertas situaciones.
Hablando en términos generales, puedes caracterizar un proceso en función de cómo utiliza los recursos del sistema de tres maneras: si es \emph{intensivo en CPU} (por ejemplo, realiza muchos cálculos numéricos), \emph{intensivo en E/S} (realiza mucha lectura o escritura en el disco) o \emph{interactivo} (requiere intervención del usuario).
Ya sabemos desde el \hyperref[sec:Chapter1]{Capítulo 1} que no tiene sentido ejecutar un trabajo interactivo en segundo plano. Pero aparte de eso, cuanto más difieran dos o más procesos en estos tres criterios, mejor es ejecutarlos simultáneamente. Por ejemplo, un cálculo estadístico intensivo en números funcionaría bien al mismo tiempo que una larga y intensiva consulta de base de datos.
Por otro lado, si dos procesos utilizan los recursos de manera similar, puede ser menos eficiente ejecutarlos al mismo tiempo que ejecutarlos secuencialmente. ¿Por qué? Básicamente, porque en tales circunstancias, el sistema operativo a menudo tiene que <<dividir el tiempo>> de los recursos en disputa.
Por ejemplo, si ambos procesos consumen muchos recursos del disco, el sistema operativo puede entrar en un modo en el que controla constantemente el interruptor del disco entre los dos procesos competidores; el sistema termina gastando al menos tanto tiempo haciendo el cambio como en los propios procesos. Este fenómeno se conoce como \emph{thrashing}; en su forma más severa, puede hacer que un sistema prácticamente se paralice. El thrashing es un problema común; tanto los administradores del sistema como los diseñadores de sistemas operativos pasan mucho tiempo tratando de minimizarlo.
\subsection{Paralelización}
Pero si tienes una computadora con múltiples CPUs \footnote{Los sistemas multiprocesador solían encontrarse solo en servidores a gran escala ubicados en salas de máquinas especiales con control climático. Hoy en día, los sistemas multiprocesador de escritorio están disponibles y se están volviendo cada vez más comunes, aunque los sistemas con más de alrededor de 4 CPUs aún tienden a estar principalmente en salas de máquinas.},
deberías preocuparte menos por el thrashing. Además, las corrutinas pueden proporcionar aumentos dramáticos de velocidad en este tipo de máquina, que a menudo se llama una computadora \emph{paralela}; de manera análoga, descomponer un proceso en corrutinas a veces se denomina \emph{paralelizar} el trabajo.
Normalmente, cuando inicias un trabajo en segundo plano en una máquina con múltiples CPUs, la computadora lo asigna al siguiente procesador disponible. Esto significa que los dos trabajos están realmente, no solo metafóricamente, ejecutándose al mismo tiempo.
En este caso, el tiempo de ejecución de las corrutinas es esencialmente igual al del trabajo de mayor duración más un poco de sobrecarga, en lugar de la suma de los tiempos de ejecución de todos los procesos (aunque si todas las CPUs comparten una unidad de disco común, aún existe la posibilidad de thrashing relacionado con la E/S). En el mejor caso, con todos los trabajos teniendo el mismo tiempo de ejecución y sin contención de E/S, obtienes un factor de aceleración igual al número de CPUs.
Paralelizar un programa a menudo no es fácil; hay varios problemas sutiles involucrados y hay mucho margen para el error. Sin embargo, vale la pena saber cómo paralelizar un script de shell, ya sea que tengas o no una máquina paralela, especialmente porque estas máquinas son cada vez más comunes, incluso en el escritorio.
Mostraremos cómo hacer esto mediante la \hyperref[box:8-3]{Tarea 8-3}, una tarea sencilla cuya solución es propicia para la paralelización.
\begin{mybox}[Tarea 8-3]\label{box:8-3}
Aumenta el script del frente del compilador de C para compilar cada archivo fuente en paralelo.
\end{mybox}
Si tienes múltiples CPUs, hay un potencial considerable para acelerar considerablemente la compilación de múltiples archivos fuente en paralelo. Cada archivo es independiente del siguiente, y por lo tanto, crear múltiples archivos objeto simultáneamente realiza más trabajo, más rápido.
Los cambios son relativamente sencillos: inicia la tubería de compilación en segundo plano y luego agrega una declaración \emph{wait} antes de realizar el paso final de enlace:
\begin{lstlisting}[language=bash]
# initialize option-related variables
do_link=true
debug=""
link_libs=""
clib="-lc"
exefile=""
# initialize pipeline components
compile=" | ccom"
assemble=" | as"
optimize=""
# process command-line options
while getopts "cgl:[lib]o:[outfile]O files ..." opt; do
case $opt in
c )
do_link=false ;;
g )
debug="-g" ;;
l )
link_libs+=" -l $OPTARG" ;;
o )
exefile="-o $OPTARG" ;;
O )
optimize=" | optimize" ;;
esac
done
shift $(($OPTIND - 1))
# process the input files
for filename in "$@"; do
case $filename in
*.c )
objname=${filename%.c}.o ;;
*.s )
objname=${filename%.s}.o compile="" ;;
*.o )
objname=$filename # just link it directly with the rest
compile="" assemble="" ;;
* )
print "error: $filename is not a source or object file."
exit 1 ;;
esac
# run a pipeline for each input file; parallelize by backgrounding
eval cat \$filename $compile $assemble $optimize \> \$objname &
objfiles+=" $objname"
compile=" | ccom" assemble=" | as"
done
wait # wait for all compiles to finish before linking
if [[ $do_link == true ]]; then
ld $exefile $objfiles $link_libs $clib
fi
\end{lstlisting}
Este es un ejemplo directo de paralelización, siendo la única <<trampa>> asegurarse de que todas las compilaciones se realicen antes de realizar el paso final de enlace. De hecho, muchas versiones de \emph{make} tienen una bandera de <<ejecutar tantos trabajos en paralelo>> precisamente para obtener la aceleración de la compilación simultánea de archivos independientes.
Pero no toda la vida es tan simple; a veces, simplemente iniciar más trabajos en segundo plano no hará el truco. Por ejemplo, considera múltiples cambios en la misma base de datos: el software de la base de datos (o algo, en algún lugar) tiene que asegurarse de que dos procesos diferentes no estén intentando actualizar el mismo registro al mismo tiempo.
Las cosas se complican aún más al trabajar a un nivel más bajo, con múltiples hilos de control dentro de un solo proceso (algo que afortunadamente no es visible a nivel de shell). Dichos problemas, conocidos como problemas de \emph{control de concurrencia}, se vuelven mucho más difíciles a medida que aumenta la complejidad de la aplicación. Los programas concurrentes complejos a menudo tienen mucho más código para manejar los casos especiales que para el trabajo real que se supone deben hacer.
Por lo tanto, no debería sorprenderte que se haya hecho y se esté haciendo mucha investigación sobre la paralelización, siendo el objetivo final idear una herramienta que paralelice el código automáticamente. (Tales herramientas existen; generalmente trabajan dentro de algún subconjunto estrecho del problema). Incluso si no tienes acceso a una máquina con múltiples CPUs, paralelizar un script de shell es un ejercicio interesante que debería familiarizarte con algunos de los problemas que rodean a las corrutinas.
\subsection{Corrutinas con tuberías bidireccionales}
Ahora que hemos visto cómo programar corrutinas que no se comunican entre sí, construiremos sobre esa base y discutiremos cómo hacer que se comuniquen, de una manera más sofisticada que con una canalización. El shell Korn tiene un conjunto de características que permiten a los programadores establecer una comunicación bidireccional entre corrutinas. Estas características no están incluidas en la mayoría de los shells Bourne.
Si inicias un proceso en segundo plano agregando |\& a un comando en lugar de \&, el shell Korn configura una tubería bidireccional especial entre el shell principal y el nuevo proceso en segundo plano. \texttt{read -p} en el shell principal lee una línea de la salida estándar del proceso en segundo plano; de manera similar, \texttt{print -p} en el shell principal alimenta la entrada estándar del proceso en segundo plano. La Figura \ref{fig:8-2} muestra cómo funciona esto.
\begin{figure}[h!]
\centering
\includegraphics[scale=0.6]{fig_10}
\caption{\small{E/S de las corrutinas}}
\label{fig:8-2}
\end{figure}
Este esquema tiene algunas posibilidades intrigantes. Observa lo siguiente: primero, el shell principal se comunica con el proceso en segundo plano de manera independiente de su propia entrada y salida estándar. En segundo lugar, el proceso en segundo plano no necesita tener idea de que un script de shell se está comunicando con él de esta manera. Esto significa que el proceso en segundo plano puede ser casi cualquier programa preexistente que use su entrada y salida estándar de manera normal.\footnote{Observa que \emph{sort(1)} no encaja del todo aquí. \emph{sort} debe leer toda su entrada antes de poder generar cualquier salida. Todavía puedes usar \emph{sort} en un coproceso, pero tendrías que cerrar el descriptor de archivo utilizado para escribir en el coproceso primero. La forma de hacer esto es mover el descriptor de archivo de entrada del coproceso a un descriptor de archivo numerado y luego cerrarlo. Ambas cosas involucran el comando \emph{exec}, que se trata en el próximo capítulo.}
La \hyperref[box:8-4]{Tarea 8-4} es una tarea que muestra un ejemplo sencillo.
\begin{mybox}[Tarea 8-4]\label{box:8-4}
Quieres tener una calculadora en línea, pero la utilidad Unix existente \emph{dc(1)} utiliza la Notación Polaca Inversa (RPN), al estilo de las calculadoras Hewlett-Packard. Preferirías tener una que funcione como el modelo de \$3.95 que recibiste con esa suscripción a una revista. Escribe un programa de calculadora que acepte notación algebraica estándar.
\end{mybox}
El objetivo aquí es escribir el programa sin volver a implementar el motor de cálculo que ya tiene \emph{dc}, en otras palabras, escribir un programa que traduzca la notación algebraica a RPN y pase la línea traducida a \emph{dc} para hacer el cálculo real. (La utilidad \emph{bc(1)} proporciona funcionalidad similar).
Supondremos que la función \emph{alg2rpn}, que realiza la traducción, ya existe: dada una línea de notación algebraica como argumento, imprime el equivalente en RPN en la salida estándar. Si tenemos esto, entonces el programa de la calculadora (que llamaremos \emph{adc}) es muy simple:
\begin{lstlisting}[language=bash]
dc |&
while read line'?adc> '; do
print -p "$(alg2rpn $line)"
read -p answer
print " = $answer"
done
\end{lstlisting}
La primera línea de este código inicia \emph{dc} como una corrutina con una tubería bidireccional. Luego, el bucle \texttt{while} solicita al usuario una línea y la lee hasta que el usuario escribe CTRL-D para el final de la entrada. El cuerpo del bucle convierte la línea a RPN, la pasa a \emph{dc} a través de la tubería, lee la respuesta de \emph{dc} e imprime después un signo igual. Por ejemplo:
\begin{lstlisting}[language=bash]
$ adc
adc> 2 + 3
= 5
adc> (7 * 8) + 54
= 110
adc> ^D
$
\end{lstlisting}
En realidad, como habrás notado, no es del todo necesario tener una tubería bidireccional con \emph{dc}. Podrías hacerlo con una tubería estándar y dejar que \emph{dc} haga su propia salida, así:
\begin{lstlisting}[language=bash]
{ while read line'?adc> '; do
print "$(alg2rpn $line)"
done
} | dc
\end{lstlisting}
La única diferencia con lo anterior es la falta del signo igual antes de imprimir cada respuesta.
¿Pero qué pasa si quisieras hacer una interfaz gráfica de usuario elegante (GUI), como el programa \emph{xcalc} que viene con muchas instalaciones del sistema X Window? Entonces, claramente, la salida propia de \emph{dc} no sería satisfactoria y necesitarías un control total de tu propia salida estándar en el proceso principal. La interfaz de usuario tendría que capturar la salida de \emph{dc} y mostrarla en la ventana de la calculadora de manera adecuada. La tubería bidireccional es una excelente solución a este problema: simplemente imagina que, en lugar de \texttt{print "\hspace{0.2cm} = \$answer"}, hay una llamada a una rutina que muestra la respuesta en la sección de <<lectura>> de la ventana de la calculadora.
Todo esto sugiere que el esquema de tubería bidireccional es excelente para escribir scripts de shell que interponen una capa de software entre el usuario (o algún otro programa) y un programa existente que utiliza entrada y salida estándar. En particular, es excelente para escribir nuevas interfaces para programas Unix estándar antiguos que esperan entrada y salida de usuario basada en caracteres, línea por línea. Las nuevas interfaces podrían ser GUI o podrían ser programas de interfaz de red que se comuniquen con usuarios a través de enlaces a máquinas remotas. En otras palabras, ¡la construcción de tuberías bidireccionales del shell Korn está diseñada para ayudar a desarrollar software muy actualizado!
\subsection{Tuberías Bidireccionales versus Tuberías Estándar}
Antes de abandonar el tema de las corrutinas, completaremos el círculo mostrando cómo se compara la construcción de tuberías bidireccionales con las canalizaciones regulares. Como probablemente hayas podido deducir hasta ahora, es posible programar una canalización estándar usando |\& con \texttt{print -p}.
Esto tiene la ventaja de reservar la salida estándar del shell principal para otro uso. La desventaja es que la salida estándar del proceso secundario se dirige a la tubería bidireccional: si el proceso principal no la lee con \texttt{read -p}, se pierde efectivamente.
\section{Subprocesos Shell y Subshell}
Las corrutinas representan claramente la relación más compleja entre procesos que define el shell Korn. Para concluir este capítulo, veremos un tipo de relación entre procesos mucho más simple: la de un subproceso de shell con su shell padre. Vimos en el \hyperref[sec:Chapter3]{Capítulo 3} que cada vez que ejecuta un script de shell, en realidad invoca otra copia del shell que es un subproceso del proceso de shell principal o principal . Ahora veámoslos con más detalle.
\subsection{Herencia de subprocesos de shell}
Las cosas más importantes que debe saber sobre los subprocesos de shell son las características que obtienen o heredan de sus padres. Estos son los siguientes:
\begin{itemize}
\item El directorio actual
\item Variables de entorno
\item Entrada, salida y error estándar más cualquier otro descriptor de archivo abierto
\item Cualquier característica definida en el archivo de entorno (ver \hyperref[sec:Chapter3]{Capítulo 3}). Tenga en cuenta que solo los shells interactivos ejecutan el archivo de entorno
\item Señales que se ignoran
\end{itemize}
Las tres primeras características son heredadas por todos los subprocesos, mientras que las dos últimas son exclusivas de los subprocesos de shell. Igual de importantes son las cosas que un subproceso de shell no hereda de su padre:
\begin{itemize}
\item Variables de shell, excepto las variables de entorno y las definidas en el archivo de entorno
\item Manejo de señales que no se ignoran
\end{itemize}
Cubrimos algo de esto anteriormente (en el \hyperref[sec:Chapter3]{Capítulo 3}), pero estos puntos son fuentes comunes de confusión, por lo que vale la pena repetirlos.
\subsection{Subshells}
Un tipo especial de subproceso del shell es el subshell. El subshell se inicia dentro del mismo script (o función) que el padre. Lo haces de manera muy similar a los bloques de código que vimos en el último capítulo. Simplemente rodea un código de shell con paréntesis (en lugar de llaves), y ese código se ejecuta en un subshell.
Por ejemplo, aquí está el programa de la calculadora, de arriba, con un subshell en lugar de un bloque de código:
\begin{lstlisting}[language=bash]
( while read line'?adc> '; do
print "$(alg2rpn $line)"
done
) | dc
\end{lstlisting}
El código dentro de los paréntesis se ejecuta como un proceso separado.\footnote{Por razones de rendimiento, el shell Korn hace todo lo posible para evitar crear realmente un proceso separado para ejecutar el código entre paréntesis y dentro de \texttt{\$(...)}. Pero los resultados siempre deben ser los mismos que \emph{si} el código se ejecutara en un proceso separado.}
Esto suele ser menos eficiente que un bloque de código. Las diferencias en funcionalidad entre subshell y bloques de código son muy pocas; se refieren principalmente a problemas de ámbito, es decir, a los dominios en los que se conocen las definiciones de cosas como variables de shell y trampas de señales. En primer lugar, el código dentro de un subshell obedece a las reglas de la herencia del subproceso del shell mencionadas anteriormente, excepto que conoce las variables definidas en el shell circundante; en cambio, piensa en los bloques como unidades de código que heredan \emph{todo} del shell exterior. En segundo lugar, las variables y trampas definidas dentro de un bloque de código son conocidas por el código de shell después del bloque, mientras que las definidas en un subshell no lo son.
Por ejemplo, considera este código:
\begin{lstlisting}[language=bash]
{
fred=bob
trap 'print "You hit CTRL-C!"' INT
}
while true; do
print "\$fred is $fred"
sleep 60
done
\end{lstlisting}
Si ejecutas este código, verás el mensaje \texttt{\$fred is bob} cada 60 segundos, y si escribes CTRL-C, verás el mensaje, \texttt{You hit CTRL-C!}. Deberás escribir CTRL-\ para detenerlo (no olvides eliminar el archivo core). Ahora cambiémoslo a un subshell:
\begin{lstlisting}[language=bash]
(
fred=bob
trap 'print "You hit CTRL-C!"' INT
)
while true; do
print "\$fred is $fred"
sleep 60
done
\end{lstlisting}
Si ejecutas esto, verás el mensaje \texttt{\$fred is}; el shell exterior no conoce la definición de \emph{fred} del subshell y, por lo tanto, piensa que es nulo. Además, el shell exterior no conoce la trampa de la señal INT del subshell, así que si presionas CTRL-C, el script se termina.
Si un lenguaje admite el anidamiento de código, las definiciones dentro de una unidad anidada deberían tener un ámbito limitado a esa unidad anidada. En otras palabras, los subshell te brindan un mejor control que los bloques de código sobre el ámbito de las variables y las trampas de señales. Por lo tanto, creemos que debes usar subshell en lugar de bloques de código si van a contener definiciones de variables o trampas de señales, a menos que la eficiencia sea una preocupación.
Este ha sido un capítulo largo y ha cubierto mucho terreno. Aquí tienes algunos ejercicios que deberían ayudarte a asegurarte de que tienes un firme entendimiento del material. El último ejercicio es especialmente difícil para aquellos sin antecedentes en compiladores, teoría del análisis sintáctico o teoría formal de lenguajes.
\begin{enumerate}
\item Escribe una función llamada \emph{pinfo} que combine los comandos \emph{jobs} y \emph{ps} imprimiendo una lista de trabajos con sus números de trabajo, identificadores de proceso correspondientes, tiempos de ejecución y comandos completos. Puntuación extra: describe por qué esto debe ser una función y no un script.
\item Toma la última versión de nuestro script de shell del compilador C, o algún otro script de shell no trivial, y <<a prueba de balas>> con trampas de señales.
\item Vuelve a hacer el programa \emph{findterms} en el último capítulo usando un subshell en lugar de un bloque de código.
\item Lo siguiente no tiene mucho que ver con el material de este capítulo en sí, pero es un ejercicio de programación clásico y te dará práctica si lo haces:
\begin{enumerate}
\item Escribe la función \emph{alg2rpn} utilizada en \emph{adc}. Así es como debes hacerlo: las expresiones aritméticas en notación algebraica tienen la forma \emph{expr op expr}, donde cada \emph{expr} es o bien un número o bien otra expresión (quizás entre paréntesis), y \emph{op} es +, -, x, / o \% (resto). En RPN, las expresiones tienen la forma \emph{expr expr op}. Por ejemplo: la expresión algebraica $2+3$ es $2 3 +$ en RPN; el equivalente en RPN de $(2+3) x (9-5)$ es $2 3 + 9 5 - x$. La principal ventaja de RPN es que elimina la necesidad de paréntesis y reglas de precedencia de operadores (por ejemplo, la regla de que \emph{x} se evalúa antes que +). El programa \emph{dc} acepta RPN estándar, pero cada expresión debe tener <<p>> al final: esto le dice a \emph{dc} que imprima su resultado, por ejemplo, el primer ejemplo anterior debería darse a \emph{dc} como $2 3 + p$.
\item Necesitas escribir una rutina que convierta la notación algebraica en RPN. Esto debería ser (o incluir) una función que se llame a sí misma (conocida como una función \emph{recursiva}) cada vez que encuentra una subexpresión. Es especialmente importante que esta función lleve un registro de dónde está en la cadena de entrada y cuánto de la cadena consume durante su procesamiento. (Pista: utiliza los operadores de coincidencia de patrones discutidos en el \hyperref[sec:Chapter4]{Capítulo 4} para facilitar la tarea de analizar cadenas de entrada). \\
Para facilitarte la vida, no te preocupes por la precedencia de operadores por ahora; simplemente convierte a RPN de izquierda a derecha. Por ejemplo, trata $3+4x5$ como $(3+4)x5$ y $3x4+5$ como $(3x4)+5$. Esto hace posible que conviertas la cadena de entrada sobre la marcha, es decir, sin tener que leerla completa antes de hacer cualquier procesamiento.
\item Mejora tu solución al ejercicio anterior para que admita la precedencia de operadores en el orden usual: x, /, \% (resto) +, -. Por ejemplo, trata $3+4x5$ como $3+(4x5)$ y $3x4+5$ como $(3x4)+5$.
\end{enumerate}
\end{enumerate}