Программа на диске и процесс в памяти
Мы много раз запускали команды и скрипты, но не задумывались о том, что происходит в системе между нажатием Enter и появлением результата. Пришло время посмотреть на эту сторону работы Linux.
Начнём с важного различия. Программа — это файл на диске, в котором записан исполняемый код. Когда мы набираем ls или bash, мы обращаемся к такому файлу. Но сам по себе файл ничего не делает. Он просто лежит в каталоге и ждёт. Когда мы запускаем программу, система создаёт процесс — работающий экземпляр программы, который получает память, идентификатор и право использовать процессор. Одна и та же программа может быть запущена несколько раз, и каждый запуск породит отдельный процесс со своими данными.
У каждого процесса есть числовой идентификатор PID — process identifier. Он назначается ядром при создании процесса и уникален среди всех живых процессов в данный момент. Кроме собственного PID, процесс знает и PPID — parent process identifier, идентификатор родительского процесса. Родитель — это тот процесс, который запустил данный. Для обычного запуска внешней команды Bash создаёт дочерний процесс. Встроенные команды, например cd, обычно выполняются в самой оболочке.
Текущую оболочку можно обозначить специальной переменной $$. Она раскрывается в PID самого Bash, в котором мы работаем. Проверим это командой ps. Сначала разберём её опции, потому что без них вывод будет малоинформативным. -p выбирает процессы по PID. Опция -o задаёт формат вывода: перечисляем через запятую, какие поля показать. Нам нужны pid, ppid и comm — последнее поле показывает имя команды. Соберём вызов:
ps -p $$ -o pid,ppid,comm
Вывод будет условным, идентификаторы зависят от среды:
PID PPID COMMAND
4242 4230 bash
Здесь 4242 — PID нашей оболочки, 4230 — PID её родителя. В WSL или Multipass родителем может быть процесс-терминал или служба, запустившая сеанс. Точные числа не важны, важна сама связь: у процесса есть идентификатор и есть родитель.
Посмотрим ещё на один процесс, который существует в любой Linux-системе, — процесс с PID 1:
ps -p 1 -o comm=
Эта команда выводит только имя команды, без заголовка. Знак равенства после имени поля убирает строку заголовка. Вывод покажет что-то вроде systemd или init. Мы не будем сейчас разбирать, что делает PID 1, — это тема отдельного урока о системных службах. Но сам факт наблюдения полезен: в системе есть процессы, которые запущены не нами, и у каждого из них своя роль.
Процесс на переднем плане
Теперь создадим собственный учебный процесс, за которым будем наблюдать. Для этого подойдёт команда sleep. Она делает очень простую вещь: приостанавливает работу на указанное число секунд. Если выполнить sleep 120, процесс будет ждать две минуты и только потом завершится. Для нас это удобно: процесс живёт достаточно долго, чтобы успеть его рассмотреть, и при этом ничего не делает и не меняет в системе.
Запустим sleep обычным способом:
sleep 120
Пока команда выполняется, приглашение терминала не возвращается. Мы не можем вводить следующие команды. Такой процесс называется процессом на переднем плане. Он занимает наш терминал и не отдаёт управление, пока не завершится. Прервать его можно комбинацией клавиш Ctrl+C. Это стандартный сигнал прерывания: терминал отправляет группе процессов переднего плана сигнал SIGINT, и sleep завершается. После этого приглашение возвращается.
Запуск в фоне
Ждать две минуты, ничего не делая, неудобно. Мы хотим, чтобы процесс работал, а терминал оставался свободным для других команд. Для этого запустим sleep с амперсандом в конце:
sleep 300 &
Амперсанд & говорит оболочке: запусти процесс в фоновом режиме. Оболочка выводит номер задания в квадратных скобках и PID процесса, после чего сразу возвращает приглашение. Мы можем вводить другие команды, пока sleep работает.
Сразу после запуска важно сохранить PID, потому что он понадобится нам дальше. Оболочка запоминает PID последнего фонового процесса в специальной переменной $!. Прочитаем её и заодно присвоим осмысленное имя, чтобы не потерять значение:
lesson_pid=$!
Теперь в переменной lesson_pid лежит PID нашего фонового sleep. Проверим, что процесс жив. Для этого у нас есть две команды. Первая — jobs:
jobs -l
Она показывает список фоновых заданий текущей оболочки. Опция -l добавляет PID. Вывод будет примерно таким:
[1]+ 5432 Running sleep 300 &
Здесь [1] — номер задания, 5432 — PID, Running — состояние. Не путайте номер задания и PID. Номер задания — это внутренний счётчик оболочки, он уникален только внутри текущего сеанса и обозначается со знаком процента: %1. PID же назначен ядром и уникален в системе. Нам сейчас нужен именно PID, он сохранён в lesson_pid.
Проверим процесс и через ps:
ps -p "$lesson_pid" -o pid,ppid,stat,comm
Вывод покажет PID, PID родителя, статус и имя команды:
PID PPID STAT COMMAND
5432 4242 S sleep
Поле STAT здесь S — процесс ожидает завершения таймера, он не активен. Родитель — наша оболочка с PID 4242. Всё сходится: мы породили процесс, он принадлежит нашей оболочке и спокойно ждёт.
Остановка, возврат на передний план и снова в фон
Пока наш sleep 300 работает в фоне, рассмотрим ещё один механизм управления. Запустим короткий процесс на переднем плане и остановим его, не завершая. Для этого подойдёт комбинация Ctrl+Z. Она отправляет процессу сигнал остановки. Процесс не завершается, а замирает и перестаёт потреблять процессорное время.
Выполним:
sleep 240
Через секунду-другую нажмём Ctrl+Z. Оболочка сообщит, что задание остановлено:
[2]+ Stopped sleep 240
Процесс продолжает существовать, но не работает. Он находится в состоянии T — остановлен. Теперь мы можем вернуть его в работу двумя способами. Команда bg продолжает выполнение задания, но в фоновом режиме:
bg
Без аргумента bg выбирает текущее задание оболочки, отмеченное знаком + в jobs; в нашем последовательном примере это только что остановленный sleep 240. После этого sleep 240 продолжит тикать, но уже в фоне. Вернём его на передний план командой fg:
fg
Теперь задание снова занимает терминал, и мы ждём его завершения или прерываем по Ctrl+C. В нашем сценарии удобнее прервать: мы уже убедились, что остановка и возврат работают. Нажмём Ctrl+C, и задание завершится. Если в оболочке есть другие задания, сначала при доступном приглашении проверьте jobs -l и укажите фактический номер учебного задания, например fg %2. Пока задание занимает передний план, новая команда jobs в этой оболочке не выполняется.
Важно работать только со своими процессами. bg и fg без аргументов действуют на задания текущей оболочки, но могут выбрать другое ваше задание, если порядок работы отличался от примера. Сейчас у нас в фоне остался первый sleep 300, сохранённый в lesson_pid.
Завершаем фоновый процесс сигналом
У нас остался один фоновый процесс — тот самый sleep 300, который мы запустили первым. Он нам больше не нужен, и урок не должен оставлять за собой работающие процессы. Перед отправкой сигнала ещё раз выполните jobs -l и ps -p "$lesson_pid" -o pid,ppid,stat,comm. Продолжайте только если PID принадлежит именно нашему ещё работающему дочернему sleep 300. Если он уже завершился за время чтения, пропустите kill: PID со временем может быть использован повторно.
Завершим подтверждённый учебный процесс командой kill:
kill -TERM "$lesson_pid"
Разберём, что здесь написано. kill — команда отправки сигналов. Первый аргумент -TERM указывает, какой сигнал отправить. SIGTERM — это стандартный запрос на завершение. Он не убивает процесс насильно, а вежливо просит его закончить работу. Большинство программ, получив SIGTERM, завершаются, освобождая ресурсы. Второй аргумент — PID процесса, которому адресован сигнал. Мы передаём сохранённое значение lesson_pid.
После отправки сигнала нужно убедиться, что процесс действительно завершился. Сразу утверждать это нельзя: сигнал — это запрос, и процесс может обработать его не мгновенно. Подождём завершения командой wait:
wait "$lesson_pid"
lesson_status=$?
printf 'Wait status: %s\n' "$lesson_status"
wait приостанавливает оболочку до тех пор, пока указанный процесс не завершится. Когда процесс закончился, команда вернёт управление. Мы сохранили статус сразу после wait. После SIGTERM в нашем Bash обычно получим 143, а после естественного завершения sleep — 0. Команда wait здесь ожидает своего дочернего процесса, а не произвольного PID системы.
Проверим, что процесса больше нет:
ps -p "$lesson_pid" -o pid,comm
Вывод покажет только заголовок, без строки процесса. Это и есть подтверждение, что наш sleep завершён. Если бы мы ошиблись в PID, ps вывела бы ошибку или чужой процесс — поэтому важно было сохранить PID сразу после запуска, не откладывая.
Почему не SIGKILL
В примере мы использовали SIGTERM, и это не случайно. Сигнал SIGTERM даёт процессу шанс завершиться корректно: закрыть файлы, дописать данные, освободить память. Сигнал SIGKILL действует иначе: его нельзя перехватить для выполнения собственной очистки; ядро завершает процесс, когда это возможно. Это последняя мера, когда процесс не реагирует на обычные запросы. Учебный sleep отлично завершается по SIGTERM, поэтому SIGKILL нам не нужен. Мы не практикуемся в жёстком завершении, потому что в реальной работе его стоит избегать.
Есть ещё команды killall и pkill, которые завершают процессы по имени, а не по PID. Мы сознательно их не используем. Они удобны, когда нужно затронуть много процессов, но опасны, если имя совпадает у разных задач. В учебной среде легко случайно завершить чужой sleep или собственный процесс, который не собирались трогать. PID точнее, и наш урок построен на нём.
Что будет с фоном после закрытия терминала
Мы запускали sleep в фоне, и он был привязан к нашей оболочке. Из этого следует важный вывод: фоновое задание не обязано пережить закрытие терминала. Когда сеанс завершается, оболочка умирает, и её дочерние процессы могут получить сигнал завершения. Это нормальное поведение по умолчанию.
Существуют инструменты, которые позволяют отвязать процесс от терминала. Команда nohup запускает программу так, чтобы она игнорировала сигнал потери терминала. Программы tmux и screen создают постоянные сеансы, которые можно отключать и подключать заново. Всё это — отдельные темы, и на этом уроке мы их не выполняем. Сейчас достаточно понимать: обычный & подходит для задач, которые завершатся в пределах сеанса, а для длительных процессов нужны другие механизмы.
Проверяем чистоту
Урок начался с двух учебных процессов, и к концу оба должны быть завершены. Первый sleep 300 мы остановили сигналом SIGTERM и дождались через wait. Второй sleep 240 был прерван комбинацией Ctrl+C после возврата на передний план. Проверим, что в списке заданий оболочки пусто:
jobs -l
Если других заданий не было, вывод станет пустым; иначе убедитесь, что исчезли именно наши учебные sleep, остальные задания оставьте как есть. Чужие процессы мы не трогали: все наши действия были адресованы только тем PID, которые мы сами создали и сохранили.
Мы разобрали разницу между программой на диске и процессом в памяти: процесс — это запущенный экземпляр программы, у которого есть PID и родительский PPID. Текущая оболочка обозначается $$, а последний фоновый процесс — $!. Мы запустили учебный sleep 300 с амперсандом, сразу сохранили его PID в переменную lesson_pid и наблюдали за ним через jobs -l и ps -p "$lesson_pid" -o pid,ppid,stat,comm. Остановка по Ctrl+Z, возврат в фон командой bg и на передний план командой fg показали, что заданием можно управлять, не завершая его. Номер задания в оболочке не равен PID: первый действует только внутри сеанса, второй уникален в системе.
Завершили мы собственный процесс сигналом SIGTERM через kill -TERM "$lesson_pid" и дождались выхода командой wait. SIGTERM — это запрос на завершение, который даёт процессу возможность корректно закончить работу. SIGKILL мы не использовали, потому что это крайняя мера для процессов, не отвечающих на обычные сигналы. Фоновое задание по умолчанию привязано к текущей оболочке и не обязано переживать закрытие терминала. В конце урока все учебные процессы завершены, чужие не тронуты.
Нужно запустить учебный sleep в фоне, проследить за его состоянием и корректно завершить, не оставив за собой работающих процессов. Важно различать номер задания оболочки и системный PID.
Номер задания и PID — не одно и то же
Вы запустили несколько фоновых процессов и смотрите на вывод jobs -l.
В строке [2]+ 5432 Running sleep 300 & что является PID?
Выбрать сигнал для вежливого завершения
У вас есть фоновый sleep, который нужно завершить. Процесс не завис, он просто больше не нужен.
Какой сигнал по умолчанию отправляет kill PID?
Жизненный цикл фонового процесса по заданному выводу
Вы запустили sleep 300 в фоне, но затем решили, что процесс не нужен. Дан вывод команд, по которому нужно восстановить корректную последовательность действий.
Дана последовательность вывода:
- [1]+ 5432 Running sleep 300 &
- PID PPID STAT COMMAND 5432 4242 S sleep
- 143
- PID COMMAND (только заголовок, без строки процесса) Опишите, какие команды могли дать эти выводы, в каком порядке и что они означают. Как вы убедитесь, что завершили именно свой процесс, а не чужой? Формат: связный текст с перечислением команд. Все PID в этом условии вымышлены. Команды нужно только восстановить в ответе, не выполнять их для этих чисел.