Внутренние коммутаторы контекста
Я хочу узнать и заполнить пробелы в моих знаниях с помощью этого вопроса
Итак, пользователь запускает поток (уровень ядра), и теперь он вызывает yield (системный вызов, который я предполагаю)
Планировщик должен теперь сохранить контекст текущего потока в TCB (который где-то хранится в ядре) и выбрать другой поток для запуска и загрузить его контекст и перейти к его CS: EIP.
Чтобы сузить дело, я работаю над Linux, работающим поверх архитектуры x86. Теперь я хочу вдаваться в подробности:
Итак, сначала у нас есть системный вызов:
1) Функция-обертка для yield приведет к вводу аргументов системного вызова в стек. Нажмите адрес возврата и поднимите прерывание с номером системного вызова, нажатым на некоторый регистр (например, EAX).
2) Прерывание изменяет режим CPU от пользователя к ядру и переходит к таблице векторов прерываний, а оттуда к фактическому системному вызову в ядре.
3) Я предполагаю, что планировщик теперь вызван, и теперь он должен сохранить текущее состояние в TCB. Вот моя дилемма. Поскольку планировщик будет использовать стек ядра, а не стек пользователя для выполнения его операции (что означает, что SS и SP должны быть изменены), как он сохраняет состояние пользователя без изменения какого-либо регистра в процессе. Я читал на форумах, что есть специальные аппаратные инструкции для сохранения состояния, но тогда как планировщик получает доступ к ним и кто выполняет эти инструкции и когда?
4) Планировщик теперь сохраняет состояние в TCB и загружает другой TCB
5) Когда планировщик запускает исходный поток, элемент управления возвращается к функции-обертки, которая очищает стек, а поток возобновляет
Боковые вопросы: работает ли планировщик как поток только для ядра (т.е. поток, который может запускать только код ядра)? Есть ли отдельный стек ядра для каждого потока ядра или каждого процесса?
Ответы
Ответ 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 тоже)