Многозадачность с использованием setjmp, longjmp

существует способ реализовать многозадачность с использованием функций setjmp и longjmp

Ответы

Ответ 1

Вы действительно можете. Есть несколько способов сделать это. Сложная часть изначально получает jmpbufs, которые указывают на другие стеки. Longjmp определяется только для аргументов jmpbuf, которые были созданы setjmp, поэтому нет никакого способа сделать это без использования сборки или использования поведения undefined. Потоки пользовательского уровня по своей сути не переносимы, поэтому переносимость не является сильным аргументом в пользу того, что вы не делаете этого действительно.

шаг 1 Вам нужно место для хранения контекстов разных потоков, поэтому создайте очередь jmpbuf stuctures для сколько угодно потоков.

Шаг 2 Вам нужно выстроить стек для каждого из этих потоков.

Шаг 3 Вам нужно получить некоторые контексты jmpbuf, у которых есть указатели стека в выделенных вами ячейках памяти. Вы можете проверить структуру jmpbuf на своем компьютере, узнать, где он хранит указатель стека. Вызовите setjmp, а затем измените его содержимое, чтобы указатель стека находился в одном из ваших выделенных стеков. Стеки обычно растут, поэтому вы, вероятно, хотите, чтобы указатель стека находился где-то рядом с самой высокой ячейкой памяти. Если вы пишете базовую программу на C и используете отладчик, чтобы разобрать ее, а затем найти инструкции, которые она выполняет при возврате из функции, вы можете узнать, какое должно быть смещение. Например, с соглашениями о вызовах системы V на x86 вы увидите, что он выдает% ebp (указатель кадра), а затем вызывает ret, который выдает адрес возврата из стека. Таким образом, при входе в функцию он выталкивает адрес возврата и указатель кадра. Каждое нажатие перемещает указатель стека вниз на 4 байта, поэтому вы хотите, чтобы указатель стека начинался с высокого адреса выделенной области, -8 байтов (как будто вы только что вызвали функцию, чтобы попасть туда). Мы заполним следующие 8 байтов.

Другое, что вы можете сделать, это написать небольшую (одну строку) встроенную сборку для управления указателем стека, а затем вызвать setjmp. Это на самом деле более портативно, потому что во многих системах указатели в jmpbuf искажены для обеспечения безопасности, поэтому вы не можете легко их изменить.

Я не пробовал, но вы могли бы избежать asm, просто преднамеренно переполнив стек, объявив очень большой массив и тем самым перемещая указатель стека.

Шаг 4 Вам нужно выйти из потоков, чтобы вернуть систему в безопасное состояние. Если вы этого не сделаете, и один из потоков вернется, он переместит адрес прямо над вашим выделенным стекем в качестве обратного адреса и переместится к некоторому месту мусора и, вероятно, segfault. Поэтому сначала вам нужно безопасное место для возвращения. Получите это, вызвав setjmp в основном потоке и сохраняя jmpbuf в глобально доступном месте. Определите функцию, которая не принимает аргументов и просто вызывает longjmp с сохраненным глобальным jmpbuf. Получите адрес этой функции и скопируйте ее в выделенные стеки, где вы оставили место для обратного адреса. Вы можете оставить указатель кадра пустым. Теперь, когда поток возвращается, он перейдет к той функции, которая вызывает longjmp, и прыгайте обратно в основной поток, где вы вызывали setjmp, каждый раз.

Шаг 5 Сразу после основного потока setjmp вы хотите иметь некоторый код, который определяет, какой поток следует переходить к следующему, вытаскивая соответствующий jmpbuf из очереди и вызывая longjmp туда. Когда в этой очереди нет нити, программа завершена.

Шаг 6 Напишите функцию переключения контекста, которая вызывает setjmp и сохраняет текущее состояние в очереди, а затем longjmp на другом jmpbuf из очереди.

Заключение Это основа. До тех пор, пока потоки продолжают вызывать контекстный коммутатор, очередь продолжает переполняться, и запускаются разные потоки. Когда поток возвращается, если есть какие-либо оставленные для запуска, один выбирается основным потоком, и если ни один не остается, процесс завершается. С относительно небольшим кодом у вас может быть довольно простая совместная многозадачность. Есть больше вещей, которые вы, вероятно, захотите сделать, например, реализовать функцию очистки, чтобы освободить стопку мертвой нити и т.д. Вы также можете реализовать preemption с использованием сигналов, но это намного сложнее, потому что setjmp не сохраняет регистр с плавающей запятой состояния или регистров флагов, которые необходимы, когда программа прерывается асинхронно.

Ответ 2

Возможно, это немного сгибает правила, но GNU pth делает это. Это возможно, но вы, вероятно, не должны попробовать это самостоятельно, кроме как научное доказательство концепции, используйте pth-реализацию, если вы хотите сделать это серьезно и в удаленной переносной форме - вы поймете, почему, когда вы читаете код создания p-й нити.

(По сути, он использует обработчик сигнала, чтобы обмануть ОС в создании нового стека, а затем longjmp оттуда и держит стек вокруг. Это работает, по-видимому, но отрывочно, как черт.)

В производственном коде, если ваша ОС поддерживает makecontext/swapcontext, используйте их вместо этого. Если он поддерживает CreateFiber/SwitchToFiber, используйте их вместо этого. И помните о неутешительной истине о том, что одно из самых убедительных способов использования сопрограмм, то есть инвертирование контроля за счет выхода из обработчиков событий, вызываемых внешним кодом, является небезопасным, поскольку вызывающий модуль должен быть реентеративным, и вы, как правило, можете Это не доказывает. Вот почему волокна по-прежнему не поддерживаются в .NET...

Ответ 3

Это форма того, что называется переключением контекста пользовательского пространства.

Это возможно, но подвержено ошибкам, особенно если вы используете стандартную реализацию setjmp и longjmp. Одна из проблем с этими функциями заключается в том, что во многих операционных системах они сохраняют только подмножество 64-разрядных регистров, а не весь контекст. Часто этого недостаточно, например. при работе с системными библиотеками (мой опыт здесь с пользовательской реализацией для amd64/windows, которая работала довольно стабильно).

Тем не менее, если вы не пытаетесь работать со сложными внешними кодовыми базами или обработчиками событий, и знаете, что делаете, и (особенно), если вы пишете свою собственную версию на ассемблере, которая сохраняет больше текущего контекста (если вы используете 32-битные окна или linux, это может и не понадобиться, если вы используете некоторые версии BSD, я думаю, что это почти наверняка), и вы отлаживаете его, уделяя пристальное внимание выводам разборки, тогда вы можете быть в состоянии для достижения того, чего вы хотите.

Ответ 4

Как уже упоминалось Шон Огден, longjmp() не подходит для многозадачности, поскольку он может перемещать стек вверх и не может прыгать между различными стеками. Ничего подобного.

Как указано пользователем414736, вы можете использовать getcontext/makecontext/swapcontext функций, но проблема заключается в том, что они не полностью в пользовательском пространстве. Они на самом деле вызов sigprocmask() syscall, потому что они переключаются сигнальная маска как часть переключения контекста. Это делает swapcontext() намного медленнее, чем longjmp(), и вы, вероятно, не хотите медленных совлокальных процедур.

Насколько я знаю, для POSIX-стандартного решения нет эта проблема, поэтому я собрал свои доступных источников. Вы можете найти контекст-манипулирование функции, извлеченные из libtask:
https://github.com/stsp/dosemu2/tree/devel/src/arch/linux/mcontext
Функции: getmcontext(), setmcontext(), makemcontext() и swapmcontext(). Они имеют сходную семантику со стандартными функциями с похожими именами, но они также имитируют семантику setjmp() в том, что getmcontext() возвращает 1 (вместо 0) при переходе с помощью setmcontext().

Кроме того, вы можете использовать порт libpcl, библиотеку сопрограмм:
https://github.com/stsp/dosemu2/tree/devel/src/base/misc/libpcl
При этом возможно реализовать быстрое совместное пространство пользователя резьб. Он работает на Linux, на i386 и x86_64 арках.