[Перевод] Что такое PID 0
Как говорится, «не будите во мне ботана». Иногда кто-нибудь беспечно задаст мне, казалось бы, невинный вопрос — и я убиваю следующие несколько часов (в описываемом случае — дней), чтобы полноценно сформулировать ответ. Обычно всё это заканчивается с моей стороны очередной филиппикой на mastodon или в каком-нибудь приватном чате. Но на сей раз не буду этим ограничиваться и напишу целый пост.
Вот с какого невинного вопроса всё началось:
А почему UID начинаются с 0, но PID начинаются с 1?
Если совсем коротко: в Unix PID (идентификаторы процессов) начинаются именно с 0! PID 0 просто не отображаются в пользовательском пространстве через традиционные API. PID 0 запускает ядро, а затем практически уходит на покой, только немного участвует в работе планировщика процессов и в управлении питанием. Кроме того, на просторах Интернета доминирует заблуждение о PID 0, всё из-за одного ошибочного утверждения в Википедии, которому уже 16 лет.
В заключении к посту я дам несколько расширенную версию этого короткого ответа, но если хотите до него дойти вместе со мной — давайте разберём достаточно длинную среднюю часть.
Но, конечно же, любой желающий может просто загуглить, что такое PID 0, верно? Зачем мне вообще всё это писать?
В Интернете кто-то неправ
Если бы на момент подготовки оригинала этого поста вы попробовали справиться в Интернете по поводу PID 0, то получили бы смесь неверной и превратной информации, а верных ответов почти не получите.
Докопавшись до истины, я спросил в Google, Bing, DuckDuckGo и Kagi, что такое PID 0 в Linux. Посмотрел топ-20 результатов в каждом поисковике, а заодно в развёрнутых описаниях и органически сформированных облаках слов, которые мне выдал ИИ. Например, Google выдал мне 2 страницы результатов.
Во всех этих выдачах полного и при этом правильного ответа я не получил. Как правило, среди первых 20 результатов попадался частично верный ответ, но совершенно не в топе и не в рекомендациях. Лучше всего справился DuckDuckGo — он выдал правильный ответ под номером 4. Хуже всех получилось у Google — правильного ответа не было вообще. В любом случае, неверные ответы настолько преобладали и согласовывались друг с другом, что, попав на сайт с верным ответом, вы бы всё равно ему не поверили.
Что интересно, в топ-2 все поисковики выдали одни и те же ресурсы. Первым шёл ответ на stackoverflow, неверный, а также явно спамерский сайт, где явно используют LLM и не стесняются этого. То, что явление PID 0 никак не удаётся объяснить правильно, отчасти связано со следующим фактом: разговор в какой-то момент соскальзывает на тему PID-циклов. Переходят к теории управления системами, а потом вырывают из неё один абзац, после чего снова возвращаются к PID Unix.
Если перейти к первоисточнику этого уклона в сторону LLM, то дела пойдут лучше, так как большая языковая модель крадёт информацию из книг, с сайтов, но всё равно большая языковая модель выдаёт типичную дозу бреда. Мне удалось вывести нейросеть на верный ответ, формулируя для неё промпт именно так, как это обычно делается —, но я уже знал верный ответ и пытался до тех пор, пока у меня не получался качественный граф относительных окрестностей (RNG).
Если сразу исключить несколько совершенно неверных ответов («никакого PID 0 нет», «он запускает init, а затем выходит», «он входит в состав system», «это всё ядро целиком», «он крутится в бесконечном цикле и ничего более»), то наиболее распространённый ответ развивается в следующем ключе: PID 0 как-то связан со страницами памяти, пространством подкачки, управлением виртуальной памяти.
Вся эта тема начинается — с чего бы вы думали? — с посвящённой PID статьи Википедии , где сказано:
Часто имеется два специальных значения PID: swapper или sched — процесс с номером 0 (часто не отображается в списке), отвечает за выгрузку страниц и является частью ядра операционной системы. Процесс 1 — обычно является процессом init, ответственным за запуск и остановку системы.
Этот текст фигурирует в Википедии уже 16 лет и за это время его кто только не цитировал, не перефразировал и не искажал в Вебе до тех пор, пока на месте истины не осталась одна ложь. Текст менялся до смешного быстро, и это даже немного грустно видеть, поскольку исходный код Linux и BSD открыт — просто зайдите и проверьте сами.
Чтобы понять, почему Википедия в данном случае неточна, нам понадобится сделать небольшой исторический экскурс.
История PID 0 в Unix
Как было сказано над катом, PID 0 имеет отношение к планированию и управлению питанием, но не к подкачке страниц. Планировщик запускает именно этот процесс, когда ядру ЦП больше нечего делать.
Естественно, конкретная реализация будет варьироваться от ядра к ядру и от версии к версии, но все те, которые мне доводилось наблюдать, следуют одному обширному паттерну: когда приходит очередь запускать PID 0, нужно поискать, нет ли какой-нибудь другой задачи, которую можно выполнить на данном ядре. Если такую задачу найти не удаётся, то данное ядро ЦП отправляется спать до тех пор, пока какой-то другой процесс не разбудит его, после чего пройдёт полный цикл, и ядро не возобновит работу.
Но, как говорится, «это не точно». В ядре Linux есть do_idle, вызываемый PID 0 в виде бесконечного цикла. nohz_run_idle_balance
пытается найти ещё какую-то работу. Цикл while отправляет ядро спать. После пробуждения schedule_idle передаёт управление планировщику, и ядро вновь приступает к работе.
Но, скажете вы, может быть, всё это характерно только для Linux? Ладно, другой пример. В ядре FreeBSD есть sched_idletd. Вызов tdq_idled пытается утащить готовые для выполнения задачи с другого ядра. Если и это сделать не получается, cpu_idle отправляет ядро спать. Затем повторить сначала.
Разумеется, всё это современные ядра; так может быть, в старину всё было иначе? Хорошо, давайте тогда поговорим о sched в 4.3BSD, версия выпущена летом 1986 года. Компьютеры уменьшаются, операционные системы тоже становятся компактнее, так, что сейчас и планировщик, и цикл ожидания умещаются в одну процедуру. Планировщик пытается найти что-нибудь, что можно назначить на выполнение. Если это не удаётся, то включается sleep и процесс засыпает до тех пор, пока не будет разбужен каким-нибудь внешним событием.
Кстати, отсюда и проистекает то смутное представление, будто PID0 и есть та самая сущность, которая называлась «sched»: действительно, в старых версиях Unix та функция, которая реализовывала PID 0, так и называлась: sched
.
Остались сомнения? Может быть, это какая-то причуда BSD, которая случайно перетекла в Linux?
Что ж, хорошо, вот sched в Unix V4 — это первая известная версия ядра Unix, написанная на C. В ней, опять же, планировщик и цикл ожидания тесно переплетены, а также есть некоторые мудрёные вещи из PDP-11, которые только попутают нашего современника. Но, в сущности, задача не меняется: нужно найти готовый для выполнения процесс и переключиться на него, либо подождать и попробовать снова.
Можно было бы углубиться в прошлое ещё сильнее. Есть исходный код Unix V1, а также ранний прототип PDP-7. Но весь этот код написан на ассемблере для PDP, и там используется некоторая мнемоника, которая, по-видимому, не рассматривается в ссылках из «курса молодого бойца по ассемблеру», которые мне удалось найти. Ядро же структурировано существенно иначе, нежели в версии, написанной на C.
Если, с учётом всего сказанного, вы всё-таки хотите докопаться до истины — скажу, что, на мой взгляд, самая мякотка в планировщике — это процедура swap. Наконец-то ясно вырисовывается, откуда растёт этот тезис, заявленный в Википедии: в самой ранней реализации Unix планировщик иногда называли «сваппером».
А называли его так (и это мы понимаем теперь, дойдя до основ Unix), потому что в одной процедуре были заключены не только планировщик и цикл ожидания, но и функционал перемещения целых образов процессов, перенос их из малой памяти ядра во вторичное хранилище. Это подтверждается и на уровне жёстких дисков, и в ссылках в коде ядра, а также в Вики-истории компьютеров: оказывается, что в стандарте PDP-11, который в ту пору использовался в Лабораториях Белла, ядро ОС и подкачка процессов выполнялись на диске RS11, тогда как пользовательская файловая система выполнялась на RK03.
(Реплика в сторону! Именно с этого и начинается разделение / vs. /usr. /usr входил в состав раннего Unix и хранился на диске RK03, тогда как меньшая файловая система root находилась на RS11. Если вы только не работаете в соответствии с PDP-11 с отдельными дисками RS11 и RK03, разделение /usr — устарело, из-за него только возникают различные проблемы на раннем этапе загрузки).
Итак, надеюсь, с историей теперь всё вполне прояснилось. В первой версии Unix, попавшей в широкое использование (Unix V5), таблица процессов начиналась с нуля, и этот процесс номер ноль инициализировал ядро, а затем крутился в функции sched, определённой в slp.c. В этих двух названиях легко угадывается, какие две основные функции первоначально выполнял этот процесс. Тем не менее, на данном этапе алгоритм планировщика задач ещё достаточно прост, поэтому почти весь код sched занят переносом процессов из памяти ядра в хранилище и обратно — собственно, именно так и обеспечивается планирование процессов. Разумно трактовать эту функцию как «сваппер» (подкачиватель), хотя в оригинале исходного кода такое название и ни раз не фигурирует.
В сущности, именно в таком виде эта структура и сохранилась до наших дней, из-за чего лишь возникают дополнительные осложнения. Перенос целых процессов привёл к тому, что была внедрена подкачка страниц по требованию, так что PID 0 вообще потерял какое-либо отношение к управлению памятью. Поскольку усложнились как алгоритмы работы планировщика, так и механика перевода ядра ЦП в режим ожидание, планирование и ожидание были разнесены в разные фрагменты кода. Получилась именно такая структура, с которой мы имеем дело вот уже пару десятилетий: в названии функции, реализующей PID 0, есть sched
или idle
, и она играет вспомогательную роль при выполнении двух этих задач.
В самом ли деле эти функции обладают PID 0?
Выше я заявляю (не подкрепляя своих слов), что те функции, на которые я ссылаюсь — это PID 0. Если бы я проследил их все, история получилась бы гораздо более многословной, но я продемонстрирую все пункты на примере Linux, а вам оставляю проследить их в других системах. Правда, попробуйте! Примечательно, насколько схожи ядра в самых разных системах, действующих в этой области, как в синхронии, так и в диахронии. Ядра усложняются, но их принадлежность к общему родословному древу по-прежнему очевидна.
Оговорюсь: ядро Linux — штука очень сложная. Я не буду разбирать все до одной операции, которые совершаются в ядре до того, как мы достигнем do_idle. Считайте их наводками, которые помогают сориентироваться, а не подробной дорожной картой. Эта статья была написана по материалам исходного кода ядра 6.9, поэтому, если вы пришли из будущего почитать мою статью — привет! Надеюсь, ваша дилитиевая матрица работает нормально, а в Linux, наверное, что-то могло измениться.
Начнём! Загрузчик переходит к первой инструкции в коде ядра. Первые несколько шагов, которые нам сейчас предстоят, крайне специфичны в каждой конкретной архитектуре процессора и аппаратной части чипсета, в который он встроен. Я всё это пропущу и начну со start_kernel. Эта функция может считаться общим знаменателем, после которого к работе приступает код, не зависящий от ядра (правда, ему всё равно сопутствуют архитектурно-специфичные вспомогательные функции).
На этот момент start_kerne
l — единственная функция, работающая на машине (да, мне известно об уровне привилегий ring минус 1, о режиме системного управления (SMM), но, как я уже говорил, я упрощаю). На многоядерных системах загрузчик/прошивка/аппаратное обеспечение устроены так, чтобы одно ядро ЦП всегда работало. Оно называется «загрузочным ядром». Мы в данном случае рассматриваем единственный поток выполнения, и только он у нас и будет, пока ядро само не запустит все остальные ядра.
Первым делом вызывается set_task_stack_end_magic(&init_task)
. Да, она выглядит важной! Это очень простая функция, записывающая магическое число сверху в пространство стека init_task — это нужно, чтобы выявлять случаи переполнения. Функция init_task статически определяется в init_task.c, а начинается она с комментария, согласно которому эта задача — первая. Но что это за задача?
task_struct, PID TID TGID и… о, нет
Здесь нам потребуется отойти от темы и разобрать один очень запутанный вопрос: ядро Linux и его же пользовательское пространство по-разному трактуют значение PID.
Единичная конструкция, используемая в ядре для выполнения задач — это task_struct. В ней представлен не весь процесс, а всего один поток выполнения. С точки зрения ядра PID — это идентификатор задачи, а не процесса. task_struct.pid — это идентификатор строго для одного данного потока.
В ядре так или иначе требуется как-то концептуализировать процесс пользовательского пространства, но это не будет отдельная аккуратная структура данных, на которую можно просто указать. Напротив, потоки в данном случае объединяются в «группы», и каждая такая группа сопровождается идентификатором группы потоков, он же TGID. Пользовательское пространство вызывает группы потоков как процессы, и поэтому в пользовательском пространстве TGID ядра называется PID.
Вдобавок, зачастую у них даже одинаковые номера. Когда создаётся новая группа потоков (напр., когда в пользовательском пространстве выполняется fork ()), новому потоку присваивается новый ID потока и, в свою очередь, этот ID становится TGID новой группы. Поэтому у однопоточных процессов TGID и TID ядра идентичны и, если справиться в ядре или в пользовательском пространстве, что это за «PID», в ответ вы получите одно и то же число. Но такая эквивалентность нарушается, как только начинают порождаться новые потоки: каждый новый поток получает свой собственный ID иотока (именно он называется в ядре «PID»), но наследует групповой ID от своего потока-родителя (и этот номер в пользовательском пространстве называется PID).
В довершение всего, теперь приходится работать с контейнерами, и из-за этого вынужденно возникает ситуация, в которой у процессов и потоков оказывается по несколько идентификаторов. Фактически, PID 1 в контейнере Docker — совсем не то же самое, что PID 1 вне контейнера. Этот номер отслеживается в отдельной структуре pid, которая ведёт учёт тех различных ID потоков, которые может иметь task_struct, в зависимости от того, PID из какого пространства имён нас интересует.
Днём я — довольный гражданин пользовательского пространства, но, заглянув в эту кроличью нору, я интерпретировал рассматриваемый «PID 0» как аналог известного мне PID 1, той самой штуки /bin/init. Но теперь вопрос приобретает двойственность! PID 0 может относиться к потоку 0, а может относиться к группе потоков 0.
Когда ядро только начинает работу, ответ, к счастью, прост: в любой трактовке init_task соответствует PID 0. Это поток с ID 0 (с точки зрения ядра это PID), это единственный поток в группе с ID 0 (а это PID с точки зрения пользовательского пространства). При этом, пока не существует никаких дочерних пространств имён PID, так что никаких других номеров у init_task быть не может.
Впоследствии ситуация становится более мутной, поскольку потоков в группе с номером 0 станет больше. Таким образом, с точки зрения пользовательского пространства у нас появится процесс с PID 0, содержащий несколько потоков, и только у одного из этих потоков будет TID 0.
В оставшейся части поста я постараюсь придерживаться такой терминологии: под «задача» или «поток» буду понимать отдельно взятый поток выполнения, ту самую штуку, которая описывается в task_struct
. Под «группой потоков» я буду понимать сущность, которую в пользовательском пространстве назвали бы «процессом». Поверьте, это не только для вас, это и для меня страшная путаница.
Поправка (от автора): от читателей поступили замечания, что выше я допустил ошибки в двух деталях! Благодарю за разъяснения, суть моих ошибок заключается в следующем.
Как правило, TID и TGID действительно могут быть устроены так, как я описал выше. Но можно создать в пользовательском пространстве и такой (новоиспечённый) процесс, единственный первичный поток в котором имеет TID, не совпадающий с его же TGID. Если выполнить функцию execve()
в многопоточном процессе из любого потока кроме первичного, то ядро убьёт все остальные потоки, а тот поток, который сейчас выполняет задачу, будет назначен ведущим в группе. При этом TID потока не изменится, поэтому новый процесс будет выполняться в потоке, чей TID не совпадает с его TGID.
Вторая ошибка более тесно связана с темой этой статьи: в Linux у всех потоков, относящихся к группе потоков 0 ID потока тоже равен 0! Этот случай встречается в нескольких местах и везде помечен как особый. Насколько мне известно, это единственный участок в ядре, где множество безусловно разных потоков имеют «одно лицо».
Ладно, возвращаемся к разбору нашего кода…
Путь к ожидающей задаче
Итак, нам известно, что именно init_task присвоен интересующий нас PID 0, хотя, на самом деле у нас теперь два разных PID 0 одновременно, поскольку сейчас мы говорим о потоке с ID 0, и этот поток относится к группе с ID 0. Откуда нам знать, что init_task описывает контекст именно того ядра ЦП, на котором сейчас выполняется задача?
Обсудим несколько вещей. Известно, что сейчас в системе действует только один поток выполнения, а init_task описывается как первая задача, она же — первый поток. Пока понятно. В качестве стека она использует стек init_stack, и именно с этим стеком мы сейчас работаем (чтобы это доказать, потребовалось бы углубиться в архитектурно-специфичный код и скрипты линковщика gcc, поэтому данное доказательство я пропущу, но, если захотите сами его проделать — что ж, удачи!). Имеем дело с состоянием __state в значении TASK_RUNNING, то есть, эта задача либо выполняется прямо сейчас, либо готова к выполнению и дожидается, пока на неё будет выделено процессорное время. Планировщик ядра пока не инициализирован, поэтому в данный момент у нас просто не может быть другой готовой к выполнению задачи. Да, здесь мы могли бы попасться на изощрённый троллинг, но все факты свидетельствуют, что это именно наша init_task. Спойлер: нет, никакого троллинга, init_task — это в самом деле исходный поток, выполняющий start_kernel.
Именно в этот период выполняется значительная доля ранней инициализации ядра. В рамках нашего поста эти процедуры можно пропустить и сразу перейти к вызову sched_init. Эта функция выполняет базовую инициализацию тех структур данных, что обеспечивают работу планировщика ЦП. Там много всего происходит, поскольку планировщик довольно большая штука. Поэтому мы заглянем только в пару наиболее важных строчек:
/*
* Для функционирования задачи, ожидающей выполнения, не требуется структура kthread,
* но она оформлена как «kthread на ядро»
* и поэтому должна сыграть свою роль, если мы хотим обойтись без её специальной обработки в таком коде, который
* действительно имеет дело с «kthread на ядро»
*/
WARN_ON(!set_kthread_struct(current));
/*
* Сделай нам ожидающий поток. Чисто технически, schedule()
* не должна вызываться из этого потока, однако
* где-то ниже это может произойти. Но, поскольку мы
* находимся в ожидающем потоке, мы просто возобновляем выполнение, как только эта
* очередь на запуск становится «ожидающей».
*/
init_idle(current, smp_processor_id());
В первой строке тот поток, что выполняется прямо сейчас, обозначается как «ожидающая задача» (idle task). При этом упоминается, что это особый поток ядра. За выполнение большинства потоков отвечает kthreadd, а в нашем случае это задача 2, и пока она не существует. Если вы работаете в Linux, то команда ps ax | grep kthreadd покажет, что в пользовательском пространстве у этой kthreadd PID 2 и, к тому же, это поток/задача с ID 2 в ядре.
Во второй строке планировщику прямо сообщается, что поток, выполняемый в настоящий момент, дожидается, пока освободится загрузочное ядро ЦП. current — это указатель на выполняемую в настоящий момент task_struct, а конкретно сейчас речь идёт о init_task. Реализация current — это ещё один фрагмент кода, сильно зависящий от архитектуры, поэтому рекомендую вам повозиться в нём, если вам интересно, а затем снова возвращаться к нашей истории.
Вернувшись к start_kernel, приходим к выводу, что оставшийся код инициализации нас не интересует, поэтому можно переходить непосредственно к вызову rest_init. Эта функция коротка и приятна: она порождает задачу 1, которая в пользовательском пространстве станет init-процессом, а также задачу 2 для kthreadd, которая возьмёт на себя управление всеми потоками ядра, что появятся в будущем.
Мы проследим жизненный цикл задачи 1, и, пусть она когда-нибудь и приобретёт PID 1 в пользовательском пространстве, ей потребуется выполнить kernel_init, чтобы начать работу. Но не сейчас. Эти новые задачи существуют и известны планировщику, но пока они не работают, так как мы ещё не попросили планировщик их запустить. (нюанс: в некоторых конфигурациях ядра планировщику может представиться возможность переключиться на задачи 1 и 2 быстрее, чем будет описано ниже, но эти первые задачи оркеструются так, что результат получается почти одинаковым.)
Наконец, rest_init вызывает cpu_startup_entry, которая переходит в бесконечный цикл, и в нём вызывает do_idle. Вот мы и добрались до момента, когда у нас на загрузочном ядре ЦП крутится задача в режиме ожидания. На первой итерации мы не отправляем ЦП спать, так как ещё остаются другие задачи, готовые к выполнению (те две, которые мы только что создали). Поэтому падаем в самый низ do_idle и заходим в schedule_idle. Планировщик, наконец, начинает работать, и мы можем уйти от задачи 0. kthreadd в задаче 2 не слишком интересна: она делает кое-что по части инициализации, а затем вновь уступает ядро ЦП, до тех пор, пока от кого-нибудь вновь не поступит запрос на создание потоков ядра. Давайте лучше проследим задачу 1, она гораздо интереснее.
Задача 1 начинается с kernel_init. Она выполняет ещё больше работы по инициализации ядра и, в частности, поднимает все драйверы устройств, а также монтирует либо initramfs, либо файловую систему, окончательно выбранную в качестве root. Затем, наконец, она вызывает run_init_process, чтобы покинуть режим ядра и выполнить программу init пользовательского пространства. Если init(1)
спрашивает ядро, а кто это пришёл, то ей сообщат, что это поток 1, относящийся к группе потоков 1. Или поток 1 в PID 1, если выражаться в обычной терминологии пользовательского пространства.
Меня удивило, что задача/pid 1 успевает сделать в ядре целую кучу работы, прежде, чем превратится в знакомый нам процесс пользовательского пространства. Значительный кусок той работы, которую я относил к загрузке ядра, строго говоря, происходит в PID 1, пусть и в совершенно ином «измерении» с точки зрения init(1)
из пользовательского пространства. Почему эти биты не относятся к задаче 0, в отличие от идущих ранее битов init
?
PID 0 в многоядерных системах
Если вы внимательно прочитали всё вышесказанное, то, вероятно, вас интересует ситуация с другими ядрами ЦП. До сих пор мы работали строго в однопоточном режиме, а после того, как инициализировали планировщик — прямо приказали ему закрепить задачу 0 за загрузочным ядром. Когда эта ситуация изменится?
Оказывается, в задаче 1! Первым делом kernel_init запускает все остальные ядра ЦП. Таким образом, в основной массе загрузочных операций, происходящих kernel_init, можно задействовать всю доступную мощность процессора, а не прозябать в единственном потоке. Запуск ядер ЦП — процесс довольно затейливый, но в рассматриваемом контексте нас наиболее интересует вызов smp_init. Эта функция, в свою очередь, вызывает fork_idle для каждого незагрузочного ядра, создаёт новый ожидающий поток и прикрепляет его к данному ядру.
Именно здесь термин «PID 0» окончательно размывается, поскольку у всех этих новых холостых задач ID потоков ненулевые, однако все они по-прежнему относятся к группе потоков 0. Поэтому, в терминологии пользовательского пространства, PID 0 — это процесс, у которого закреплено по одному потоку на каждое ядро, причём, поток 0 закреплён за загрузочным ядром.
Поправка (от автора): некоторые читатели указывали мне на ошибки в предыдущем абзаце! Благодарен вам за разъяснения, вот их суть. Как было упомянуто в моей предыдущей поправке, ожидающие задачи нужно оформлять как специальные случаи, и все ожидающие потоки на всех ядрах буквально «на одно лицо»: ID потока 0, ID группы потоков тоже 0. В коде есть несколько мест, где происходит подобное, всё дело в разных наборах полей, в которые записываются TID и TGID, но всё это происходит в пределах вызова fork_idle.
Первым делом fork_idle
вызывает функцию copy_process, чтобы сделать новую задачу, фактически — скопировать ту, что выполняется прямо сейчас. Как правило, в таком случае под новую задачу выделялся бы новый TID. Но существует специальный случай, в котором мы обходимся без выделения новой struct pid
, если с вызывающей стороны поступит сигнал, что там сейчас как раз делается задача для ожидания. Затем fork_idle
вызывает init_idle_pids, которая затем явно сбрасывает все идентификаторы задачи, добиваясь соответствия init_struct_pid
, который обеспечивает идентичность init_task
. В результате, все задачи, пребывающие в состоянии ожидания на всех ядрах ЦП, оказываются идентичны той init_task
, которую мы проследили на раннем этапе загрузки ядра. Кроме того, все они имеют PID 0 в трактовке как пространства ядра, так и пользовательского пространства.
После этого smp_init выполняет bringup_nonboot_cpus, которая выводит архитектурно-специфичные серенады, чтобы разбудить все ядра. Запускаясь, каждое ядро также выполняет небольшую архитектурно-специфичную настройку, чтобы представиться, затем выполняет cpu_startup_entry
и do_idle
, точно такие же операции, которые загрузочное ядро проделывало с задачей 0. Теперь все ядра активны и могут выполнять задачи, а kernel_init может спокойно выполнять оставшуюся часть загрузки.
Выводы — это не моё
Вот и всё! В качестве резюме:
PID 0 действительно существует, это тот самый поток, который запускает ядро операционной системы, он предоставляется загрузочным ядром ЦП.
PID 0 выполняет ранние этапы инициализации ядра, а потом превращается в ожидающую задачу на загрузочном ядре ЦП и выполняет минимальную вспомогательную работу при планировании задач и управлении питанием.
PID 0 всё это делает с разной степенью изящества, но примерно одинаковыми широкими мазками, и так со времени возникновения первых ядер Unix. Можете почитать исходный код многих из них и убедиться в этом самостоятельно. Что, конечно же, круто.
PID 0 никак не связан с управлением памятью. В ранних ядрах Unix он делал в рамках планирования процессов некоторые вещи, касающиеся управления памятью. Минуло уже много десятилетий с тех пор, как PID 0 этим больше не занимается.
В Linux аббревиатура «PID» неоднозначна, так как в пользовательском пространстве и в пространстве ядра она обозначает разные вещи: в ядре это TID, а в пользовательском пространстве — TGID. На практике PID 0 обычно понимается в том смысле, в котором он фигурирует в пространстве ядра, поскольку ни одна из сущностей, слагающих PID 0, через традиционные API Unix в пользовательском пространстве не видна.
На многоядерных Linux-системах каждому ядру процессора присваивается ожидающий поток. Все эти ожидающие потоки относятся к группе потоков 0, которая в пользовательском пространстве называется PID 0. Также в ядре предусмотрен один специальный случай, и у всех этих потоков ID один и тот же — 0.
По-видимому, все справочные сайты в Интернете устроены одинаково: там перефразируют Википедию. Такие вещи, порой очень неловкие, выясняются, когда из Википедии в Интернет попадает некорректная информация, которая затем снова и снова повторяется на протяжении 16 лет.
Спасибо, что разделили со мной эту хронику и позволили рассказать, каких узлов мне довелось навязать в поисках ответов на короткие и странные вопросы.