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:
- 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.
- 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.
Hay que insistir 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.
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.
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 y con las
especificaciones del sistema de ficheros, podemos llevar la
copia de la partición a casa y
extraer la información. 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.
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.
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).
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.
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:
ISTRIP
: activa el borrado automático
del bit séptimo de los caracteres de la entrada
(no permite ASCIIs mayores que 127).
IXON
, IXOFF
e IXANY
:
relacionados con el control de flujo. Permite que el
terminal
interrumpa temporalmente la salida
mediante la pulsación
de un carácter (CTRL+S, habitualmente)
y su reanudación
(normalmente con CTRL+Q).
ISIG
: permite al terminal mandar
señales al
grupo de procesos de primer plano (comportamiento por
defecto).
ECHO
: activa el eco de carácter.
ICANON
: activa el modo canónico.
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. */
[...]
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:
TIME=0
y MIN
=0: la lectura vuelve
de inmediato. Lee caracteres si los hay y, si no,
read
devuelve 0.
TIME=0
y MIN
>0: la lectura
se queda
bloqueada hasta que haya MIN
caracteres
al menos que leer.
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.
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.
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:
- Activar el bit: lo logramos haciendo un OR con la macro.
cbits=cbits | ESTEBIT;
- Desactivar el bit: lo logramos haciendo un AND con el
complementario de la macro.
cbits=cbits & ~ESTEBIT;
- Cambiar el bit de encendido a apagado o viceversa:
lo logramos haciendo un XOR con la macro.
cbits=cbits ^ ESTEBIT;
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 */;
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.
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.
Ó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
Funciones de biblioteca relacionadas.
curses
- biblioteca de funciones para el manejo del terminal
LPEs.