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 programa /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. Escojamos cualquiera de ellos. Cuando ejecutamos un programa y se arranca un proceso, puede ejecutar 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 programa, siempre pensamos que se ejecutan las instrucciones una detrs de otra. Pero siempre pensamos en las instrucciones de nuestro proceso, no en las de los dems. La implementacin de la

-2abstraccin 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 procesos ejecutando. Cada proceso tiene su conjunto de registros, que incluyen un contador de programa. 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 PC addl bx, si subl $4, di movl bx, cx ... Rio (proceso #1) PC

... cmpl si, di jls label movl bx, cx addl bx, si ... Rio (proceso #3)

... addl bx, di addl bx, si PC subl $4, di movl bx, cx ... Memoria del Sistema Rc (proceso #2)

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.

-3En 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 contiene 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 caracteres 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 nuestro 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 ; strip 8.take ; ls -l 8.take --rwxr-xr-x M 19 nemo nemo 21713 Jul 6 22:49 8.take

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 programa. 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 --rw-r--r-- M 19 nemo nemo 328 Jul

6 23:06 8.global 6 23:06 global.8

Est claro que no hay espacio en los 328 bytes para la variable global del programa, que necesita 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 ejemplo 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 realiza 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 indican las direcciones se deben usar. Por tanto, el sistema sabe dnde tiene que cargar la imagen 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 direcciones 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

-5porque 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 programa cargado. La opcin -n hace que nm ordene su salida por direccin:
; nm -n 8.global 1020 1033 1073 10e2 1124 1180 1188 11fb 122a 12e7 130a 1315 1442 1455 145d 1465 146d 14a0 14ac 14b4 2000 2004 2008 200c 2010 2014 2024 2024 212c 10212c T T T T T T T T T T T T T T T T T T T T D D D d D d B B B B main _main atexit atexitdont exits _exits getpid memset lock canlock unlock atol atoi sleep open close read _tas pread etext argv0 _tos _nprivates onexlock _privates _exits edata onex global 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 direcciones 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 Segmento Data Texto del programa 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 restricciones. 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 segmento se puede leer y escribir. Sin embargo, su contenido no se puede ejecutar. El contenido 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 mediante 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 memoria. 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 ejecutando, 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 proceso 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

-7partido de los datos no inicializados, por ejemplo, al programar grandes tablas hash que contengan pocos elementos (sparse). Se pueden implementar con arrays grandes que no estn inicializados, 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 proporcionado 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 permitan 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 simple, 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 funcionar 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 programa proporcionando los argumentos en distinto orden:
8.echo 8.echo 8.echo 8.echo 8.echo repeat after me -n repeat after me -v repeat after me -n -v repeat after me -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 proporciona 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 procesando las opciones. Despus de ARGEND, tanto argc como argv reflejan el vector de argumentos 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 combinaciones 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 siguientes 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 nuestro 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 siguiente 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 "repeat" "after" "me" ; 8.becho -v ; 8.becho -v -d usage: 8.becho [-nv] [-d delims] args

note the space before the ""

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 nuestro 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 programas. 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 hacerlo. 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 sistema operativo, podemos usar una llamada al sistema para acabar con un proceso. El sistema libera 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 llamadas al sistema que devuelven un valor entero. Las llamadas que devuelve una cadena de caracteres 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 necesidades. 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 biblioteca 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 elemento 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 caracteres 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 sistema 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 nombre 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 necesitamos 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 variables de entorno se usan sin conocimiento del usuario. Debido a la sintaxis de las variables de entorno podemos tener problemas a la hora de ejecutar 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 llamada al sistema getenv. Cuando usemos esta llamada, debemos comprobar los errores. La llamada 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 sistema 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 nemo nemo nemo nemo 280 0:00 0:00 281 0:02 0:07 303 0:00 0:00 305 0:00 0:00 306 0:00 0:00 ... el resto de la salida se omite ... 13 13 13 13 13 13 13 13 13 13 1148K 1148K 1148K 248K 1148K Pread Pread Await Await Await rio rio rio rc rio

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 ejecutando 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 implementa 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 proceso 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 programas 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 procesos. 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 proceso 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 dispositivos de entrada/salida son muy lentos comparados con el procesador. Mientras que un proceso est leyendo o escribiendo en un dispositivo, otro proceso puede usar el procesador para ejecutar 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

Broken

Muerte

Nacimiento

Ready

Blocked

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 utilidad 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 argumento, 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 proceso. 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 programa con la cadena: fault read addr=0x0. El estatus comienza con el nombre del programa 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 corresponde 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 ejecutaba 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. Usualmente, 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 programa 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. Podemos 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 llamar 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 procesos, y las variables de entorno. La forma de usar estas abstracciones es realizando llamadas al sistema. 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 ofrecen 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), nanosegundos 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 significa obtener una cadena que representa la fecha actual del reloj del sistema. Otros sistemas operativos 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 representa 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 10 135 ... 2 20 246 247 268 269 30 300 32 320 348 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 directorio que representa al shell que estamos usando, podramos hacer esto:
; echo $pid 938 ; cd /proc/938 ; lc args fd ctl fpregs

kregs mem

note noteid

notepg ns

proc regs status profile segment text

wait

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 programa. 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 ejecutamos, 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 Text R 00001000 00016000 Data 00016000 00019000 Bss 00019000 0003f000 1 4 1 1

La pila comienza en 0xdefff000, que es nmero bastante alto, y acaba en 0xdffff000. Seguramente el proceso no llegue a usar nunca todo ese espacio de pila. Se puede observar que el segmento 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 crecer 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 comparten 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 * 0 MKFILE afont apid auth bootdisk bootfile cflag cfs cmd ; cpu cputype disk ether0 facedom fn#sigexit font fs home i ifs init location menuitem monitor mouseport nobootprompt objtype part partition path pid planb plumbsrv prompt rcname role rootdir sdC0part sdC1part service status sysaddr sysname tabstop terminal timezone user vgasize wctl wsys

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 pantallas 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 yes ; echo yes ; echo yes ; echo x y z

$opta $optb $optc $args

3 4

Que imprimira /bin/ls /blahblah (suponiendo que /blahblah no existe)? Podra ls /blahblah imprimir lo mismo? Por qu? 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? Qu hacen los siguientes programas? Por qu?


; cd / ; cd .. ; pwd

6 7 8 9 10

Despus de leer date(1), cambia la variable de entorno timezone para ver la fecha y hora en New Jersey (costa este, EEUU). Cmo podemos saber los argumentos que se han pasado a un proceso que ya ha sido arrancado? Busca otra solucin al problema anterior. Qu podemos hacer si queremos depurar maana un proceso que tenemos roto , pero tenemos que reiniciar la mquina ahora mismo? Qu puede pasar si usamos el depurador acid para inspeccionar 8.out despus de ejecutar la siguiente lnea? Por qu?
; strip 8.out

- 27 .

- 28 -

También podría gustarte