Пссс, парень, есть немного времени поговорить о пингвинах и их богатом внутреннем мире? Сегодня мы погрузимся еще глубже в устройство linux и разберем один из моих любимых вопросов на собеседованиях: «в чем разница между программой и процессом?».
Когда твоя мама называет системный блок «процессором», ей можно это простить. Она далека от этих терминов и не должна в них разбираться. Хорошо, что она умеет включать свой компьютер, заходить в браузер и смотреть видео с котятами.
Когда стажер-эникей не знает про switch context процессора, он просто еще не дорос до этого. Немного времени, энтузиазма и терпения, и он с твоей помощью разберется во всем.
Когда ты просишь системного администратора linux рассказать про программы и процессы, и он начинает «плыть», это тревожный звоночек.
Так в чем же разница?
В статье про оперативную память я постоянно повторял: «процессу выделяется», «процесс запрашивает», «ресурсы процесса». И ни разу не сказал про программы.
И все это не просто так.
Программа - это набор инструкций для процессора. Хранится в энергонезависимой памяти (если проще - на диске), но с самого диска не работают, а загружаются в оперативную память.
Сложно? Окей.
Программа - это кусок кода, который записан на диске, и который сам по себе не запустится.
Ну хорошо, в программе у нас хранится только код, но как нам его запустить? Вот тут мы и сталкиваемся с процессами.
Процесс - это совокупность программного кода и системных ресурсов.
Именно процессу выделяется процессорное время и оперативная память для запуска внутри себя программы.
Простыми словами: вы написали какой-то код на python, сохранили его в файле с расширением ".py". Это программа. А вот интерпретатор python, который запустил и выполнил ваш скрипт - это процесс.
Больше про программы говорить особо нечего, поэтому:
Поговорим о процессах.
Для начала нужно понять, кто вообще запускает процесс. Слышали, наверно, такой термин, как "ядро операционной системы"? Так вот, если коротко: ядро - это часть операционной системы, которое является интерфейсом взаимодействия между процессами и железом.
Еще проще - ядро - это PM в вашей команде, который общается и с пользователями (процессы) и с командой разработчиков (ресурсы железа). Он распределяет нагрузку, определяет важность задач и не позволяет одному пользователю загрузить всю команду.
Ядро создает процессы, выделяет им ресурсы и завершает их.
Итак, наш процесс должен как-то обратиться к ядру. Но каким образом?
Для этого существует язык взаимодействия (в linux это POSIX) и системные вызовы на этом языке.
fork() - создание процесса.
fork - это единственный способ создать процесс. Но создастся из ниоткуда он не может, поэтому новый процесс всегда клонирует родительский. Ладно, вру - не всегда. В системе при старте инициализируется 2 процесса без родителей: systemd в пространстве пользователя и kthreadd в пространстве ядра.
На скриншоте мы видим, что у процессов с PID 1 и PID 2 указан PPID (parent process id) 0.
- fork() создает сущность "процесс".
- Стартует в одном процессе, а завершается в двух.
- Дочернему процессу возвращается код 0 (значит, успешно создался).
- Родительскому процессу возвращается идентификатор (PID) дочернего процесса, который создался.
- В зависимости от того, что fork() вернул, процесс понимает, кто он.
exec() - загрузка программы в процесс.
Единственный функционал exec() - это замещение в процессе программного кода родителя кодом нового процесса.
exit() - завершение работы программы.
- Сообщает от том, что программа отработала.
- Внутри себя оставляет только код возврата, который доступен процессу-родителю (status 0 - успешно).
- Не завершает процесс. Процесс все еще остается в системе, имеет свой PID, но уже не занимает никаких ресурсов.
- Переводит процесс в статус "зомби".
wait() - считывание кода возврата из процесса.
- Родительский процесс ставит процесс на паузу.
- Считывает из процесса код возврата.
- Дочерний процесс завершается и полностью исчезает из системы.
По такой схеме происходит создание и завершение каждого процесса.
Состояния процессов.
Во время своей работы, процесс может принимать разные состояния. Важно понимать, что ни один процесс не будет выполняться от начала до конца без пауз. Ядро всегда будет основываться на приоритетах и приостанавливать процессы, которым в конкретный момент не нужно процессорное время.
Состояния всегда обозначаются одной буквой.
Давайте разбираться, что они означают:
R - running. Процесс выполняется в данный момент.
S - sleep. Процесс ожидает в режиме сна.
D - disk. Процесс ожидает ввода-вывода с диска. (в этом состоянии его невозможно завершить, и у системы растет LA)
T - terminated. Процесс остановлен.
Z - zombie. Процесс переведен в статус "зомби".
Зомби процессы.
Zombie (Z) - обязательное состояние абсолютно любого процесса.
Процесс переходит в статус зомби сразу после системного вызова exit() и находится в нем до вызова wait() от родительского процесса.
В данном состоянии процесс уже освободил все ресурсы, кроме своего PID.
Завершить zombie может только родитель, вручную этот процесс вы не сможете убрать никак. Ладно, вру. Если вам критически важно завершить зомби процесс - его нужно сделать сиротой.
Сирота - это процесс, который остался без "родителя".
- Вручную завершаем работу процесса-родителя.
- Зомби процесс становится "сиротой".
- Сирота становится моментально усыновлен процессом init (PID 1).
- Процесс init, ставший родителем, пошлет системный вызов wait()
- После системного вызова процесс, бывший зомби, завершится.
Когда-нибудь я научусь писать заключения в своих статьях, а пока что - всем любви и добра. Берегите себя и свои сервера.