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

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

4 ответов


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

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

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

Шаг 3 Вам нужно получить некоторые контексты jmpbuf, которые имеют указатели стека в местах памяти, которые вы только что выделили. Вы можете проверить структуру jmpbuf на своем компьютере, узнать, где он хранит указатель стека. Вызовите setjmp, а затем измените его содержимое так что указатель стека находится в одном из выделенных стеков. Стеки обычно растут вниз, поэтому вам, вероятно, нужен указатель стека где-то рядом с самым высоким местоположением памяти. Если вы пишете базовую программу на языке C и используете отладчик для ее разборки, а затем находите инструкции, которые она выполняет при возврате из функции, вы можете узнать, каким должно быть смещение. Например, с соглашениями о вызове system 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 из очереди.

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


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

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

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


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

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

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


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

Как упоминалось user414736, вы можете использовать getcontext / makecontext / swapcontext функции, но проблема в том, что они не полностью в пространстве пользователя. Они на самом деле вызовите syscall sigprocmask (), потому что они переключаются маска сигнала как часть переключения контекста. Это делает 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, библиотеку coroutine:
https://github.com/stsp/dosemu2/tree/devel/src/base/misc/libpcl
С этим, возможно снабдить быстрый кооперативный потребител-космос нарезка резьбы. Он работает на linux, на i386 и x86_64 арках.