compiler construction - una - ¿Cómo funcionan las llamadas al sistema?
miscelanea de llamadas al sistema (6)
Entiendo que un usuario puede poseer un proceso y que cada proceso tiene un espacio de direcciones (que contiene ubicaciones de memoria válidas, este proceso puede hacer referencia). Sé que un proceso puede invocar una llamada al sistema y pasarle parámetros, como cualquier otra función de la biblioteca. Esto parece sugerir que todas las llamadas al sistema están en un espacio de direcciones de proceso compartiendo memoria, etc. Pero tal vez, esto es solo una ilusión creada por el hecho de que en el lenguaje de programación de alto nivel, las llamadas al sistema se parecen a cualquier otra función, cuando un proceso lo llama
Pero, ahora déjame dar un paso más profundo y analizar más de cerca lo que sucede bajo el capó. ¿Cómo compila el compilador una llamada al sistema? Quizás empuja el nombre de la llamada del sistema y los parámetros suministrados por el proceso en una pila y luego pone la instrucción de ensamblaje como "TRAP" o algo así - básicamente, la instrucción de ensamblaje para llamar a una interrupción de software.
Esta instrucción de ensamblaje TRAP se ejecuta mediante hardware al alternar primero el bit de modo de usuario a kernel y luego establecer el puntero de código para indicar el comienzo de las rutinas de servicio de interrupción. A partir de este punto, el ISR se ejecuta en modo kernel, que recoge los parámetros de la pila (esto es posible porque kernel tiene acceso a cualquier ubicación de memoria, incluso los que pertenecen a procesos de usuario) y ejecuta la llamada al sistema y en end renuncia a la CPU, que alterna el bit de modo y el proceso de usuario comienza desde donde se detuvo.
Es mi entendimiento correcto?
Adjuntamos un diagrama aproximado de mi comprensión:
Los programas normales generalmente no "compilan llamadas de sistema". Para cada syscall generalmente se utiliza una función de biblioteca de espacio de usuario correspondiente (generalmente implementada en libc en sistemas tipo Unix). Por ejemplo, la función mkdir()
reenvía sus argumentos al mkdir
syscall.
En los sistemas GNU (creo que es lo mismo para los demás), se syscall()
una función syscall()
desde la función ''mkdir ()''. La función / macros de syscall generalmente se implementan en C. Por ejemplo, eche un vistazo a INTERNAL_SYSCALL
en sysdeps/unix/sysv/linux/i386/sysdep.h
o syscall
en sysdeps/unix/sysv/linux/i386/sysdep.S
(glibc )
Ahora, si sysdeps/unix/sysv/linux/i386/sysdep.h
, puedes ver que la llamada al kernel está hecha por ENTER_KERNEL
que históricamente llamaba a la interrupción 0x80
en las CPU i386. Ahora llama a una función (supongo que está implementado en linux-gate.so
que es un archivo SO virtual mapeado por el kernel, contiene la forma más eficiente de hacer un syscall para tu tipo una CPU).
Sí, lo tienes muy bien. Sin embargo, un detalle, cuando el compilador compila una llamada al sistema, utilizará el número de la llamada al sistema en lugar del nombre . Por ejemplo, aquí hay una lista de syscalls de Linux (para una versión anterior, pero el concepto sigue siendo el mismo).
Sí, su comprensión es absolutamente correcta, un programa C puede llamar directamente al sistema, cuando ocurre una llamada al sistema puede ser una serie de llamadas hasta la captura del ensamblaje. Creo inmensamente que su comprensión puede ayudar a un novato. Compruebe este código en el que estoy llamando al sistema llamado "sistema".
#include < stdio.h >
#include < stdlib.h >
int main()
{
printf("Running ps with "system" system call ");
system("ps ax");
printf("Done./n");
exit(0);
}
Si desea realizar una llamada al sistema directamente desde su programa, puede hacerlo fácilmente. Depende de la plataforma, pero digamos que quería leer desde un archivo. Cada llamada al sistema tiene un número. En este caso, coloca el número de la read_from_file
sistema read_from_file
en el registro EAX. Los argumentos para la llamada al sistema se colocan en diferentes registros o la pila (dependiendo de la llamada al sistema). Una vez que los registros se llenan con los datos correctos y está listo para realizar la llamada al sistema, ejecuta la instrucción INT 0x80
(depende de la arquitectura). Esa instrucción es una interrupción que hace que el control vaya al SO. El sistema operativo luego identifica el número de llamada del sistema en el registro EAX, actúa en consecuencia y devuelve el control al proceso que realiza la llamada al sistema.
La forma en que se usan las llamadas al sistema es propensa a cambios y depende de la plataforma dada. Al utilizar bibliotecas que proporcionan interfaces fáciles para estas llamadas al sistema, usted hace que sus programas sean más independientes de la plataforma y su código será mucho más legible y más rápido de escribir. Considere implementar llamadas al sistema directamente en un lenguaje de alto nivel. Necesitará algo como ensamblaje en línea para garantizar que los datos se coloquen en los registros correctos.
Usted realmente llama a la biblioteca C runtime. No es el compilador quien inserta TRAP, es la biblioteca C la que envuelve a TRAP en una llamada a la biblioteca. El resto de tu comprensión es correcta.
Tu comprensión es bastante cercana; el truco es que la mayoría de los compiladores nunca escribirán llamadas al sistema, porque las funciones que los programas llaman (por ejemplo, getpid(2)
, chdir(2)
, etc.) son realmente proporcionadas por la biblioteca C estándar. La biblioteca C estándar contiene el código para la llamada al sistema, ya sea que se llame a través de INT 0x80
o SYSENTER
. Sería un programa extraño que hace llamadas al sistema sin una biblioteca haciendo el trabajo. (Aunque perl
proporciona una función syscall()
que puede hacer directamente llamadas al sistema! Loco, ¿verdad?)
Luego, la memoria. El kernel del sistema operativo a veces tiene fácil acceso al espacio de direcciones para la memoria de proceso del usuario. Por supuesto, los modos de protección son diferentes, y los datos suministrados por el usuario deben copiarse en el espacio de direcciones protegido del kernel para evitar la modificación de los datos proporcionados por el usuario mientras la llamada del sistema está en vuelo :
static int do_getname(const char __user *filename, char *page)
{
int retval;
unsigned long len = PATH_MAX;
if (!segment_eq(get_fs(), KERNEL_DS)) {
if ((unsigned long) filename >= TASK_SIZE)
return -EFAULT;
if (TASK_SIZE - (unsigned long) filename < PATH_MAX)
len = TASK_SIZE - (unsigned long) filename;
}
retval = strncpy_from_user(page, filename, len);
if (retval > 0) {
if (retval < len)
return 0;
return -ENAMETOOLONG;
} else if (!retval)
retval = -ENOENT;
return retval;
}
Esto, si bien no es una llamada al sistema en sí misma, es una función auxiliar llamada por funciones de llamada del sistema que copia nombres de archivos en el espacio de direcciones del kernel. Comprueba para asegurarse de que todo el nombre de archivo reside dentro del rango de datos del usuario, llama a una función que copia la cadena desde el espacio de usuario y realiza algunas comprobaciones de cordura antes de la devolución.
get_fs()
y funciones similares son remanentes de x86-roots de Linux. Las funciones tienen implementaciones de trabajo para todas las arquitecturas, pero los nombres siguen siendo arcaicos.
Todo el trabajo adicional con segmentos se debe a que el kernel y el espacio de usuario pueden compartir una parte del espacio de direcciones disponible. En una plataforma de 32 bits (donde los números son fáciles de comprender), el kernel generalmente tendrá un gigabyte de espacio de direcciones virtuales, y los procesos de los usuarios típicamente tendrán tres gigabytes de espacio de direcciones virtuales.
Cuando un proceso llama al kernel, el kernel ''arregla'' los permisos de la tabla de la página para permitirle acceder a todo el rango, y obtiene el beneficio de las entradas TLB precargadas para la memoria proporcionada por el usuario. Gran éxito. Pero cuando el kernel debe cambiar el contexto al espacio de usuario, debe eliminar el TLB para eliminar los privilegios de caché en las páginas de espacio de direcciones del kernel.
Pero el truco es que un gigabyte de espacio de direcciones virtuales no es suficiente para todas las estructuras de datos del kernel en máquinas enormes. Mantener los metadatos de los sistemas de archivos almacenados en caché y bloquear los controladores de dispositivos, las pilas de red y las asignaciones de memoria para todos los procesos en el sistema, puede tomar una gran cantidad de datos.
Existen diferentes ''splits'' disponibles: dos conciertos para el usuario, dos conciertos para el núcleo, un concierto para el usuario, tres conciertos para el kernel, etc. A medida que el espacio para el kernel aumenta, el espacio para los procesos del usuario disminuye. De modo que hay una división de memoria 4:4
que proporciona cuatro gigabytes al proceso del usuario, cuatro gigabytes al kernel, y el kernel debe manipular los descriptores de los segmentos para poder acceder a la memoria del usuario. El TLB se vacía al entrar y salir de las llamadas al sistema, que es una penalización de velocidad bastante significativa. Pero permite que el kernel mantenga estructuras de datos significativamente más grandes.
Las tablas de páginas mucho más grandes y los rangos de direcciones de plataformas de 64 bits probablemente hacen que todo el aspecto anterior sea pintoresco. Eso espero, de todos modos.