Как на самом деле работают функции schedule()+switch_to() из ядра Linux?

Я пытаюсь понять, как на самом деле работает процесс расписания в ядре Linux. Мой вопрос не об алгоритме планирования. Речь идет о том, как работают функции schedule() и switch_to().

Я попытаюсь объяснить. Я видел это:

Когда процесс исчерпал временной интервал, флаг need_resched устанавливается с помощью scheduler_tick(). Ядро проверяет флаг, видит, что он установлен, и вызывает schedule() (относится к вопросу 1) для переключения на новый процесс. Этот флаг является сообщением о том, что расписание должно быть вызвано как можно скорее, поскольку другой процесс заслуживает запуска. При возврате в пользовательское пространство или выходе из прерывания проверяется флаг need_resched. Если он установлен, ядро ​​вызывает планировщик, прежде чем продолжить.

Заглянув в исходники ядра (linux-2.6.10 — версия, на которой основана книга «Разработка ядра Linux, второе издание»), я также увидел, что некоторые коды могут вызывать функцию schedule() добровольно, давая право на запуск другому процессу. Я видел, что функция switch_to() — это та, которая на самом деле переключает контекст. Я просмотрел некоторые коды, зависящие от архитектуры, пытаясь понять, что на самом деле делает switch_to().

Это поведение вызвало некоторые вопросы, на которые я не смог найти ответы:

  1. Когда switch_to() завершается, какой текущий процесс выполняется? Процесс, вызвавший schedule()? Или следующий процесс, который был выбран для запуска?

  2. Когда schedule() вызывается прерыванием, выбранный для запуска процесс запускается после завершения обработки прерывания (после некоторого RTE)? Или до этого?

  3. Если функция schedule() не может быть вызвана из прерывания, когда устанавливается флаг need_resched?

  4. Когда работает обработчик прерывания таймера, какой стек используется?

Я не знаю, смогу ли я объясниться. Если я не смог, я надеюсь, что смогу сделать это после некоторых ответов (или вопросов). Я уже просмотрел несколько источников, пытаясь понять этот процесс. У меня есть книга "Linux Kernel Development, sec ed", и я тоже ею пользуюсь. Я немного знаю о MIP и архитектуре H8300, если это поможет объяснить.


person derf    schedule 29.06.2011    source источник


Ответы (1)


  1. После вызова switch_to() стек ядра переключается на стек задачи, названной в next. Изменение адресного пространства и т. д. обрабатывается, например, в context_switch().
  2. schedule() нельзя вызывать в атомарном контексте, в том числе из прерывания (см. проверку в schedule_debug()). Если требуется изменение расписания, устанавливается флаг задачи TIF_NEED_RESCHED, который проверяется в путь возврата прерывания.
  3. См. 2.
  4. Я считаю, что со стеками по умолчанию 8K прерывания обрабатываются любым стеком ядра, который в данный момент выполняется. Если используются стеки 4K, я полагаю, что есть отдельный стек прерываний (автоматически загружаемый благодаря некоторой магии x86), но я не совсем уверен в этом.

Чтобы быть немного более подробным, вот практический пример:

  1. Происходит прерывание. ЦП переключается на процедуру обработки прерываний, которая помещает номер прерывания в стек, а затем переходит к общий_прерыватель
  2. common_interrupt вызывает do_IRQ, который отключает вытеснение, затем обрабатывает IRQ
  3. В какой-то момент принимается решение о смене задач. Это может быть от прерывания таймера или от вызова пробуждения. В любом случае вызывается set_task_need_resched, устанавливая флаг задачи TIF_NEED_RESCHED.
  4. В конце концов ЦП возвращается из do_IRQ в исходном прерывании и переходит к Путь выхода IRQ. Если это IRQ было вызвано из ядра, оно проверяет, установлен ли TIF_NEED_RESCHED, и если да, то вызывает preempt_schedule_irq, который ненадолго разрешает прерывания при выполнении schedule().
  5. Если IRQ был вызван из пользовательского пространства, мы сначала проверяем есть ли что-нибудь, что нужно сделать перед возвратом. Если это так, мы переходим к retint_careful. , который проверяет как ожидающее перепланирование (и напрямую вызывает schedule(), если необходимо), так и проверку ожидающих сигналов, а затем возвращается к следующему раунду в retint_check, пока не будут установлены более важные флаги.
  6. Наконец, мы восстанавливаем GS и возвращаемся из обработчика прерывания .

Что касается switch_to(); что делает switch_to() (на x86-32):

  1. Сохраните текущие значения EIP (указатель инструкции) и ESP (указатель стека), чтобы вернуться к этой задаче позже.
  2. Переключите значение current_task. В этот момент current указывает на новую задачу.
  3. Переключитесь на новый стек, затем поместите EIP, сохраненный задачей, на которую мы переключаемся, в стек. Позже будет выполнен возврат, используя этот EIP в качестве адреса возврата; вот как он возвращается к старому коду, который ранее вызывал switch_to()
  4. Вызовите __switch_to(). В этот момент current указывает на новую задачу, и мы находимся в стеке новой задачи, но различные другие состояния ЦП не были обновлены. __switch_to() обрабатывает переключение состояния таких вещей, как FPU, дескрипторы сегментов, регистры отладки и т. д.
  5. По возвращении из __switch_to() возвращается адрес возврата, который switch_to() вручную поместил в стек, возвращая выполнение туда, где оно было до switch_to() в новой задаче. Теперь выполнение переключенной задачи полностью возобновлено.

x86-64 очень похож, но должен немного больше сохранять/восстанавливать состояние из-за другого ABI.

person bdonlan    schedule 29.06.2011
comment
Извините, я все еще не понимаю. Пример: предположим, что у нас запущена задача «А». 1 - происходит прерывание таймера. 2 - Запускается обработчик прерывания таймера. 3 - Пришло время вызвать schedule() и мы это делаем. 4 - Задача 'B' была выбрана функцией schedule(), функция switch_to() уже завершилась, а задача 'B' является текущей задачей (теперь мы используем стек задачи 'B' и все еще выполняем код прерывания). 5 - Прерывание таймера завершается и возобновляется выполнение задачи «B». Этот пример правильный? Если нет, то как происходит процесс? - person derf; 30.06.2011
comment
Спасибо за ваше время и терпение, но я думаю, что я что-то упускаю. Вы предоставляете мне больше информации, чем я могу обработать в данный момент (и я, конечно, очень благодарен за это). Не могли бы вы объяснить в более общем виде? Чего я действительно не понимаю, так это того, что происходит, когда switch_to() завершается? Начинается ли выполнение выбранной задачи (следующей) в этот момент до возврата кода прерывания? какой стек является текущим на данный момент? - person derf; 30.06.2011
comment
Думаю, я понял. Честно говоря, у нас нет прерываний, давайте проигнорируем все остальные детали и будем работать только с switch_to(). Предположим, у меня есть две задачи, A и B, каждая из которых содержит 10 инструкций. Задача A выполняет инструкцию 3, а задача B — инструкцию 6. Задача A — текущая задача. Итак, задача A вызывает switch_to(), оставляя задачу A на инструкции 3 и возобновляя задачу B на инструкции 6. Задача B переходит на 7 и вызывает switch_to(), оставляя B на 7 и возобновляя A на 3. Задача A переходит на 4, снова вызывает switch_to(), и процесс продолжается. Это правильно? - person derf; 30.06.2011
comment
Более или менее, за исключением того, что там есть еще несколько слоев вызовов функций, поэтому на практике есть только несколько точек, к которым возвращается switch_to. - person bdonlan; 30.06.2011
comment
Но когда switch_to() переходит от задачи A к задаче B, задача B фактически начинает выполняться после переключения? Потому что я понимаю, если это происходит от одной задачи к другой, но если switch_to() вызывается из прерывания (например, из schedule()), мне кажется, что это оставит код прерывания до она завершается и сразу переходит к новой задаче. Если бы это было правильно, новая задача запускалась бы в режиме ядра, несмотря ни на что, потому что IRET не выполнялся бы. Есть что-то, что я упускаю, что делает этот сценарий бессмысленным. - person derf; 30.06.2011
comment
Сам switch_to() никогда не переходит от пользователя к ядру. Когда переключение происходит при возврате из прерывания, после того, как оно возвращается позже, оно все равно проходит через остальную часть кода возврата из прерывания, который выполняет фактический переход между ядром и пользователем. - person bdonlan; 30.06.2011
comment
Хорошо, я знаю, что switch_to() не выполняет переход между режимами ядра‹-›пользователя. Давайте сделаем это по шагам, я довольно медленный, как вы можете видеть. РЖУ НЕ МОГУ. Задача A вызывает schedule(), задача B выбирается следующей и вызывается switch_to(). Запускается ли задача B после возврата switch_to()? Действительно ли switch_to() запускает код задачи B? - person derf; 30.06.2011
comment
Это весь код ядра, поэтому сам код трудно назвать «задачей А» или «задачей Б». Он имитирует вызов функции в __switch_to из задачи B, поэтому в результате, когда он возвращается, он возвращается туда, откуда ему нужно возобновить работу в задаче B. - person bdonlan; 30.06.2011
comment
Я понимаю, что это весь код ядра. То, что я пытался сделать, это изолировать шаги. Допустим, мы говорим о потоках, а не о задачах. Может ли один поток ядра вызывать schedule() для переключения на другой поток ядра? (при условии, что у нас есть только эти потоки ядра). - person derf; 30.06.2011
comment
Конечно. Не гарантируется на самом деле переключение просто вызовом schedule() (другой поток может быть спящим, или он может быть на другом процессоре, или что-то в этом роде). Но он может попросить планировщик переключиться на какой-то другой поток, и если условия правильные, он действительно переключится. - person bdonlan; 30.06.2011
comment
Можем ли мы поговорить по электронной почте? Я думаю, что я делаю что-то не так здесь.. Я думаю, что это не должно было быть чатом, и я не могу перенести это обсуждение в чат. Я могу показать вам свою электронную почту, если вы согласны, конечно. - person derf; 30.06.2011
comment
@derf, набери 20 очков репутации, затем нажми «автоматически переместить это обсуждение в чат» :) Или, если хочешь, зайди на #c++ @ irc.rizon.net и найди bd_ - person bdonlan; 30.06.2011
comment
Хорошо, я думаю, что я почти там. Итак, давайте пока оставим в стороне подробности о другом процессоре или спящем режиме.. давайте представим, что нам нужно запускать потоки на одном и том же процессоре.. когда один поток вызывает schedule(), другой поток возобновляется в какой именно точке? в конце switch_to()? (извините за повторение, мне нужно ясно видеть это, чтобы гарантировать, что мы находимся на одной странице, поэтому я могу объяснить свою точку зрения) - person derf; 30.06.2011
comment
Когда вы сосредоточены так далеко, трудно точно ответить, в какой точке возобновляется другая нить. Он возобновляется, шаг за шагом загружается его состояние. Когда вы считаете, что он возобновился? Когда current обновится? Когда его стек используется? Когда состояние FP было восстановлено? Когда находится его таблица страниц? Когда он переходит в состояние R=Running? Когда функция, приостановившая его, возобновляет выполнение? - person bdonlan; 30.06.2011
comment
Когда функция, которая приостановила ее, возобновляет выполнение. На этом все остальное сделано, верно? - person derf; 30.06.2011
comment
Киндасорта. Сам switch_to() встроен в context_switch(), поэтому в зависимости от того, что вы называете «функцией, которая ее приостановила», либо после возврата __switch_to(), либо после возврата context_switch(), либо после возврата schedule()... - person bdonlan; 30.06.2011
comment
Итак, я просто перехожу с одной позиции на другую из context_switch(), верно? что-то вроде этого: поток A вызывает schedule(), и в какой-то момент выполнение потока A приостанавливается, переключаясь на поток B.. позже поток B вызывает schedule(), и в какой-то момент поток B приостанавливается, переключаясь в поток A, который возобновляется в какой-то момент schedule() внутри кода потока A (потому что он встроен).. schedule() возвращается из последнего вызова, а поток A продолжает работать.. это правильно? - person derf; 30.06.2011
comment
Ладно, надеюсь, мы почти у цели :) Что в этом случае больше или меньше? - person derf; 30.06.2011
comment
Все еще использую последний пример. Я думал о двух потоках, вызывающих schedule() добровольно.. Итак, поток B вызвал schedule(), приостановился в какой-то точке schedule() и переключился на какую-то точку schedule() из поток A.. теперь, во время выполнения потока A, произошло прерывание таймера, и оттуда был вызван schedule() (это снова переключится обратно на поток B).. но если я вызову schedule() до полного завершения прерывания , это не оставит прерывание незавершенным и не возобновится до некоторой точки schedule() внутри потока B? - person derf; 30.06.2011
comment
@derf, обработчик прерывания будет вызываться до расписания. Часть кода очистки прерывания будет отложена до тех пор, пока не будет запущена снова. В любом случае, это становится слишком длинным, если вам нужны дополнительные разъяснения, либо прыгайте в IRC, либо найдите достаточно представителей, чтобы использовать чат SO (достаточно еще одного голоса), или задайте другой дополнительный вопрос :) - person bdonlan; 30.06.2011
comment
Хорошо, я пытаюсь набрать достаточное количество репутации :) - person derf; 30.06.2011
comment
@bdonlan позвольте нам продолжить это обсуждение в чате - person derf; 30.06.2011