Está en la página 1de 28

2 Programas y procesos

______
2.1. Procesos
Un programa en ejecucin es un proceso. El nombre programa no se utiliza para referirse a un
programa en ejecucin, porque ambos conceptos son distintos. La diferencia es la misma que la
que puede haber entre una galleta y la receta para hacer galletas. Un programa es un montn de
datos, no es nada que est vivo. Al contrario, un proceso es algo vivo, dinmico, que est
haciendo cosas (ejecutando). Un proceso tiene un conjunto de registros que est usando, tiene un
contador de programa que indica la siguiente instruccin que debe ejecutar, y tiene una pila. Esto
significa que tiene un flujo de control que ejecuta una instruccin tras otra, como ya sabemos.
La diferencia queda bastante clara si se considera que puedes ejecutar simultneamente el
mismo programa varias veces. Por ejemplo, la figura 2.1 muestra el sistema de ventanas con tres
ventanas. Cada una de ellas ejecuta un shell. Por tanto tenemos tres procesos ejecutando el pro-
grama /bin/rc, aunque slo hay un programa para estos procesos, que est almacenado en un
fichero llamado /bin/rc. Ms aun, si cambiamos el directorio de trabajo en un shell, los otros
shells no se ven afectados Prubalo! Supongamos que el programa rc guarda en una variable su
directorio de trabajo. Cada proceso que ejecuta el shell tiene su propia variable para el directorio
de trabajo. Sin embargo, el programa nicamente tiene una variable declarada con ese propsito.
Figura 2.1: Tres procesos /bin/rc, pero slo un programa /bin/rc.
Entonces, Qu es un proceso? Consideremos todos los programas que hemos hecho. Esco-
jamos cualquiera de ellos. Cuando ejecutamos un programa y se arranca un proceso, puede ejecu-
tar independientemente de los dems programas del sistema. O acaso hemos tenido en cuenta
al reloj del sistema, al shell, al navegador, o a cualquier otro programa para programar nuestros
propios programas? No. Necesitaramos tener mucha memoria para poder tener en cuenta todo
esto a la hora de escribir un programa. Los sistemas operativos nos dan la abstraccin de proceso
para no tener en cuenta al resto de programas que estn ejecutando, para olvidarnos de ellos.
Cada proceso tiene la ilusin de que tiene su propio procesador. Cuando escribimos un pro-
grama, siempre pensamos que se ejecutan las instrucciones una detrs de otra. Pero siempre pen-
samos en las instrucciones de nuestro proceso, no en las de los dems. La implementacin de la
- 2 -
abstraccin llamada proceso en el sistema operativo es la que nos ofrece esta fantasa.
Cuando una mquina tiene varios procesadores, se pueden ejecutar mltiples programas en
paralelo, esto es, al mismo tiempo. Aunque esto es comn en la actualidad, todava hay mquinas
con un nico procesador. En ocasiones nos encontramos con mquinas que tienen dos o cuatro
procesadores. Pero la cuestin es que ejecutas ms programas que procesadores tiene tu mquina.
Cuenta el nmero de ventanas en tu terminal. Cada una ejecuta al menos un programa. Est claro
que tu ordenador no tienen tantos procesadores.
Lo que ocurre es que el sistema operativo se las apaa para dejar ejecutar a cada programa
en ejecucin un tiempo limitado. La figura 2.2 muestra la memoria de un sistema con tres proce-
sos ejecutando. Cada proceso tiene su conjunto de registros, que incluyen un contador de pro-
grama. Esta figura es slo una instantnea tomada en momento dado. En un instante, el sistema
puede permitir al proceso 1 (que ejecuta rio) que proceda y ejecute su cdigo. Despus, el timer
hardware que ha puesto el sistema salta, y avisa al sistema de que el tiempo para este proceso ha
terminado. En este punto, el sistema puede saltar al proceso 2, que est ejecutando rc. Cuando
el tiempo para el proceso 2 termina, el sistema puede saltar al proceso 3 que ejecuta rio.
Cuando su tiempo acabe, se podr saltar al proceso 1 de nuevo, y retomarlo donde se haba
quedado.
...
addl bx, di
addl bx, si
subl $4, di
movl bx, cx
...
Rio
(proceso #1)
...
cmpl si, di
jls label
movl bx, cx
addl bx, si
...
Rio
(proceso #3)
PC
PC
...
addl bx, di
addl bx, si
subl $4, di
movl bx, cx
...
Rc
(proceso #2)
PC
Memoria del
Sistema
Figura 2.2: Ejecucin concurrente de varios procesos en el mismo sistema.
Todo lo que hemos contado ocurre entre bastidores. El sistema operativo sabe que slo hay
un flujo de control por procesador, y salta de un proceso a otro para transferir el control. Para el
usuario del sistema, todo lo que importa es que cada proceso ejecuta independientemente del
resto, como si tuviese su propio procesador.
Decimos que son procesos concurrentes, porque parece que ejecutan simultneamente. En
algunos casos, realmente ejecutan en paralelo (cuando tenemos ms de un procesador). En la
mayora de los casos, estaremos hablando de una ejecucin pseudo-paralela. Realmente, esto
no es importante para el programador. l tan slo ve procesos concurrentes que parecen ejecutar
simultneamente.
- 3 -
En este captulo estudiaremos los procesos que obtenemos al ejecutar nuestros programas.
Antes de nada, es importante saber lo que hay dentro de un programa y dentro de un proceso.
2.2. Programas cargados
Cuando se compila un cdigo fuente y se enlaza, se genera un fichero binario. Este fichero con-
tiene toda la informacin necesaria para que ejecute el programa, esto es, para crear un proceso
con el fin de ejecutarlo. Dentro del binario, la informacin se organiza en diferentes secciones.
Un fichero binario comienza con unas palabras que describen las secciones que contiene. Estas
palabras iniciales se denominan cabecera, y usualmente indica la arquitectura para la que son las
instrucciones del binario y el tamao y el offset (distancia) de cada una de las secciones.
Una de las secciones del fichero contiene el texto del programa, que es el conjunto de
instrucciones mquina. En otra seccin distinta se almacenan e inicializan las variables globales
del programa . Hay que tener en cuenta que el programa no sabe nada sobre el significado de esas
variables, son slo datos. Para las variables globales no inicializadas, nicamente se indica el
tamao total, ya que como no tiene un valor inicial, es absurdo reservar espacio en el fichero.
Normalmente tambin se incluye informacin para ayudar al depurador, como cadenas de carac-
teres con el nombre de los smbolos y sus direcciones.
En el ltimo captulo vimos cmo podemos usar nm para imprimir informacin acerca de
los smbolos de los ficheros objeto y los binarios. Es importante remarcar que nicamente nue-
stro programa sabe el significado de los bytes de los datos del programa, esto es, de las variables.
Para el sistema, los datos de nuestro programa no significan absolutamente nada. El sistema no
sabe nada sobre nuestro programa (en realidad, nadie sabe nada excepto nosotros mismos). La
informacin que muestra nm se obtiene de la tabla de smbolos que se incluye en el binario para
ayudar en la depuracin.
Podemos eliminar la tabla de smbolos de un binario. Por ejemplo, podemos borrarlo del
binario que generamos para el programa take.c. El comando strip sirve para esto. Para ver
el tamao del binario, podemos usar la opcin -l del comando ls, que como ya sabemos, lista la
informacin de los ficheros, incluido el tamao:
; ls -l 8.take
--rwxr-xr-x M 19 nemo nemo 36348 Jul 6 22:49 8.take
; strip 8.take
; ls -l 8.take
--rwxr-xr-x M 19 nemo nemo 21713 Jul 6 22:49 8.take
El nmero que se encuentra antes de la fecha de modificacin es el tamao en bytes del fichero.
El fichero ha pasado de tener 36348 bytes a tener 21713 bytes. La diferencia es el tamao de la
tabla de smbolos. Despus de eliminarla, mn no nos puede decir nada acerca de los smbolos del
binario. Ahora, sabe lo mismo que el sistema: nada.
; nm 8.take
;
El sistema sigue un convenio para saber la direccin en la que se debe comenzar a ejecutar el pro-
grama. Sin embargo, no le importa lo que hace el cdigo que se encuentra all.
Un programa almacenado en un fichero es distinto que el mismo programa cuando ha sido
cargado en memoria para ejecutarse. Estn relacionados, pero no son iguales. Consideremos el
siguiente programa, que no hace mucho, pero tiene una variable global de 1 MByte:
- 4 -

global.c

________
#include <u.h>
#include <libc.h>
char global[1 * 1024 * 1024];
void
main(int, char*[])
{
exits(nil);
}
Asumiendo que el programa se llama global.c, podemos compilarlo y enlazarlo con la opcin
-o del enlazador para especificar que el binario se llame 8.global. Es una buena prctica
nombrar el fichero binario segn el nombre del programa, especialmente cuando hay mltiples
programas en el mismo directorio:
; 8c -FVw global.c
; 8l -o 8.global global.8
; ls -l 8.global global.8
--rwxr-xr-x M 19 nemo nemo 3380 Jul 6 23:06 8.global
--rw-r--r-- M 19 nemo nemo 328 Jul 6 23:06 global.8
Est claro que no hay espacio en los 328 bytes para la variable global del programa, que nece-
sita 1 MByte de almacenamiento. La explicacin es que nicamente se almacena en el binario que
hay una variable global, pero no su contenido. Podemos cambiar el tamao del array, por ejem-
plo a 2 MByte, y veremos que el tamao del binario no cambia.
Cuando el shell pide al sistema (mediante una llamada al sistema) que ejecute el binario
8.global, el sistema carga el programa en memoria. La parte del ncleo del sistema que real-
iza esta operacin se denomina cargador o loader Cmo puede cargar el sistema un programa?
Lo hace leyendo la informacin del binario:
La cabecera del binario indica la memoria necesaria para albergar el texto del programa, y
en el binario tenemos una imagen del texto. Entonces, el sistema nicamente carga en una
zona de la memoria el texto del binario. Hay convenciones para cada arquitectura que indi-
can las direcciones se deben usar. Por tanto, el sistema sabe dnde tiene que cargar la ima-
gen del texto.
La cabecera tambin indica el tamao de las variables globales que han sido inicializadas, y
el fichero tiene tambin una imagen con los datos. El sistema carga esa imagen en otra zona
de memoria. Hay que tener en cuenta que el sistema no sabe dnde empieza una variable ni
cul es su tamao. El sistema slo sabe el nmero de bytes que tiene la imagen y la
direccin donde tiene que copiarla.
Para las variables globales no inicializadas, la cabecera del binario slo indica el tamao
total. El sistema reserva esa cantidad de memoria para nuestro programa. Es lo nico que
tiene que hacer. Por cortesa, Plan 9 inicializa toda esa memoria con todos los bytes a cero.
Esto significa que todas las variables globales sin inicializar estn inicializadas a valores
nulos por omisin. Esto est muy bien, ya que los programas se pueden comportar mal si las
variables no estn correctamente inicializadas, y un valor nulo parece un buen valor por
omisin verdad?
Ahora veremos como nm imprime las direcciones para los smbolos. Estas direcciones son direc-
ciones de memoria que slo tienen sentido cuando el programa se ha cargado en memoria. De
hecho, el manual de Plan 9 se refiere al enlazador como cargador. Esas direcciones son virtuales
- 5 -
porque el sistema usa el hardware de memoria virtual para mantener a cada proceso en su espacio
de direcciones. Aunque sean direcciones virtuales, las direcciones son absolutas, no son relativas
(offsets) a un punto de origen. Usando nm podemos aprender ms sobre el aspecto de un pro-
grama cargado. La opcin -n hace que nm ordene su salida por direccin:
; nm -n 8.global
1020 T main
1033 T _main
1073 T atexit
10e2 T atexitdont
1124 T exits
1180 T _exits
1188 T getpid
11fb T memset
122a T lock
12e7 T canlock
130a T unlock
1315 T atol
1442 T atoi
1455 T sleep
145d T open
1465 T close
146d T read
14a0 T _tas
14ac T pread
14b4 T etext
2000 D argv0
2004 D _tos
2008 D _nprivates
200c d onexlock
2010 D _privates
2014 d _exits
2024 B edata
2024 B onex
212c B global
10212c B end
La figura 2.3 muestra la organizacin en memoria del programa cuando se carga. Observando la
salida de nm podemos deducir varias cosas. Primero, el cdigo del programa (texto) usa direc-
ciones entre la 0x1020 y la 0x14b4.
El ltimo smbolo, etext, es un smbolo definido por el enlazador con el fin de marcar el
final del texto. Los datos estn entre la 0x2000 y la 0x10212c. Tambin hay un smbolo llamado
end, tambin definido por el enlazador, que marca el final de los datos. No hay que confundir
este smbolo con edata, que indica el final de los datos inicializados.
En decimal, la direccin de end es 1.057.068 bytes. Eso es ms que 1 MByte, que es
mucha ms memoria que los 3 KBytes que ocupa el fichero binario Puedes ver la diferencia?
Todava podemos observar ms cosas. No hemos tenido en cuenta la pila del programa.
Como ya sabrs, los programas necesitan una pila para ejecutar. La pila es la zona de memoria
donde se almacenan los datos necesarios para para realizar llamadas a procedimiento, saber la
direccin a la que hay que retornar y almacenar los parmetros y las variables locales. Por tanto,
el tamao del programa una vez cargado en memoria va a ser aun mayor. Para saber la cantidad
de memoria que va a consumir un binario cuando sea cargado, no usamos ls, sino que usamos
mn.
La memoria del programa cargado, por tanto del proceso, se organiza como muestra la
figura 2.3. Pero esto es nicamente una invencin del sistema operativo. Es una abstraccin que
nos ofrece el sistema para hacernos la vida ms fcil, implementada usando el hardware de
- 6 -
Segmento Text
Texto del
programa
Segmento Data
Datos
inicializados
Segmento BSS
Datos
no inicializados
...
Segmento Stack
Pila
0x0 etext edata end
Figura 2.3: Imagen de la memoria del programa global.
memoria virtual. Esta abstraccin se denomina memoria virtual. Cada proceso cree que es el
nico programa cargado en memoria. Esto se puede observar en las direcciones que nos muestra
el comando nm. Todos los procesos que ejecuten este programa tendrn las mismas direcciones,
que son absolutas, y adems podrn ejecutar al mismo tiempo en la mismo ordenador.
La memoria virtual de un proceso en Plan 9 tiene varios segmentos. Esto tambin es una
abstraccin del sistema que no tienen nada que ver con la segmentacin de hardware que se
puede encontrar en los procesadores modernos. Un segmento de memoria es una porcin de
memoria contigua con alguna propiedad. Los segmentos de un proceso de Plan 9 son:
El segmento de texto. Contiene las instrucciones del programa, que pueden ser ejecutadas
pero no pueden ser modificadas. El sistema, junto con el hardware, asegura estas restric-
ciones. El segmento se inicializa con la imagen de texto del fichero binario.
El segmento de datos. Contiene los datos inicializados del programa. El contenido del seg-
mento se puede leer y escribir. Sin embargo, su contenido no se puede ejecutar. El con-
tenido se inicializa con los datos que estn almacenados en el fichero binario.
El segmento de datos no inicializados, denominado segmento BSS. Es similar al segmento
de datos, pero todo su contenido se inicializa a cero. El nombre del segmento viene de una
instruccin de una mquina muy antigua que ya no se usa. Este segmento tiene inicialmente
el tamao que se indica en la cabecera del fichero binario. Sin embargo, puede crecer medi-
ante una llamada al sistema. Cada vez que se llama a malloc(2), se consume memoria de
este segmento. Por tanto, cuando el segmento se queda sin espacio, se le pide al sistema que
lo haga crecer. Esa es la razn por la que hay un hueco entre este segmento y el segmento de
pila, para que pueda crecer.
El segmento de pila. El segmento se puede leer y escribir, pero no ejecutar. Este segmento
crece automticamente cuando necesita ms espacio, no como el resto de segmentos. Se usa
para mantener la pila del proceso.
Todo esto es importante y tiene un gran impacto en el comportamiento de nuestros programas.
Normalmente, no se carga todo el cdigo al segmento de texto desde fichero binario de una vez,
sino que se va cargando a medida que se va necesitando. Los binarios se copian en memoria de
pgina de memoria en pgina de memoria a medida que se referencian las direcciones de memo-
ria. A esto se le llama paginacin en demanda o carga en demanda. Es importante saber esto
porque, si borramos un binario del sistema de ficheros mientras que un proceso lo est ejecu-
tando, ste programa fallar y se quedar roto cuando necesite leer una pgina y el fichero ya no
exista. Lo mismo pasar cuando reemplacemos el binario por otra versin mientras hay algn pro-
ceso ejecutndolo.
Ya que la memoria es virtual y slo se reserva cuando se usa por primera vez, cualquier
parte que no se haya usado del segmento BSS est libre. No se consume la memoria hasta que no
se toca por primera vez. No obstante, si inicializamos la memoria con un bucle, recorriendo todas
las posiciones del array asignndolas un valor, toda la memoria queda reservada. Podemos sacar
- 7 -
partido de los datos no inicializados, por ejemplo, al programar grandes tablas hash que con-
tengan pocos elementos (sparse). Se pueden implementar con arrays grandes que no estn inicial-
izados, cuya memoria fsica no se reservar al iniciar el programa. Cuando se necesite usar la
tabla, el sistema reservar la memoria y la inicializar a cero, haciendo que todas las entradas sean
valores nulos. En este ejemplo, la inicializacin manual de la memoria tendra un gran impacto
sobre el uso de la memoria (se reservara toda esa memoria fsica desde el primer momento).
2.3. Nacimiento y muerte de los procesos
Los programas se ejecutan, no se llaman. De hecho, los programas nunca retornan, sino que su
proceso acaba cuando desea o cuando tiene un comportamiento incorrecto. Aunque no se les
llame, podemos pasar argumentos a un programa para controlar su comportamiento.
Cuando el shell pide la ejecucin de un programa, el sistema lo carga en la memoria y le
asigna un flujo de control o flujo de ejecucin. Entonces, se inicializan los valores necesarios
para los registros del procesador del nuevo proceso, incluido el contador de programa y el puntero
de pila, que apuntar a una pila nueva (y casi vaca). Cuando compilamos un programa de C, el
cargador pone main a la direccin donde el se empezar a ejecutar el cdigo del programa. De
esta forma, nuestro programa comienza ejecutando main. Los argumentos proporcionados a un
programa (por ejemplo, cuando ejecutamos un comando con opciones en el shell) se copian a la
pila del nuevo programa.
Los argumentos que se pasan a la funcin main del programa son un array de cadenas de
caracteres (el vector de argumentos, argv) y un entero, que indica el nmero de elementos en el
array. Implementemos un programa que imprima sus argumentos.

echo.c

______
#include <u.h>
#include <libc.h>
void
main(int argc, char* argv[])
{
int i;
for (i = 0; i < argc; i++)
print("%d: %s\n ", i, argv[i]);
exits(nil);
}
Si ejecutamos este programa, podremos ver los argumentos que se le pasan al programa desde la
lnea de comandos:
; 8c -FVw echo.c
; 8l -o 8.echo echo.8
; ./8.echo one little program
0: ./8.echo
1: one
2: little
3: program
;
Hay varias cosas que remarcar en el programa. Primero, el primer argumento que se le ha propor-
cionado al programa es El nombre del programa! Especficamente, es el nombre como se le ha
pasado al shell. Segundo, hemos dado una ruta relativa como nombre del comando. Recuerda que
- 8 -
./8.echo se refiere al fichero 8.echo en el directorio de trabajo del shell. Por tanto, ese el
valor de argv[0] en nuestro programa. Viendo el valor de argv[0], los programas pueden
conocer su propio nombre. Esto es bastante til para imprimir mensajes de diagnstico que per-
mitan al usuario identificar el programa que est teniendo los errores.
Hay un comando estndar de Plan 9 que hace (casi) lo mismo que el programa que hemos
escrito antes. Ese comando es echo. El comando echo imprime sus argumentos separndolos
con un espacio, y acabando con un carcter de nueva lnea. El carcter de nueva lnea se puede
omitir con la opcin -n.
; echo hi there
hi there
;
; echo -n hi there
hi there;
Fjate en el prompt del shell justo despus de la salida de echo. Aunque este comando es sim-
ple, es muy til, por ejemplo para generar cadenas de texto o para saber cuantos argumentos
recibira un comando.
El programa que hemos hecho antes no es un echo perfecto. Como poco, el echo estndar
puede recibir una opcin -n para pedirle que slo imprima sus argumentos (que no incluya la
nueva lnea). Vamos a aadir dos opciones a nuestro programa. La opcin -n omitir la nueva
lnea, y la opcin -v imprimir corchetes alrededor de cada argumento, para que podamos ver el
comienzo y el final de cada uno de ellos. Si no se le pasa ninguna opcin, el programa fun-
cionar como la herramienta estndar, imprimiendo un argumento detrs de otro y con una nueva
lnea. En realidad, la nica dificultad reside en controlar que el usuario pueda ejecutar el pro-
grama proporcionando los argumentos en distinto orden:
8.echo repeat after me
8.echo -n repeat after me
8.echo -v repeat after me
8.echo -n -v repeat after me
8.echo -nv repeat after me
Es necesario que las opciones se puedan combinar de cualquiera de estas formas. Adems, el
usuario debe ser capaz de imprimir con echo argumentos como -word-, y el programa no debe
confundir este argumento con una opcin porque empiece por un guin. Por lo general, se usan
dos guiones seguidos para indicar que no se van a proporcionar ms opciones al programa y que
todo lo que sigue son slo argumentos:
8.echo -- -word--
Es un poco molesto procesar argv y argc a mano Verdad? Por esa razn, el sistema nos pro-
porciona unas macros de C para ayudarnos a procesarlos. Las macros de C son definiciones que
procesa el preprocesador de C antes de compilar, que reemplaza el nombre de la macro por su
contenido. El siguiente programa es un ejemplo.
- 9 -

aecho.c

_______
#include <u.h>
#include <libc.h>
void
main(int argc, char* argv[])
{
int nflag = 0;
int vflag = 0;
int i;
ARGBEGIN{
case v:
vflag = 1;
break;
case n:
nflag = 1;
break;
default:
fprint(2, "usage: %s [-nv] args\n", argv0);
exits("usage");
}ARGEND;
for (i = 0; i < argc; i++)
if (vflag)
print("[%s] ", argv[i]);
else
print("%s ", argv[i]);
if (!nflag)
print("\n");
exits(nil);
}
Las macros ARGBEGIN y ARGEND iteran sobre el vector de argumentos, eliminando y proce-
sando las opciones. Despus de ARGEND, tanto argc como argv reflejan el vector de argumen-
tos sin ninguna opcin. Entre las dos macros debemos escribir el cuerpo de una estructura de
control switch de C (que proporciona la macro ARGBEGIN), con un case por cada opcin.
Las macros tienen en cuenta las posibles combinaciones de las opciones. Probemos varias combi-
naciones a la hora de ejecutar el programa ahora:
- 10 -
; 8.aecho repeat after me
repeat after me
; 8.aecho -v repeat after me
[repeat] [after] [me]
; 8.aecho -vn repeat after me
[repeat] [after] [me] ; we gave a return here.
; 8.aecho -d repeat after me
usage: 8.aecho [-nv] args
; 8.aecho -- -d repeat after me
-d repeat after me
En todos los casos menos en el ltimo, argc es 3 despus de ARGEND, y argv tiene las sigu-
ientes cadenas: repeat, after, y me.
Otra ventaja de usar las macros es que inicializan una variable global llamada argv0 para
apuntar al argv[0] original en main, que apunta al nombre del programa. Usamos esa variable
para imprimir errores de diagnstico que indican la forma de uso del programa, que es lo que hay
que hacer cuando alguien ejecuta el programa de forma incorrecta.
En algunos casos, una opcin puede tener un argumento. Por ejemplo, podemos hacer que el
usuario pueda personalizar el carcter que indica el principio y el final de cada argumento en nue-
stro programa, esto es, reemplazar [ y ] por otros caracteres cuando est usando la opcin -v.
Esto se puede hacer aadiendo una opcin -d. Por ejemplo:
8.aecho -v -d"" repeat after me
Esto se puede implementar usando otra macro llamada ARGF. Esta macro se usa dentro del
case para una opcin, y retorna un puntero al argumento de la opcin (al argumento, o al sigu-
iente modificador si no se ha dado argumento, o nil si no hay nada en el vector de argumentos
despus de la opcin que se est procesando). El programa resultante es el que sigue.

becho.c

________
#include <u.h>
#include <libc.h>
void
usage(void)
{
fprint(2, "usage: %s [-nv] [-d delims] args\n", argv0);
exits("usage");
}
void
main(int argc, char* argv[])
{
int nflag = 0;
int vflag = 0;
char* delims = "[]";
int i;
- 11 -
ARGBEGIN{
case v:
vflag = 1;
break;
case n:
nflag = 1;
break;
case d:
delims = ARGF();
if (delims == nil || strlen(delims) < 2)
usage();
break;
default:
usage();
}ARGEND;
for (i = 0; i < argc; i++)
if (vflag)
print("%c%s%c ", delims[0], argv[i], delims[1]);
else
print("%s ", argv[i]);
if (!nflag)
print("\n");
exits(nil);
}
A continuacin se muestran varios ejemplos de uso de nuestro programa:
; 8.becho -v -d"" repeat after me
"repeat" "after" "me"
; 8.becho -vd "" repeat after me note the space before the ""
"repeat" "after" "me"
; 8.becho -v
; 8.becho -v -d
usage: 8.becho [-nv] [-d delims] args
Si falta un argumento para una opcin, normalmente se termina el programa mostrando el modo
de uso (llamando a una funcin usage). La macro EARGF se usa en lugar de ARGF con ese fin
(llamar a una funcin en caso de que no haya argumento para la opcin). Podemos cambiar nue-
stro cdigo para que use esta macro:
case d:
delims = EARGF(usage());
if (strlen(delims) < 2)
usage();
break;
De esta forma, EARGF ejecutara la funcin que indica cmo se debe usar el programa (y aborta la
ejecucin) cuando se usa la opcin -d sin ningn argumento. En nuestro caso, tenemos un if
adicional para comprobar que el argumento tiene al menos los dos caracteres que necesitamos.
La mayora de los programas de Plan 9 que aceptan varias opciones usan estas macros para
procesarlas. Esto significa que la sintaxis de invocacin es similar en la mayora de los progra-
mas. Como ya hemos visto, podemos combinar las opciones en un nico argumento, usar varios
argumentos como opciones, proporcionar argumentos a las opciones inmediatamente despus de
la letra correspondiente, terminar el procesamiento de las opciones con --, etc.
Como ya habrs notado, el programa acaba su ejecucin llamando a exits, cuya pgina de
manual es exits(2). Esta llamada al sistema acaba con el proceso que la llama. El proceso puede
dejar una cadena de caracteres como nico legado. Dicha cadena de caracteres informar de lo
- 12 -
que le ha ocurrido. Esta cadena informa sobre el estatus de salida del programa, esto es, qu le ha
pasado al programa. Si se llama con una cadena vaca o con un puntero a nil, entonces la
convencin dice que el proceso ha salido con un estatus correcto, y que todo sali bien; el proceso
ha cumplido con su trabajo y ha acabado. En otro caso, se considera que no ha acabado bien, y la
cadena describe el problema que tuvo el proceso para realizar su trabajo. Por ejemplo

sic.c

_____
#include <u.h>
#include <libc.h>
void
main(int, char*[])
{
exits("sic!");
}
reportara la cadena sic! al sistema cuando exits acaba la ejecucin del proceso. La siguiente
ejecucin muestra el estatus del proceso, haciendo un echo de $status. As podemos saber el
estatus del ltimo comando que se ejecut en el shell, en este caso, el programa sali quejndose
porque es un programa depresivo (los programas tambin tienen sus momentos de crisis):
; 8.sic
; echo $status
8.sic 2046: sic!
;
Los programas deben salir con el estatus apropiado dependiendo de lo que les haya pasado. Por
tanto, ls acaba con xito cuando puede listar los ficheros que se le han pasado como argumentos,
y reporta fallo cuando no puede listarlos. De la misma manera, rm acaba con xito cuando puede
borrar los ficheros que se le han indicado, y acaba con un estatus errneo cuando no puede hac-
erlo. Lo mismo se aplica a los dems comandos.
Antes, cuando dijimos que un programa empieza ejecutando en main, mentimos (no nos lo
tengas en cuenta). No lo hace. En realidad, empieza a ejecutar otra funcin, que es la que llama a
main, y que cuando main retorna, llama a exits para acabar la ejecucin. Esta es la razn por
la que nuestros programas siempre acaban de ejecutar cuando llegan al final de main. No hay
nada mgico en ello. Un proceso no termina de ejecutar simplemente porque una funcin retorne
(su flujo de ejecucin no desaparece). Sin embargo, como el proceso es una abstraccin del sis-
tema operativo, podemos usar una llamada al sistema para acabar con un proceso. El sistema lib-
era todos los recursos que estaba consumiendo y el proceso pasa a la historia. Al fin y al cabo, un
proceso no es ms que una estructura de datos.
En pocas palabras, si nuestro programa no llama a exits, la funcin que llama a main
realizar esa llamada al sistema cuando main retorne. Pero es mucho mejor que llamemos
explcitamente a exits desde nuestro programa. De otra forma, no podemos estar seguros del
valor que se usar como estatus de salida.
2.4. Errores en las llamadas al sistema
En este captulo y en los sucesivos, realizaremos muchas llamadas al sistema desde programas
escritos en C. En la mayora de los casos no habr problemas con las llamadas que se realicen.
Pero en otros casos podemos cometer errores y la llamada al sistema no podr realizar su trabajo.
Por ejemplo, esto puede ocurrir cuando tratamos de cambiar el directorio de trabajo del proceso a
un directorio que no existe.
Prcticamente todas las funciones que vamos a usar (las llamadas al sistema tambin son
funciones) pueden tener problemas para completar su trabajo. En Plan 9, cuando una llamada al
sistema encuentra un error o no es capaz de hacer su trabajo, la funcin retorna un valor que alerta
- 13 -
de ello. Dependiendo de la funcin, el valor que indica el fallo de la misma puede cambiar. En
general, un valor de retorno absurdo indica que el trabajo no se ha podido realizar correctamente.
Por ejemplo, la llamada al sistema open (que veremos ms adelante) devuelve un nmero
entero positivo. Sin embargo, si falla, retorna -1. Esa es la convencin para la mayora de lla-
madas al sistema que devuelven un valor entero. Las llamadas que devuelve una cadena de carac-
teres devuelven un puntero nulo ( nil) en caso de error. En todo caso, la pgina de manual de
una llamada al sistema indica su valor de retorno en caso de error.
Siempre debemos comprobar errores. No comprobar errores es como conducir a ciegas.
Si no compruebas errores, preprate para una depuracin infernal. Hay un libro excelente que
debera leer todo programador y que ensea problemas prcticos relacionados con la
programacin y las tcnicas para evitar horas de depuracin [1].
Aunque las llamadas al sistema nos avisen de los errores retornando un valor absurdo, Plan
9 guarda una cadena de caracteres con la descripcin del error. Esta cadena de error nos da
informacin sobre el error y es muy til para arreglar el problema. Creenos, querrs imprimirla
para saber lo que ha pasado cuando te encuentres con problemas.
Hay varias formas de imprimir esa cadena. La mejor de ellas es usar el formato %r con
print. Este formato hace que print print pida a Plan 9 la cadena de error y la imprima junto
con otra salida que le hayamos dado. Este programa de ejemplo usa el formato.

err.c

_____
#include <u.h>
#include <libc.h>
void
main(int , char* [])
{
if (chdir("magic") < 0){
print("chdir failed: %r\n");
exits("failed");
}
/* ... do other things ... */
exits(nil);
}
Ejecutemos el programa:
; 8.err
chdir failed: magic file does not exist
El programa intent usar chdir para cambiar el directorio de trabajo al directorio magic. El
problema es que ese directorio no existe, y la llamada al sistema ha fallado y ha retornado -1. Un
buen programa debe comprobar que chdir no devuelve -1 cuando se le llama, y si ocurre, debe
reportar el problema al usuario. Fjate que usamos el formato %r en print para informar del
error al usuario.
Si el programa no puede seguir por culpa del error, debe abortar su ejecucin indicando que
ha fallado. A esto se le llama un error fatal. Esto es tan comn que hay una funcin que imprime
un mensaje y despus sale del programa. Dicha funcin se llama sysfatal y se usa como
sigue:
if (chdir("magic") < 0)
sysfatal("chdir failed: %r");
En algunos casos necesitaremos obtener la cadena de error de la llamada al sistema que ha
- 14 -
fallado. Por ejemplo, para modificarla e imprimirla en un mensaje adaptado a nuestras necesi-
dades. La llamada al sistem rerrstr lee la cadena de error y la almacena como una cadena de
caracteres en un buffer que le debemos proporcionar. Veamos un ejemplo:
char error[128];
...
rerrstr(error, sizeof error);
Despus de la llamada, error contendr la cadena de error.
Cualquier funcin que se ofrece desde una biblioteca debe reportar sus errores. Si estamos
escribiendo una funcin de este tipo, debemos pensar cmo hacerlo. Una forma es usar el mismo
mecanismo que usa Plan 9. Esto est bien ya cualquier programador puede usar nuestra bib-
lioteca y tratar sus errores como trata los errores del sistema, sin importarle si es una biblioteca o
es una llamada al sistema.
La llamada al sistem werrstr escribe un nuevo valor en la cadena de error. Se usa como
la funcin print. Utilizando esta funcin, podemos implementar otra funcin que saque un ele-
mento de una pila y avisa de los errores de una forma adecuada:
int
pop(Stack * s)
{
if (isempty(s)){
werrstr("pop on an empty stack");
return -1;
}
... do the pop otherwise ...
}
Ahora podramos escribir cdigo como:
...
if (pop(s) < 0){
print("pop failed: %r\n");
...
}
Cuando haya un error en pop, se imprimir algo como:
pop failed: pop on an empty stack
2.5. Entorno
Otra forma de dar argumentos a un proceso es definir variables de entorno. A cada proceso se
le proporciona un conjunto de cadenas nombre=valor que se conocen como variables de entorno.
Se usan para modificar el comportamiento de algunos programas. Es aconsejable definir una
variable de entorno cuando se requiere un mismo argumento en repetidas ocasiones. Usualmente,
todos los procesos que ejecutan en la misma ventana comparten las variables de entorno.
Por ejemplo, la variable home contiene la ruta de nuestro directorio home. El comando cd
usa dicha variable. De otra forma Cmo iba a saber cd cul es nuestro directorio home? Tanto el
nombre como el valor de una variable de entorno son cadenas de caracteres, no lo olvides.
Podemos definir variables de entorno en un shell usando el smbolo de igual. Ms adelante
usaremos el shell para referirnos al valor de una variable de entorno, usando el smbolo del dlar.
Despus de leer las lneas de comandos, el shell reemplaza cada cadena que empieza por este
smbolo por el valor de la variable de entorno con el nombre indicado despus del mismo. Por
ejemplo, el primer comando en la siguiente sesin define la variable de entorno dir:
- 15 -
; dir=/a/very/long/path
; cd $dir
; pwd
/a/very/long/path
;
La segunda lnea de comandos usa $dir y, por tanto, el shell reemplaza dicha cadena de carac-
teres por el valor de la variable de entorno llamada dir: /a/very/long/path. Ten en
cuenta que cd no sabe nada sobre $dir. Podemos comprobar esto usando el comando echo, ya
que este comando escribe sus argumentos tal y como los recibe:
; echo $dir
/a/very/long/path
;
Los siguientes dos comandos hacen lo mismo. Sin embargo, uno recibe un argumento y el otro
no. La salida de pwd podra ser la misma despus de cualquiera de ellos:
; cd $home
; cd
En algunos casos es aconsejable definir una variable de entorno para la ejecucin de un nico
comando, de tal forma que la variable slo quede definida para ese proceso. Esto se puede hacer
definiendo la variable en la misma lnea, justo antes de indicar el nombre del comando a ejecutar,
como en el siguiente ejemplo:
; temp=/tmp/foobar echo $temp
/tmp/foobar
; echo $temp
;
En este punto, podemos entender lo que significa $status. Es el valor de la variable de entorno
status. El shell actualiza esta variable cada vez que sabe cmo acab algn comando que ejecut.
Esto lo hace antes de sacar el prompt para recibir una nueva orden. El contenido de esa cadena es
la cadena que us el comando cuando llam a exits.
Otra variable interesante es $path. Esta variable es una lista de rutas donde el shell debe
buscar el fichero ejecutable del comando que ejecuta el usuario. Cuando tecleamos un comando,
no tiene que empezar con / o con ./, sino que el shell busca el ejecutable en los directorios que
se especifican en esta variable (siguiendo el orden). Si se encuentra el ejecutable, se ordena al sis-
tema que lo ejecute. Veamos el contenido de la variable path en un sistema Plan 9 tpico:
; echo $path
. /bin
;
Contiene, en este orden, el directorio de trabajo (punto) y /bin. Si escribimos ls, el shell
prueba con ./ls, y si no existe, prueba con /bin/ls. Si escribimos ip/ping, el shell prueba
con ./ip/ping, y despus con /bin/ip/ping. Verdad que es bastante simple?
Otras dos variables de entorno que resultan bastante tiles son user, que contiene el nom-
bre del usuario y sysname, que contiene el nombre de la mquina.
Nos podemos definir tantas variables como queramos. Pero atencin. Las variables de
entorno normalmente se dejan de lado a la hora de depurar. Si un programa acepta argumentos de
la lnea de comandos, entonces pondremos los argumentos en la lnea de comandos. Si necesita-
mos una variable de entorno para pasar un argumento al programa cada vez que se ejecuta, tal vez
nos tengamos que replantear los argumentos que debe aceptar el programa. Este problema se
puede solucionar fcilmente definiendo los valores por omisin apropiados para los argumentos
de un programa. Los argumentos de la lnea de comandos hacen que la invocacin al programa
- 16 -
sea explcita, ms clara a simple vista, y por tanto, ms fciles de depurar. Sin embargo, las vari-
ables de entorno se usan sin conocimiento del usuario.
Debido a la sintaxis de las variables de entorno podemos tener problemas a la hora de ejecu-
tar echo, o cualquier otro programa, con argumentos que contengan un dlar o un igual. Ambos
caracteres son especiales. Podemos decirle al shell que tome esos caracteres literalmente, que no
haga nada con ellos. Eso lo podemos hacer englobndolos entre comillas simples (quoting). De
esta forma, el shell no har nada con ellos, los escapar:
; echo $user
nemo
; echo $user is $user
$user is nemo
;
Ntese que el shell siempre se comporta de la misma manera ante la misma lnea de comandos.
Por ejemplo, la primera palabra (que es el nombre del comando) no es especial, y podemos hacer
esto:
; cmd=pwd
; $cmd
/usr/nemo
;
El escapado tambin funciona siempre de la misma forma. Probemos con el programa echo que
implementamos hace tiempo:
; 8.echo this is weird
0: echo
1: this is
2: weird
;
Como se puede observar, argv[1] contiene la cadena this is, incluyendo el espacio en
blanco. El shell no parti la cadena en dos, porque la hemos escapado. Las nuevas lneas tambin
se pueden escapar.
; echo how many
;; lines
how many
lines
El prompt es diferente porque el shell tiene que leer ms entrada para completar la parte
escapada, ya que hay una comilla simple sin cerrar. Cuando escapamos caracteres, les quitamos
su significado especial. Tambin funciona con la barra invertida:
; echo \
;; espera a que continue la lnea
; ... hasta que presionemos return
echo imprime la lnea vaca
; echo \
\
;
Para conseguir el valor de una variable de entorno desde un programa de C, podemos usar la lla-
mada al sistema getenv. Cuando usemos esta llamada, debemos comprobar los errores. La lla-
mada retorna una cadena de caracteres residente en memoria dinmica con el valor de la variable
de entorno. En caso de que no exista la variable, retorna nil.
- 17 -

env.c

______
#include <u.h>
#include <libc.h>
void
main(int, char*[])
{
char* home;
home = getenv("home");
if (home == nil)
sysfatal("we are homeless");
print("home is %s\n", home);
exits(nil);
}
Si lo ejecutamos:
; 8.env
home is /usr/nemo
Una llamada relacionada es putenv, que acepta un nombre y un valor, y escribe la variable de
entorno correspondiente. Tanto el nombre como el valor son cadenas de caracteres.
2.6. Nombres de proceso y estados
El nombre de un proceso no es el nombre del programa que ejecuta. Cada proceso tiene un
nmero nico que lo identifica, otorgado por el sistema cuando comienza lo crea. Este nmero se
llama id de proceso o pid. El pid identifica, por tanto, a un proceso.
El pid de un proceso es un nmero entero, y el sistema intenta no reusarlos. El pid sirve para
ordenar acciones sobre un proceso. En todo caso, no es ms que un nombre que se inventa el sis-
tema para cada proceso. La variable de entorno pid contiene el pid del shell. Ten en cuenta que
el valor de una variable de entorno es una cadena de caracteres, no un entero. Esta variable de
entorno es bastante til para crear ficheros temporales para un shell en concreto.
Para conseguir el pid del proceso que ejecuta nuestro programa, podemos usar getpid:

pid.c

_____
#include <u.h>
#include <libc.h>
void
main(int, char*[])
{
int pid;
pid = getpid();
print("my pid is %d\n", pid);
exits(nil);
}
- 18 -
Si ejecutamos este programa:
; 8.pid
my pid is 345
; 8.pid
my pid is 372
;
El primer proceso tena el pid 345. Tambin se puede decir que el primer proceso era el 345. El
segundo era el 372. Cada vez que ejecutamos un programa, el proceso tendr un pid diferente.
El comando ps (process status) lista los procesos del sistema. El segundo campo de cada
lnea (hay una lnea por proceso) es el pid. Veamos un ejemplo:
; ps
nemo 280 0:00 0:00 13 13 1148K Pread rio
nemo 281 0:02 0:07 13 13 1148K Pread rio
nemo 303 0:00 0:00 13 13 1148K Await rio
nemo 305 0:00 0:00 13 13 248K Await rc
nemo 306 0:00 0:00 13 13 1148K Await rio
... el resto de la salida se omite ...
El ltimo campo de una lnea indica el programa que est ejecutando el proceso. El tercer campo
empezando por la derecha indica la cantidad de memoria virtual que usa el proceso. Ya podemos
saber cunta memoria consume nuestro programa cuando se carga.
El segundo campo por la derecha es muy interesante. Veremos nombres como Pread y
Await. Esos nombres reflejan el estado del proceso. El estado del proceso indica lo que est
haciendo el proceso en este justo instante. Por ejemplo, los procesos 280 y 281, que estn ejecu-
tando rio, estn leyendo algo (Pread), y el resto de los procesos listados a continuacin estn
esperando a que algo ocurra (Await). Para entender esto, es necesario saber antes como imple-
menta el sistema operativo la abstraccin de proceso.
En este caso, slo tenemos un procesador, pero hay mltiples procesos que parecen ejecutar
simultneamente. Esto forma parte de la abstraccin de proceso. Hay mltiples programas que
ejecutan independientemente del resto. Ninguno de ellos transfiere el control a los dems.
Adems, el procesador provee de un nico flujo de control. Lo que ocurre es que cuando un pro-
ceso entra al kernel realizando una llamada al sistema, o por una interrupcin, el sistema guarda
el estado del proceso (esencialmente, el valor de los registros), y salta a otro proceso recuperando
su estado anteriormente guardado. Si se conmuta de un proceso a otro lo suficientemente rpido,
como puede hacerse con los procesadores actuales, parece que todos los procesos ejecutan al
mismo tiempo. A cada proceso se le asigna una pequea cantidad de tiempo del procesador, y
cuando se agota, se salta a otro proceso. Esta porcin de tiempo se conoce como quantum o
cuanto, y suele rondar los 100 ms, que es un tiempo considerable teniendo en cuenta la velocidad
de los procesadores modernos.
La transferencia de control de un proceso a otro se denomina cambio de contexto, debido a
que el estado de un proceso (registros, pila, etc.) se denomina contexto. Es importante tener claro
que el que realiza la transferencia de control es el kernel O acaso metemos jumps a otros progra-
mas en nuestros programas? No. El sistema se encarga de realizar los cambios de contexto.
La parte del kernel que decide al proceso al que le toca ejecutar se llama planificador o
scheduler, porque planifica la ejecucin de los procesos. Se llama planificacin o scheduling a
las decisiones tomadas por el planificador para multiplexar el procesador entre los distintos proce-
sos. En Plan 9, y en la mayora de los otros sistemas, el planificador es capaz de quitar y dar el
procesador a los procesos sin que estos tengan que realizar una llamada al sistema. Para ello, se
utilizan las interrupciones. A este tipo de planificacin se la denomina preemptive scheduling,
que se suele traducir como planificacin expulsiva o planificacin apropiativa.
- 19 -
Cuando hay un nico procesador, slo puede haber un proceso ejecutando (running), y el
resto estar listo (ready) para ejecutar. Esos son dos de los estados que puede tener un proceso
(running y ready). Fjate en la figura 2.4. El proceso que est ejecutando pasa a estar listo para
ejecutar cuando se acaba su tiempo de procesador. Entonces, el sistema elige otro proceso para
que pase a estar ejecutando. Los estados no son otra cosa que constantes definidas por el sistema
para implementar la abstraccin de proceso.
En ocasiones, un proceso puede estar leyendo de un terminal, de una conexin de red, o de
algn dispositivo. En esos casos, el proceso tiene que esperar a que le lleguen los datos. El pro-
ceso podra esperar en un bucle, pero sera un despilfarro de procesador. La idea es que cuando un
proceso tiene que esperar por entrada/salida, el sistema elige a otro proceso para ejecutar. Los dis-
positivos de entrada/salida son muy lentos comparados con el procesador. Mientras que un pro-
ceso est leyendo o escribiendo en un dispositivo, otro proceso puede usar el procesador para eje-
cutar una gran cantidad de sus instrucciones. El tiempo necesario para ejecutar unas cuantas
instrucciones, comparado con el necesario para hacer entrada/salida, es muy reducido. Es como
comparar el tiempo necesario para ir a tu casa con el tiempo necesario para ir a la luna. Esta idea
es fundamental para el concepto de multiprogramacin, que es como se llama a las tcnicas que
permiten que varios programas puedan estar cargados en el mismo ordenador simultneamente.
Running
Ready Blocked Nacimiento
Broken Muerte
Figura 2.4: Estados de los procesos y sus transiciones.
Para permitir que un proceso no cuente a la hora de repartir el procesador, se le etiqueta
como bloqueado (blocked). En realidad, ste es otro de los posibles estados. Todos los procesos
que se listaron anteriormente estn bloqueados. Por ejemplo, Pread y Await significan que el
proceso est bloqueado (el primero indica que lo est para acabar una lectura). Cuando ocurre el
evento por el que un proceso est esperando, el estado del proceso cambia y vuelve a estar listo
para ejecutar. Entonces, en algn momento el sistema le elegir para ejecutar y tendr su rebanada
de tiempo.
En Plan 9, el estado mostrado para los procesos que estn bloqueados indica la razn por la
que lo estn. Por eso, ps muestra distintos estados. Esto nos ayuda a saber qu est pasando con
nuestros procesos.
Hay otro estado, roto (broken), que significa que un proceso ha tenido un error y ha dejado
de ejecutar. Cuando un proceso hace algo ilegal (por ejemplo, tiene un error), queda en este
estado. Por ejemplo, dividir entre cero o dereferenciar un puntero nulo causa una excepcin (un
error). Las excepciones, como las interrupciones, son tratadas por el hardware y el sistema se
encarga de manejarlas. Despus de un error de este tipo en un proceso, el sistema lo deja roto. Un
proceso roto no ejecuta nunca ms, pero el sistema guarda su estado para que lo podamos depurar.
Una vez depurado, podemos pedir al sistema que lo elimine de la lista de procesos.
- 20 -
2.7. Depuracin
Cuando hemos cometido un error programando y nuestro programa se queda roto, es de gran utili-
dad saber qu le ha pasado. Hay varias formas de descubrirlo. Para probarlas, vamos a escribir un
programa que falle. El programa imprimir un saludo con el nombre que se le pase como argu-
mento, pero lo har sin comprobar que se le ha pasado al menos un argumento y adems lo
imprimir sin usar el formato adecuado de print.

hi.c

____
#include <u.h>
#include <libc.h>
void
main(int, char*argv[])
{
/* Wrong! */
print("hi ");
print(argv[1]);
exits(nil);
}
Si compilamos y ejecutamos el programa:
; 8.hi
8.hi 788: suicide: sys: trap: fault read addr=0x0 pc=0x000016ff
La ltima lnea es un mensaje impreso por el shell. Est esperando a que 8.hi termine su
ejecucin. Cuando acaba, el shell vio que algo sali mal e imprimi el mensaje de diagnstico
para avisar al usuario. Si imprimimos el valor de la variable de entorno status, veremos lo
siguiente:
; echo $status
8.hi 788: sys: trap: fault read addr=0x0 pc=0x000016ff
Por lo tanto, el legado que deja 8.hi al terminar su ejecucin, su estatus de salida, es lo que
imprime el shell si detecta errores. Podemos deducir si el estatus no proviene de una llamada a
exits por parte de 8.hi. En este caso, lo que ha ocurrido es que se ha intentado leer la
direccin de memoria 0x0. Esa direccin no es vlida, no pertenece a ningn segmento del pro-
ceso. Cuando se intenta leer una direccin que no pertenece a ningn segmento del proceso, se
levanta una excepcin (un fallo). De eso mismo es de lo que nos est avisando el estatus del pro-
grama con la cadena: fault read addr=0x0. El estatus comienza con el nombre del pro-
grama y el pid del proceso, para que se pueda identificar rpidamente el programa que ha fallado.
Despus se da ms informacin, como el contador de programa (PC) del proceso cuando se
intent leer la direccin 0x0, que era 0x000016ff. A continuacin seguiremos con la autopsia del
proceso.
El programa src sabe cmo obtener el fichero fuente del programa y la lnea que corre-
sponde un contador de programa.
; src -n -s 0x000016ff 8.hi
/sys/src/libc/fmt/dofmt.c:37
Hay que pasarle el nombre del binario como argumento. La opcin -n hace que se imprima el
nombre del fichero fuente y la lnea indicada. Si no se usa, src pedir al editor que muestre el
fuente, seleccionando dicha lnea. La opcin -s nos permite pasar una direccin de memoria o
un smbolo para localizar en el fuente. Por cierto, este programa es una fuente infinita de
sabidura. Si quieres saber cmo implementar, por ejemplo, cat, puedes ejecutar en un shell:
- 21 -
src /bin/cat.
Por el nombre del fuente que se ha impreso, podemos saber que el problema reside en la
biblioteca estndar de C, en dofmt.c. Qu es ms probable? Que haya un bug en la librera
estndar de C, o que hayamos cometido un error en nuestro programa? El misterio se puede
resolver viendo la pila del proceso que est roto. Podemos leer de la pila porque todava est all,
recuerda que el sistema operativo sigue manteniendo el estado del proceso roto, aunque no vaya a
volver a ejecutar nunca:
; ps
... muchos otros procesos...
nemo 788 0:00 0:00 24K Broken 8.hi
;
Para imprimir la pila, usaremos el depurador acid:
; acid 788
/proc/788/text:386 plan 9 executable
/sys/lib/acid/port
/sys/lib/acid/386
acid:
El depurador es una herramienta muy potente, descrita en [2]. Nosotros slo vamos a usar un par
de funciones. Despus de obtener el prompt de acid, le pedimos que imprima la pila mediante
su comando stk:
acid: stk()
dofmt(fmt=0x0,f=0xdfffef08)+0x138 /sys/src/libc/fmt/dofmt.c:37
vfprint(fd=0x1,args=0xdfffef60,fmt=0x0)+0x59 /sys/src/libc/fmt/vfprint.c:30
print(fmt=0x0)+0x24 /sys/src/libc/fmt/print.c:13
main(argv=0xdfffefb4)+0x12 /usr/nemo/9intro/hi.c:8
_main+0x31 /sys/src/libc/386/main9.s:16
acid:
El comando stk vuelca la pila del proceso. Podemos observar que el programa fall cuando eje-
cutaba la funcin dofmt, del fichero dofmt.c. La funcin se llam desde vfprint, que a la
vez se llam desde print, que se llam desde main El parmetro fmt de print es cero! Eso
no debera pasar, porque print espera como primer parmetro una cadena de caracteres vlida, y
se le est pasando un puntero nulo. Ah est nuestro bug.
Podemos recopilar mucha ms informacin sobre el programa. Por ejemplo, podemos
obtener el valor de las variables locales de las funciones a las que se llam antes de fallar, que se
encuentran en la pila.
acid: lstk()
dofmt(fmt=0x0,f=0xdfffef08)+0x138 /sys/src/libc/fmt/dofmt.c:37
nfmt=0x0
rt=0x0
rs=0x0
r=0x0
rune=0x15320000
t=0xdfffee08
s=0xdfffef08
n=0x0
vfprint(fd=0x1,args=0xdfffef60,fmt=0x0)+0x59 /sys/src/libc/fmt/vfprint.c:30
f=0x0
buf=0x0
n=0x0
- 22 -
print(fmt=0x0)+0x24 /sys/src/libc/fmt/print.c:13
args=0xdfffef60
main(argv=0xdfffefb4)+0x12 /usr/nemo/9intro/hi.c:8
_main+0x31 /sys/src/libc/386/main9.s:16
Cuando nuestros programas se quedan rotos, el uso de lstk() en acid es impagable. Usual-
mente, es todo lo que necesitamos para arreglar un bug. Podemos tener toda la informacin de lo
que pas desde main hasta el punto de fallo. Si nuestro programa hubiera comprobado errores,
las cosas habran sido mucho ms fciles, porque en muchos casos el error de diagnostico habra
sido suficiente para localizar el fallo en el programa.
Podemos observar cmo main no es la primera funcin de nuestro programa. Parece que
_main, de la biblioteca estndar de C, llam a main (la que pensbamos que era la primera
funcin en ejecutar!).
La ltima nota sobre depuracin no trata de lo que hay que hacer despus de que un pro-
grama falle, sino de lo que hay que hacer antes. Existe una funcin de biblioteca llamada
abort. Su cdigo es bastante simple:
void
abort(void)
{
while(*(int*)0)
;
}
La funcin atraviesa un puntero nulo! Ahora ya sabes lo que le pasa al pobre que llame a
abort. Es importante que, a la hora de programar, nos preparemos para cosas que no deberan
ocurrir en teora. Al final siempre ocurren. Una herramienta para prepararnos es abort. Pode-
mos incluir cdigo que compruebe cosas que no deberan pasar. As, nos anticiparemos a errores
que pueden resultar difciles de depurar. Si nuestro cdigo detecta alguno de esos casos, debe lla-
mar a abort. El proceso se quedar roto, para que lo podamos depurar antes de que las cosa se
pongan peor.
2.8. Todas las cosas son ficheros
Hemos visto dos abstracciones relacionadas con los procesos en Plan 9: los mismos proce-
sos, y las variables de entorno. La forma de usar estas abstracciones es realizando llamadas al sis-
tema.
Eso est bien, pero Plan 9 se ide pensando en que es natural estar conectado siempre a una
red. Ya sabes que tus ficheros no estn en la mquina local, en el terminal, sino que estn en una
mquina remota. Los diseadores del sistema se dieron cuenta de que los ficheros (como
abstraccin) son fciles de usar. Tambin se dieron cuenta de que ya se conoce bien cmo disear
sistemas que usan ficheros remotos (esto es, ficheros que estn en otros sistemas).
De ah viene la idea. La mayora de las abstracciones de Plan 9 para usar el hardware ofre-
cen una interfaz de ficheros. El sistema te miente y te hace pensar que hay muchas cosas que
son ficheros, pero en realidad no lo son. La cuestin es que parecen ser ficheros, y se pueden usar
como tales.
La motivacin para hacer las cosas de esta manera es que se obtienen interfaces simples
para programar y usar el sistema, y que adems se puede acceder a los recursos desde mquinas
remotas. De esta forma, se pueden depurar programas desde mquinas remotas, y somos capaces
de usar (casi) todos los recursos de una mquina a travs de la red. Lo nico que tenemos que
hacer es usar las mismas herramientas que se utilizamos para los ficheros de nuestro terminal,
aunque los ficheros estn en otro terminal.
- 23 -
Consideremos la fecha (con fecha nos referimos a la fecha y la hora). Cada mquina que
ejecuta Plan 9 tiene su idea de la fecha. Internamente, se guarda un contador para apuntar el paso
del tiempo, que depende del reloj del hardware. Sin embargo, al usuario del sistema le parece un
fichero:
; cat /dev/time
1152301434 1152301434554319872 ...
Leyendo /dev/time obtenemos una cadena de caracteres que representa la fecha, medida de
varias formas: segundos desde epoch (una fecha concreta que se usa como convenio), nanosegun-
dos desde epoch, y los ticks de reloj desde epoch.
Acaso /dev/time es un fichero real? Existe en nuestro disco? No Cmo se podra
tener un fichero en el disco con la fecha y hora actual? Esperaramos que un fichero cambiara
sus datos de forma mgica cada nanosegundo para tener la fecha precisa? No. Lo que ocurre es
que, cada vez que se lee del fichero /dev/time, el sistema sabe que se lee de ese fichero y
acta en consecuencia. En ese caso, no se leen bloques del disco, sino que el sistema nos da la
fecha actual.
Si te parece confuso, piensa que los ficheros son una abstraccin del sistema. El sistema
puede decidir lo que significa la lectura o la escritura de un fichero. Para los ficheros reales,
supone la lectura o escritura de bloques en el disco duro. Pero, para /dev/time, la lectura sig-
nifica obtener una cadena que representa la fecha actual del reloj del sistema. Otros sistemas oper-
ativos ofrecen la fecha mediante una llamada al sistema time. Plan 9 ofrece la fecha mediante
un fichero falso. La funcin de biblioteca time, descrita en time(2), lee la fecha de ese fichero y
la retorna en un nmero entero.
Consideremos ahora a los procesos Cmo sabe ps los procesos que se estn ejecutando en
el sistema? Es simple, en Plan 9, el sistema de ficheros /proc no representa tampoco bloques
en el disco. Es un directorio virtual, falso, o sinttico, llmalo como quieras. Ese directorio rep-
resenta a todos los procesos que estn ejecutando. Si se lista el directorio, el sistema nos da un
directorio por cada proceso:
; lc /proc
1 1320 2 246 268 30 32 348
10 135 20 247 269 300 320 367
...
Pero, como ya hemos dicho, no son ficheros en el disco. Son la interfaz que nos ofrece Plan 9
para manejar a los procesos que estn ejecutando. Cada uno de los ficheros de este directorio es
un directorio, nombrado segn el pid del proceso al que representa. Por ejemplo, para ir al direc-
torio que representa al shell que estamos usando, podramos hacer esto:
; echo $pid
938
; cd /proc/938
; lc
args fd kregs note notepg proc regs status wait
ctl fpregs mem noteid ns profile segment text
Esos ficheros son la interfaz para acceder a la informacin del proceso con pid 938, que es el que
ejecuta nuestra shell. Muchos de esos ficheros sirven para que el depurador pueda operar sobre el
proceso, y para permitir que programas como ps recopilen informacin. Por ejemplo, mira las
primeras lneas que imprimi acid cuando rompimos un proceso en la seccin anterior:
; acid 788
/proc/788/text:386 plan 9 executable
Acid est leyendo /proc/788/text, que parece ser un fichero que contiene el binario del pro-
grama. El depurador tambin usa /proc/788/regs, para leer los registros del procesador para
ese proceso, y /proc/788/mem, para leer la pila que queremos que sea volcada para su
- 24 -
inspeccin.
Aunque muchos de estos ficheros estn principalmente pensados para el depurador, otros
estn pensados para que los use el usuario. Recuerda que no son ficheros reales, sino que son la
interfaz para operar sobre la lista de procesos del sistema. Ahora ya podemos matar a un proceso.
Si escribimos la cadena kill en el fichero llamado ctl, mataremos a ese proceso. No estaremos
escribiendo la cadena en ningn fichero del disco. Esa cadena no queda almacenada en ningn
sitio. Si escribimos esa cadena en el fichero ctl del directorio perteneciente al shell que ejecuta-
mos, lo ms probable es que la ventana en la que estamos trabajando desaparezca, porque no hay
ningn otro proceso usndola.
; echo kill >/proc/$pid/ctl
...Dnde estar mi ventana? ...
Ya vimos la organizacin de la memoria de un proceso. Hay distintos segmentos en la memoria
de un proceso. Uno de los ficheros virtuales que forman parte de la interfaz para los procesos nos
ofrece los segmentos del proceso, con sus direccin de comienzo y de fin:
; cat /proc/$pid/segment
Stack defff000 dffff000 1
Text R 00001000 00016000 4
Data 00016000 00019000 1
Bss 00019000 0003f000 1
La pila comienza en 0xdefff000, que es nmero bastante alto, y acaba en 0xdffff000. Segura-
mente el proceso no llegue a usar nunca todo ese espacio de pila. Se puede observar que el seg-
mento de pila no crece. El sistema ofrece en demanda la memoria fsica que usa un proceso para
su pila, segn se va requiriendo. Cuando se tiene memoria virtual, no hay necesidad de hacer cre-
cer los segmentos de memoria. Podemos ver que el segmento de texto es slo de lectura (por la R
impresa). Tambin podemos ver que hay cuatro procesos usando dicho segmento Cmo? Hay
cuatro procesos que ejecutan un shell, esto es, ejecutando el cdigo de /bin/rc. Todos com-
parten ese segmento (de otra forma, desperdiciaramos memoria).
Se puede observar que las direcciones desde 0x0 hasta 0x0fff no son vlidas. No se pueden
usar los primeros 4 KB de memoria virtual (la primera pgina). Esta es la forma de capturar el
error cuando se usa un puntero nulo.
Ya hemos visto casi toda la interfaz ofrecida por Plan 9 para procesos. Las variables de
entorno son otro ejemplo, ya que el sistema usa de nuevo una interfaz de ficheros. Para ver las
variables de entorno que estn definidas, podemos listar un directorio (virtual) que se inventa Plan
9. El directorio es /env.
; lc /env
* cpu init planb sysname
0 cputype location plumbsrv tabstop
MKFILE disk menuitem prompt terminal
afont ether0 monitor rcname timezone
apid facedom mouseport role user
auth fn#sigexit nobootprompt rootdir vgasize
bootdisk font objtype sdC0part wctl
bootfile fs part sdC1part wsys
cflag home partition service
cfs i path status
cmd ifs pid sysaddr
;
Cada uno de estos ficheros virtuales representa una variable de entorno. Para nuestros programas
(y para nosotros), parecen ficheros reales que estn almacenados en el disco, porque los podemos
listar, ver su contenido, escribirlos, etc. Pero no lo son.
- 25 -
Podemos ver un fichero llamado sysname, otro llamado user, y otro llamado path.
Esto significa que nuestro shell tiene las variables de entorno sysname, user, y path. Veamos si
es verdad:
; echo $user
nemo
; cat /env/user
nemo;
El fichero /env/user parece contener nemo, (sin un carcter de nueva lnea al final). Ese es
precisamente el valor impreso por echo, que es el valor de la variable de entorno user. La
implementacin de getenv, que usamos anteriormente para retornar el valor de una variable de
entorno, consiste en leer el fichero correspondiente y devolver una cadena de caracteres de C con
el valor devuelto.
Esta simple idea, representar casi todo mediante ficheros, es muy potente. Por ejemplo, el
fichero /dev/text representa el texto que se muestra en la ventana (cuando se lee desde dentro
de ese ventana). Para hacer una copia de la sesin que hemos tenido con un shell, slo tenemos
que hacer:
; cp /dev/text $home/saved
Lo mismo se puede hacer para la imagen que se muestra en la ventana, que tambin se representa
mediante un fichero, /dev/window. Esto es justo lo que hemos hecho para capturar las pan-
tallas que se muestran en este libro. Podemos hacer este tipo de cosas con cualquier programa
que manipule ficheros, no slo funciona con cp. Por ejemplo, lp sirve para imprimir un fichero.
Si queremos una captura de pantalla, podemos ejecutar:
; lp /dev/screen
Problemas
1 Por qu la primera direccin de memoria usada para el programa global no era 0x0 ?
2 Escribe un programa que defina las variables de entorno para los argumentos que se le
pasan. Por ejemplo, despus de ejecutar
; args -ab -d x y z
debera suceder lo siguiente:
; echo $opta
yes
; echo $optb
yes
; echo $optc
yes
; echo $args
x y z
3 Que imprimira /bin/ls /blahblah (suponiendo que /blahblah no existe)?
Podra ls /blahblah imprimir lo mismo? Por qu?
4 Di lo que ocurre cuando se ejecuta el siguiente comando
; cd
;
despus de ejecutar el siguiente programa:
- 26 -
#include <u.h>
#include <libc.h>
void
main(int, char*[])
{
putenv("home", "/tmp");
exits(nil);
}
Por qu?
5 Qu hacen los siguientes programas? Por qu?
; cd /
; cd ..
; pwd
6 Despus de leer date(1), cambia la variable de entorno timezone para ver la fecha y hora
en New Jersey (costa este, EEUU).
7 Cmo podemos saber los argumentos que se han pasado a un proceso que ya ha sido
arrancado?
8 Busca otra solucin al problema anterior.
9 Qu podemos hacer si queremos depurar maana un proceso que tenemos roto , pero ten-
emos que reiniciar la mquina ahora mismo?
10 Qu puede pasar si usamos el depurador acid para inspeccionar 8.out despus de ejecu-
tar la siguiente lnea? Por qu?
; strip 8.out
- 27 -
.
- 28 -

También podría gustarte