Операционные системы
Теория: Прерывания и сигналы
Когда операционная система работает, она должна реагировать на всё, что происходит — нажатие клавиши, поступление пакета по сети, завершение процесса или ошибку устройства. Если бы она проверяла состояние каждого устройства вручную, всё зависало бы мгновенно. Вместо этого система использует прерывания и сигналы — механизмы, которые позволяют ей мгновенно реагировать на события, не дожидаясь очереди.
Аппаратные прерывания
Когда вы нажимаете клавишу на клавиатуре, в компьютере происходит целая цепочка аппаратных и программных событий. Внутри системной платы есть контроллер клавиатуры — раньше это был отдельный микросхемный чип вроде Intel 8042, теперь его функции встроены в южный мост чипсета. Этот контроллер фиксирует нажатие и посылает процессору сигнал — аппаратное прерывание (hardware interrupt).
Прерывание — это способ устройства «сказать» процессору: «Оставь всё, что ты сейчас делаешь, и обрати на меня внимание».
Процессор получает этот сигнал и немедленно приостанавливает выполнение текущей программы. Чтобы после обработки события можно было вернуться туда же, где выполнение было прервано, он сохраняет своё состояние: адрес текущей инструкции (счётчик команд), регистры, флаги и часть контекста в стек.
После этого процессор переходит в режим ядра (kernel mode) и обращается к контроллеру прерываний. На старых ПК использовался PIC (Programmable Interrupt Controller), где каждому устройству был жёстко назначен номер прерывания — IRQ (Interrupt Request Line). Например:
- системный таймер — IRQ0,
- клавиатура — IRQ1,
- диск — IRQ14.
Такая схема была простой, но негибкой: если добавить новое устройство, свободного номера IRQ могло не оказаться.
Современные системы используют контроллер нового поколения — APIC (Advanced Programmable Interrupt Controller). Он умеет динамически распределять прерывания между ядрами процессора, а вместо фиксированных номеров применяет векторы прерываний или механизмы MSI / MSI-X (Message Signaled Interrupts). Теперь сигнал не обязательно идёт «по проводу» — устройство просто посылает специальное сообщение контроллеру. Это позволяет:
- равномерно распределять нагрузку по ядрам,
- обслуживать современные шины — PCI Express, USB, NVMe,
- не зависеть от старой таблицы IRQ.
Когда сигнал от клавиатуры поступает в контроллер прерываний, APIC решает, какому ядру его передать, и сообщает: «Есть событие для IRQ1». Выбранное ядро приостанавливает текущие инструкции, сохраняет контекст и вызывает зарегистрированный в ядре обработчик для этого прерывания.
В Linux для клавиатуры это обычно функция keyboard_interrupt(). Её задача минимальна: считать скан-код (числовое значение клавиши), сбросить флаг «готовности» устройства и положить этот код в буфер ядра. Никакого отображения на экране на этом этапе не происходит — обработчик не знает, какая раскладка выбрана и в каком окне вы печатаете. Он просто записывает событие «нажата клавиша с кодом 0x1E».
Через несколько миллисекунд пользовательское приложение (например, оболочка или графическая среда) считывает этот буфер, преобразует скан-код в символ и отображает его на экране. Таким образом, весь путь от клавиши до буквы на экране проходит через аппаратное прерывание, контроллер, обработчик ядра и, наконец, программу пользователя.
Чтобы восстановить исходное состояние, процессор возвращает значения регистров и флагов, восстанавливает счётчик инструкций и продолжает выполнение с того же места, где был прерван. Для программы, которую прервали, всё выглядит так, будто между двумя инструкциями просто прошла доля микросекунды.
В многоядерных системах таких прерываний могут быть десятки тысяч в секунду, и контроллер APIC распределяет их между ядрами, чтобы ни одно не было перегружено. Этот механизм лежит в основе всей реакции системы на внешние события — от нажатия клавиши до передачи пакета по сети или завершения чтения с диска.
**Программные прерывания **
Если аппаратные прерывания приходят снаружи — от клавиатуры, таймера, сетевой карты, — то программные рождаются внутри процессора. Они происходят тогда, когда обычная программа обращается к ядру за чем-то, что ей самой делать нельзя.
Каждый процесс в операционной системе работает в пользовательском режиме (user mode). В этом режиме процесс защищён от прямого доступа к железу и к памяти ядра: он не может сам прочитать диск, выделить физическую память или обратиться к сетевой карте. Чтобы сделать что-то «привилегированное», процесс вызывает системный вызов (system call) — а это и есть форма программного прерывания.
Как это выглядит изнутри
Когда код программы выполняет, например, open("log.txt"), происходит целая цепочка событий:
- Функция
open()находится не в ядре, а в стандартной библиотеке (libcна Linux,kernel32.dllна Windows). Она подготавливает параметры вызова и вызывает инструкцию процессора, которая переключает CPU из пользовательского режима в режим ядра. - На архитектуре x86 это раньше было
int 0x80, а сегодня —syscall. Процессор останавливает выполнение пользовательского кода, сохраняет регистры и передаёт управление в ядро. - Ядро получает номер системного вызова (например, 2 — для
open) и аргументы. - Выполняется соответствующая функция в таблице системных вызовов (
sys_open()), ядро делает всё необходимое — проверяет права, находит файл, создаёт дескриптор. - После этого ядро возвращает результат обратно в процесс и переключает процессор в пользовательский режим.
То есть программное прерывание — это официальный способ программы “постучаться” в ядро. Оно не имеет ничего общего с ошибками или исключениями: это плановый, контролируемый переход.
Пример на ассемблере (Linux, x86)
Чтобы увидеть, что под капотом делает любой вызов write() или read(), можно написать маленький пример:
Здесь int 0x80 — это и есть программное прерывание. Оно переключает CPU в режим ядра, где Linux выполняет нужную системную функцию (sys_write), а затем возвращает результат обратно.
Пример на Python
В Python мы не видим прерываний напрямую, но они происходят за кулисами при каждом системном вызове. Например:
Когда вы вызываете os.write, Python не пишет файл сам — он вызывает системный вызов ядра Linux через программное прерывание syscall. Если посмотреть через strace, можно увидеть это вживую:
Вывод покажет, что Python вызывает именно системные вызовы:
Каждая строка — это переход из пользовательского режима в режим ядра, выполненный через программное прерывание.
Windows и macOS
В Windows та же идея, но механизм другой. Функции CreateFile, ReadFile, CreateProcess из kernel32.dll — лишь обёртки. Реальные вызовы делают переход в ядро через ntdll.dll, где вызываются внутренние функции NtCreateFile, NtReadFile, NtCreateProcess. Процессор переключается в режим ядра через инструкцию sysenter или syscall — тот же принцип, другая упаковка.
macOS использует тот же механизм, что Linux: системные вызовы через syscall, только таблица и номера другие. Если вызвать dtruss -p <PID> — это аналог strace, — можно увидеть, какие прерывания и системные вызовы выполняет конкретный процесс.
Сигналы: когда ядро общается с процессами
Если прерывания — это способ устройств обращаться к процессору, то сигналы — способ ядра обращаться к процессам. Сигнал — это короткое уведомление, которое операционная система посылает процессу, чтобы сообщить о событии или потребовать действия. Например:
- SIGINT — запрос прерывания (Ctrl+C);
- SIGTERM — вежливое завершение;
- SIGKILL — мгновенное уничтожение;
- SIGSTOP и SIGCONT — пауза и возобновление;
- SIGSEGV — ошибка доступа к памяти.
Когда вы нажимаете Ctrl+C в терминале, оболочка не «ломает» программу — она посылает ей сигнал SIGINT.
Если процесс не переопределил поведение, он завершается по умолчанию.
Работа с сигналами руками
Команда kill используется, чтобы послать сигнал процессу по PID:
По умолчанию kill посылает SIGTERM — предложение завершиться корректно.
Если процесс игнорирует сигнал, можно использовать kill -9, который соответствует SIGKILL.
Этот сигнал не перехватывается и всегда срабатывает.

