Прерывание системных вызовов при перехвате сигнала

29

Из чтения страниц справочника по вызовам read()и write()вызовам выясняется, что эти вызовы прерываются сигналами независимо от того, должны они блокировать или нет.

В частности, предположим

  • процесс устанавливает обработчик для некоторого сигнала.
  • устройство открыто (скажем, терминал) с O_NONBLOCK не установленным (т.е. работает в режиме блокировки)
  • затем процесс выполняет read()системный вызов для чтения с устройства и в результате выполняет путь управления ядром в пространстве ядра.
  • в то время как прецесс выполняет его read()в пространстве ядра, сигнал, для которого был установлен ранее обработчик, доставляется этому процессу, и вызывается его обработчик сигнала.

Читая справочные страницы и соответствующие разделы SUSv3 «Объем системных интерфейсов (XSH)» , можно обнаружить, что:

я. Если a read()прервано сигналом до того, как он прочитает какие-либо данные (т. errnoЕ. Ему пришлось заблокировать, потому что данные не были доступны), он возвращает -1 с установленным на [EINTR].

II. Если a read()прервано сигналом после того, как он успешно прочитал некоторые данные (т. Е. Было возможно немедленно начать обслуживание запроса), он возвращает количество прочитанных байтов.

Вопрос A): Правильно ли предположить, что в любом случае (блок / нет блока) доставка и обработка сигнала не полностью прозрачны для read()?

Случай I. кажется понятным, поскольку блокировка read()обычно переводит процесс в TASK_INTERRUPTIBLEсостояние, так что при доставке сигнала ядро ​​переводит процесс в TASK_RUNNINGсостояние.

Однако, когда read()блок не нужно блокировать (случай ii.) И обрабатывает запрос в пространстве ядра, я бы подумал, что поступление сигнала и его обработка будут прозрачными во многом как поступление и правильная обработка HW Прерывание будет. В частности, я бы предположил, что после доставки сигнала процесс будет временно переведен в пользовательский режим для выполнения его обработчика сигнала, из которого он в конечном итоге вернется, чтобы завершить обработку прерванного read()(в пространстве ядра), чтобы read()запустить его переход к завершению, после которого процесс возвращается к точке сразу после вызова read()(в пользовательском пространстве) со всеми доступными байтами, считанными в результате.

Но ii. кажется, подразумевает, что read()прервано, поскольку данные доступны сразу, но он возвращает возвращает только некоторые данные (вместо всех).

Это подводит меня ко второму (и последнему) вопросу:

Вопрос B): Если мое предположение в соответствии с пунктом А) является верным, почему read()прерывание происходит, даже если ему не нужно блокировать, потому что есть данные, доступные для немедленного удовлетворения запроса? Другими словами, почему read()не возобновляется после выполнения обработчика сигнала, что в конечном итоге приводит к возвращению всех доступных данных (которые были доступны в конце концов)?

darbehdar
источник

Ответы:

29

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

Вид реализации ОС

Рассмотрим, что произойдет, если системный вызов будет прерван сигналом. Обработчик сигнала выполнит код режима пользователя. Но обработчик syscall является кодом ядра и не доверяет никакому коду пользовательского режима. Итак, давайте рассмотрим варианты обработчика системного вызова:

  • Завершить системный вызов; сообщить, сколько было сделано для пользовательского кода. При желании код приложения перезапускает системный вызов каким-либо образом. Вот как работает Unix.
  • Сохраните состояние системного вызова и разрешите коду пользователя возобновить вызов. Это проблематично по нескольким причинам:
    • Во время выполнения пользовательского кода может произойти что-то, что сделает недействительным сохраненное состояние. Например, при чтении из файла файл может быть обрезан. Таким образом, коду ядра потребуется много логики для обработки этих случаев.
    • Нельзя позволить сохраненному состоянию сохранять какую-либо блокировку, потому что нет никакой гарантии, что код пользователя когда-либо возобновит системный вызов, и тогда блокировка будет сохранена навсегда.
    • Ядро должно предоставлять новые интерфейсы для возобновления или отмены текущих системных вызовов, в дополнение к обычному интерфейсу для запуска системного вызова. Это много осложнений для редкого случая.
    • Сохраненное состояние должно будет использовать ресурсы (по крайней мере, память); эти ресурсы должны быть выделены и удерживаться ядром, но должны учитываться при распределении процесса. Это не непреодолимо, но это осложнение.
      • Обратите внимание, что обработчик сигнала может выполнять системные вызовы, которые сами прерываются; поэтому вы не можете просто иметь статическое выделение ресурсов, охватывающее все возможные системные вызовы.
      • А что если ресурсы не могут быть распределены? Тогда системный вызов все равно должен потерпеть неудачу. Это означает, что приложение должно иметь код для обработки этого случая, поэтому такой дизайн не упростит код приложения.
  • Оставайтесь в процессе (но приостановлено), создайте новый поток для обработчика сигнала. Это, опять же, проблематично:
    • Ранние реализации Unix имели отдельный поток на процесс.
    • Обработчик сигнала рискует перешагнуть через ботинки системного вызова. В любом случае, это проблема, но в текущем дизайне Unix она содержится.
    • Ресурсы должны были бы быть выделены для нового потока; смотри выше.

Основное отличие от прерывания состоит в том, что код прерывания является доверенным и сильно ограниченным. Обычно не разрешается выделять ресурсы, или работать вечно, или блокировать и не освобождать их, или делать какие-либо другие неприятные вещи; Поскольку обработчик прерываний написан самим разработчиком ОС, он знает, что он не сделает ничего плохого. С другой стороны, код приложения может делать что угодно.

Представление дизайна приложения

Когда приложение прерывается в середине системного вызова, должен ли системный вызов продолжаться до завершения? Не всегда. Например, рассмотрим программу, подобную оболочке, которая читает строку из терминала, и пользователь нажимает Ctrl+C, вызывая SIGINT. Чтение не должно завершаться, вот в чем весь сигнал. Обратите внимание, что этот пример показывает, что readсистемный вызов должен быть прерываемым, даже если байт еще не был прочитан.

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

readСистемный вызов так , как это происходит потому , что это примитивно , что имеет смысл, учитывая общий дизайн операционной системы. Что это значит, грубо говоря, «читать как можно больше, до предела (размер буфера), но останавливаться, если что-то случится». Чтобы фактически прочитать полный буфер, readнужно запустить в цикле, пока не будет прочитано как можно больше байтов; это функция более высокого уровня fread(3). В отличие read(2)от системного вызова, freadэто библиотечная функция, реализованная в пользовательском пространстве поверх read. Подходит для приложения, которое читает файл или умирает при попытке; он не подходит для интерпретатора командной строки или для сетевой программы, которая должна корректно регулировать соединения, а также для сетевой программы, которая имеет параллельные соединения и не использует потоки.

Пример чтения в цикле представлен в системном программировании Linux Роберта Лава:

ssize_t ret;
while (len != 0 && (ret = read (fd, buf, len)) != 0) {
  if (ret == -1) {
    if (errno == EINTR)
      continue;
    perror ("read");
    break;
  }
  len -= ret;
  buf += ret;
}

Это заботится case iи case iiи еще немного.

Жиль "ТАК - перестань быть злым"
источник
Большое спасибо Жилю за очень краткий и ясный ответ, который подтверждает сходные взгляды, высказанные в статье о философии проектирования UNIX. Мне кажется очень убедительным, что поведение прерывания системного вызова связано с философией проектирования UNIX, а не с техническими ограничениями или препятствиями
Дарбехдар
@darbehdar Это все три: философия проектирования Unix (здесь в основном то, что процессы менее надежны, чем ядро ​​и могут выполнять произвольный код, а также что процессы и потоки не создаются неявно), технические ограничения (на распределение ресурсов) и дизайн приложения (там бывают случаи, когда сигнал должен отменить системный вызов).
Жиль "ТАК - перестань быть злым"
2

Чтобы ответить на вопрос А :

Да, доставка и обработка сигнала не совсем прозрачны для read().

read()Работает на полпути может занимать несколько ресурсов , в то время как она прерывается сигналом. И обработчик сигнала может также вызывать другие read()(или любые другие безопасные асинхронные сигналы ). Таким образом, read()прерванный сигналом должен быть сначала остановлен, чтобы высвободить ресурсы, которые он использует, в противном случае read()вызываемый из обработчика сигнала получит доступ к тем же ресурсам и вызовет повторяющиеся проблемы.

Потому что системные вызовы, отличные от тех, которые read()могут быть вызваны из обработчика сигнала, могут также занимать идентичный набор ресурсов, как и read()раньше Чтобы избежать повторяющихся проблем, описанных выше, самый простой и безопасный способ - останавливать прерывание read()каждый раз, когда происходит сигнал во время его работы.

Джастин
источник