Подтвердить что ты не робот

Внутренние коммутаторы контекста

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

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

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

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

  • Тема А переходит в ядро, переходя из режима пользователя в режим ядра;
  • Thread A в контексте ядра - переключается на Thread B в ядре;
  • Thread B выходит из ядра, перейдя из режима ядра обратно в пользовательский режим.

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

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

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

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

Ответ 2

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

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

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

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

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

Ответ 3

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

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

При входе в режим ядра сразу же после того, как CPU создаст фрейм стека прерываний, ядро ​​подталкивает содержимое остальных регистров процессора в стек ядра. Обратите внимание, что это сохраняет значения только для тех регистров, которые могут использоваться кодом ядра. Например, ядро ​​не сохраняет содержимое регистров 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

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

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

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

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