Programación en C

Leer Syscall Linux

Leer Syscall Linux
Entonces necesitas leer datos binarios? Es posible que desee leer desde un FIFO o socket? Verá, puede usar la función de biblioteca estándar de C, pero al hacerlo, no se beneficiará de las características especiales proporcionadas por Linux Kernel y POSIX. Por ejemplo, es posible que desee utilizar tiempos de espera para leer en un momento determinado sin recurrir al sondeo. Además, es posible que deba leer algo sin importarle si se trata de un archivo o socket especial o cualquier otra cosa. Su única tarea es leer algunos contenidos binarios y obtenerlos en su aplicación. Ahí es donde brilla la lectura del syscall.

Leer un archivo normal con una llamada al sistema de Linux

La mejor forma de empezar a trabajar con esta función es leyendo un archivo normal. Esta es la forma más sencilla de usar esa llamada al sistema, y ​​por una razón: no tiene tantas restricciones como otros tipos de flujo o tubería. Si lo piensa, eso es lógico, cuando lee la salida de otra aplicación, necesita tener una salida lista antes de leerla, por lo que deberá esperar a que esta aplicación escriba esta salida.

Primero, una diferencia clave con la biblioteca estándar: no hay almacenamiento en búfer en absoluto. Cada vez que llame a la función de lectura, llamará al kernel de Linux, por lo que esto llevará tiempo: es casi instantáneo si lo llama una vez, pero puede ralentizarlo si lo llama miles de veces en un segundo. En comparación, la biblioteca estándar almacenará la entrada por usted. Entonces, siempre que llame a read, debería leer más de unos pocos bytes, sino un gran búfer como unos pocos kilobytes - excepto si lo que necesita son realmente pocos bytes, por ejemplo, si comprueba si existe un archivo y no está vacío.

Sin embargo, esto tiene una ventaja: cada vez que llama a read, está seguro de obtener los datos actualizados, si alguna otra aplicación modifica actualmente el archivo. Esto es especialmente útil para archivos especiales como los de / proc o / sys.

Es hora de mostrarte un ejemplo real. Este programa C comprueba si el archivo es PNG o no. Para hacerlo, lee el archivo especificado en la ruta que proporciona en el argumento de la línea de comando y verifica si los primeros 8 bytes corresponden a un encabezado PNG.

Aquí está el código:

#incluir
#incluir
#incluir
#incluir
#incluir
#incluir
#incluir
 
typedef enum
IS_PNG,
DEMASIADO CORTO,
INVALID_HEADER
pngStatus_t;
 
unsigned int isSyscallSuccessful (const ssize_t readStatus)
return readStatus> = 0;
 

 
/ *
* checkPngHeader está comprobando si la matriz pngFileHeader corresponde a un PNG
* encabezado de archivo.
*
* Actualmente solo verifica los primeros 8 bytes de la matriz. Si la matriz es menor
* de 8 bytes, se devuelve TOO_SHORT.
*
* pngFileHeaderLength debe estar en el kength of tye array. Cualquier valor inválido
* puede dar lugar a un comportamiento indefinido, como el bloqueo de la aplicación.
*
* Devuelve IS_PNG si corresponde a un encabezado de archivo PNG. Si hay al menos
* 8 bytes en la matriz pero no es un encabezado PNG, se devuelve INVALID_HEADER.
*
* /
pngStatus_t checkPngHeader (const unsigned char * const pngFileHeader,
size_t pngFileHeaderLength) carácter constante sin firmar esperadoPngHeader [8] =
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A;
int i = 0;
 
if (pngFileHeaderLength < sizeof(expectedPngHeader))
return TOO_SHORT;
 

 
para (i = 0; i < sizeof(expectedPngHeader); i++)
if (pngFileHeader [i] !=pectedPngHeader [i])
return INVALID_HEADER;
 


 
/ * Si llega aquí, los primeros 8 bytes se ajustan a un encabezado PNG. * /
return IS_PNG;

 
int main (int argumentoLength, char * argumentList [])
char * pngFileName = NULL;
unsigned char pngFileHeader [8] = 0;
 
ssize_t readStatus = 0;
/ * Linux usa un número para identificar un archivo abierto. * /
int pngFile = 0;
pngStatus_t pngCheckResult;
 
si (argumentoLongitud != 2)
fputs ("Debe llamar a este programa usando isPng su nombre de archivo.\ n ", stderr);
return EXIT_FAILURE;
 

 
pngFileName = listadeargumentos [1];
pngFile = open (pngFileName, O_RDONLY);
 
if (pngFile == -1)
perror ("Error al abrir el archivo proporcionado");
return EXIT_FAILURE;
 

 
/ * Leer algunos bytes para identificar si el archivo es PNG. * /
readStatus = read (pngFile, pngFileHeader, sizeof (pngFileHeader));
 
if (isSyscallSuccessful (readStatus))
/ * Verifique si el archivo es PNG ya que obtuvo los datos. * /
pngCheckResult = checkPngHeader (pngFileHeader, readStatus);
 
if (pngCheckResult == TOO_SHORT)
printf ("El archivo% s no es un archivo PNG: es demasiado corto.\ n ", pngFileName);
 
más si (pngCheckResult == IS_PNG)
printf ("El archivo% s es un archivo PNG!\ n ", pngFileName);
 
demás
printf ("El archivo% s no está en formato PNG.\ n ", pngFileName);
 

 
demás
perror ("Error al leer el archivo");
return EXIT_FAILURE;
 

 
/ * Cerrar el archivo… * /
if (close (pngFile) == -1)
perror ("Error al cerrar el archivo proporcionado");
return EXIT_FAILURE;
 

 
pngFile = 0;
 
return EXIT_SUCCESS;
 

Mira, es un ejemplo completo, funcional y compilable. No dudes en compilarlo tú mismo y probarlo, realmente funciona. Deberías llamar al programa desde una terminal como esta:

./ isPng su nombre de archivo

Ahora, centrémonos en la llamada de lectura en sí:

pngFile = open (pngFileName, O_RDONLY);
if (pngFile == -1)
perror ("Error al abrir el archivo proporcionado");
return EXIT_FAILURE;

/ * Leer algunos bytes para identificar si el archivo es PNG. * /
readStatus = read (pngFile, pngFileHeader, sizeof (pngFileHeader));

La firma de lectura es la siguiente (extraída de las páginas de manual de Linux):

ssize_t read (int fd, void * buf, size_t count);

Primero, el argumento fd representa el descriptor de archivo. He explicado un poco este concepto en mi artículo de fork.  Un descriptor de archivo es un int que representa un archivo abierto, socket, pipe, FIFO, dispositivo, bueno, hay muchas cosas donde los datos se pueden leer o escribir, generalmente de una manera similar a una secuencia. Voy a profundizar más sobre eso en un artículo futuro.

La función abierta es una de las formas de decirle a Linux: quiero hacer cosas con el archivo en esa ruta, búsquelo donde está y déme acceso a él. Le devolverá este int llamado descriptor de archivo y ahora, si desea hacer algo con este archivo, use ese número. No olvide llamar al cierre cuando haya terminado con el archivo, como en el ejemplo.

Por lo tanto, debe proporcionar este número especial para leer. Luego está el argumento buf. Aquí debe proporcionar un puntero a la matriz donde read almacenará sus datos. Finalmente, el recuento es la cantidad de bytes que leerá como máximo.

El valor de retorno es de tipo ssize_t. Tipo raro, ¿no es así?? Significa "tamaño_t firmado", básicamente es un int largo. Devuelve el número de bytes que lee correctamente, o -1 si hay un problema. Puede encontrar la causa exacta del problema en la variable global errno creada por Linux, definida en . Pero para imprimir un mensaje de error, usar perror es mejor ya que imprime errno en su nombre.

En archivos normales, y solo en este caso, read devolverá menos de count solo si ha llegado al final del archivo. La matriz buf que proporcionas deber ser lo suficientemente grande como para caber al menos en los bytes de recuento, o su programa puede fallar o crear un error de seguridad.

Ahora, leer no solo es útil para archivos normales y si quieres sentir sus superpoderes - Sí, sé que no está en ningún cómic de Marvel, pero tiene verdaderos poderes - querrá usarlo con otras corrientes como tuberías o enchufes. Echemos un vistazo a eso:

Archivos especiales de Linux y llamada al sistema de lectura

El hecho de que read funcione con una variedad de archivos como tuberías, sockets, FIFO o dispositivos especiales como un disco o un puerto serie es lo que lo hace realmente más poderoso. Con algunas adaptaciones, puedes hacer cosas realmente interesantes. En primer lugar, esto significa que, literalmente, puede escribir funciones que trabajen en un archivo y usarlo con una tubería en su lugar. Es interesante pasar datos sin tocar el disco, lo que garantiza el mejor rendimiento.

Sin embargo, esto también desencadena reglas especiales. Tomemos el ejemplo de una lectura de una línea desde la terminal en comparación con un archivo normal. Cuando llama a leer en un archivo normal, solo necesita unos pocos milisegundos para Linux para obtener la cantidad de datos que solicita.

Pero cuando se trata de terminal, esa es otra historia: digamos que pides un nombre de usuario. El usuario está escribiendo en la terminal su nombre de usuario y presiona Enter. Ahora sigue mi consejo anterior y llamas a leer con un búfer grande como 256 bytes.

Si la lectura funcionaba como lo hizo con los archivos, esperaría a que el usuario escribiera 256 caracteres antes de regresar! Su usuario esperaría una eternidad y luego, lamentablemente, mataría su aplicación. Ciertamente no es lo que quieres y tendrías un gran problema.

De acuerdo, podría leer un byte a la vez, pero esta solución es terriblemente ineficaz, como le dije anteriormente. Debe funcionar mejor que eso.

Pero los desarrolladores de Linux pensaron leer de manera diferente para evitar este problema:

  • Cuando lee archivos normales, intenta tanto como sea posible leer los bytes de recuento y obtendrá bytes del disco de forma activa si es necesario.
  • Para todos los demás tipos de archivos, devolverá tan pronto como hay algunos datos disponibles y a lo sumo contar bytes:
    1. Para terminales, es generalmente cuando el usuario presiona la tecla Enter.
    2. Para los sockets TCP, es tan pronto como su computadora recibe algo, no importa la cantidad de bytes que obtenga.
    3. Para FIFO o tuberías, generalmente es la misma cantidad que la que escribió la otra aplicación, pero el kernel de Linux puede entregar menos a la vez si eso es más conveniente.

Para que pueda llamar de forma segura con su búfer de 2 KiB sin permanecer bloqueado para siempre. Tenga en cuenta que también puede interrumpirse si la aplicación recibe una señal. Como la lectura de todas estas fuentes puede llevar segundos o incluso horas - hasta que el otro lado decida escribir, después de todo - ser interrumpido por señales permite dejar de permanecer bloqueado durante demasiado tiempo.

Sin embargo, esto también tiene un inconveniente: cuando desee leer exactamente 2 KiB con estos archivos especiales, deberá verificar el valor de retorno de lectura y llamar a read varias veces. la lectura rara vez llenará todo el búfer. Si su aplicación usa señales, también deberá verificar si la lectura falló con -1 porque fue interrumpida por una señal, usando errno.

Déjame mostrarte cómo puede ser interesante usar esta propiedad especial de read:

#define _POSIX_C_SOURCE 1 / * sigaction no está disponible sin este #define. * /
#incluir
#incluir
#incluir
#incluir
#incluir
#incluir
/ *
* isSignal indica si la llamada al sistema de lectura ha sido interrumpida por una señal.
*
* Devuelve VERDADERO si la llamada al sistema de lectura ha sido interrumpida por una señal.
*
* Variables globales: lee errno definido en errno.h
* /
unsigned int isSignal (const ssize_t readStatus)
return (readStatus == -1 && errno == EINTR);

unsigned int isSyscallSuccessful (const ssize_t readStatus)
return readStatus> = 0;

/ *
* shouldRestartRead indica cuando la llamada al sistema de lectura ha sido interrumpida por un
* señal de evento o no, y dado que esta razón de "error" es transitoria, podemos
* reiniciar de forma segura la llamada de lectura.
*
* Actualmente, solo verifica si la lectura ha sido interrumpida por una señal, pero
* podría mejorarse para comprobar si se leyó el número de bytes de destino y si es
* no es el caso, devuelva VERDADERO para leer de nuevo.
*
* /
unsigned int shouldRestartRead (const ssize_t readStatus)
return isSignal (readStatus);

/ *
* Necesitamos un controlador vacío ya que la llamada al sistema de lectura se interrumpirá solo si el
* se maneja la señal.
* /
void emptyHandler (int ignorado)
regreso;

int main ()
/ * Es en segundos. * /
const int alarmInterval = 5;
const struct sigaction emptySigaction = emptyHandler;
char lineBuf [256] = 0;
ssize_t readStatus = 0;
unsigned int waitTime = 0;
/ * No modifique sigaction excepto si sabe exactamente lo que está haciendo. * /
sigaction (SIGALRM, & emptySigaction, NULL);
alarma (alarmInterval);
fputs ("Su texto: \ n", stderr);
hacer
/ * No olvides el '\ 0' * /
readStatus = read (STDIN_FILENO, lineBuf, sizeof (lineBuf) - 1);
if (isSignal (readStatus))
waitTime + = alarmInterval;
alarma (alarmInterval);
fprintf (stderr, "% u segundos de inactividad ... \ n", tiempo de espera);

while (shouldRestartRead (readStatus));
if (isSyscallSuccessful (readStatus))
/ * Termina la cadena para evitar un error al proporcionarla a fprintf. * /
lineBuf [readStatus] = '\ 0';
fprintf (stderr, "Escribiste% lu caracteres. Aquí está su cadena: \ n% s \ n ", strlen (lineBuf),
lineBuf);
demás
perror ("Error al leer desde stdin");
return EXIT_FAILURE;

return EXIT_SUCCESS;

Una vez más, esta es una aplicación C completa que puede compilar y ejecutar.

Hace lo siguiente: lee una línea de la entrada estándar. Sin embargo, cada 5 segundos, imprime una línea que le dice al usuario que aún no se ha proporcionado ninguna entrada.

Ejemplo si espero 23 segundos antes de escribir "Penguin":

$ alarm_read
Tu texto:
5 segundos de inactividad ..
10 segundos de inactividad ..
15 segundos de inactividad ..
20 segundos de inactividad ..
Pingüino
Escribiste 8 caracteres. Aquí está tu cadena:
Pingüino

Eso es increíblemente útil. Se puede usar para actualizar a menudo la interfaz de usuario para imprimir el progreso de la lectura o del procesamiento de su aplicación que está haciendo. También se puede utilizar como mecanismo de tiempo de espera. También puede ser interrumpido por cualquier otra señal que pueda ser útil para su aplicación. De todos modos, esto significa que su aplicación ahora puede responder en lugar de quedarse atascada para siempre.

Entonces, los beneficios superan el inconveniente descrito anteriormente. Si se pregunta si debería admitir archivos especiales en una aplicación que normalmente trabaja con archivos normales - y así llamando leer en un bucle - Yo diría que lo haga, excepto si tiene prisa, mi experiencia personal a menudo demuestra que reemplazar un archivo con una tubería o FIFO puede literalmente hacer que una aplicación sea mucho más útil con pequeños esfuerzos. Incluso hay funciones C prefabricadas en Internet que implementan ese bucle para usted: se llama funciones de lectura.

Conclusión

Como puede ver, fread y read pueden parecer similares, no son. Y con solo unos pocos cambios en cómo funciona la lectura para el desarrollador de C, la lectura es mucho más interesante para diseñar nuevas soluciones a los problemas que encuentra durante el desarrollo de la aplicación.

La próxima vez, te diré cómo funciona write syscall, ya que leer es genial, pero poder hacer ambas cosas es mucho mejor. Mientras tanto, experimenta con read, conócelo y te deseo un feliz año nuevo!

Cómo invertir la dirección de desplazamiento del mouse y los paneles táctiles en Windows 10
Ratón y Panel táctils no solo facilitan la informática, sino que también hacen que sea más eficiente y requieran menos tiempo. No podemos imaginar una...
Cómo cambiar el tamaño, el color y el esquema del puntero del mouse y del cursor en Windows 10
El puntero y el cursor del mouse en Windows 10 son aspectos muy importantes del sistema operativo. Esto también se puede decir de otros sistemas opera...
Motores de juegos gratuitos y de código abierto para desarrollar juegos de Linux
Este artículo cubrirá una lista de motores de juegos de código abierto y gratuitos que se pueden usar para desarrollar juegos 2D y 3D en Linux. Existe...