Внутреннее устройство переключения контекста

Я хочу узнать и заполнить пробелы в своих знаниях с помощью этого вопроса.

Итак, пользователь запускает поток (на уровне ядра), и теперь он вызывает yield (системный вызов, как я полагаю). Планировщик теперь должен сохранить контекст текущего потока в TCB (который хранится где-то в ядре) и выбрать другой поток для запуска, загрузить его контекст и перейти к его CS:EIP. Чтобы сузить круг вопросов, я работаю над Linux, работающим на архитектуре x86. Теперь я хочу вдаваться в подробности:

Итак, сначала у нас есть системный вызов:

1) Функция-оболочка для yield помещает аргументы системного вызова в стек. Нажмите адрес возврата и вызовите прерывание, поместив номер системного вызова в какой-либо регистр (скажем, EAX).

2) Прерывание изменяет режим ЦП с пользовательского на ядро ​​и переходит к таблице векторов прерываний, а оттуда к фактическому системному вызову в ядре.

3) Я предполагаю, что сейчас вызывается планировщик, и теперь он должен сохранить текущее состояние в TCB. Вот моя дилемма. Поскольку планировщик будет использовать стек ядра, а не стек пользователя для выполнения своей операции (что означает, что SS и SP должны быть изменены), как он сохраняет состояние пользователя без изменения каких-либо регистров в процессе. Я читал на форумах, что есть специальные аппаратные инструкции для сохранения состояния, но как тогда планировщик получает к ним доступ и кто выполняет эти инструкции и когда?

4) Планировщик теперь сохраняет состояние в TCB и загружает другой TCB.

5) Когда планировщик запускает исходный поток, управление возвращается к функции-оболочке, которая очищает стек, и поток возобновляется.

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


person Bruce    schedule 27.09.2012    source источник


Ответы (3)


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

Итак, когда поток A вызывает sched_yield() и заменяется потоком B, происходит следующее:

  1. Поток A входит в ядро, переходя из пользовательского режима в режим ядра;
  2. Поток A в ядре переключает контекст на поток B в ядре;
  3. Поток B выходит из ядра, переходя из режима ядра обратно в режим пользователя.

Каждый пользовательский поток имеет как стек пользовательского режима, так и стек режима ядра. Когда поток входит в ядро, текущее значение стека пользовательского режима (SS:ESP) и указатель инструкций (CS:EIP) сохраняются в стеке режима ядра потока, а ЦП переключается в стек режима ядра - с помощью системного вызова int $80 механизм, это делает сам ЦП. Остальные значения регистров и флаги также сохраняются в стеке ядра.

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

Когда поток переключает контекст, он вызывает планировщик (планировщик не работает как отдельный поток - он всегда выполняется в контексте текущего потока). Код планировщика выбирает процесс для следующего запуска и вызывает функцию switch_to(). Эта функция по сути просто переключает стеки ядра - она ​​сохраняет текущее значение указателя стека в TCB для текущего потока (называемого struct task_struct в Linux) и загружает ранее сохраненный указатель стека из TCB для следующего потока. На этом этапе он также сохраняет и восстанавливает некоторые другие состояния потока, которые обычно не используются ядром, например регистры с плавающей запятой / SSE. Если переключаемые потоки не используют одно и то же пространство виртуальной памяти (т. Е. Они находятся в разных процессах), таблицы страниц также переключаются.

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

person caf    schedule 03.10.2012
comment
Отличный ответ !! Итак, планировщик использует стек ядра потока, из которого он переключается? Также, пожалуйста, предоставьте несколько источников для ваших замечательных знаний. - person Bruce; 03.10.2012
comment
@Bruce: На мой взгляд, лучший источник - это источник, например switch_to процедура x86. Это помогает читать его вместе с документацией по платформе (например, Руководство разработчика программного обеспечения для архитектур Intel 64 и IA-32, которое находится в свободном доступе от Intel). - person caf; 04.10.2012
comment
@caf Отличный ответ! Значит, регистры пользовательского пространства нигде не сохраняются (кроме SS, CS, EIP, ESP), верно? А где TCB сохраняется в ядре, в куче? - person WindChaser; 07.05.2015
comment
@WindChaser: Возможно, вы пропустили эту часть: Остальные значения регистров и флаги затем также сохраняются в стеке ядра.. task_struct динамически выделяется ядром (хотя ядро ​​на самом деле не имеет понятия кучи) и добавляется в глобальный связанный список задач. - person caf; 08.05.2015
comment
@caf Вы имеете в виду, что регистры типа EAX, EDX, MSR также будут восстановлены из стека ядра? - person WindChaser; 08.05.2015
comment
@WindChaser: Да, см. Сборку SAVE_ALL языковой макрос. MSR являются привилегированными и не сохраняются / не восстанавливаются. - person caf; 08.05.2015
comment
Я думаю, что OP спросил о переключении потоков на уровне ядра и ответе на переключение на уровне пользователя. Я прав? Почему поток уровня ядра должен будет переключиться в режим ядра? - person Amnesiac; 19.09.2015
comment
@Amnesiac: когда OP говорит о потоке уровня ядра, они говорят о потоке управления, который управляется планировщиком ядра, в отличие от потоков, полностью обрабатываемых в пользовательском пространстве (зеленые потоки). - person caf; 20.09.2015
comment
@caf: разве поток уровня ядра не означает, что он работает в режиме ядра? - person Amnesiac; 21.09.2015
comment
@Amnesiac: Не в этом случае - ясно, что означает OP, потому что в пункте 2 говорится о переходе из пользовательского режима в режим ядра. - person caf; 21.09.2015
comment
@caf Когда поток входит в ядро, текущее значение стека пользовательского режима (SS: ESP) и указатель инструкций (CS: EIP) сохраняются в стеке режима ядра потока, а ЦП переключается в режим ядра. stack - с механизмом системных вызовов int $ 80 Как можно получить доступ к стеку режима ядра из пользовательского пространства? Разве это не нарушило бы основные принципы безопасности? - person HighOnMeat; 15.04.2020
comment
@HighOnMeat: это делается самим процессором как определенная часть способа обработки шлюза-ловушки. Чтобы это произошло, ядру пришлось установить шлюз прерывания в таблице дескриптора прерывания, поэтому ядро, по сути, санкционировало его и полностью подготовлено к этому. - person caf; 16.04.2020
comment
@caf Я понимаю эту часть. Однако меня больше всего беспокоит доступ ЦП к области памяти ядра при работе в пользовательском режиме. Ниже Бенуа говорит об еще одном «теневом» sp с «защищенным» стеком пользовательского режима, но это меня еще больше сбивает с толку. Но в этом все же есть смысл. Однако в этом ответе кажется, что доступ к стеку режима ядра осуществляется до входа в ловушку, что означает, что если бы кто-то взял под контроль поток, он мог бы получить доступ к пространству ядра, но не Это?? - person HighOnMeat; 16.04.2020
comment
@HighOnMeat: эти значения помещаются в стек режима ядра с использованием записи в режиме супервизора как части механизма обработки исключений. Код пользовательского режима не может писать в сам стек режима ядра и не может влиять ни на что, кроме записываемых значений. Стек ядра, в который они записаны, не используется ничем другим - текущий ЦП выполнял код пользовательского пространства, а любой другой ЦП, который мог выполнять код пространства ядра в то время, обязательно работает в другом стеке ядра в этот момент. . - person caf; 17.04.2020

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

Подводя итог, можно сказать, что поток имеет (обычно) два стека, а само ядро ​​имеет один. Стек ядра стирается в конце каждого входа ядра. Интересно отметить, что, начиная с версии 2.6, ядро ​​само распределяется по потокам для некоторой обработки, поэтому поток ядра имеет свой собственный стек защищенного уровня помимо общего стека ядра.

Некоторые ресурсы:

  • 3.3.3 Выполнение переключения процессов в разделе Общие сведения о ядре Linux, O'Reilly
  • 5.12.1 Процедуры обработки исключений или прерываний в руководстве Intel 3A (системное программирование). Номер главы может варьироваться от издания к другому, поэтому поиск по «Использование стека при переходах к прерываниям и процедурам обработки исключений» должен помочь вам найти нужный.

Надеюсь на эту помощь!

person Benoit    schedule 28.09.2012
comment
На самом деле я запутался больше, чем раньше :-). Можете ли вы дать ссылку на свой ответ. Это может помочь. - person Bruce; 29.09.2012
comment
Когда вы говорите ... стек переключается из стека пользовательского уровня потока (куда вы вставили аргументы) в стек защищенного уровня потока. Текущий контекст потока, прерванного системным вызовом, фактически сохраняется в этом защищенном стеке., Как он переключает указатель стека, чтобы он указывал на стек защищенного уровня, в то же время сохраняя исходный указатель стека (и все регистры) на указанную стопку? - person mclaassen; 13.08.2014
comment
@mclaassen Хороший вопрос; это зависит от арки. Обычно существует 2 указателя стека, управляемых внутренне. В ARM есть 2 регистра указателя стека (sps 'normal' и 'interrupt': psp и msp в документе). На Intel предыдущий SP помещается в стек Ring0 и, таким образом, восстанавливается оттуда. - person Benoit; 13.08.2014

Само ядро ​​вообще не имеет стека. То же самое и с процессом. У него тоже нет стека. Потоки - это только граждане системы, которые рассматриваются как исполнительные единицы. Благодаря этому можно планировать только потоки, и только потоки имеют стеки. Но есть один момент, который интенсивно используется в коде режима ядра - система в каждый момент времени работает в контексте текущего активного потока. Благодаря этому само ядро ​​может повторно использовать стек текущего активного стека. Обратите внимание, что только один из них может одновременно выполнять либо код ядра, либо код пользователя. Из-за этого, когда ядро ​​вызывается, оно просто повторно использует стек потока и выполняет очистку, прежде чем вернуть управление прерванным действиям в потоке. Тот же механизм работает для обработчиков прерываний. Тот же механизм используется обработчиками сигналов.

В свою очередь стек потоков делится на две изолированные части, одна из которых называется пользовательским стеком (потому что она используется, когда поток выполняется в пользовательском режиме), а вторая называется стеком ядра (потому что она используется, когда поток выполняется в режиме ядра). . Как только поток пересекает границу между пользовательским режимом и режимом ядра, ЦП автоматически переключает его из одного стека в другой. Оба стека по-разному отслеживаются ядром и процессором. Для стека ядра ЦП постоянно сохраняет в памяти указатель на вершину стека ядра потока. Это просто, потому что этот адрес постоянен для потока. Каждый раз, когда поток входит в ядро, он обнаруживает пустой стек ядра и каждый раз, когда он возвращается в пользовательский режим, он очищает стек ядра. В то же время CPU не учитывает указатель на вершину пользовательского стека, когда поток работает в режиме ядра. Вместо этого во время входа в ядро ​​ЦП создает специальный кадр стека «прерывания» на вершине стека ядра и сохраняет значение указателя стека пользовательского режима в этом кадре. Когда поток выходит из ядра, ЦП восстанавливает значение ESP из ранее созданного кадра стека «прерывания» непосредственно перед его очисткой. (на устаревшей x86 пара инструкций int / iret обрабатывает вход и выход из режима ядра)

Во время перехода в режим ядра сразу после того, как ЦП создаст кадр стека «прерывания», ядро ​​помещает содержимое остальных регистров ЦП в стек ядра. Обратите внимание, что он сохраняет значения только для тех регистров, которые могут использоваться кодом ядра. Например, ядро ​​не сохраняет содержимое регистров SSE только потому, что оно никогда не коснется их. Точно так же непосредственно перед тем, как попросить CPU вернуть управление обратно в пользовательский режим, ядро ​​выталкивает ранее сохраненное содержимое обратно в регистры.

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

Переключение потоков выполняется только в режиме ядра. Это означает, что и исходящие, и входящие потоки выполняются в режиме ядра, оба используют свои собственные стеки ядра, и у обоих стеки ядра имеют кадры «прерывания» с указателями на вершину пользовательских стеков. Ключевым моментом переключения потоков является переключение между стеками потоков ядра, а именно:

pushad; // save context of outgoing thread on the top of the kernel stack of outgoing thread
; here kernel uses kernel stack of outgoing thread
mov [TCB_of_outgoing_thread], ESP;
mov  ESP , [TCB_of_incoming_thread]    
; here kernel uses kernel stack of incoming thread
popad; // save context of incoming thread from the top of the kernel stack of incoming thread

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

Также обратите внимание, что каждый раз после переключения стека и перед возвратом в пользовательский режим ядро ​​перезагружает сознание процессора новым значением вершины стека ядра. Делая это, он гарантирует, что когда новый активный поток попытается войти в ядро ​​в будущем, он будет переключен процессором на свой собственный стек ядра.

Также обратите внимание, что не все регистры сохраняются в стеке во время переключения потока, некоторые регистры, такие как FPU / MMX / SSE, сохраняются в специально выделенной области в TCB исходящего потока. Ядро использует здесь другую стратегию по двум причинам. Во-первых, не все потоки в системе их используют. Перенос их содержимого в стек и извлечение его из стека для каждого потока неэффективно. А во втором - специальные инструкции для «быстрого» сохранения и загрузки их содержимого. И в этих инструкциях стек не используется.

Также обратите внимание, что фактически часть ядра стека потоков имеет фиксированный размер и выделяется как часть TCB. (верно для Linux, и я верю и для Windows)

person ZarathustrA    schedule 25.10.2016
comment
Не могли бы вы пояснить, где же тогда хранится другая часть пользовательского стека (поскольку кадр прерывания предназначен только для указателя стека) во время парковки потока, то есть WAIT? - person uptoyou; 19.09.2019
comment
Переключение потоков выполняется в режиме ядра. Таким образом, чтобы сделать это, поток должен войти в режим ядра. Однако каждый раз, когда поток переключается из пользовательского режима в режим ядра, ядро ​​сохраняет состояние регистров ЦП в части ядра стека потоков и восстанавливает их при переключении обратно в пользовательский режим. - person ZarathustrA; 23.09.2019
comment
да, спасибо, вы упомянули об этом. Однако есть также локальные переменные, аргументы функции, указатель возврата функции, которые, как я полагаю, находятся в пользовательском стеке. Если да, то где поток переходит в режим ядра, где хранятся эти пользовательские переменные? Я имею в виду те, которые находятся в оперативной памяти, но еще не достигли регистров процессора. - person uptoyou; 24.09.2019
comment
Они хранятся в пользовательской части стека потоков, которая хранится в пользовательской памяти. Когда вы переключаетесь в режим ядра, ядро ​​переключается на часть ядра стека потоков и не использует пользовательскую часть, поэтому все данные в пользовательской части стека сохраняются в том же состоянии. - person ZarathustrA; 24.09.2019