Операционные системы

Теория: Сигналы: управление процессами через ядро

Одним из старейших и самых универсальных механизмов межпроцессного взаимодействия в UNIX являются сигналы. Это способ послать процессу короткое уведомление — событие, на которое он должен отреагировать. Сигналы не содержат данных, только сам факт: “что-то произошло”.

Когда одно приложение хочет уведомить другое, оно вызывает системный вызов kill(pid, сигнал). Несмотря на название, kill не обязательно «убивает» процесс — это просто способ послать ему сигнал, специальное уведомление, которое ядро доставляет целевому процессу.

Внутри ядра у каждого процесса есть очередь сигналов. Когда приходит сигнал, ядро ставит его в очередь и решает, что делать: вызвать зарегистрированный обработчик, применить действие по умолчанию (например, завершить процесс) или проигнорировать.

Есть важная особенность: обычные сигналы не накапливаются. Если тот же сигнал уже находится в очереди, повторные экземпляры просто отбрасываются. Это предотвращает «засорение» системы тысячами одинаковых уведомлений. Исключение составляют реальные временные сигналы (real-time signals), появившиеся позже — они очередь поддерживают и доставляются все по порядку.

На практике сигналы используются для управления жизненным циклом программ: обновления конфигурации (SIGHUP), мягкой остановки (SIGTERM), перезапуска, приостановки (SIGSTOP) или аварийного завершения (SIGKILL).

На практике сигналы используются для управления жизненным циклом программ: остановки, перезапуска, обновления конфигурации или аварийного завершения.

Пример:

sleep 100 &
kill -l

Команда kill -l показывает список всех сигналов и их номера. Наиболее важные из них:

  • SIGTERM (15) — штатное завершение. Программа может перехватить этот сигнал и корректно завершить работу, сохранив данные.
  • SIGKILL (9) — принудительное завершение. Ядро немедленно уничтожает процесс, минуя обработчики. Игнорировать его невозможно.
  • SIGINT (2) — прерывание с клавиатуры (Ctrl+C). Отправляется процессу на переднем плане терминала.
  • SIGHUP (1) — сигнал “обнови конфигурацию”. Часто используется демонами для перезапуска без остановки. По умолчанию SIGHUP завершает процесс (hangup). Демоны переопределяют поведение и используют его как «reload»
  • SIGSTOP / SIGCONT — временная остановка и возобновление выполнения.

Разберём этот пример пошагово — что происходит на уровне системы и зачем нужна каждая команда:

  • sleep 100 &
  • ps -ef | grep sleep
  • kill -SIGTERM <PID>
  1. sleep 100 &

    Команда sleep приостанавливает выполнение на указанное количество секунд (в данном случае — 100).

    Флаг & запускает её в фоне, то есть оболочка не ждёт завершения и сразу возвращает управление пользователю.

    В этот момент ядро создаёт новый процесс sleep, присваивает ему собственный PID, а bash сохраняет этот номер во внутренней переменной $! (можно вывести echo $!).

  2. ps -ef | grep sleep

    Команда ps -ef выводит список всех процессов в системе с их PID, PPID, пользователями и командами запуска.

    Опция -e (или --everyone) показывает процессы всех пользователей, -f (или --full) добавляет расширенную информацию — родителя (PPID), время запуска и аргументы. Конвейер | grep sleep фильтрует список, оставляя только строки с именем процесса sleep.

    Так можно быстро узнать PID нужного процесса, чтобы управлять им далее.

  3. kill -SIGTERM <PID>

    Команда kill не всегда «убивает» процесс — она отправляет сигнал. Здесь используется сигнал SIGTERM (номер 15), означающий «заверши работу корректно».

    После получения этого сигнала программа sleep прекращает выполнение, освобождает ресурсы и сообщает оболочке, что завершена.

    Если бы процесс игнорировал SIGTERM (некоторые демоны это делают), можно было бы использовать kill -9 <PID> — это SIGKILL, ядро переводит процесс в фазу завершения и освобождает ресурсы, но запись остаётся как зомби до wait() родителя. Если поток в непрерывном ожидании I/O (D), «не медленность» визуально откладывается.

Таким образом, эта тройка команд демонстрирует полный цикл управления процессом: создание (sleep &), поиск (ps | grep) и завершение (kill). Всё это работает через системные вызовы fork(), exec() и kill(), а ядро отслеживает их в таблице процессов, контролируя состояние каждого активного PID.

Сигналы и их обработка в Bash

В Linux процессы могут не только выполняться, но и взаимодействовать между собой с помощью сигналов. Сигнал — это короткое уведомление, которое ядро или другой процесс посылает для управления поведением программы. С помощью сигналов можно приостановить, возобновить, завершить или заставить процесс выполнить определённое действие.

Например, при нажатии Ctrl+C терминал посылает текущей программе сигнал SIGINT (Interrupt — прервать), а при выключении службы systemd передаёт процессу SIGTERM (Terminate — завершить). Если процесс завис, администратор может послать SIGKILL — принудительный сигнал, который не обрабатывается программой и немедленно завершает её на уровне ядра.

Обработка сигналов в Bash

Иногда нужно не просто завершить процесс, а выполнить перед этим какое-то действие: сохранить данные, вывести сообщение или освободить ресурсы. Для этого в bash существует команда trap, которая задаёт обработчик сигнала.

Пример простого обработчика:

#!/bin/bash
trap 'echo "Завершение работы..."; exit 0' SIGINT SIGTERM
echo "PID $$"
while true; do
  sleep 1
done

Разбор кода:

  • trap 'команды' SIGINT SIGTERM — назначает команды, которые нужно выполнить при получении указанных сигналов. Здесь это вывод сообщения и завершение скрипта через exit 0.
  • $$ — PID текущего процесса. Выводится, чтобы можно было послать сигнал вручную (kill -SIGTERM <PID>).
  • Бесконечный цикл while true; do sleep 1; done имитирует работу сервиса, который ждёт события. Если отправить этому скрипту SIGINT (нажать Ctrl+C) или SIGTERM, вместо мгновенного прерывания выполнится обработчик, и программа завершится аккуратно:
Завершение работы...

Без trap она бы просто оборвалась — ядро уничтожило бы процесс без возможности выполнить завершающие действия.

Как это работает на уровне ядра

Когда процесс получает сигнал, ядро проверяет, зарегистрирован ли для него обработчик. Если да — управление передаётся функции, указанной через trap. Если нет — выполняется стандартное поведение:

  • SIGTERM и SIGINT — завершение процесса,
  • SIGHUP — завершение и потеря сессии,
  • SIGKILL и SIGSTOP — всегда принудительные, игнорировать их невозможно.

Сигналы передаются только между процессами одного пользователя, если только инициатор не root. Это ограничение встроено в ядро и предотвращает вмешательство обычных пользователей в чужие процессы.

Сигналы в службах и демонах

Механизм сигналов лежит в основе управления системными службами. Демоны — фоновые процессы вроде nginx, sshd, cron — обрабатывают сигналы как команды:

  • SIGHUP заставляет перечитать конфигурацию (без перезапуска);
  • SIGTERM инициирует мягкое завершение;
  • SIGUSR1 и SIGUSR2 используют для пользовательских событий (например, ротации логов).

Пример:

sudo kill -SIGHUP $(cat /var/run/nginx.pid)

Эта команда не «убивает» nginx, а заставляет его перечитать конфигурацию — graceful reload.

Взаимосвязь сигналов и действий системы

Сигналы сопровождают Linux на каждом уровне работы:

  • Ctrl+C в терминале → оболочка посылает SIGINT.
  • Остановка службы systemd → процессу отправляется SIGTERM.
  • Команда kill -9 → ядро применяет SIGKILL, без возможности перехвата. Именно через сигналы система остаётся управляемой: каждый процесс может завершаться осознанно, корректно очищая ресурсы и оставляя систему в стабильном состоянии.

Адресное пространство процесса

После создания ядро выделяет процессу виртуальное адресное пространство — набор адресов, отображаемых на физическую память. Каждый процесс видит свою память как одну непрерывную область, хотя на деле страницы могут быть разбросаны по разным участкам оперативной памяти или выгружены в swap.

В типичном процессе память делится на несколько областей:

  • text — исполняемый код программы (только для чтения);
  • data — глобальные и статические переменные;
  • heap — динамически выделяемая память (через malloc, realloc и другие вызовы);
  • stack — кадры вызовов функций и локальные переменные.

Посмотреть, как ядро видит память процесса, можно через виртуальную файловую систему /proc:

cat /proc/$$/maps | head

Здесь $$ — PID текущего процесса. Команда покажет список участков памяти и адреса, выделенные под код, стек, динамическую память и библиотеки.

PID, PPID и дерево процессов

Каждому процессу ядро присваивает PID (Process ID) — уникальный идентификатор. У него есть и PPID (Parent Process ID) — номер родителя, который его создал. Эта иерархия образует дерево процессов: systemd (PID 1) запускает оболочку, а та — пользовательские программы.

Посмотреть дерево можно командой:

pstree -p

А конкретные процессы — так:

ps -eo pid,ppid,stat,cmd | head

Поле STAT показывает состояние: R — выполняется, S — спит, Z — зомби. PID системных служб обычно хранятся в файлах /run/*.pid, например /run/nginx.pid.

Выполнение и планировщик

Процессор физически может выполнять только один поток на ядро, но ядро Linux создаёт иллюзию параллельности за счёт планировщика. Каждый процесс получает небольшой временной квант, после чего планировщик сохраняет его состояние (регистры, указатель стека, счетчик команд) и переключается на другой. Так создаётся многозадачность, при которой десятки процессов делят одно ядро, не мешая друг другу.

Приоритет процессов регулируется через параметр nice: чем меньше значение, тем выше приоритет. Диапазон — от -20 (максимальный) до 19 (минимальный).

ps -eo pid,pri,ni,cmd | head

Поле PRI показывает приоритет ядра, а NI — значение nice. Изменить приоритет можно командой:

nice -n 10 ./script.sh

Взаимодействие процессов (IPC)

Процессы изолированы друг от друга, но часто должны обмениваться данными. Для этого Linux предоставляет механизмы межпроцессного взаимодействия (IPC): каналы, очереди сообщений, разделяемую память, сокеты и сигналы.

Самый простой пример IPC — канал (pipe):

ps aux | grep ssh

ps выводит список процессов, а grep получает этот поток через канал, не создавая временных файлов. Канал существует только в памяти и исчезает сразу после завершения команд. Такая модель лежит в основе конвейеров (|), на которых построена вся философия UNIX.

Завершение процесса

Когда процесс достигает конца своей программы или вызывает системную функцию exit(), ядро начинает процедуру его завершения. Этот момент выглядит простым со стороны пользователя, но для системы — это цепочка точных действий, гарантирующих, что ресурсы не будут потеряны и данные не повредятся.

Ядро снимает с процесса все выделенные ресурсы: освобождает оперативную память, закрывает открытые файловые дескрипторы и сетевые сокеты, снимает блокировки. После этого запись о процессе удаляется из таблицы процессов, чтобы освободить место для новых PID. Прежде чем окончательно удалить запись, ядро посылает родительскому процессу сигнал SIGCHLD, уведомляя его, что один из потомков завершился. Родитель может обработать этот сигнал или вызвать системный вызов wait(), чтобы получить код завершения.

Если родитель не делает этого, запись остаётся в таблице — процесс считается зомби. Зомби не выполняет никаких действий, но удерживает PID и статус выхода, пока родитель не прочитает эти данные. Когда родитель сам завершается, «осиротевшие» зомби автоматически передаются процессу systemd (PID 1), который очищает их из таблицы. Так Linux поддерживает порядок в системе и предотвращает утечки идентификаторов.

Всё это можно наблюдать на практике. Если запустить длительный процесс в фоне:

sleep 60 &

оболочка сразу вернёт управление, а PID нового процесса можно узнать через:

ps -ef | grep sleep

Команда kill <PID> отправит сигнал SIGTERM, прося процесс завершиться корректно, закрыв свои ресурсы. Если процесс не реагирует — например, завис в системном вызове или игнорирует сигналы, — используется вариант kill -9 <PID>, который отправляет SIGKILL. Этот сигнал нельзя перехватить или проигнорировать: ядро немедленно удаляет процесс из памяти, не дожидаясь его реакции.

Для синхронного ожидания завершения всех фоновых задач используется команда wait. Она блокирует оболочку до тех пор, пока каждый из фоновых процессов не завершится:

sleep 3 &
wait
echo "Все процессы завершены"

После wait оболочка знает, что все дочерние процессы освободили ресурсы, и система находится в чистом состоянии.

Так Linux гарантирует, что ни один процесс не останется «подвешенным»: каждый завершённый экземпляр проходит через фиксированную процедуру освобождения памяти, уведомления родителя и очистки таблицы. Этот механизм делает многозадачность устойчивой и предсказуемой даже при тысячах параллельно работающих процессов.

Практическое наблюдение

Состояние системы можно изучать интерактивно с помощью top или htop. Обе программы показывают нагрузку процессора, использование памяти, PID, PPID и приоритеты.

top

Чтобы увидеть структуру процессов с приоритетами и временем выполнения:

ps -eo pid,ppid,pri,ni,stat,comm | head

Эти данные позволяют оценить, какие процессы активно используют CPU, какие ждут ввода-вывода и какие висят в фоне.

Что хранит ядро

Для ядра процесс — это не просто исполняющийся код, а сложная струк��ура данных. Каждый процесс в Linux представлен в памяти системой структур, главная из которых — `task_struct**. Эта структура описывает всё, что ядру нужно знать о процессе: его идентификаторы (PID, PPID, UID), состояние выполнения, приоритет, список открытых файлов, таблицы страниц памяти, используемые ресурсы и время работы на процессоре.

Кроме базовых полей, task_struct хранит указатели на родителя и потомков, ссылки на дескрипторы потоков, а также на объекты ядра, отвечающие за планирование, память и сигналы. Через неё планировщик получает доступ к данным о приоритете и времени последнего выполнения, менеджер памяти — к таблицам страниц и регионам виртуального адресного пространства, а подсистема сигналов — к обработчикам SIGINT, SIGKILL и других событий.

Каждый раз, когда ядро переключает контекст между процессами, оно сохраняет содержимое регистров и указателей стека из task_struct одного процесса и восстанавливает значения другого. Благодаря этому переключение происходит за микросекунды: ядро просто меняет активную структуру задачи и обновляет процессорные регистры.

Посмотреть, как ядро видит процессы, можно через /proc:

cat /proc/self/status | head

В этом файле отражены поля из task_struct: PID, PPID, состояние (State:), приоритет (nice, policy), использование памяти (VmRSS, VmSize) и даже количество открытых дескрипторов (FDSize). Эта информация динамически генерируется ядром — для каждого процесса она уникальна и обновляется в реальном времени.

Так task_struct становится сердцем всей многозадачности Linux. Через неё ядро знает, кто сейчас выполняется, сколько ресурсов он потребляет, и какие у него потомки. Без этой структуры система не смогла бы ни планировать выполнение, ни гарантировать изоляцию, ни безопасно завершать процессы.

Итог

Процесс в Linux — это изолированная среда выполнения, управляемая ядром. Он имеет собственное адресное пространство, идентификаторы, приоритет и состояние. Планировщик обеспечивает многозадачность, IPC связывает процессы между собой, а ядро следит за ресурсами и завершением.

Эта модель делает систему устойчивой и гибкой: каждое действие — от запуска браузера до работы сетевого демона — это процесс, рождённый, управляемый и завершённый ядром.

Рекомендуемые программы

+7 800 100 22 47

бесплатно по РФ

+7 495 085 21 62

бесплатно по Москве

108813 г. Москва, вн.тер.г. поселение Московский,
г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3
ОГРН 1217300010476
ИНН 7325174845