Tutorial de llamadas al sistema Linux con C

Linux System Call Tutorial With C



En nuestro último artículo sobre Llamadas al sistema Linux , Definí una llamada al sistema, discutí las razones por las que uno podría usarlas en un programa y profundicé en sus ventajas y desventajas. Incluso di un breve ejemplo en asamblea dentro de C. Ilustraba el punto y describía cómo hacer la llamada, pero no hizo nada productivo. No es exactamente un ejercicio de desarrollo emocionante, pero ilustró el punto.

En este artículo, usaremos llamadas al sistema reales para hacer un trabajo real en nuestro programa C. Primero, revisaremos si necesita usar una llamada al sistema, luego proporcionaremos un ejemplo usando la llamada sendfile () que puede mejorar drásticamente el rendimiento de la copia de archivos. Finalmente, repasaremos algunos puntos para recordar al usar llamadas al sistema Linux.







Si bien es inevitable que use una llamada al sistema en algún momento de su carrera de desarrollo en C, a menos que esté apuntando a un alto rendimiento o una funcionalidad de tipo particular, la biblioteca glibc y otras bibliotecas básicas incluidas en las principales distribuciones de Linux se encargarán de la mayoría de tus necesidades.



La biblioteca estándar glibc proporciona un marco multiplataforma bien probado para ejecutar funciones que de otro modo requerirían llamadas al sistema específicas del sistema. Por ejemplo, puede leer un archivo con fscanf (), fread (), getc (), etc., o puede usar la llamada al sistema de Linux read (). Las funciones de glibc proporcionan más funciones (es decir, mejor manejo de errores, E / S formateado, etc.) y funcionarán en cualquier sistema que admita glibc.



Por otro lado, hay momentos en los que el rendimiento sin concesiones y la ejecución exacta son fundamentales. La envoltura que proporciona fread () agregará una sobrecarga y, aunque es menor, no es completamente transparente. Además, es posible que no desee o necesite las funciones adicionales que proporciona el contenedor. En ese caso, lo mejor es una llamada al sistema.





También puede utilizar llamadas al sistema para realizar funciones que aún no son compatibles con glibc. Si su copia de glibc está actualizada, esto difícilmente será un problema, pero desarrollar en distribuciones más antiguas con kernels más nuevos puede requerir esta técnica.

Ahora que ha leído las renuncias de responsabilidad, las advertencias y los posibles desvíos, analicemos algunos ejemplos prácticos.



¿En qué CPU estamos?

Una pregunta que la mayoría de los programas probablemente no piensan hacer, pero válida de todos modos. Este es un ejemplo de una llamada al sistema que no se puede duplicar con glibc y no se cubre con un contenedor glibc. En este código, llamaremos a la llamada getcpu () directamente a través de la función syscall (). La función syscall funciona de la siguiente manera:

syscall(SYS_call,arg1,arg2,);

El primer argumento, SYS_call, es una definición que representa el número de la llamada al sistema. Cuando incluye sys / syscall.h, estos se incluyen. La primera parte es SYS_ y la segunda parte es el nombre de la llamada al sistema.

Los argumentos para la llamada van en arg1, arg2 arriba. Algunas llamadas requieren más argumentos y continuarán en orden desde su página de manual. Recuerde que la mayoría de los argumentos, especialmente para devoluciones, requerirán punteros a matrices de caracteres o memoria asignada a través de la función malloc.

ejemplo1.c

#incluir
#incluir
#incluir
#incluir

En tprincipal() {

no firmadoUPC,nodo;

// Obtener el núcleo de la CPU actual y el nodo NUMA a través de la llamada al sistema
// Tenga en cuenta que esto no tiene un contenedor glibc, por lo que debemos llamarlo directamente
syscall(SYS_getcpu, &UPC, &nodo,NULO);

// Mostrar información
printf ('Este programa se ejecuta en el núcleo de la CPU% u y el nodo NUMA% u. orte orte',UPC,nodo);

regreso 0;

}

Para compilar y ejecutar:

gcc ejemplo1.c -o ejemplo1
./Ejemplo 1

Para obtener resultados más interesantes, puede girar subprocesos a través de la biblioteca pthreads y luego llamar a esta función para ver en qué procesador se está ejecutando su subproceso.

Sendfile: rendimiento superior

Sendfile proporciona un excelente ejemplo de cómo mejorar el rendimiento a través de llamadas al sistema. La función sendfile () copia datos de un descriptor de archivo a otro. En lugar de utilizar múltiples funciones fread () y fwrite (), sendfile realiza la transferencia en el espacio del kernel, lo que reduce la sobrecarga y, por lo tanto, aumenta el rendimiento.

En este ejemplo, vamos a copiar 64 MB de datos de un archivo a otro. En una prueba, usaremos los métodos estándar de lectura / escritura en la biblioteca estándar. En el otro, usaremos llamadas al sistema y la llamada sendfile () para enviar estos datos de una ubicación a otra.

test1.c (glibc)

#incluir
#incluir
#incluir
#incluir

#define BUFFER_SIZE 67108864
#define BUFFER_1 'buffer1'
#define BUFFER_2 'buffer2'

En tprincipal() {

EXPEDIENTE*incorrecto, *fIn;

printf (' ortePrueba de E / S con funciones glibc tradicionales. orte orte');

// Coge un búfer BUFFER_SIZE.
// El búfer tendrá datos aleatorios, pero eso no nos importa.
printf ('Asignación de búfer de 64 MB:');
carbonizarse *buffer= (carbonizarse *) malloc (TAMAÑO DEL BÚFER);
printf ('HECHO orte');

// Escribe el búfer en fOut
printf ('Escribiendo datos en el primer búfer:');
incorrecto= fopen (BUFFER_1, 'wb');
escribir (buffer, tamaño de(carbonizarse),TAMAÑO DEL BÚFER,incorrecto);
fcerrar (incorrecto);
printf ('HECHO orte');

printf ('Copiando datos del primer archivo al segundo:');
fIn= fopen (BUFFER_1, 'rb');
incorrecto= fopen (BUFFER_2, 'wb');
fread (buffer, tamaño de(carbonizarse),TAMAÑO DEL BÚFER,fIn);
escribir (buffer, tamaño de(carbonizarse),TAMAÑO DEL BÚFER,incorrecto);
fcerrar (fIn);
fcerrar (incorrecto);
printf ('HECHO orte');

printf ('Liberando búfer:');
gratis (buffer);
printf ('HECHO orte');

printf ('Eliminando archivos:');
retirar (BUFFER_1);
retirar (BUFFER_2);
printf ('HECHO orte');

regreso 0;

}

test2.c (llamadas al sistema)

#incluir
#incluir
#incluir
#incluir
#incluir
#incluir
#incluir
#incluir
#incluir

#define BUFFER_SIZE 67108864

En tprincipal() {

En tincorrecto,fIn;

printf (' ortePrueba de E / S con sendfile () y llamadas al sistema relacionadas. orte orte');

// Coge un búfer BUFFER_SIZE.
// El búfer tendrá datos aleatorios, pero eso no nos importa.
printf ('Asignación de búfer de 64 MB:');
carbonizarse *buffer= (carbonizarse *) malloc (TAMAÑO DEL BÚFER);
printf ('HECHO orte');


// Escribe el búfer en fOut
printf ('Escribiendo datos en el primer búfer:');
incorrecto=abierto('buffer1',O_RDONLY);
escribir(incorrecto, &buffer,TAMAÑO DEL BÚFER);
cerrar(incorrecto);
printf ('HECHO orte');

printf ('Copiando datos del primer archivo al segundo:');
fIn=abierto('buffer1',O_RDONLY);
incorrecto=abierto('buffer2',O_RDONLY);
enviar archivo(incorrecto,fIn, 0,TAMAÑO DEL BÚFER);
cerrar(fIn);
cerrar(incorrecto);
printf ('HECHO orte');

printf ('Liberando búfer:');
gratis (buffer);
printf ('HECHO orte');

printf ('Eliminando archivos:');
desconectar('buffer1');
desconectar('buffer2');
printf ('HECHO orte');

regreso 0;

}

Compilación y ejecución de pruebas 1 y 2

Para crear estos ejemplos, necesitará las herramientas de desarrollo instaladas en su distribución. En Debian y Ubuntu, puede instalar esto con:

aptoInstalar en pcconstruir-esenciales

Luego compila con:

gcctest1.c-otest1&& gcctest2.c-otest2

Para ejecutar ambos y probar el rendimiento, ejecute:

tiempo./test1&& tiempo./test2

Debería obtener resultados como este:

Prueba de E / S con funciones glibc tradicionales.

Asignación de búfer de 64 MB: HECHO
Escribiendo datos en el primer búfer: HECHO
Copiando datos del primer archivo al segundo: HECHO
Liberación de búfer: HECHO
Eliminando archivos: HECHO
real 0m0.397s
usuario 0m0.000s
sys 0m0.203s
Prueba de E / S con sendfile () y llamadas al sistema relacionadas.
Asignación de búfer de 64 MB: HECHO
Escribiendo datos en el primer búfer: HECHO
Copiando datos del primer archivo al segundo: HECHO
Liberación de búfer: HECHO
Eliminando archivos: HECHO
0m0.019s reales
usuario 0m0.000s
sys 0m0.016s

Como puede ver, el código que usa las llamadas al sistema se ejecuta mucho más rápido que el equivalente de glibc.

Cosas para recordar

Las llamadas al sistema pueden aumentar el rendimiento y proporcionar funcionalidad adicional, pero no están exentas de desventajas. Tendrá que sopesar los beneficios que brindan las llamadas al sistema frente a la falta de portabilidad de la plataforma y, a veces, la funcionalidad reducida en comparación con las funciones de la biblioteca.

Cuando utilice algunas llamadas al sistema, debe tener cuidado de utilizar los recursos devueltos por las llamadas al sistema en lugar de las funciones de la biblioteca. Por ejemplo, la estructura FILE utilizada para las funciones fopen (), fread (), fwrite () y fclose () de glibc no es la misma que el número de descriptor de archivo de la llamada al sistema open () (devuelto como un número entero). Mezclar estos puede dar lugar a problemas.

En general, las llamadas al sistema Linux tienen menos carriles bumper que las funciones glibc. Si bien es cierto que las llamadas al sistema tienen algún manejo e informe de errores, obtendrá una funcionalidad más detallada de una función glibc.

Y finalmente, unas palabras sobre seguridad. Las llamadas al sistema interactúan directamente con el kernel. El kernel de Linux tiene amplias protecciones contra las travesuras de la tierra del usuario, pero existen errores no descubiertos. No confíe en que una llamada al sistema validará su entrada o lo aislará de los problemas de seguridad. Es aconsejable asegurarse de que los datos que entregue a una llamada al sistema estén desinfectados. Naturalmente, este es un buen consejo para cualquier llamada a la API, pero no puede tener cuidado al trabajar con el kernel.

Espero que haya disfrutado de esta inmersión más profunda en la tierra de las llamadas al sistema Linux. Para obtener una lista completa de llamadas al sistema Linux, consulte nuestra lista maestra.