реализация линукс-потока

Linux

предисловие

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

Все потоки внутри одного и того же процесса совместно используют глобальные переменные:

  • технологическая инструкция
  • большинство данных
  • открытые файлы (т.е. дескрипторы)
  • Функции обработки сигналов и обработки сигналов
  • текущий рабочий каталог
  • Идентификатор пользователя и идентификатор группы

Но у каждой темы свои:

  • идентификатор потока
  • набор регистров, включая программный счетчик и указатель стека
  • errno
  • сигнальная маска
  • приоритет

Linux — это операционная система, соответствующая стандарту POSIX, поэтому Linux также необходимо предоставить реализацию потока, соответствующую стандарту POSIX. Механизмом потоков в исходной системе Linux был LinuxThreads, а NPTL (собственная библиотека потоков POSIX) была добавлена ​​после версии 2.6.

Потоки ядра и пользовательские потоки

Для механизма реализации потока обычно можно выбрать его реализацию в ядре или вне ядра.Разница между этими двумя методами заключается в том, планируется ли поток в ядре или вне ядра. Планирование в ядре больше подходит для одновременного использования многопроцессорных ресурсов.Ядро может планировать выполнение разных потоков одного и того же процесса на разных процессорах.Когда поток заблокирован, ядро ​​может назначать процессор другому тому же процессу. нить. Накладные расходы на переключение контекста при планировании вне ядра ниже, потому что переключение потоков не должно попадать в состояние ядра.

Модель процесса-потока

Когда ядро ​​поддерживает как процессы, так и потоки, может быть реализована модель потока-процесса «многие ко многим», то есть поток процесса планируется ядром, и в то же время он также может использоваться как планировщик пула потоков пользовательского уровня. Выберите соответствующий поток пользовательского уровня для запуска в его пространстве. Это может не только удовлетворить потребности многопроцессорных систем, но и минимизировать накладные расходы на планирование.

Потоки, реализованные вне ядра, можно разделить на две модели: «один-к-одному» и «многие-к-одному». В первой используется один процесс ядра, соответствующий одному потоку, а планирование потоков эквивалентно планированию процессов, а передается ядру на доработку, тогда как у последнего многопоточность реализована полностью вне ядра, а планирование также осуществляется в пользовательском режиме. Последнее является реализацией упомянутой выше простой модели потоков на уровне пользователя.Очевидно, что этому планировщику потоков вне ядра на самом деле нужно только завершить переключение стека выполнения потоков, а накладные расходы на планирование очень малы, но при в то же время, потому что все сигналы ядра основаны на процессе, поэтому он не может найти поток, поэтому эту реализацию нельзя использовать в многопроцессорной системе.

Linux легкий процесс

Ядро Linux обеспечивает поддержку только легковесных процессов, что ограничивает реализацию более эффективной модели потоков.Однако Linux фокусируется на оптимизации накладных расходов при планировании процессов, что также в определенной степени компенсирует этот недостаток. В настоящее время модель потока «один к одному» принята механизмом потока Linux, планирование передано ядру, а механизм управления потоком, включая обработку сигналов, реализован на уровне пользователя.

В ядре Linux в версии 2.0.x реализованы упрощенные процессы, и приложения могут использовать унифицированныйcloneИнтерфейс системного вызова, используйте различные параметры, чтобы указать, следует ли создавать облегченный процесс или обычный процесс. В ядре,cloneВызов будет вызван после передачи параметра и интерпретацииdo_fork, эта функция ядра такжеfork,vforkОкончательная реализация системного вызова:

intdo_fork(unsignedlongclone_flags,unsignedlongstack_start,structpt_regs*regs,unsignedlongstack_size);

在do_fork, разные clone_flags приведут к разному поведению (совместное использование разных ресурсов) Ниже перечислены функции нескольких флагов.

CLONE_VMеслиdo_forkуказано, когдаCLONE_VMswitch, пространство памяти созданного легковесного процесса будет указывать на тот же адрес, что и родительский процесс, то есть созданный легковесный процесс будет совместно использовать адресное пространство памяти с родительским процессом.

CLONE_FS

еслиdo_forkуказано, когдаCLONE_FSПереключатель, для облегченных процессов будет совместно использовать тот же корневой каталог, где файловая система и текущая информация о каталоге с родителем. Другими словами, облегченный процесс не отделяет информацию, связанную с файловой системой, любой поток в процессе изменения текущего каталога, корневого каталога и другой информации будет иметь прямое влияние на другие потоки.

CLONE_FILES

еслиdo_forkуказано, когдаCLONE_FILESswitch созданный облегченный процесс и родительский процесс будут совместно использовать открытые файлы. Такое совместное использование позволяет любому потоку получать доступ к открытым файлам, поддерживаемым процессом, и операции над ними напрямую отражаются в других потоках процесса.

CLONE_SIGHAND

еслиdo_forkуказано, когдаCLONE_FILESswitch, облегченный процесс и родительский процесс будут совместно обрабатывать сигнал. То есть дочерний процесс обрабатывает сигналы точно так же, как и родительский процесс, и может изменять друг друга.

Хотя linux поддерживает облегченные процессы, нельзя сказать, что он поддерживает потоки ядра, поскольку «потоки» и «процессы» linux на самом деле находятся на уровне планирования и совместно используют пространство идентификаторов процессов. попытки реализации библиотеки могут только максимально приблизиться к большей части семантики POSIX и по функциям.

Механизм потоков LinuxThreads

LinuxThreads — это библиотека потоков, используемая на платформе Linux. Он реализует модель потока «один к одному», основанную на упрощенном процессе ядра.Одна сущность потока соответствует одному упрощенному процессу ядра, а управление между потоками реализовано в библиотеке функций вне ядра. Для LinuxThreads он использует(CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND)параметры для вызоваcloneСоздайте «потоки», которые представляют общую память, счетчики доступа к общей файловой системе, общие таблицы файловых дескрипторов и общие методы обработки сигналов.

Управление потоками

Первоначальный дизайн LinuxThreads предполагал, что переключение контекста между связанными процессами происходит быстро, так что каждого потока ядра достаточно для обработки множества связанных потоков пользовательского уровня. Одной из хорошо известных функций LinuxThreads является поток управления. В LinuxThreads для каждого процесса специально создается поток управления, который отвечает за обработку работы по управлению, связанной с потоком. Когда процесс вызывается в первый разpthread_createПри создании потока создается и запускается поток управления.

В пространстве процесса поток управления взаимодействует с другими потоками через пару «каналов управления (manager_pipe[2])». Канал создается до создания потока управления. После успешного запуска потока управления чтение канал управления Конец и конец записи соответственно назначаются двум глобальным переменным__pthread_manager_readerа также__pthread_manager_request, после чего каждый пользовательский поток проходит__pthread_manager_requestОтправить запрос в поток управления, но сам поток управления не используется напрямую__pthread_manager_reader, конец канала для чтения (manager_pipe[0]) такой же, как__clone()Один из параметров передается потоку управления.Основная задача потока управления — слушать считываемый конец конвейера и отвечать на взятый из него запрос.

После того, как поток управления выполнит ряд действий по инициализации, он входит в цикл while(1). В цикле поток управляет концом канала чтения, запрашивая ( __poll() ) с тайм-аутом 2 секунды. Перед обработкой запроса проверьте, завершился ли его родительский поток, и если да, завершите весь процесс. Очистите, если есть завершенные дочерние потоки, которые необходимо очистить. Затем следует прочитать запрос в конвейере и выполнить соответствующую операцию (переключатель) в соответствии с типом запроса.

Каждый поток LinuxThreads имеет как идентификатор потока, так и идентификатор процесса, где идентификатор процесса — это номер процесса, поддерживаемый ядром, а идентификатор потока выделяется и поддерживается LinuxThreads.

Ограничения LinuxThreads

LinuxThreads, как правило, рассчитаны на хорошую работу, однако производительность, масштабируемость и доступность могут быть проблематичными в стрессовых приложениях. Давайте взглянем на некоторые ограничения дизайна LinuxThreads:

  • Проблема идентификатора процесса: ядро ​​Linux не поддерживает потоки в полном смысле этого слова.LinuxThreads использует упрощенные процессы, которые имеют то же представление планирования ядра, что и обычные процессы, для реализации поддержки потоков. Эти упрощенные процессы имеют независимые идентификаторы процессов и обладают теми же возможностями, что и обычные процессы, в планировании процессов, обработке сигналов и вводе-выводе. В глазах читателей исходного кода это ядро ​​Linux.cloneПоддержка параметра CLONE_PID не реализована. Согласно определению POSIX, все потоки одного и того же процесса должны иметь общий идентификатор процесса и идентификатор родительского процесса, что невозможно в текущей модели «один к одному».
  • Поток управления легко становится узким местом, что является общей проблемой этой структуры, в то же время поток управления отвечает за очистку пользовательского потока, поэтому, хотя поток управления заблокировал большую часть сигналов, как только поток управления умирает, пользовательский поток должен очищаться вручную, а пользовательский поток не знает о состоянии потока управления, и последующие запросы, такие как создание потока, не будут обрабатываться.
  • Сигнал используется для реализации синхронных примитивов, что влияет на время отклика операции. Кроме того, концепция передающих сигналов до основного процесса нет. Следовательно, это не соответствует методу обработки сигналов в POSIX.
  • Обработка сигналов в LinuxThreads устанавливается для каждого потока, а не для каждого процесса, поскольку каждый поток имеет независимый идентификатор процесса. Поскольку сигнал отправляется в выделенный поток, сигнал сериализуется, то есть сигнал передается через этот поток другим потокам. Это резко контрастирует с требованием стандарта POSIX о параллельной обработке потоков. Например, в LinuxThreads сигналы, отправленные через kill(), доставляются отдельным потокам, а не обрабатываются коллективно. Это означает, что если есть поток, блокирующий сигнал, LinuxThreads может поставить поток в очередь и выполнить обработку только тогда, когда поток открывает сигнал, а не немедленно обрабатывать сигнал, как в других потоках, которые не блокируют сигнал.
  • Поскольку каждый поток в LinuxThreads является процессом, информация об ID пользователя и группы может не быть общей для всех потоков одного процесса. Например, многопоточный процесс setuid()/setgid() может быть разным для разных потоков.
  • Поскольку каждый поток — это отдельный процесс, каталог /proc будет заполнен многочисленными записями процессов, которые на самом деле должны быть потоками.
  • Поскольку каждый поток является процессом, для каждого приложения может быть создано только ограниченное количество потоков.
  • Поскольку метод вычисления локальных данных потока основан на расположении адреса стека, доступ к этим данным медленный. Другим недостатком является то, что пользователь не может надежно указать размер стека, потому что пользователь может случайно сопоставить адрес стека с областью, которая в противном случае предназначена для использования. Концепция роста спроса (также известная как концепция плавающих стеков) была реализована в версии 2.4.10 ядра Linux. До этого LinuxThreads использовал фиксированный стек.

NPTL

NPTL (Native POSIX Thread Library) — это новая реализация потоков Linux, которая преодолевает недостатки LinuxThreads, а также соответствует требованиям POSIX. Он предлагает значительные улучшения как в производительности, так и в стабильности по сравнению с LinuxThreads. Как и LinuxThreads, NPTL также реализует модель «один к одному».

Частично причиной появления NPTL является улучшение LinuxThreads, которое разработано со следующими целями:

  • Эта новая библиотека потоков должна быть совместима с POSIX.
  • Эта реализация многопоточности также должна хорошо работать в системах с большим количеством процессоров.
  • Создание нового потока для небольшого набора задач должно иметь низкие начальные затраты.
  • Библиотека потоков NPTL должна быть бинарно совместима с LinuxThreads.
  • Эта новая библиотека потоков должна использовать преимущества поддержки NUMA.

Преимущества НПТЛ

NPTL обычно принимает решение, похожее на LinuxThreads, ядро ​​по-прежнему видит процесс, и новый поток проходит через него.clone()генерируется системным вызовом. NPTL имеет много преимуществ перед LinuxThreads:

  • NPTL не использует управляемые потоки. Некоторые требования по управлению потоками, такие как отправка сигнала уничтожения всем потокам, являющимся частью процесса, не нужны, эти функции реализует само ядро. Ядро также обрабатывает освобождение памяти, используемой стеком каждого потока. Он даже управляет завершением всех потоков, ожидая очистки родительского потока, что позволяет избежать проблемы зомби-процессов.
  • Поскольку NPTL не использует управляемые потоки, его модель потоков обеспечивает лучшую масштабируемость и механизмы синхронизации в системах NUMA и SMP.
  • Используя библиотеку потоков NPTL с новой реализацией ядра, можно избежать использования сигналов для синхронизации потоков. С этой целью NPTL ввелfutexновый механизм.futexРаботайте с общей областью памяти, чтобы ее можно было совместно использовать между процессами, что обеспечивает механизм межпроцессной синхронизации POSIX. Мы также можем поделитьсяfutex. Такое поведение делает возможной синхронизацию между процессами. Фактически, NPTL включает в себяPTHREAD_PROCESS_SHAREDМакросы, которые позволяют разработчикам разрешить процессам пользовательского уровня совместно использовать мьютексы между потоками разных процессов.
  • Поскольку NPTL совместим с POSIX, он обрабатывает сигналы отдельно для каждого процесса; getpid() возвращает один и тот же идентификатор процесса для всех потоков. Например, если посылается сигнал SIGSTOP, останавливается весь процесс; с LinuxThreads останавливается только поток, получивший этот сигнал. Это позволяет лучше использовать отладчики в приложениях на основе NPTL, таких как GDB.
  • Поскольку у всех потоков в NPTL есть родительский процесс, использование ресурсов (таких как процент ЦП и памяти), сообщаемый родительским процессом, учитывается для всего процесса, а не для потока.
  • Одной из возможностей реализации, представленной библиотекой потоков NPTL, является поддержка ABI (Application Binary Interface). Это помогает достичь обратной совместимости с LinuxThreads. Эта функция достигается за счет использованияLD_ASSUME_KERNELосуществленный.

futex

futex (Fast Userspace muTexes) означает быстрое взаимное исключение пользовательской области Это механизм синхронизации (взаимного исключения), предоставляемый linux для лучшей производительности. futex появился в серии стабильных ядер 2.6.x.

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