Uso de búferes para pasar o recibir información.
Muchas llamadas al sistema requieren que se le pase o devuelven
un bloque de datos. Habitualmente, estos bloques de datos se
declaran en los prototipos de las llamadas al sistema como
punteros a carácter (char *
).
En todos los casos, es necesario no sólo pasar el puntero a la
función, sino que este puntero señale a una
zona de la
memoria válida. Tenemos varias opciones para conseguirlo:
Las posibilidades para usar el búfer son dos:
Pasar datos a la llamada al sistema:
Un ejemplo lo tenemos en la llamada al sistema
write
, cuyo prototipo es:
ssize_t write(int fildes, const void *buf, ssize_t nbyte);
El segundo parámetro es un puntero a la zona de
memoria donde se encuentran los datos que queremos
escribir en un fichero y el tercer parámetro es el
número de bytes que queremos escribir. El tercer
parámetro es necesario porque el puntero
sólo contiene
la dirección de memoria donde se almacenan los datos,
no la longitud. Para comunicar la longitud de los
datos a la función, se usa nbyte
.
Nótese que el tipo es void *
. Esto indica
que el compilador admitirá cualquier tipo de puntero
ahí. Además viene precedido por la palabra clave
const
, que indica que la función no
modificará los datos que se le pasan
en el búfer.
Si la llamada al sistema tiene como parámetro una
cadena de caracteres, no es necesario pasarle la
longitud de los datos. La función la sabrá
porque la
cadena de caracteres acaba cuando encuentre el
carácter 0 ('\0'
). Ejemplo de
función
con cadena de caracteres como parámetro:
int unlink(const char *path);
Recibir datos de la llamada al sistema:
Las llamadas al sistema que devuelven datos en un
búfer que nosotros le pasemos requiren que nosotros
le especifiquemos una dirección de memoria mediante
un puntero, le reservemos la memoria (bien implícita
o explícitamente) y le indiquemos
cuál es
el tamaño de la zona que le hemos reservado.
Esto es así para que la llamada no se pase escribiendo
en el búfer y sobreescriba otras
áreas de datos del
programa. Un buen ejemplo de llamada al sistema que
devuelve datos en un búfer previamente reservado es:
ssize_t read(int fildes, void *buffer, size_t nbyte);
Obsérvese cómo el buffer ahora no es
const
como en write
pues
va a ser modificado. Como ejemplo de uso, para leer
un máximo de 50 bytes del
teclado, podíamos usar un fragmento como este:
char buffer[50];
[...]
read(0, buffer, 50);
Si en lugar de este fragmento, usáramos este otro:
char *buffer;
[...]
read(0, buffer, 50);
el compilador no se quejaría pues el tipo de los
parámetros es el correcto, pero al ejecutarlo, la
llamada al sistema escribiría
lo leído en una zona
de memoria prohibida y daría el error Bus
error
o el programa empezaría a comportarse
de forma inestable e inesperada.
Las llamadas al sistema que devuelven datos en un
búfer pasado por el programador suelen devolver el
tamaño de los datos devueltos, es decir,
qué parte
del búfer que le pasamos hay que considerar. Es el
caso de la función read
, que devuelve
el número de bytes leídos del fichero
y almacenados en el búfer.
-
Algunas llamadas al sistema usan tipos de datos específicos y
que se salen de los habituales. Como ejemplo, tomemos la llamada
read
de nuevo:
ssize_t read(int fildes, void *buffer, size_t nbyte);
Si leemos la página de manual de read
, veremos que
la función devuelve el -1 si hubo un error u otro entero mayor
o igual que 0 si no lo hubo y que especifica el número de
caracteres que se han devuelto en el búfer. ¿No es esto,
al fin y al cabo, un entero? ¿Por qué el tipo
devuelto por la función es
ssize_t
? ¿Qué tipo de dato se
esconde detrás de él?
Para responder a estas preguntas, tendremos que ir a los ficheros
de cabecera de read
. Partiendo de
/usr/include/unistd.h
, llegamos a
/usr/include/sys/types.h
, fichero donde se
se encuentran definidos muchos tipos de dato especiales. En
ese fichero se puede leer:
[...]
# ifndef _SSIZE_T
# define _SSIZE_T
typedef long ssize_t;
# endif /* _SSIZE_T */
[...]
Así que ssize_t
no es más que
un long
.
¿Por qué, entonces, no se define read
de modo que
devuelva un long
? Alguna razón:
- Por si la definición de lo que devuelve
la función depende
del estándar de UNIX que estemos usando. Ver
el tercer LPE de la sexta
sesión.
- Por si se amplía en un futuro el sistema y, debido a su
mayor capacidad, hay que ampliar el tipo de dato que se
le pasa a la llamada.
- Para mejor especificar cuál es el contenido de la
variable.
-
Algunas llamadas al sistema, como es el caso de
open
,
tienen muchas opciones no excluyentes de funcionamiento.
open
sirve para abrir un fichero. Pero ese fichero
se puede abrir para lectura, para escritura, para añadir, para
borrarlo y reescribirlo, etc. y estas opciones pueden no ser
excluyentes, es decir, puedo especificar más de una a la vez.
Las llamadas al sistema de este tipo suelen especificar las
opciones que se desean mediante un campo de bits. Veamos
el prototipo de open:
int open(const char *path, int oflag, ... /* (mode_t mode) */ );
Los puntos suspensivos y los paréntesis indican que
el último
parámetro es opcional. En el segundo parámetro
(oflag
), es donde se van a dar las opciones de
apertura del fichero. Se trata de un entero que guarda en su
interior un campo de bits. Un campo de bits es un entero que
expone su significado cuando se expresa en binario. Cada dígito
binario del campo expresará la presencia (1) o ausencia (0) de
una propiedad. En el caso del campo oflag
, el
significado de cada bit es:

Por ejemplo, si queremos abrir el fichero jarabes.txt
para lectura y escritura y para añadir a lo que ya contiene,
el campo de bits quedaría así:

Esto nos da el número binario 1010, que en decimal es el 10. Por
lo tanto, la orden open
que habría
que dar sería:
open("jarabes.txt",10);
Como este modo de trabajar es bastante complicado y el resultado
resulta un poco críptico, en los ficheros de cabecera del
compilador de C existen unas macros para ayudar en la
especificación de las opciones de open
. Algunas de
estas macros y su valor en binario se dan en esta tabla:
Macro |
Valor (binario) |
O_RDONLY |
0 |
O_WRONLY |
1 |
O_RDWR |
10 |
O_APPEND |
1000 |
O_CREAT |
100000000 |
O_TRUNC |
1000000000 |
O_EXCL |
10000000000 |
O_NONBLOCK |
100000000000000 |
Para poder especificar varias de estas macros en la llamada
al sistema, se usa el operador OR
entre ambas.
Veamos que funciona con el ejemplo anterior:
Por lo tanto, la orden anterior también se puede escribir en
C como:
open("jarabes.txt", O_RDWR | O_APPEND);
Tabla de ficheros.
Todos los procesos (programas en ejecución) de UNIX tienen una
tabla con la información de los ficheros que tienen abiertos.
Cuando el programa abre un fichero nuevo, el Sistema Operativo
localiza una entrada libre de la tabla y deposita ahí la
información del nuevo fichero. Al índice de la tabla
donde se
almacena la información se le denomina descriptor de
fichero. Es el número que devuelve la llamada al sistema
open
y que servirá para referirnos al fichero
en todas las operaciones que realicemos con él.
Hay tres descriptores de fichero siempre abiertos en un programa
en C al comienzo de su ejecución: el 0, el 1 y el 2. Corresponden
a la entrada estándar, la salida estándar y el
error estándar. Estarán, en condiciones normales
conectados con el teclado, la pantalla y la pantalla,
respectivamente:
Para cambiar este comportamiento por defecto,
existen los
caracteres de redirección de la shell.
Así, para que la salida
estándar de la orden
ls -l
vaya al fichero
listado.txt
en lugar de a la pantalla, hay que
teclear:
ls -l > listado.txt
Se pueden encadenar
órdenes de modo que la salida estándar de uno vaya a
la entrada
estándar del siguiente mediante el carácter
de tubería de la shell
(
|
). Así, si queremos que la salida de la orden
ls -l
efectúe una pausa cada vez que pasa
de página
podemos combinarlo con la orden
more
así:
ls -l | more
Ejemplo: escritura de "Hola mundo" en un fichero mediante
llamadas al sistema.
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
[...]
int fd;
char mensaje[]="Hola, mundo\n";
[...]
fd=open("hola.txt",O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd==-1) /* Error */ ...
[...]
if (write(fd,mensaje,strlen(mensaje))==-1) /* Error */ ...
[...]
close(fd);
[...]
-
Todos los ficheros abiertos tienen un indicador para saber por
dónde se va en su lectura o escritura. Es el puntero de acceso
al fichero. El tipo de este puntero es
off_t
, pero
en realidad en un número entero. Este número
indica cuál es
la próxima posición del fichero que se va a leer o escribir.
La primera posición del fichero tiene el valor 0, la última
posición del fichero tiene un valor igual al tamaño
del fichero.
Para poder saber o modificar la posición actual de un fichero,
se usa la llamada al sistema lseek
, cuyo prototipo
es:
off_t lseek(int fildes, off_t desplazamiento, int dOnde);
Si queremos cambiar la posición del puntero de acceso,
realizamos la llamada al sistema con el descriptor del fichero
cuyo puntero queremos modificar. Para modificar el puntero
se indica un desplazamiento (segundo parámetro) tanto positivo
como negativo, con respecto a uno de tres puntos de referencia
(tercer parámetro):
Tercer parámetro |
Punto de referencia |
SEEK_SET |
Comienzo del fichero |
SEEK_CUR |
Punto actual |
SEEK_END |
Fin del fichero |
Algunos ejemplos:
- Situar el puntero en la posicion 1000:
lseek(fd,1000,SEEK_SET);
- Dejar el puntero como está:
lseek(fd,0,SEEK_CUR);
- Hacer retroceder el puntero tres posiciones:
lseek(fd,-3,SEEK_CUR);
- Situar el puntero al final del fichero:
lseek(fd,0,SEEK_END);
Para saber la posición del puntero, basta con saber que la
función devuelve la posición del puntero
después de haberlo
desplazado. Con esto podemos además saber la longitud de un
fichero.
Ejercicio fácil
Haced un pequeño programa que, solamente con ayuda de
llamadas al sistema, imprima por la pantalla el tamaño
en bytes del fichero cuyo nombre le pasáis en su
único argumento. Comprobad que el resultado es el correcto
con ls -l
Bloqueo de ficheros
Bloquear el acceso a parte de un fichero consiste en evitar
que otro proceso pueda acceder a esa parte del fichero mientras
lo estemos usando. Para hacerlo, se ha de usar la llamada
al sistema(*):
#include <unistd.h>
int lockf(int fildes, int funciOn, off_t tamaNo);
El primer parámetro es un descriptor del fichero que queremos
bloquear. funciOn
puede ser F_LOCK
o F_ULOCK
para bloquear o desbloquear el fichero
respectivamente. El bloqueo se produce desde la posición actual
del puntero del fichero abarcando una longitud expresada por el
parámetro tamaNo
. Si tamaNo
es cero,
se bloquea/desbloquea hasta el final del fichero. También existe
la posibilidad de consultar si tenemos acceso, sin bloquearse
o bloqueándose si no tenemos acceso.
Podéis consultar el manual.
Supongamos que queremos bloquear el acceso a un fichero mientras
escribimos. La secuencia sería: abrir el fichero, bloquearlo
con lockf(fd,F_LOCK,0)
, escribir, mover el puntero
al principio con lseek
y desbloquearlo con
lockf(fd,F_ULOCK,0)
. Fijaos bien en que hay que
mover el puntero de nuevo al principio del fichero, pues se
ha movido al realizar la escritura.
Considerad también que estos bloqueos son bloqueos
colaborativos. Es decir, si un proceso bloquea parte de un
fichero, esto no impide que otro proceso abra
el fichero, lea o escriba en dicha parte del fichero.
Lo único que impide es que otro proceso pueda bloquear esa
misma parte del fichero. Tenedlo en cuenta.
___________________
(*) Técnicamente, lockf
es una función de biblioteca construida generalmente sobre
la llamada al sistema fcntl
, pero la tratamos como llamada al sistema
aquí pues esta última tiene varias funciones adicionales
y una interfaz más complicada.
Primera práctica.
Esta es una práctica que no hay que entregar. El programa
admitirá el nombre de un fichero como argumento. Abrirá el
fichero, si puede, e irá imprimiendo por la salida estándar
dicho fichero cambiando todas las 'a' por 'o'. Haced una
comprobación no paranoica de errores (basta con usar bien
perror
). Solamente usad llamadas
al sistema (no usar printf
ni fopen
)
en la parte principal de la práctica.
Soluciones y comentarios.
-
Un fichero proyectado en memoria es, como su nombre indica,
un fichero cuyo contenido aparece momentáneamente en una zona
de memoria principal, de modo que podemos allí modificarlo.
La llamada al sistema que permite hacer esto es:
void *mmap(void *addr, size_t longitud, int prot, int flags, int fildes, off_t desplazamiento);
Lo mejor es dejar que el sistema operativo decida
en qué posición
de memoria virtual va a realizar la proyección indicando 0 como
primer parámetro. La posición donde se
realizará la proyección nos será devuelta por la
función, si la llamada tiene éxito.
El segundo parámetro es el tamaño de la zona que se va a
proyectar. El tercero, indica el tipo de acceso que se va a
realizar, que tiene que ser concordante con el modo en que se
abrió el fichero. Véase
el resumen
de las llamadas de esta sesión.
El cuarto, dejadlo siempre a MAP_SHARED
. El quinto
es un descriptor del fichero que queremos proyectar (lo
conseguís con open
. El último es el
desplazamiento,
con respecto al comienzo del fichero, donde empieza la zona que
queréis proyectar.
No hay que olvidar deshacer la proyección con munmap
y cerrar el fichero cuando se acabe.
Segunda práctica.
Esta segunda práctica consiste en un programa que admite el
nombre de un fichero por la línea de órdenes de la shell.
Esta práctica no es para entregar.
Efectúa una proyección en memoria de dicho fichero
y sustituye
todas sus 'a' por 'o'. Deshace la proyección y cierra el fichero.
Comprobación de errores no paranoica.
¿Qué habría que usar para
hacer esto si no se dispusiera de ficheros proyectados en memoria
en el sistema operativo?
¿Qué ventajas suponen estos ficheros?
Soluciones y comentarios.
Devolución de datos en variables y estructuras.
Algunas llamadas al sistema devuelven información en estructuras
de C. Es el caso de la familia de llamadas stat
, que
dan información acerca de los ficheros. El prototipo de
stat
es el siguiente:
int stat(const char *path, struct stat *buf);
Como ya dijimos en el apartado de búferes,
la función requiere
un puntero a una zona de memoria previamente reservada. Los pasos
que hay que seguir para obtener datos de la llamada al sistema
son:
- Declarar una variable del tipo de la estructura que
requiere la función.
Esto reservará automáticamente el
espacio de memoria para almacenar los valores de la
estructura.
struct stat ss;
- Pasarle a la función la dirección de
memoria que se ha
reservado para los datos de la estructura mediante el
operador
&
de C:
stat("MiKasa.txt",&ss);
- Acceder a los datos de la estructura. Por ejemplo:
printf("La longitud de \"MiKasa.txt\" es %ld.\n", ss.st_size);
Es completamente incorrecto lo siguiente y se producirá
un acceso a memoria ilegal en tiempo de ejecución, aunque el
compilador no proteste en tiempo de compilación:
struct stat *ssp;
[...]
stat("MiKasa.txt",ssp);
printf("La longitud de \"MiKasa.txt\" es %ld.\n", ssp->st_size);
Todo lo dicho también es de aplicación al caso de variables
normales. El paso por referencia en C, ya que no existe
per se, se consigue pasando un puntero por valor que
apunte a la zona de la memoria donde se almacenan los datos
de la variable.
Getdirentries (obtención de los
ficheros contenidos en un subdirectorio).

Muchos programas pueden requerir saber el nombre de los ficheros
que hay en un subdirectorio. Para ello, existe en HPUX la llamada
al sistema getdirentries
. En Linux, esta llamada
no es directamente compatible.
El funcionamiento de la llamada es un tanto
complejo y de bajo nivel, fundamentalmente debido a que
el tamaño de los
subdirectorio puede ser arbitrariamente grande y puede ser
necesario llamarla en varias ocasiones hasta conseguir el nombre
de todos los ficheros del subdirectorio.
Para conseguir el nombre y número de inodo de los ficheros de
un directorio hay que llamar repetidamente a
getdirentries
hasta que la función nos devuelva
un 0, con lo que indica que ya nos ha pasado el nombre de todos
los ficheros del directorio (puede también devolver -1, lo que
indicaría un error).
Por cada vez que llamamos a getdirentries
, la
función nos devolverá información de unos
cuantos ficheros.
Observemos el prototipo de la función:
int getdirentries(int fildes, struct direct *buf, size_t nbytes, off_t *base_p);
Los parámetros de la llamada al sistema son:
fildes
será un descriptor de fichero del
subdirectorio que queremos examinar. Lo obtendremos con
open
. Lo abriremos como si se tratase de
un fichero normal, pues al fin y al cabo, un subdirectorio
es un fichero normal, con un formato y un contenido
específicos.
buf
, aunque parece una estructura donde la
función devolverá algo,
es una excepción a lo visto más
arriba. Se trata de un búfer de caracteres
cuyo tamaño
ha de ser un múltiplo del tamaño
del bloque del sistema
de ficheros. Esta información se puede obtener haciendo
un stat
sobre el directorio.
nbytes
es el tamaño
del búfer anterior, para
que getdirentries
no se pase escribiendo
fuera del área reservada para dicho búfer.
- En
base_p
, la función devolverá
la posición
del puntero de fichero del directorio después de la
llamada. Es un valor no muy relevante,
pero aún así hay
que proporcionar una variable de tipo off_t
donde almacenar dicho valor.
Veamos con un ejemplo cuál es la información
que aparece en el
búffer después de la llamada para
el directorio siguiente (la
función nos devolvería 29):
<T>/usuarios/gyermo/PRIVADO/SO/PRACTS/CANCIÓN$ ls -ia | cat
360000 ..
158446 .
158447 Enrique
158448 Ana

Como consejo práctico, para obtener la información
que contiene
el buffer
, que habremos declarado como
char *
, podemos usar un puntero auxiliar de tipo
struct direct
. Con ese puntero auxiliar podemos
acceder una a una a cada entrada de directorio del búfer. Para
incrementarlo y pasar a la siguiente, tenemos que fijarnos en
el campo d_reclen
, que viene expresado en bytes.
Hay que tener cuidado en no incrementar el puntero auxiliar
directamente, sino
hacer una conversión a char *
antes, para que los
incrementos del puntero vengan expresados en bytes y no en
unidades del tamaño de la estructura.
La línea de código que pasa a la siguiente entrada
sería algo así:
struct direct *sdp;
[...]
sdp=(struct direct *) ((char *)sdp+sdp->d_reclen);
Se debe continuar leyendo del búfer hasta que se supere el
tamaño de lo escrito por getdirentries
que es
el valor que nos devolvió la función.

La llamada al sistema equivalente en Solaris a
getdirentries
se llama getdents
.
Su prototipo es:
#include <sys/dirent.h>
int getdents(int fildes, struct dirent *buf, size_t nbyte);
Es completamente igual a su hermana de HPUX salvo que la
función carace del último parámetro que,
por otra parte,
tampoco es que sirviera para mucho.
También devuelve los mismos valores que
getdirentries
y con idéntico significado. La
estructura struct dirent
difiere, por otro lado,
ligeramente de struct direct
:
struct dirent
{ino_t d_ino;
off_t d_off;
unsigned short d_reclen;
char d_name[1];
};
d_ino
equivale a d_fileno
. d_reclen
y d_name
se llaman igual en las dos.
Y d_off
tiene un significado no
especificado que hay que respetar. El tamaño máximo de un nombre de
fichero es MAXNAMLEN
caracteres.

En el caso del Linux que tenéis en clase, la llamada al sistema
también es getdents
. Podéis consultar su
página de manual con man 2 getdents
. Lo curioso
del asunto es que en la presente versión de Linux no basta
con incluir los ficheros de cabecera y hacer la llamada al sistema
dentro de vuestro código. Hay que efectuar la llamada al
sistema a través de la interfaz syscall
, como
se muestra en el código de ejemplo que sigue:
#include <fcntl.h> /* open */
#include <sys/stat.h> /* open */
#include <sys/types.h> /* open */
#include <sys/syscall.h> /* syscall */
#include <unistd.h> /* syscall,getdents */
#include <stdlib.h> /* malloc */
#include <stdio.h> /* printf */
#include <linux/types.h> /* getdents /*
#include <linux/dirent.h> /* getdents /*
#include <linux/unistd.h> /* getdents /*
/* Macro para que syscall "parezca" getdents. */
/* NOtese que __NR_getdents es: */
/* subrayado, subrayado, NR, subrayado, getdents */
#define getdents(a,b,c) syscall(__NR_getdents,a,b,c)
int main(void)
{int fd; int ret; struct dirent *sdp;
sdp=malloc(2048); /* En realidad un mUltiplo del bloque */
fd=open(".",O_RDONLY);
if (fd==-1)
{perror("open"); return 1;}
ret=getdents(fd,sdp,2048);
if (ret==-1)
{perror("getdents"); return 1;}
printf("%d %d %d %s\n",sdp->d_ino,sdp->d_off,sdp->d_reclen,sdp->d_name);
return 0;
}
En el código se ha definido la macro getdents
para
que podáis usar la llamada como si realmente estuviera definida
en vuestro código. Es posible que esta situación se corrija
en otras distribuciones o en el futuro. Cualquier otra llamada
al sistema puede ser invocada con la interfaz syscall
,
a bajo nivel.
Tercera práctica.

Esta es una práctica que no hay que entregar.
Esta práctica vale doble en cuanto al mejor que la entregue.
El programa que hay que realizar no admite ningún argumento
por la línea de órdenes de la shell.
El programa listará el nombre de todos los
ficheros del directorio actual que no posean más de un enlace
duro. Hacer una comprobación no paranoica de errores.
Soluciones y comentarios.

El programa que hay que realizar no admite ningun argumento
por la línea de órdenes de la shell.
El programa listará el
nombre de todos los ficheros del directorio actual en los
que haya trascurrido más de una semana desde que fueron
modificados la última vez hasta que fueron accedidos por
última vez. Hacer una comprobación no paranoica de
errores.
Soluciones y comentarios.
Órdenes de la shell relacionadas.
cat
- escribe en la salida estándar todo lo que le llegue
por la entrada estándar
mv
- mueve o cambia el nombre de un fichero o directorio
ls
- da información de los ficheros dentro de un directorio
mkdir
- crea un directorio nuevo
rmdir
- borra un directorio vacío
cd
- cambia el directorio de trabajo actual
Funciones de biblioteca relacionadas.
malloc
- reserva un área de memoria dinámica
free
- libera un área de memoria dinámica
fopen
, fread
,
fwrite
, fclose
,
fprintf
, fscanf
,
fgets
, rewind
,
fseek
, ftell
.
- funciones de alto nivel de manejo de ficheros
opendir
, readdir
,
telldir
, seekdir
,
rewinddir
, closedir
- funciones de alto nivel para ver el contenido
de los directorios.
LPEs.
- ¿Qué falla en el siguiente código?
read
sólo me lee 4 caracteres:
char *buffer;
[...]
buffer=malloc(256);
[...]
read(0,buffer,sizeof(buffer));
Pista: El tamaño es lo importante.
Solución.
- El compilador no encuentra el fichero de cabecera
ndir.h
:
<Tejo>/home/so/PRACTS$ c89 prueba.c -o prueba
cpp: "prueba.c", line 1: error 4036: Can't open include file 'ndir.h'.
Pista: No hay que buscar tres pies al gato.
Solución.
- Hay que tener cuidado en Solaris, pues en su configuración
por defecto, en la página de manual tanto
mmap
como munmap
pone que admiten punteros
comodín (void *
).
Sin embargo, en la práctica,
exigen char *
. No tiene mayor problema. Haced
un casting para evitar el warning del
compilador para solucionarlo.