una solicita sistema operativo memoria llamadas llamada las interfaz hacer defina como linux multithreading assembly gas

solicita - ¿Es posible crear subprocesos sin llamadas al sistema en el ensamblaje GAS de Linux x86?



linux llamadas al sistema (7)

"Doctor, doctor, duele cuando hago esto". Doctor: "No hagas eso".

La respuesta corta es que puede realizar una programación multiproceso sin llamar a costosas primitivas de administración de tareas del sistema operativo. Simplemente ignore el sistema operativo para las operaciones de programación de hilos. Esto significa que debe escribir su propio programador de hilos y, simplemente, nunca volver a transferir el control al sistema operativo. (Y usted tiene que ser más listo de alguna manera sobre su hilo de arriba que los chicos bastante inteligentes de sistema operativo). Elegimos este enfoque precisamente porque las llamadas al proceso de Windows / hilo / fibra eran demasiado costosas para admitir granos de cálculo de unos cientos de instrucciones.

Nuestro lenguaje de programación PARLANSE es un lenguaje de programación paralelo: ver http://www.semdesigns.com/Products/Parlanse/index.html

PARLANSE se ejecuta en Windows, ofrece "granos" paralelos como el constructo de paralelismo abstracto y planifica tales granos mediante una combinación de un planificador manuscrito altamente afinado y un código de programación generado por el compilador PARLANSE que tiene en cuenta el contexto del grano para minimizar la programación gastos generales. Por ejemplo, el compilador asegura que los registros de un grano no contengan información en el punto donde la programación (p. Ej., "Esperar") podría ser requerida, y por lo tanto el código del planificador solo tiene que guardar la PC y SP. De hecho, con bastante frecuencia el código del programador no obtiene control en absoluto; un grano bifurcado almacena simplemente la PC y el SP bifurcados, cambia a la pila preasignada por el compilador y salta al código de grano. La finalización del grano reiniciará el forker.

Normalmente hay un enclavamiento para sincronizar granos, implementado por el compilador utilizando instrucciones LOC LOC DEC que implementan lo que equivale a contar semáforos. Las aplicaciones pueden dividir lógicamente millones de granos; el programador limita que los granos principales generen más trabajo si las colas de trabajo son lo suficientemente largas para que el trabajo no sea útil. El programador implementa el robo de trabajo para permitir que las CPU con escasez de trabajo atrapen los granos preparados de las colas de trabajo de CPU vecinas. Esto se ha implementado para manejar hasta 32 CPU; ¡pero estamos un poco preocupados de que los proveedores de x86 realmente puedan dañar el uso con más que eso en los próximos años!

PARLANSE es un idioma maduro; lo hemos estado utilizando desde 1997, y hemos implementado una aplicación paralela de varios millones de líneas.

Mientras aprendía el "lenguaje de ensamblador" (en linux en una arquitectura x86 usando GNU como ensamblador), uno de los momentos de aha era la posibilidad de usar llamadas al sistema . Estas llamadas al sistema son muy útiles ya veces incluso son necesarias a medida que su programa se ejecuta en espacio de usuario .
Sin embargo, las llamadas al sistema son bastante costosas en términos de rendimiento, ya que requieren una interrupción (y por supuesto una llamada al sistema) lo que significa que se debe hacer un cambio de contexto desde su programa activo actual en espacio de usuario al sistema que se ejecuta en kernel-space.

El punto que quiero plantear es este: actualmente estoy implementando un compilador (para un proyecto universitario) y una de las características adicionales que quería agregar es el soporte para código multiproceso con el fin de mejorar el rendimiento del programa compilado. . Debido a que parte del código de subprocesos múltiples será generado automáticamente por el propio compilador, esto casi garantizará que también habrá bits muy pequeños de código de subprocesos múltiples en él. Para ganar un rendimiento, debo estar seguro de que el uso de hilos hará que esto suceda.

Sin embargo, mi temor es que, para usar el enhebrado, debo hacer llamadas al sistema y las interrupciones necesarias. Los pequeños hilos pequeños (autogenerados) se verán muy afectados por el tiempo que lleve realizar estas llamadas al sistema, lo que incluso podría llevar a una pérdida de rendimiento ...

mi pregunta es por lo tanto doble (con una pregunta de bonificación adicional debajo):

  • ¿Es posible escribir código de ensamblador que puede ejecutar múltiples subprocesos simultáneamente en múltiples núcleos a la vez, sin la necesidad de llamadas al sistema?
  • ¿Obtendré una ganancia de rendimiento si tengo hilos muy pequeños (pequeños como en el tiempo total de ejecución del hilo), pérdida de rendimiento o no vale la pena el esfuerzo?

Supongo que el código ensamblador multiproceso no es posible sin las llamadas al sistema. Incluso si este es el caso, ¿tiene una sugerencia (o incluso mejor: algún código real) para implementar los hilos lo más eficiente posible?


Implementar el enrutamiento en modo usuario.

Históricamente, los modelos de subprocesamiento se generalizan como N: M, es decir, N subprocesos de modo de usuario que se ejecutan en subprocesos de M kernel-model. El uso moderno es 1: 1, pero no siempre fue así y no tiene por qué ser así.

Usted es libre de mantener en un único kernel un número arbitrario de subprocesos de modo de usuario. Es solo que es su responsabilidad cambiar entre ellos con la suficiente frecuencia para que todo se vea concurrente. Tus hilos son, por supuesto, cooperativos en lugar de preventivos; Básicamente se dispersaron las llamadas yield () a través de su propio código para garantizar el cambio regular.


Las llamadas al sistema ya no son tan lentas, con syscall o sysenter lugar de int . Sin embargo, solo habrá una sobrecarga al crear o destruir los hilos. Una vez que se están ejecutando, no hay llamadas al sistema. Los subprocesos de modo de usuario no lo ayudarán realmente, ya que solo se ejecutan en un núcleo.


Primero, debe aprender a usar hilos en C (pthreads, puntos POSIX). En GNU / Linux probablemente querrá usar hilos POSIX o hilos GLib. Entonces, simplemente puede llamar a la C desde el código de ensamblaje.

Aquí hay algunos consejos:


Si desea obtener rendimiento, deberá aprovechar los hilos del kernel. Solo el núcleo puede ayudarlo a ejecutar código simultáneamente en más de un núcleo de CPU. A menos que su programa esté vinculado a E / S (o realice otras operaciones de bloqueo), realizar multiprocesamiento cooperativo en modo usuario (también conocido como fibers ) no le permitirá obtener ningún rendimiento. Simplemente realizará interruptores de contexto adicionales, pero la única CPU que ejecuta su subproceso real todavía se ejecutará al 100% de cualquier manera.

Las llamadas al sistema se han vuelto más rápidas. Las CPU modernas tienen soporte para la instrucción sysenter , que es significativamente más rápida que la antigua instrucción int . Ver también este artículo sobre cómo Linux hace las llamadas al sistema de la manera más rápida posible.

Asegúrese de que el subprocesamiento múltiple generado automáticamente tenga los subprocesos en ejecución el tiempo suficiente para que obtenga rendimiento. No trates de paralelizar partes cortas de código, solo perderás el tiempo de desove y unir los hilos. También tenga cuidado con los efectos de memoria (aunque son más difíciles de medir y predecir): si varios subprocesos tienen acceso a conjuntos de datos independientes, se ejecutarán mucho más rápido que si estuvieran accediendo a los mismos datos repetidamente debido al problema de coherencia de caché .


Ya bastante tarde, pero yo también estaba interesado en este tipo de tema. De hecho, no hay nada tan especial acerca de los hilos que requiera específicamente que el kernel intervenga, EXCEPTO para la paralelización / rendimiento.

BLUF obligatorio :

Q1: No. Por lo menos, las llamadas iniciales al sistema son necesarias para crear múltiples hilos del kernel en los distintos núcleos de CPU / hyper-threads.

Q2: depende. Si crea / destruye hilos que realizan pequeñas operaciones, está desperdiciando recursos (el proceso de creación de hilos excedería en gran medida el tiempo utilizado por la banda de rodadura antes de que salga). Si crea N subprocesos (donde N es ~ # de cores / hipersubprocesos en el sistema) y los vuelve a realizar, la respuesta PODRÍA ser sí, dependiendo de su implementación.

Q3: PODRÍA optimizar la operación si CONOCÍA con anticipación un método preciso de ordenar operaciones. Específicamente, podría crear lo que equivale a una cadena de ROP (o una cadena de llamadas hacia adelante, pero esto en realidad puede terminar siendo más complejo de implementar). Esta cadena ROP (como la ejecuta un subproceso) ejecutará continuamente las instrucciones ''ret'' (a su propia pila) donde esa pila se antepone continuamente (o se agrega en el caso en que se transfiere al principio). En dicho modelo (¡extraño!), El planificador mantiene un puntero al ''extremo de cadena ROP'' de cada subproceso y le escribe nuevos valores mediante los cuales el código circula a través de la memoria ejecutando código de función que finalmente da como resultado una instrucción ret. Una vez más, este es un modelo extraño, pero es intrigante, no obstante.

En mi valor de 2 centavos de contenido.

Recientemente, creé lo que efectivamente funciona como hilos en ensamblaje puro administrando varias regiones de pila (creadas a través de mmap) y manteniendo un área dedicada para almacenar la información de control / individualización para los "hilos". Es posible, aunque no lo diseñé de esta manera, crear un gran bloque de memoria a través de mmap que subdivide en el área "privada" de cada subproceso. Por lo tanto, solo se requeriría una única llamada de sistema (aunque las páginas de guardia entre ellas serían inteligentes, estas requerirían llamadas de sistema adicionales).

Esta implementación usa solo la cadena de kernel base creada cuando el proceso genera y solo hay un único subproceso de modo de usuario a lo largo de toda la ejecución del programa. El programa actualiza su propio estado y se programa a sí mismo a través de una estructura de control interno. Las E / S y otras cosas se manejan a través de opciones de bloqueo cuando es posible (para reducir la complejidad), pero esto no es estrictamente necesario. Por supuesto, hice uso de mutexes y semáforos.

Para implementar este sistema (completamente en el espacio de usuario y también a través de un acceso no root, si así lo desea) se requiere lo siguiente:

Una noción de lo que los hilos se reducen a: Una pila para operaciones de pila (un poco auto explicativa y obvia) Un conjunto de instrucciones para ejecutar (también obvio) Un pequeño bloque de memoria para contener los contenidos del registro individual

Lo que un programador se reduce a: un administrador para una serie de hilos (tenga en cuenta que los procesos nunca se ejecutan realmente, solo su (s) hilo (s) lo hacen) en una lista ordenada especificada por el planificador (generalmente prioridad).

Un conmutador de contexto de subprocesos: un MACRO inyectado en varias partes de código (generalmente pongo estos al final de las funciones de trabajo pesado) que equivale aproximadamente a ''rendimiento de subprocesos'', que guarda el estado del subproceso y carga el estado de otro subproceso.

Por lo tanto, es posible (completamente en ensamblado y sin llamadas al sistema distintas de mmap inicial y mprotect) crear construcciones similares a subprocesos de modo de usuario en un proceso no raíz.

Solo agregué esta respuesta porque menciona específicamente el ensamblaje x86 y esta respuesta se obtuvo completamente a través de un programa autocontenido escrito completamente en ensamblaje x86 que logra los objetivos (sin capacidades multi-core) de minimizar las llamadas al sistema y también minimiza el subproceso del sistema gastos generales.


La respuesta corta es que no puedes. Cuando escribe código ensamblador, se ejecuta secuencialmente (o con ramas) en una y solo una cadena lógica (es decir, hardware). Si desea que parte del código se ejecute en otro hilo lógico (ya sea en el mismo núcleo, en un núcleo diferente en la misma CPU o incluso en una CPU diferente), necesita que el sistema operativo configure el puntero de instrucción del otro hilo ( CS:EIP ) para señalar el código que desea ejecutar. Esto implica utilizar llamadas al sistema para que el sistema operativo haga lo que usted desea.

Los subprocesos de usuario no le darán el soporte de subprocesamiento que desea, ya que todos se ejecutan en el mismo subproceso de hardware.

Editar: Incorporando la respuesta de Ira Baxter con Parlanse . Si se asegura de que su programa tenga un subproceso ejecutándose en cada subproceso lógico para empezar, entonces puede construir su propio planificador sin depender del sistema operativo. De cualquier manera, necesita un programador para manejar saltos de un hilo a otro. Entre llamadas al programador, no hay instrucciones especiales de ensamblaje para manejar el multi-threading. El programador no puede confiar en ningún ensamblaje especial, sino en las convenciones entre las partes del planificador en cada subproceso.

De cualquier manera, ya sea que use o no el sistema operativo, todavía tiene que depender de algún planificador para manejar la ejecución de ejecución cruzada.