Max Hernandez

Laberinto - Ejemplo de Canvas en HTML5

domingo, 20 de mayo de 2012

Llamadas al sistema

Una llamada al sistema es cuando un programa llama a un servicio del Kernel del sistema operativo. Generalmente estos servicios se encuentran disponibles como instrucciones de lenguaje ensamblador pero algunas veces también pueden llamarse (sin tener conocimiento de ello) desde lenguajes de alto nivel.


Kernel o núcleo
Este se considera el principal componente de los sistemas operativos sirve de interfaz entre aplicaciones y el proceso de los datos de bajo nivel hecho por el hardware del equipo. Entre sus funciones están administrar los recursos del sistema como memoria , uso de CPU por las aplicaciones o acceso a interfaces de entrada y salida del equipo.

Ejemplos de esto son:
  • Exit: Para terminar la ejecución de un programa.
  • Fork: Para crear hilos de ejecución.
  • Time: Para ver la hora actual.
  • Open: Para abrir archivos
  • Close: para cerrar archivos
 Como puede observarse estas son funciones muy utilizadas en cualquier lenguaje de programación pero sin darnos cuenta estas son administradas por el núcleo del sistema operativo.

Llamadas al sistema en linux 

Conceptos útiles:

El espacio kernel: En este espacio residen todas las subrutinas que forman parte del kernel tales como módulos o drivers y cualquier otra que tenga que ver con la interfaz kernel.

 El espacio de usuario: En este espacio se encuentran los programas que utiliza el usuario final, tales como shell y cualquiera de los programas que he estado programando de ejemplos en ensamblador.




Existen diferentes implementaciones en el mundo Linux en los procesadores x86 más antiguos se utilizaban interrupciones para migrar de un espacio de usuario a un espacio Kernel ( lo cuál es un objetivo intermedio de las llamadas al sistema).
En Linux en las llamadas al sistema se utiliza el registro "%eax" para identificar la llamada al sistema que se quiere invocar, la cual se especifica en una biblioteca de Ansi C. Para empezar la llamada al sistema se invoca al vector de interrupción de Linux (0x80) que contiene las direcciones de memoria de los controladores de interrupciones. Cuando una llamada al sistema es generada, el sistema operativo salva el estado de ejecución del proceso y empieza la ejecución del controlador de llamadas al sistema en el vector de interrupciones.

Una vez esto el controla de llamadas al sistema provoca la ejecución de la función "system_call", esta función controla todas las llamadas al sistema, y toma como parámetro el valor contenido en el registro %eax para identificar la llamada en especifico. Una vez realizadas una serio de verificaciones se invoca la llamada real al sistema utilizando la funcion "system_call_table" y el indice en "%eax". Una vez que el kernel termina su ejecución invoca ala función "system_call_exit" y se llama a las transiciones de resumen "resume_userspace" nuevamente para regresar al espacio de usuario para regresar a la aplicación del usuario.




Por último en la página que esta abajo encontré la lista de llamadas al sistema implementadas en el Kernel de Linux, que utilicé como guía para mi ejemplo de este tema.


Ejemplo de llamada al sistema en ensamblador x86


Parametros

En la tabla de llamadas al sistema de linux se puede ver que en a partir de la tercera columna se encabezan nombres de registros y en las celdas tipos de variables. Esto es por que estos registros se utilizan como parámetros para las funciones de llamadas al sistema, el primer parámetro va en el registro "%ebx" el segundo va en "%ecx", el tercero en "%edx" y el cuatro en "%esi", el quinto en "%edi" y el sexto y ultimo en "%ebp".


Hilos usando llamadas al sistema
Como ejemplo de una llamada al sistema decidí implementar la llamada al sistema numero dos del kernel de linux con el que cuento, esta de lo que se encarga es de partir nuestro proceso en dos y crear una copia exacta del mismo que corre de manera independiente del proceso del cual fue creado.
 
.LC0:
 .string "A: %d\n"
.LC1:
 .string "B: %d\n"
.LC2:
 .string "%d\n"
  
 
.globl A  ;Esta función solo sirve para imprimir el resultado
A:             ;con el nombre del thread que lo manda.
 pushq %rbp
 movq  %rsp,  %rbp

 movq 16(%rsp), %rsi
 movq $.LC0,   %rdi 
 movl $0,   %eax
 call printf

 leave
 ret

.globl B  ;Esta hace lo mismo con el threadB
B:
 pushq %rbp
 movq  %rsp,  %rbp

 movq 16(%rsp), %rsi
 movq $.LC1,   %rdi 
 movl $0,   %eax
 call printf

 leave
 ret


.globl threads
threads:
 pushq %rbp  ;Recuperamos la base y el top de la función en el stack de memoria 
 movq %rsp,  %rbp  
 subq $8, %rsp

 movq $50, 16(%rsp)
 movq  $2,  %rax  ;Aquí es donde se define el parámetro al kernel de la función del sistema a la cual queremos llamar
 int $0x80 ;Aquí se hace la interrupción y la llamada a system_call
 
 cmpq  $0, %rax ;El parametro de la llamada se devuelve en el registro %eax
 jz .threadA           ;Si el valor es cero significa que se esta en el proceso padre
 
 cmpq  $0, %rax ; En caso contrario se esta en un proceso hijo
 jg .threadB
 
 cmpq  $0, %rax
 jl .threadB

 
.threadA: ; estas funciones solo aumentan y disminuyen el valor en 16(%rsp), despues salen y terminan su ejecución
 addq $1, 16(%rsp) ; aquí se aumenta
 pushq 16(%rsp)
 call A
 popq %rax
 
 cmpq $100, 16(%rsp)
 jge  .fin

 cmpq $2000, 16(%rsp)
 jl .threadA

.threadB:
 subq $1, 16(%rsp) ; aqui se disminuye
 
 pushq 16(%rsp)
 call B
 popq %rax

 cmpq $0, 16(%rsp)
 jle  .fin

 cmpq $0, 16(%rsp)
 jg .threadB

.fin:
 movq $1, %rax
 int $0x80
 

Lo que imprimió en la terminal es lo siguiente:

Como puede observarse cada impresión en la terminal se hace de forma asíncrona entre las dos funciones, ademas de que a pesar de que parece que comparten la misma dirección de memoria como contador, eso no es verdad pues como vemos la misma variable tiene valores diferentes para cada thread, lo cual nos dice que el kernel nos asigna un nuevo stack de memoria exclusivo para el nuevo thread.


1 comentario: