PRÁCTICAS DE LABORATORIO DE SISTEMAS OPERATIVOS

NOVENA SESIÓN


  1. Dispositivos en UNIX.

    En la sesión de hoy aprenderemos un poco acerca de los dispositivos en UNIX observando el comportamiento de uno de ellos, el terminal. El manejo de los dispositivos en UNIX está gobernado por unos ficheros virtuales especiales, los ficheros de dispositivo. Normalmente, se encuentran ubicados en el directorio /dev, aunque no es obligatorio. Los hay de dos tipos:
    1. Dispositivos de bloque: se caracterizan por transferir la información en grupos de tamaño fijo denominados bloques. Como ejemplo tenemos el disco magnético.
    2. Dispositivos de tipo carácter: pueden transmitir la información carácter a carácter. Ejemplo: un modem.


    Aparte de por su tipo, los ficheros de dispositivo quedan realmente identificados por dos números: su número mayor y su numero menor. Normalmente, el número mayor indica el tipo de dispositivo mientras que el número menor suele indicar distintas instancias de un mismo dispositivo. Por ejemplo, un mismo número mayor puede identificar los puertos serie del ordenador y cada número menor cada uno de los que pueda haber en realidad en el ordenador (lo que en MS-DOS sería COM1, COM2, etc.).

    Echad un vistazo a los ficheros del directorio /dev del servidor con:
    ls -l /dev | more 
    Podréis observar una b delante de todos los ficheros de dispositivo de bloques y una c delante de todos aquellos de tipo carácter. También veréis los dos números (mayor y menor) de cada dispositivo. Puede ocurrir que necesitéis seguir algún enlace simbólico hasta llegar a la entrada de dispositivo. Insisto en que el nombre del fichero de dispositivo o su ubicación no son importantes. En Linux, por ejemplo a las particiones del disco duro IDE primario de la primera controladora se suele acceder por los ficheros de dispositivo /dev/hda1, /dev/hda2, etc. Sin embargo si hacemos otro fichero de dispositivo con los mismos números mayor y menor en /LaCasaDe/Pepita también podríamos acceder a las particiones.

  2. Creación de los ficheros de dispositivos.

    Al ser ficheros especiales, los dispositivos no se crean con las órdenes habituales de manejo de ficheros. Para crearlos se usa la orden de la shell mknod que ya vimos para el caso de tuberías. En HPUX, la orden se encuentra ubicada en el directorio /sbin, que no está en el PATH, por lo que es necesario escribir la ruta completa para poder ejecutarla. En esta orden podremos especificar de qué tipo queremos que sea el dispositivo y cuáles serán sus números mayor y menor. Suele existir en el propio directorio /dev un script que ayuda a seleccionar estos números: MAKEDEV. Al contrario que ocurría cuando creábamos una tubería, hace falta ser superusuario (root) para poder crear ficheros de dispositivo, por razones que veremos más adelante. También, al igual que veíamos con las tuberías, existe la posibilidad de usar una llamada al sistema para poder crear dispositivos desde un programa en C. La llamada se llama mknod y se encuentra en el resumen.

  3. Operaciones sobre los dispositivos.

    El hecho de que los dispositivos se construyan como ficheros especiales tiene el objetivo de poder acceder a ellos como si de un fichero normal se tratara (transparencia). Podemos usar open, read, write, etc. con un significado además bastante evidente. Si escribimos en un fichero de dispositivo de una partición, por ejemplo, estaremos escribiendo datos brutos sobre esa partición. Si abrimos el fichero asociado a un modem, estaremos estableciendo una conexión. Y así todo. Sin embargo, habrá operaciones que no se puedan realizar con las tradicionales de apertura, lectura, escritura y cierre de ficheros. Por ejemplo, si queremos establecer la velocidad de transferencia de un modem, tendremos que hacerlo con una llamada al sistema diferente. Esta llamada es ioctl cuyo nombre es una abreviatura de Input/Output control o control de entrada/salida. La llamada es una especie de cajón de sastre donde se incluyen todas las opciones de configuración y control de los dispositivos. Su prototipo es:
        int ioctl(int fildes, int peticiOn, ... /* argumentos opcionales */);
    El primer parámetro es un descriptor de fichero abierto sobre un fichero especial del dispositivo. El segundo es una macro que definirá el tipo de operación que queremos hacer sobre el dispositivo. Si la operación lleva algún parámetro, vendrán a continuación. Cada dispositivo tendrá sus propias operaciones.

    Como podréis intuir, los dispositivos son unos elementos muy delicados del ordenador. Por ejemplo, de nada sirve tener un sistema de ficheros muy bonito y bien protegido si resulta que la partición donde se encuentra tiene dado el permiso de lectura para todos los usuarios. Siendo así, con que hagamos una copia de la partición, con las especificaciones del sistema de ficheros, podemos llevar la copia de la partición a casa y extraer la información como ya hicimos en la sesión tercera. Ni comentar siquiera si esta partición tuviera dado el permiso de escritura lo que podría pasar. Los fichero de dispositivo suelen pertenecer a root y tener denegado el permiso de lectura y escritura para el resto de usuarios.

  4. Terminales.

    Un ejemplo de fichero de dispositivo es el terminal. Un terminal está compuesto por un teclado y una pantalla. Cuando se escribe en un terminal, lo escrito aparece en la pantalla; cuando se lee de un terminal, los caracteres leídos han sido tecleados antes. Las opciones del terminal se podrán establecer mediante llamadas a ioctl.

    Cuando ejecutáis la orden who, al lado de cada login de las personas conectadas al ordenador aparece en qué terminal del ordenador están conectadas:
    <Tejo>/home/so/public_html$ who | grep so
    so         pts/tc       Mar 27 11:05
    so         pts/te       Mar 27 11:09 
    En este caso yo estoy conectado desde dos terminales cuyos fichero de dispositivo son /dev/pts/tc y /dev/pts/te. En realidad, en este caso son pseudoterminales. Comprobad que escribiendo en vuestro terminal aparece lo escrito en la pantalla:
    <Tejo>/home/so/public_html$ echo "prueba" > /dev/pts/te
    prueba
    <Tejo>/home/so/public_html$ 
    Si lo hacéis con el terminal del otra persona, probablemente no salga nada por los permisos asociados con ese terminal. Escribiendo en el fichero asociado al terminal es como funciona la orden de la shell write. Podéis evitar que la gente os escriba con write quitando permisos al grupo y a los demás del terminal. También tenéis la opción de usar mesg n que, en realidad, hace lo mismo.

  5. Modos de funcionamiento de un terminal.

    Desde los inicios de UNIX, el sistema operativo estuvo orientado hacia máquinas servidoras a las cuales se conectaban usuarios remotamente a través de un terminal. En aquellos entonces, la conexión se realizaba por puertos serie de baja velocidad y por líneas sujetas a muchos errores. Actualmente, la transmisión de texto tecleado en un terminal no supone ningún inconveniente. Es debido a estas razones históricas que las máquinas UNIX disponen de dos comportamientos para sus terminales, el modo canónico y el modo no canónico.

    En el modo no canónico en cuanto se escribe un carácter en el terminal, aparece en la pantalla. En cuanto se teclea un carácter en el teclado, está disponible para ser leído por el programa.

    En el modo canónico, no sucede esto. Si en un programa ejecutamos una llamada al sistema read sobre el terminal (entrada estándar, por ejemplo) la llamada no volverá por más que vayamos tecleando caracteres hasta que no introduzcamos el carácter de retorno del carro. Es decir, en el modo canónico existe un búfer intermedio de entrada que no se vacía hasta que se recibe un carácter de retorno del carro. Además de esto, el propio terminal puede editar la línea que se va a mandar y configurará un carácter para borrar caracteres tecleados erróneamente. De este modo, se ahorraba antiguamente tener que mandar caracteres de retroceso innecesarios, la línea que se mandaba era ya la definitiva.

    Si queremos hacer aplicaciones en UNIX que respondan, por ejemplo, a las flechas del cursor instantáneamente, tenemos que poner el terminal en modo no canónico. Existen algunas bibliotecas de funciones que ya realizan esta función por nosotros. Es el caso de la familia de bibliotecas curses. En ella se encuentran funciones equivalentes a las que se encuentran en la cabecera conio.h de MS-DOS. Un ejemplo de aplicación que usa el modo no canónico es el lector de correo, que tenéis en tejo, elm.

    Notad, no obstante, que el búfer intermedio de la entrada/salida canónica de un terminal es independiente del propio de la función printf de la biblioteca libc de C. Si queréis que vuestra aplicación responda a un mensaje del tipo:
    ¿Está usted seguro? (s/N) 
    sin que el usuario tenga que dar al retorno del carro, de nada os sirve hacer fflush(stdin); para ello.

    La pregunta que podría surgiros ahora es: si resulta que cuando tecleo algo los caracteres que estoy tecleando no están disponibles para el proceso, ¿quién es quien los escribe en la pantalla? Porque yo los veo aparecer. La respuesta es el propio terminal, que tiene activado el modo de eco de carácter. Por eso cuando un programa está ejecutando unos cálculos y nosotros tecleamos algo, lo vemos aparecer por la pantalla y, cuando el programa acaba, lo que hemos tecleado está disponible para quien lo lea (en este caso la shell).

  6. Sesiones, grupos de procesos, terminal de control y señales.

    Los procesos que están en ejecución se agrupan en grupos de procesos y los grupos de procesos en sesiones. Una sesión suele corresponder con los procesos lanzados en una misma conexión de un usuario. Un grupo de procesos suele estar formado por un mismo proceso y sus hijos. Por cada proceso que lanzamos desde la shell, se crea un nuevo grupo de procesos. Podéis observar cómo se agrupan los procesos en grupos y sesiones mediante la orden:
    <Tejo>/home/so/public_html$ export UNIX95=""
    <Tejo>/home/so/public_html$ ps -fju so
    UID        PID  PPID  PGID   SID  C    STIME TTY          TIME CMD
    so        7813  7812  7813  7812  0 11:05:33 pts/tc      00:00 -ksh
    so        7905  7813  7905  7812  0 11:12:17 pts/tc      00:06 vi sesion9.htm
    so        7880  7879  7880  7879  1 11:09:23 pts/te      00:00 -ksh
    so        8025  7880  8025  7879  5 11:30:35 pts/te      00:00 ps -fju so
    En HPUX, es necesario definir la macro UNIX95 porque la opción -j de ps pertenece al estándar XPG4 que no está definido por defecto.

    Cada grupo de procesos tiene un líder del grupo. Además, cada grupo de procesos puede tener o no un terminal de control. El terminal de control está relacionado con el envío de señales del terminal a los procesos. Si está así configurado, que lo está por defecto, cuando pulsamos CTRL+C el terminal envía una señal SIGINT a los grupos de procesos de los cuales es su terminal de control. Estos procesos, por defecto, mueren. Y así ocurre con otras señales.

    Dentro de una sesión, hay un grupo que se denomina grupo de primer plano (foreground group). Los otros serán grupos de segundo plano. Los procesos de primer plano tienen más control sobre el terminal. Pueden leer y escribir de él. Los de segundo plano, por defecto, pueden escribir en él, pero se paran (reciben una señal de parada) si tratan de leer de él:
    <Tejo>/home/so/public_html$ (echo "escribo sin problemas..."; sleep 3; \
    >                            read LINEA; sleep 3;                      \
    >                            echo "Se introdujo $LINEA") &
    escribo sin problemas...
    [1]     8067
    <Tejo>/home/so/public_html$
    [1] + Stopped (tty output)     (echo "escribo sin problemas..."; sleep 3; \;
                                    read LINEA; sleep 3;                      \;
                                    echo "Se introdujo $LINEA") &
    <Tejo>/home/so/public_html$ jobs
    [1] + Stopped (tty output)     (echo "escribo sin problemas..."; sleep 3; \;
                                    read LINEA; sleep 3;                      \;
                                    echo "Se introdujo $LINEA") &
    <Tejo>/home/so/public_html$ fg %1
    (echo "escribo sin problemas..."; sleep 3; \;
     read LINEA; sleep 3;                      \;
     echo "Se introdujo $LINEA")
    Hola
    Se introdujo Hola
    <Tejo>/home/so/public_html$  
    Esto es, de todos modos, configurable.

    Mediante las llamadas getsid, setsid, setpgrp y setpgid podemos actuar sobre las sesiones o grupos a los que pertenece un proceso. Ved el resumen. Una única puntualización: al ejecutar la llamada al sistema setpgrp, el proceso lidera un nuevo grupo de procesos pero, a cambio, se queda sin terminal de control. El primer fichero especial que abra con open y no especifique como opción del open la macro O_NOCTTY, pasará a ser el terminal de control del proceso.

  7. IOCTLs asociados a los terminales.

    Mediante una llamada a ioctl, podemos establecer algunas opciones del terminal o consultar sus propiedades. En el resumen tenéis las macros que podéis usar con ioctl. De ellas, la que es más importante es TCGETA y TCSETA, que permiten establecer los atributos del terminal. Estas llamadas usan una estructura de tipo struct termio cuya dificultad de manejo radica en que cuatro de sus campos son campos de bits, que seleccionan opciones mediante macros del mismo modo que hacía open:
    struct termio
       {unsigned short c_iflag;     /* modos de entrada */
        unsigned short c_oflag;     /* modos de salida  */
        unsigned short c_cflag;     /* modos de control */
        unsigned short c_lflag;     /* modos locales    */
        unsigned char  c_cc[NCC];   /* caracteres de control */
        };      
    El tipo de los campos de la estructura varía en Solaris. Los flags son del tipo tcflag_t y el array lo es del tipo cc_t. Podéis consultar al respecto la página de manual de termio.

    Cada uno de los campos de bits controla un aspecto del terminal. En el resumen podéis ver algunas macros para los distintos campos de bits:

    En cuanto al campo c_cc, es un array donde se encuentran los caracteres de control del terminal. Existen unas macros para el índice del array para facilitar su acceso. Por ejemplo, si queremos que el carácter de interrupción deje de ser CTRL+C y pase a ser CTRL+U, haríamos:
    struct termio st;
    [...]
    st.c_cc[INTR]='U'-'@'; /* CTRL+U tiene un ASCII igual a la posición de la
                              letra U en el abecedario inglés.                 */
    [...]       


  8. Posibilidades de entrada en el modo no canónico

    Dentro del modo no canónico hay definidas dos variables, TIME y MIN, que definen cuatro modos de funcionamiento diferentes dependiendo de su valor:
    1. TIME=0 y MIN=0: la lectura vuelve de inmediato. Lee caracteres si los hay y, si no, read devuelve 0.
    2. TIME=0 y MIN>0: la lectura se queda bloqueada hasta que haya MIN caracteres al menos que leer.
    3. TIME>0 y MIN=0: la lectura se queda bloqueada un máximo de TIME décimas de segundo hasta que haya algún carácter que leer.
    4. TIME>0 y MIN>0: ha de haber al menos MIN caracteres para que read vuelva y volverá también si la pulsación entre dos de ellos supera TIME décimas de segundo.
    Las variables TIME y MIN forman parte del array c_cc de struct termio. Para acceder a estas variables, se han de usar los índices VTIME y VMIN, respectivamente.

    Para ver la configuración actual del terminal, podéis teclear stty -a. Con esta orden también se puede alterar alguna propiedad del terminal. Es la que usamos al principio del Laboratorio para realizar la configuración de la sesión de trabajo en el fichero .profile.
    <Tejo>/home/so/public_html$ stty -a
    speed 9600 baud; line = 0;
    rows = 0; columns = 0
    min = 1; time = 0;
    intr = ^C; quit = ^\; erase = ^H; kill = ^U
    eof = ^D; eol = ^@; eol2 = ; swtch = 
    stop = ^S; start = ^Q; susp = ; dsusp = 
    werase = ; lnext = 
    parenb -parodd cs8 -cstopb hupcl -cread -clocal -loblk -crts
    -ignbrk brkint ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl -iuclc
    ixon -ixany ixoff -imaxbel -rtsxoff -ctsxon -ienqak
    isig icanon -iexten -xcase echo echoe echok -echonl -noflsh
    -echoctl -echoprt -echoke -flusho -pendin
    opost -olcuc onlcr -ocrnl -onocr -onlret -ofill -ofdel -tostop
    <Tejo>/home/so/public_html$ stty susp ^Z
    <Tejo>/home/so/public_html$ 
    Con esta última orden hacemos que el carácter CTRL-Z sea el carácter de suspesión de procesos. Esto hace que cuando se está ejecutando un proceso de primer plano, si pulsamos CTRL-Z pase automáticamente a segundo plano y se pare. Es una tecla habitual en UNIX para este cometido, pero en tejo (HPUX) no está definida por defecto. En Solaris, sí lo está. Para introducir el CTRL-Z es necesario que pulséis antes CTRL-V, el carácter de escape de la shell.

  9. Modificación de un bit de un campo de bits dejando el resto inalterados

    Si tenemos un campo de bits, puede que, en determinadas circunstancias, deseemos modificar un determinado bit sin variar el resto. Por ejemplo, sea el campo de bits cbits y la macro ESTEBIT que señala uno de los bits del campo. Podemos, dejando el resto de bits inalterado, hacer lo siguiente:
    1. Activar el bit: lo logramos haciendo un OR con la macro.
      cbits=cbits | ESTEBIT; 
    2. Desactivar el bit: lo logramos haciendo un AND con el complementario de la macro.
      cbits=cbits & ~ESTEBIT; 
    3. Cambiar el bit de encendido a apagado o viceversa: lo logramos haciendo un XOR con la macro.
      cbits=cbits ^ ESTEBIT; 


  10. Activación/desactivación de una propiedad del terminal.

    Dada la complicación y la gran variedad de opciones del terminal, lo que se suele hacer habitualmente no es definir una configuración partiendo de cero, sino tomar la que ya está y modificarla. Para ello, debemos usar lo del apartado anterior. Por ejemplo, si deseamos desactivar el modo canónico, la secuencia sería:
    struct termio st;
    [...]
    if (ioctl(1,TCGETA,&st)==-1) /* Error */;
    [...]
    st.c_lflag=st.c_lflag & ~ICANON;
    [...]
    if (ioctl(1,TCSETA,&st)==-1) /* Error */; 


  11. El terminal VT100.

    Hemos visto cómo mediante la llamada ioctl podíamos cambiar el modo de un dispositivo. Sin embargo, el caso de un terminal es algo más complicado. Las opciones que podemos cambiar del funcionamiento del terminal serán, en todo caso, las de la parte del servidor. No podemos, ni se podía antiguamente, cambiar las opciones del terminal situado "al otro lado del cable" directamente. En vuestro caso, no podéis desde el servidor cambiar la configuración de la aplicación emuladora de terminal que estáis usando. Para lograr cierto grado de control remoto sobre el terminal se usaron y se usan los códigos de control del terminal. Por ejemplo, para mover el cursor a un punto de la pantalla (una propiedad "al otro lado del cable"), se usa una serie de códigos de control, que al escribirlos hace que dichos códigos no aparezcan por la pantalla, sino que el terminal físico (o el programa de emulación) mueva el cursor.

    Hace tiempo había verdaderos terminales. Eran teclados y pantallas cuya única misión era actuar de terminal. Su circuitería estaba preparada para conectarse, mediante puerto serie al ordenador central y para nada más. De estos terminales, fue muy popular el modelo VT-100 de Tektronics. Vosotros, en los PCs del aula, no tenéis uno de esos terminales, sino un programa que emula su comportamiento. Mediante la variable de entorno TERM indicáis al sevidor UNIX que use los códigos de control del terminal VT-100. Vedlo con echo $TERM.

    En el caso del terminal VT-100, para llevar el cursor a la posición de home (esquina superior izquierda de la pantalla) hay que escribir en el terminal justo tres caracteres: el carácter de ESCAPE, cuyo código ASCII es 27, el carácter de corchete abierto ('[') y una hache mayúscula. Este sortilegio lleva el cursor a la parte superior izquierda de la pantalla, probadlo:
    printf "^[[H"
    Para poder probarlo, tenéis que teclear entre las comillas del printf exactamente esto: CTRL-V Esc [ H. El código de escape aparece como ^[ pues es equivalente a CTRL+[. En el resumen podéis ver muchos códigos de control más del terminal. Tened en cuenta que puede que una determinada propiedad se vea diferente en cada uno de los emuladores del VT-100 que probéis.

    Las teclas especiales (flechas, Insert, Supr, etc.) también tienen códigos especiales en cada terminal.

  12. Práctica.

    Pongamos en práctica todo lo aprendido en esta sesión. Haced un programa que limpie la pantalla, imprima un mensaje que ponga:
    ¿Desea que el programa acabe? (s/n) 
    con la s en modo inverso para resaltar que es la opción por defecto. El mensaje debe aparecer en la línea 3, columna 7 de la pantalla. No uséis tput para ello. Usad directamente los códigos del terminal vt100 que vienen en el resumen. El programa se debe quedar esperando 10 segundos como máximo a que el usuario pulse una tecla. A la sola pulsación de una 's' el programa acaba con un código de retorno 0. Si se pulsa una 'n' el programa acaba con un código de retorno 1. Si se pulsa otra letra, el programa acaba con un código de retorno 2. Si no se pulsa nada y pasan los diez segundos, el programa acaba con código de retorno 3. No usar alarm. Dejad el terminal como estaba al principio, incluso si se pulsa CTRL+C para interrumpir su funcionamiento.

    Soluciones y comentarios.

  13. Órdenes de la shell relacionadas.

    fg
    pasa una orden a primer plano
    bg
    pasa una orden a segundo plano
    mknod
    crea nuevos ficheros especiales
    write
    escribe a otro usuario conectado al ordenador
    mesg
    permite o deniega la llegada de mensajes a nuestro terminal
    stty
    permite ver o variar la configuración del terminal
    tput
    escribe cadenas de control del terminal


  14. Funciones de biblioteca relacionadas.

    curses
    biblioteca de funciones para el manejo del terminal


  15. LPEs.


© 2000 Guillermo González Talaván.