2010-04-01 14 views
17

hay una manera de implementar la multitarea utilizando setjmp y longjmp funcionesmultitarea utilizando setjmp, longjmp

+1

[Picoro (pequeñas co-rutinas) de Tony Finch] (http://dotat.at/cgi/git?p=picoro.git;a=blob;f=picoro.c;hb=HEAD). Las co-rutinas están en el arte de la computación de Knuth y son multitareas cooperativas. Además, Simon Tatham tiene una [página web de co-rutinas] (http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html) con buenas explicaciones. –

+0

Además, se debe tener cuidado; el 'setjmp()' y 'longjmp()' se implementan con mayor frecuencia/siempre en el ensamblador y se asemejan al código de cambio de contexto del sistema operativo. Sin embargo, es posible que no guarden un estado como * floating point *, * SIMD state *, etc. Ya sea que se trate de un error de implementación o de un estándar, no lo sé. Sin embargo, este problema a menudo existirá en la práctica. Saber qué estado guardar puede ser un impulso significativo a las velocidades de cambio de contexto. –

+0

Vea: ['setjmp()' y fpmode] (http://www-personal.umich.edu/~williams/archive/computation/setjmp-fpmode.html) para más información sobre otro estado de la CPU. –

Respuesta

12

De hecho, puede. Hay un par de formas de lograrlo. La parte difícil es obtener inicialmente los jmpbufs que apuntan a otras pilas. Longjmp solo se define para los argumentos de jmpbuf creados por setjmp, por lo que no hay forma de hacerlo sin utilizar el ensamblaje o explotar el comportamiento indefinido. Los hilos de nivel de usuario no son inherentemente portátiles, por lo que la portabilidad no es un argumento fuerte para no hacerlo realmente.

paso 1 Se necesita un lugar para almacenar los contextos de diferentes hilos, por lo que hacer una cola de Construcciones para jmpbuf sin embargo muchos hilos que desee.

Paso 2 Necesita malloc a stack para cada uno de estos subprocesos.

Paso 3 Usted necesita obtener algunos contextos jmpbuf que han apilar punteros en las posiciones de memoria que acaba de ser asignado. Puede inspeccionar la estructura jmpbuf en su máquina, averiguar dónde almacena el puntero de la pila. Llame a setjmp y luego modifique su contenido para que el puntero de la pila esté en una de sus pilas asignadas. Las pilas generalmente crecen, por lo que probablemente desee que su puntero de pila esté cerca de la ubicación de memoria más alta. Si escribe un programa C básico y utiliza un depurador para desmontarlo, y luego encuentra las instrucciones que ejecuta cuando regresa de una función, puede averiguar cuál debe ser el desplazamiento.Por ejemplo, con las convenciones de llamada del sistema V en x86, verá que muestra% ebp (el puntero de marco) y luego llama a ret, que saca la dirección de retorno de la pila. Por lo tanto, al ingresar a una función, empuja la dirección de retorno y el puntero del marco. Cada pulsación mueve el puntero de pila hacia abajo en 4 bytes, por lo que desea que el puntero de pila comience en la dirección alta de la región asignada, -8 bytes (como si acabara de llamar a una función para llegar allí). Llenaremos los 8 bytes a continuación.

La otra cosa que puede hacer es escribir un conjunto en línea muy pequeño (una línea) para manipular el puntero de la pila, y luego llamar a setjmp. Esto es realmente más portátil, porque en muchos sistemas los punteros en un jmpbuf están dañados por la seguridad, por lo que no puede modificarlos fácilmente.

No lo he intentado, pero es posible que pueda evitar el asm simplemente desbordando deliberadamente la pila declarando una matriz muy grande y moviendo así el puntero de la pila.

Paso 4 Usted necesita salir de hilos para devolver el sistema a un estado seguro. Si no lo hace, y uno de los hilos regresa, tomará la dirección justo encima de su pila asignada como dirección de retorno y saltará a alguna ubicación de basura y probablemente segfault. Entonces primero necesitas un lugar seguro al que regresar. Obtenga esto llamando a setjmp en el hilo principal y almacenando el jmpbuf en una ubicación accesible a nivel mundial. Define una función que no toma argumentos y simplemente llama a longjmp con el jmpbuf global guardado. Obtenga la dirección de esa función y cópiela en las pilas asignadas donde dejó espacio para la dirección de devolución. Puedes dejar el puntero del marco vacío. Ahora, cuando retorna un hilo, irá a esa función que llama a longjmp, y vuelve directamente al hilo principal donde llamó a setjmp, todo el tiempo.

Paso 5 Justo después setjmp del hilo principal, que desea tener un código que determina cuál de ellos para saltar al siguiente, tirando de la jmpbuf adecuada de la cola y llamando longjmp para ir allí. Cuando no quedan hilos en esa cola, el programa está hecho.

Paso 6 escribir una función de cambio de contexto que exige setjmp y almacena el estado actual de nuevo en la cola, y luego en otra longjmp jmpbuf de la cola.

Conclusión Eso es lo básico. Siempre que los hilos sigan llamando al cambio de contexto, la cola sigue siendo repoblada y se ejecutan diferentes subprocesos. Cuando vuelve un hilo, si queda algo para ejecutar, uno es elegido por el hilo principal, y si no queda ninguno, el proceso finaliza. Con relativamente poco código, puede tener una configuración bastante cooperativa de multitarea. Hay más cosas que probablemente desee hacer, como implementar una función de limpieza para liberar la pila de un hilo muerto, etc. También puede implementar preferencia utilizando señales, pero eso es mucho más difícil porque setjmp no guarda el registro de coma flotante los registros state o flags, que son necesarios cuando el programa se interrumpe de forma asíncrona.

+0

Algunas implementaciones específicas de setjmp/longjmp pueden funcionar de tal manera que uno puede confundirlas para que se comporten como se desee, y es posible que algunos compiladores incluso * especifiquen * que sus implementaciones funcionan de una manera particular que permitiría tal cosa sin tener que confiar en comportamientos no documentados/indefinidos cuando se dirigen a dichos compiladores, pero sugiero usar algunas líneas de código ensamblador para hacer los interruptores de pila/registro. Usar setjmp/longjmp no es más portátil que el código de ensamblaje, pero podría dar la ilusión de portabilidad. – supercat

+0

Habiendo dicho eso, creo que hay mucho que decir acerca de la multitarea cooperativa. Muchos compiladores documentan expresamente qué registros (si los hay) deben preservarse mediante módulos externos de lenguaje ensamblador. Una multitarea preferente tendría que conservar todos los registros que un compilador podría estar usando, lo que podría ser un problema si, por ejemplo, un compilador aprovecha una unidad de aceleración de multiplicación y aceleración de hardware que el multitarea no conoce, pero los multitarea cooperativos evitan tales problemas. Habiendo dicho eso ... – supercat

+1

... cosas como las excepciones de C++, dependiendo de cómo se implementen, pueden funcionar bien o no, incluso con la multitarea cooperativa. Uno debería investigar cómo se implementan las excepciones para saber qué se requiere de las pilas mantenidas por los hilos en ejecución. – supercat

8

Se puede romper las reglas un poco, pero PTH GNU hace esto. Es posible, pero probablemente no deberías probarlo tú mismo, excepto como ejercicio académico de prueba de concepto, utiliza la implementación pth si quieres hacerlo en serio y de forma remotamente portátil: entenderás por qué cuando lees el pth código de creación de hilo.

(Esencialmente se utiliza un manejador de señales para engañar al sistema operativo en la creación de una pila fresca, a continuación, longjmp es salir de allí y mantiene la pila de alrededor. Funciona, evidentemente, pero es incompleta como el infierno.)

En la producción código, si su sistema operativo admite makecontext/swapcontext, utilícelos en su lugar. Si es compatible con CreateFiber/SwitchToFiber, utilícelos en su lugar. Y tenga en cuenta la decepcionante verdad de que uno de los usos más convincentes de las corutinas, es decir, invertir el control cediendo fuera de los controladores de eventos llamados por código extraño, es inseguro porque el módulo que llama debe ser reentratado, y usted generalmente puede Demuestre eso. Esta es la razón por la cual las fibras aún no son compatibles con .NET ...

+2

El Netscape Portable Runtime (NSPR) también parece definir macros para hacer esto usando un método más simple pero más peludo: simplemente llaman a setjmp y luego cambian el puntero de pila de la máquina y el puntero de instrucción en el búfer. Google "_MD_INIT_CONTEXT" para una lectura entretenida. – user414736

3

Esto es una forma de lo que se conoce como cambio de contexto de espacio de usuario.

Es posible pero propenso a errores, especialmente si utiliza la implementación predeterminada de setjmp y longjmp. Un problema con estas funciones es que en muchos sistemas operativos solo guardarán un subconjunto de registros de 64 bits, en lugar de todo el contexto. Esto a menudo no es suficiente, p. cuando se trata de bibliotecas de sistemas (mi experiencia aquí es con una implementación personalizada para amd64/windows, que funcionó bastante estable).

Dicho esto, si no está tratando de trabajar con bases de código externas complejas o manejadores de eventos, y sabe lo que está haciendo, y (especialmente) si escribe su propia versión en ensamblador que ahorra más de la actual contexto (si está utilizando Windows de 32 bits o Linux esto puede no ser necesario, si utiliza algunas versiones de BSD, supongo que casi definitivamente lo es), y lo depura prestando especial atención a la salida de desensamblaje, entonces puede estar capaz de lograr lo que quieres

1

Como ya fue mencionado por Sean Ogden, longjmp() no es bueno para la multitarea, como sólo se puede mover la pila hacia arriba y no puede salto entre diferentes pilas. No vayas con eso.

Según lo mencionado por user414736, puede utilizar getcontext/makecontext/swapcontext funciones, pero el problema con ellos es que no están plenamente en el espacio de usuario. De hecho, llaman a la syscall sigprocmask() ya que cambian la máscara de señal como parte del cambio de contexto. Esto hace que swapcontext() sea mucho más lento que longjmp(), y es probable que no desee las co-rutinas lentas.

Que yo sepa, no existe una solución POSIX estándar para este problema, así que compilé la mía de diferentes fuentes disponibles . Puede encontrar el contexto de manipulación funciones extraídos de libtask aquí:
https://github.com/stsp/dosemu2/tree/devel/src/arch/linux/mcontext
Las funciones son: getmcontext(), setmcontext(), makemcontext() y swapmcontext(). Tienen la semántica similar a las funciones estándar con nombres similares, pero también imitan la semántica setjmp() en ese getmcontext() devuelve 1 (en lugar de 0) cuando salta a por setmcontext().

Además de eso se puede utilizar un puerto de libpcl, la biblioteca corrutina:
https://github.com/stsp/dosemu2/tree/devel/src/base/misc/libpcl
Con esto, es posible llevar a cabo el ayuno del espacio de usuario cooperativa roscado. Funciona en Linux, en i386 y x86_64 arcos.

Cuestiones relacionadas