Все современные процессоры способны прерывать выполняющуюся в данный момент машинную инструкцию. Они сохраняют достаточно состояния (обычно, но не всегда, в стеке), чтобы впоследствии можно было возобновить выполнение, как будто ничего не произошло (прерванная инструкция будет перезапущена с нуля, как правило). Затем они начинают выполнять обработчик прерываний , который представляет собой просто машинный код, но размещается в специальном месте, чтобы процессор знал заранее, где он находится. Обработчики прерываний всегда являются частью ядра операционной системы: компонент, который работает с наибольшей привилегией и отвечает за контроль выполнения всех других компонентов. 1,2
Прерывания могут быть синхронными , что означает, что они запускаются самим ЦП как прямой ответ на то, что выполняла текущая инструкция, или асинхронными , что означает, что они происходят в непредсказуемое время из-за внешнего события, такого как данные, поступающие в сеть порт. Некоторые люди резервируют термин «прерывание» для асинхронных прерываний и вместо этого называют синхронные прерывания «ловушками», «ошибками» или «исключениями», но все эти слова имеют другие значения, поэтому я буду придерживаться «синхронного прерывания».
В настоящее время большинство современных операционных систем имеют представление о процессах . По своей сути, это механизм, с помощью которого компьютер может запускать более одной программы одновременно, но это также является ключевым аспектом того, как операционные системы конфигурируют защиту памяти , что является особенностью большинства (но, увы, еще не все ) современные процессоры. Это идет вместе с виртуальной памятью, которая является способностью изменять отображение между адресами памяти и фактическими местоположениями в RAM. Защита памяти позволяет операционной системе предоставлять каждому процессу свой частный фрагмент оперативной памяти, к которому имеет доступ только он. Это также позволяет операционной системе (действующей от имени какого-либо процесса) определять области ОЗУ как доступные только для чтения, исполняемые, совместно используемые группой взаимодействующих процессов и т. Д. Также будет фрагмент памяти, доступный только для ядро. 3
Пока каждый процесс обращается к памяти только так, как это разрешено центральным процессором, защита памяти невидима. Когда процесс нарушает правила, процессор сгенерирует синхронное прерывание, попросив ядро разобраться. Регулярно случается, что процесс на самом деле не нарушает правила, только ядру нужно проделать некоторую работу, прежде чем процесс можно будет продолжить. Например, если страницу памяти процесса необходимо «выселить» в файл подкачки, чтобы освободить место в ОЗУ для чего-то другого, ядро пометит эту страницу как недоступную. В следующий раз, когда процесс попытается его использовать, процессор сгенерирует прерывание защиты памяти; ядро извлечет страницу из раздела подкачки, вернет ее на прежнее место, снова отметит ее как доступную и возобновит выполнение.
Но предположим, что процесс действительно нарушил правила. Он пытался получить доступ к странице, на которой никогда не было отображено ОЗУ, или пытался выполнить страницу, которая помечена как не содержащая машинный код, или что-то еще. Семейство операционных систем, обычно известных как «Unix», использует сигналы для решения этой ситуации. 4 Сигналы похожи на прерывания, но они генерируются ядром и отправляются процессами, а не генерируются оборудованием и отправляются ядром. Процессы могут определять обработчики сигналовв своем собственном коде и сообщить ядру, где они находятся. Затем эти обработчики сигналов будут выполняться, прерывая нормальный поток управления, когда это необходимо. Все сигналы имеют номер и два имени, одно из которых является загадочным сокращением, а другое - несколько менее загадочной фразой. Сигнал, который генерируется, когда процесс нарушает правила защиты памяти, - это (по соглашению) номер 11, а его имена SIGSEGV
и «Ошибка сегментации». 5,6
Важное различие между сигналами и прерываниями заключается в том, что для каждого сигнала существует поведение по умолчанию . Если операционная система не может определить обработчики для всех прерываний, это ошибка в ОС, и весь компьютер рухнет, когда процессор попытается вызвать отсутствующий обработчик. Но процессы не обязаны определять обработчики сигналов для всех сигналов. Если ядро генерирует сигнал для процесса, и этот сигнал был оставлен в своем поведении по умолчанию, ядро просто пойдет дальше и сделает то, что по умолчанию, и не будет беспокоить процесс. Поведение большинства сигналов по умолчанию - «ничего не делать» или «завершить этот процесс и, возможно, также создать дамп ядра». SIGSEGV
является одним из последних.
Итак, подведем итог: у нас есть процесс, который нарушает правила защиты памяти. Процессор приостановил процесс и сгенерировал синхронное прерывание. Ядро выставило это прерывание и сгенерировало SIGSEGV
сигнал для процесса. Давайте предположим, что процесс не настроил обработчик сигнала SIGSEGV
, поэтому ядро выполняет поведение по умолчанию, которое заключается в прекращении процесса. Это имеет тот же эффект, что и _exit
системный вызов: открытые файлы закрываются, память освобождается и т. Д.
До этого момента ничто не распечатывало никаких сообщений, которые может видеть человек, и оболочка (или, в более общем случае, родительский процесс только что завершившегося процесса) вообще не была задействована. SIGSEGV
идет к процессу, который нарушил правила, а не к его родителю. Следующий шаг в последовательности, однако, является то, чтобы уведомить об этом родительском процессе , что его ребенок был прекращен. Это может произойти несколько различных способов, простейшие из которых являются , когда родитель уже ждет этого уведомления, используя один из wait
системных вызовов ( wait
, waitpid
, wait4
и т.д.). В этом случае ядро просто вызовет возврат этого системного вызова и предоставит родительскому процессу кодовый номер, называемый состоянием выхода., 7 Статус выхода информирует родителя, почему дочерний процесс был прерван; в этом случае он узнает, что дочерний процесс был прекращен из-за поведения SIGSEGV
сигнала по умолчанию .
Затем родительский процесс может сообщить о событии человеку, напечатав сообщение; программы оболочки почти всегда делают это. Ваш crsh
код для этого не включен, но в любом случае это происходит, потому что подпрограмма библиотеки C system
запускает полнофункциональную оболочку /bin/sh
«под капотом». crsh
является прародителем в этом сценарии; отправляется уведомление родительского процесса /bin/sh
, которое печатает свое обычное сообщение. Затем /bin/sh
сам завершает работу, так как ему больше нечего делать, и реализация библиотеки C system
получает это уведомление о выходе. Вы можете увидеть это уведомление о выходе в своем коде, проверив возвращаемое значениеsystem
; но это не скажет вам, что процесс внука умер в результате segfault, потому что он был поглощен промежуточным процессом оболочки.
Сноски
Некоторые операционные системы не реализуют драйверы устройств как часть ядра; однако все обработчики прерываний по-прежнему должны быть частью ядра, как и код, который настраивает защиту памяти, потому что аппаратное обеспечение не позволяет ничего, кроме ядра, делать эти вещи.
Может существовать программа под названием «гипервизор» или «менеджер виртуальной машины», которая даже более привилегирована, чем ядро, но для целей этого ответа ее можно считать частью аппаратного обеспечения .
Ядро - это программа , но это не процесс; это больше похоже на библиотеку. Все процессы время от времени выполняют части кода ядра в дополнение к своему собственному коду. Может быть несколько «потоков ядра», которые только исполняют код ядра, но они нас здесь не касаются.
Единственная ОС, с которой вам, скорее всего, придется иметь дело, которая не может считаться реализацией Unix, - это, конечно, Windows. Он не использует сигналы в этой ситуации. (Действительно, он не имеет сигналов; в Windows <signal.h>
интерфейс полностью подделан библиотекой C.) Вместо этого он использует то, что называется « структурной обработкой исключений ».
Некоторые нарушения защиты памяти генерируют SIGBUS
(«Ошибка шины») вместо SIGSEGV
. Граница между ними не указана и варьируется от системы к системе. Если вы написали программу, для которой определен обработчик SIGSEGV
, вероятно, будет хорошей идеей определить тот же обработчик SIGBUS
.
«Ошибка сегментации» - это имя прерывания, сгенерированного для нарушений защиты памяти одним из компьютеров, на которых работал исходный Unix , возможно, PDP-11 . « Сегментация » - это тип защиты памяти, но в настоящее время термин « ошибка сегментации » в общем относится к любому виду нарушения защиты памяти.
Все остальные способы, которыми родительский процесс может быть уведомлен о завершении дочернего процесса, заканчиваются вызовом родительского процесса wait
и получением статуса выхода. Просто сначала что-то происходит.
crsh
отличная идея для такого рода экспериментов. Спасибо, что сообщили нам всем об этом и идее.