Можно ли создавать потоки без системных вызовов в сборке Linux x86 GAS?

При изучении "языка ассемблера" (в Linux на архитектуре x86 с использованием GNU в качестве ассемблера) одним из моментов aha была возможность использовать систему звонки. Эти системные вызовы очень удобны и иногда даже необходимы, поскольку ваша программа работает в пространстве пользователя.
Однако системные вызовы довольно дороги с точки зрения производительности, так как они требуют прерывания (и, конечно же, системного вызова), что означает, что контекстный переключатель должен быть сделан из вашей текущей активной программы в пространстве пользователя в систему, запущенную в пространстве ядра.

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

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

мой вопрос поэтому двоякий (с дополнительным бонусным вопросом под ним):

  • Можно ли написать ассемблер код, который может запускать несколько потоков одновременно на нескольких ядрах один раз, без потребности в системе вызовы?
  • Получаю ли я прирост производительности, если у меня есть действительно крошечные потоки (такие же крошечные, как в общем времени выполнения потока), потеря производительности или вообще не стоит усилий?

Я предполагаю, что многопоточный ассемблерный код невозможен без системных вызовов. Даже если это так, есть ли у вас предложение (или даже лучше: какой-то реальный код) для эффективного внедрения потоков?

Ответ 1

Короткий ответ заключается в том, что вы не можете. Когда вы пишете код сборки, он выполняется последовательно (или с ветвями) на одном и только одном логическом (то есть аппаратном) потоке. Если вы хотите, чтобы какой-то код выполнялся на другом логическом потоке (будь то на том же ядре, на другом ядре на одном CPU или даже на другом процессоре), вам нужно, чтобы ОС установила другой указатель инструкции потока ( CS:EIP), чтобы указать код, который вы хотите запустить. Это подразумевает использование системных вызовов, чтобы заставить ОС делать то, что вы хотите.

Пользовательские потоки не будут предоставлять вам поддержку потоковой передачи, потому что все они работают на одном и том же аппаратном потоке.

Изменить: Включение Ира Бакстера в ответ с Parlanse. Если вы убедитесь, что ваша программа имеет поток, запущенный в каждом логическом потоке, чтобы начать, вы можете создать свой собственный планировщик, не полагаясь на ОС. В любом случае, вам нужен планировщик для обработки переходов из одного потока в другой. Между вызовами планировщика нет специальных инструкций по сборке для обработки многопоточности. Сам планировщик не может полагаться на какую-либо специальную сборку, а скорее на соглашения между частями планировщика в каждом потоке.

В любом случае, независимо от того, используете ли вы ОС, вам все равно придется полагаться на какой-то планировщик, чтобы обрабатывать выполнение кросс-потоков.

Ответ 2

"Доктор, доктор, мне больно, когда я это делаю". Доктор: "Не делай этого".

Короткий ответ: вы можете выполнять многопоточное программирование без вызов дорогостоящих примитивов управления задачами ОС. Просто игнорируйте ОС для потока операции планирования. Это означает, что вам нужно написать свой собственный поток планировщик, и просто никогда не передавайте управление обратно в ОС. (И вы должны быть умнее как-то о ваших потоках накладных расходов чем симпатичные умные OS-ребята). Мы выбрали этот подход именно потому, что процесс windows/thread/ волоконные вызовы были слишком дорогими для поддержки вычислений зерно нескольких сотен инструкций.

Наша программа программирования PARLANSE - это параллельный язык программирования: См. http://www.semdesigns.com/Products/Parlanse/index.html

PARLANSE работает под Windows, предлагает параллельные "зерна" в качестве абстрактного parallelism строить и планировать такие зерна с помощью комбинации настраиваемый ручной планировщик и код планирования, сгенерированный Компилятор PARLANSE, который учитывает контекст зерна для минимизации накладных расходов на планирование. Например, компилятор гарантирует, что регистры зерна не содержат информации в точке где может потребоваться планирование (например, "ожидание" ), и, таким образом, код планировщика должен сохранять только ПК и SP. По факту, довольно часто код планировщика вообще не получает контроля; раздвоенное зерно просто хранит ПК и SP, переключается на сборник-предустановленный стек и прыгает на зерно код. Завершение зерна перезапустит forker.

Обычно существует блокировка для синхронизации зерен, реализована компилятором, используя собственные инструкции LOCK DEC, которые реализуют что составляет подсчет семафоров. Приложения может ловить логически миллионы зерен; лимиты планировщика родительские зерна могут генерировать больше работы, если рабочие очереди достаточно долго, поэтому больше работы не будет полезно. Планировщик реализует кражу работы, позволяя захваченным процессорам, работающим на работе, готовые зерна образуют соседние рабочие очереди ЦП. Это были реализованы для обработки до 32 процессоров; но мы немного обеспокоены что производители x86 могут фактически использовать болото больше, чем что в ближайшие несколько лет!

PARLANSE - зрелая langauge; мы используем его с 1997 года, и реализовали в нем несколько миллионов параллельных приложений.

Ответ 3

Реализация потоковой обработки пользовательского режима.

Исторически моделирование потоков обобщаются как N: M, то есть N потоков пользовательского режима, работающих на M потоках модели ядра. Современное использование - 1:1, но это не всегда так, и это не обязательно должно быть таким.

Вы можете поддерживать в одном потоке ядра произвольное количество потоков пользовательского режима. Это просто то, что вы несете ответственность за то, чтобы переключаться между ними достаточно часто, чтобы все выглядело одновременно. Ваши потоки, конечно, скорее кооперативны, чем упреждающие; вы в основном раскалываете вызовы yield() во всем своем собственном коде, чтобы обеспечить регулярное переключение.

Ответ 4

Если вы хотите повысить производительность, вам придется использовать потоки ядра. Только ядро ​​может помочь вам запустить код одновременно на нескольких ядрах ЦП. Если ваша программа не связана с вводом-выводом (или выполнением других операций блокировки), то совместная многопоточность пользовательского режима (также известная как волокна) не является чтобы получить вам любую производительность. Вы просто будете выполнять дополнительные переключатели контекста, но один процессор, который работает в вашем реальном потоке, по-прежнему будет работать на 100% в любом случае.

Системные вызовы стали быстрее. Современные процессоры поддерживают инструкцию sysenter, которая значительно быстрее, чем старая команда int. См. Также в этой статье о том, как Linux делает системные вызовы самым быстрым способом.

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

Ответ 5

Совсем немного поздно, но я сам был заинтересован в этой теме. На самом деле, нет ничего особенного в потоках, которые специально требуют, чтобы ядро ​​вмешивалось в EXCEPT для распараллеливания/производительности.

Обязательный BLUF:

Q1: Нет. По крайней мере, первоначальные системные вызовы необходимы для создания нескольких потоков ядра в различных ядрах процессора/гиперпотоках.

Q2: Это зависит. Если вы создаете/уничтожаете потоки, выполняющие крошечные операции, вы теряете ресурсы (процесс создания потоков значительно превысит время, используемое протектором перед его выходом). Если вы создаете N потоков (где N - это количество ядер или гиперпотоков в системе) и повторно задайте их, тогда ответ МОЖЕТ быть дам в зависимости от вашей реализации.

Q3: ВЫ МОЖЕТЕ оптимизировать работу, если вы заранее запомните точный метод операций заказа. В частности, вы можете создать то, что составляет ROP-цепочку (или цепочку прямого вызова, но на самом деле это может оказаться сложнее реализовать). Эта ROP-цепочка (выполняемая потоком) будет непрерывно выполнять инструкции "ret" (в свой собственный стек), где этот стек непрерывно добавляется (или добавляется в том случае, когда он переходит к началу). В такой (странной!) Модели планировщик сохраняет указатель на каждый поток "конец ROP-цепочки" и записывает в него новые значения, посредством чего код перемещается по функциональному коду памяти, что в конечном итоге приводит к команде ret. Опять же, это странная модель, но тем не менее она интригует.

В моем содержании на 2 цента.

Недавно я создал то, что эффективно работает как потоки в чистой сборке, управляя различными областями стека (созданные через mmap) и поддерживая выделенную область для хранения информации управления/индивидуализации для "потоков". Возможно, хотя я и не проектировал его таким образом, чтобы создать единый большой блок памяти через mmap, который я разделяю на каждую область 'private'. Таким образом, потребуется только один системный стол (хотя защитные страницы между ними были бы умными, для них потребовались бы дополнительные системные вызовы).

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

Для реализации этой системы (полностью в пользовательском пространстве, а также при отсутствии доступа к корневому каталогу при необходимости) потребовалось следующее:

Понятие о том, какие потоки сводятся к: Стек для операций стека (как я сам объясняю и очевидный) Набор инструкций для выполнения (также очевидный) Небольшой блок памяти для хранения содержимого отдельного регистра

Что планировщик сводит до: Менеджер для серии потоков (обратите внимание, что процессы никогда не выполняются, просто их поток (ы)) в упорядоченном списке планировщика (обычно приоритет).

Переключатель контекста потока: МАКРО, введенный в различные части кода (я обычно помещаю их в конец сверхпрочных функций), который примерно равен "потоку", который сохраняет состояние потока и загружает другое состояние потока.

Таким образом, действительно возможно (полностью в сборке и без системных вызовов, отличных от начального mmap и mprotect), чтобы создавать в потокоподобных конструкциях usermode в некорневом процессе.

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

Ответ 6

Сначала вы должны научиться использовать threds в C. На GNU/Linux вы, вероятно, захотите использовать потоки Posix или потоки GLib. Затем вы можете просто вызвать C из кода сборки.

Вот несколько указателей:

Ответ 7

Системные вызовы теперь не такие медленные, с syscall или sysenter вместо int. Тем не менее, при создании или уничтожении потоков будут накладные расходы. Когда они запущены, системных вызовов нет. Нитки пользовательского режима на самом деле вам не помогут, поскольку они работают только на одном ядре.