Как выполняется условие conditional_wait() на уровне ядра и оборудования/сборки?

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

Было бы здорово, если бы кто-нибудь мог объяснить, как эта процедура conditional_wait() реализована в ядре и уровне аппаратного обеспечения/сборки?

Как блокировка освобождается и повторно используется атомарно? Как это обеспечивает ядро?

Что здесь сон здесь означает? Означает ли это, что контекст переключается на другой процесс/поток?

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

Edit:

Кажется, что "futex" - это парень, который управляет этим сигналом ожидания/сигнала. Чтобы сузить мой вопрос: Как система futex вызывается для ожидания и уведомления переменных условия реализована/работает на низком уровне?

Ответ 1

На высоком уровне (и поскольку вы спрашиваете этот quesiton, высокий уровень - это то, что вам нужно), это не так сложно. Во-первых, вам нужно знать уровни ответственности. Есть в основном 3 слоя:

  • Уровень оборудования - обычно то, что может быть закодировано в одной инструкции ASM.
  • Уровень ядра - то, что ядро ​​ОС
  • Уровень приложения - то, что приложение делает

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

  • атомная арифметика - аппаратное обеспечение может блокировать определенную область памяти (убедитесь, что ни один другой поток не обратился к ней), выполните арифметическую операцию и разблокируйте регион. Это может работать только на арифметике, поддерживаемой чипом (без квадратных корней!) И на размерах, поддерживаемых аппаратным обеспечением.
  • Пункты памяти или ограждения, то есть вводят барьер в потоке инструкций, так что, когда CPU перезаписывает инструкции или использует тайники памяти, они не пересекают эти ограждения, и кеш будет свежим.
  • Условная настройка (compare-and-set) - установить область памяти в значение A, если она равна B, и сообщить о состоянии этой операции (если она установлена ​​или нет)

Это почти весь процессор может сделать. Как вы видите, здесь нет futex, mutex или условных переменных. Этот материал создается ядром, имеющим операции с поддержкой процессора.

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

Когда mutex изначально создается, ядро ​​связывает с ним область памяти. Эта область будет удерживать значение блокировки или разблокировки мьютекса. Позже ядро ​​попросит заблокировать мьютекс, он сначала инструктирует CPU о выделении памяти. Мьютекс должен выступать в качестве барьера, так что все прочитанное /wrttten после того, как мьютекс будет приобретен (или выпущен), будет видимым для остальных процессоров. Затем он использует поддерживаемую процессором команду сравнения и установки для установки значения области памяти в 1, если она была установлена ​​в 0. (есть более сложные повторные мьютексы, но не затрудняйте изображение ими). Гарантируется процессором, что даже если более одного потока пытается сделать это одновременно, только один будет успешным. Если операция завершается успешно, мы теперь "удерживаем мьютекс". После того, как ядро ​​попросит освободить мьютекс, область памяти установлена ​​в 0 (нет необходимости делать это условно, поскольку мы знаем, что мы удерживаем мьютекс!), И выдается еще один барьер памяти. Ядро также обновляет статус мьютекса в нем - см. Ниже.

Если блокировка mutex завершается неудачно, ядро ​​добавляет поток к ним таблицы, которые перечисляют потоки, ожидающие выхода определенного мьютекса. Когда мьютекс освобожден, ядро ​​проверяет, какие потоки (-ы) ждут этот мьютекс, и "расписания" (т.е. Готовятся к выполнению), один из них (в случае, если есть более одного, который будет запланирован или пробужден, зависит от множество факторов, в простейшем случае это просто случайное). Запланированный поток начинает выполняться, снова блокирует мьютекс (в этот момент он может снова сработать!), И цикл жизни продолжается.

Надеюсь, что это делает хотя бы полуощущение:)