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

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

5 ответов

Решение

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

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

Шаг 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 из очереди.

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

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

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

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

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

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

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

Я сделал что-то подобное для учебы. https://github.com/Kraego/STM32L476_MiniOS/blob/main/Usercode/Concurrency/scheduler.c

Переключение контекста / потока выполняется setjmp / longjmp. Сложность заключалась в том, чтобы правильно распределить стек (см. AllocateStack ()), это зависит от вашей платформы.

Это просто демонстрация того, как это может работать, я бы никогда не использовал это в продакшене.

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

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

Другие вопросы по тегам