Перерва системних дзвінків при вході сигналу


29

Після читання довідкових сторінок на дзвінках read()та write()дзвінках видно, що ці дзвінки перериваються сигналами незалежно від того, чи потрібно їх блокувати чи ні.

Зокрема, припустимо

  • процес встановлює обробник для деякого сигналу.
  • пристрій відкрито (скажімо, термінал) з O_NONBLOCK не встановленим (тобто працює в режимі блокування)
  • Потім процес робить read()системний виклик для зчитування з пристрою і в результаті виконує шлях управління ядром у просторі ядра.
  • в той час як прецесія виконує його read()в просторі ядра, сигнал, для якого обробник був встановлений раніше, передається до цього процесу і викликається його обробник сигналу.

Читаючи підручні сторінки та відповідні розділи у SUSv3 "Об'єм системних інтерфейсів (XSH)" , можна виявити , що:

i. Якщо read()сигнал переривається сигналом, перш ніж він зчитує будь-які дані (тобто його потрібно було заблокувати, оскільки відсутні дані), він повертає -1 із errnoвстановленим на [EINTR].

ii. Якщо read()сигнал переривається сигналом після того, як він успішно прочитав деякі дані (тобто можна було негайно почати обслуговування запиту), він повертає кількість прочитаних байтів.

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

Справа i. здається зрозумілим, оскільки блокування read()звичайно розміщує процес у TASK_INTERRUPTIBLEстані, так що коли сигнал подається, ядро ​​переводить процес у TASK_RUNNINGстан.

Однак, коли read()не потрібно блокувати (випадок II.) І обробляти запит у просторі ядра, я б подумав, що надходження сигналу та його обробка буде прозорим, як і прихід і правильне поводження з HW переривання було б. Зокрема, я вважав би, що після доставки сигналу процес буде тимчасово переведений в режим користувача, щоб виконати його обробник сигналу, з якого він повернеться в кінцевому підсумку, щоб закінчити обробку перерваного read()(в ядрі-просторі), щобread() запустити його Курс до завершення, після чого процес повертається до пункту одразу після виклику до read()(у користувальницькому просторі), у результаті чого всі наявні байти читаються.

Але іі. Здається, означає, щоread() це перервано, оскільки дані доступні негайно, але вони повертають, повертає лише деякі дані (замість усіх).

Це підводить мене до мого другого (і остаточного) питання:

Питання B): Якщо моє припущення під пунктом A) правильне, чому read()переривання переривається, хоча його не потрібно блокувати, оскільки доступні дані для задоволення запиту негайно? Іншими словами, чому read()після відновлення обробника сигналу не поновлюється, зрештою, в результаті повертаються всі наявні дані (які були доступні врешті)?

Відповіді:


29

Короткий зміст: ви вірні, що прийом сигналу не є прозорим, ні у випадку, коли я (перерваний, не прочитавши нічого), ні у випадку ii (перерваний після часткового читання). Якщо в іншому випадку мені потрібно буде внести кардинальні зміни як в архітектуру операційної системи, так і в архітектуру додатків.

Вигляд реалізації ОС

Поміркуйте, що станеться, якщо системний виклик перерваний сигналом. Обробник сигналу виконає код у режимі користувача. Але обробник syscall є кодом ядра і не довіряє жодному коду в режимі користувача. Тож давайте вивчимо вибір обробника syscall:

  • Припинити системний виклик; повідомити, скільки було зроблено для коду користувача. Код програми залежить від необхідності певним чином перезапустити системний виклик. Ось так працює Unix.
  • Збережіть стан системного виклику та дозвольте коду користувача відновити виклик. Це проблематично з кількох причин:
    • Поки користувальницький код працює, може статися щось, що втратить збережений стан. Наприклад, якщо читати з файлу, файл може бути усічений. Таким чином, коду ядра знадобиться багато логіки для обробки цих випадків.
    • Збереженому стану не можна дозволити зберігати жодний замок, оскільки немає гарантії, що код користувача коли-небудь відновить системний виклик, і тоді блокування буде зберігатися назавжди.
    • Ядро повинно відкрити нові інтерфейси для відновлення або скасування поточних системних викликів, крім звичайного інтерфейсу для запуску системного виклику. Це дуже багато ускладнень для рідкісного випадку.
    • Збережений стан повинен використовувати ресурси (принаймні пам'ять); ці ресурси повинні бути виділені та утримуватись ядром, але відраховуватись від розподілу процесу. Це непереборне, але це ускладнення.
      • Зверніть увагу, що обробник сигналів може здійснювати системні дзвінки, які самі перериваються; тож ви не можете просто мати статичний розподіл ресурсів, який охоплює всі можливі системні дзвінки.
      • А що робити, якщо ресурси не можна виділити? Тоді систематичний виклик все одно повинен вийти з ладу. Що означає, що додатку потрібно мати код для обробки цього випадку, тому ця конструкція не спростить код програми.
  • Залишається в процесі роботи (але призупинено), створіть нову нитку для обробника сигналу. Це, знову ж таки, проблематично:
    • Ранні реалізації Unix мали один потік на один процес.
    • Обробник сигналу ризикує переступити над взуттям системи. Це питання все одно, але в нинішньому дизайні Unix він міститься.
    • Необхідно було б виділити ресурси для нової нитки; Дивись вище.

Основна відмінність переривання полягає в тому, що код переривання є довірчим і сильно обмеженим. Зазвичай заборонено виділяти ресурси, або запускатись назавжди, або брати замки і не випускати їх, або робити якісь інші неприємні речі; оскільки обробник переривань написаний самим реалізатором ОС, він знає, що це нічого поганого не зробить. З іншого боку, код програми може робити все, що завгодно.

Вид дизайну програми

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

Тому програма повинна сказати, що ядро ​​скаже системний виклик. У дизайні unix це відбувається автоматично: сигнал робить повернення syscall. Інші конструкції потребують способу відновлення або скасування системного виклику за своїм бажанням.

readСистемний виклик так , як це відбувається тому , що це примітивно , що має сенс, враховуючи загальний дизайн операційної системи. Це означає, приблизно, "читайте стільки, скільки можете, до межі (розмір буфера), але зупиняйтесь, якщо трапиться щось інше". Насправді прочитати повний буфер означає запуск readциклу, поки не буде прочитано стільки, скільки можливо байтів; це функція вищого рівня, fread(3). На відміну від read(2)системного виклику fread- це функція бібліотеки, реалізована в просторі користувача поверхread . Він підходить для програми, яка читає файл або вмирає, намагаючись; це не підходить для інтерпретатора командного рядка або для мережевої програми, яка повинна чітко дроселювати з'єднання, а також для мережевої програми, яка має одночасні з'єднання і не використовує потоки.

Приклад читання в циклі наведено в системному програмуванні Linux Robert Love:

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

@darbehdar Це все три: філософія дизайну Unix (тут головним чином, що процеси менш довіряють, ніж ядро, і можуть запускати довільний код, також, що процеси та потоки створюються невірно), технічні обмеження (щодо розподілу ресурсів) та дизайн додатків (там є випадки, коли сигнал повинен скасувати системний виклик).
Жиль 'ТАК - перестань бути злим'

2

Щоб відповісти на запитання A :

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

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

Оскільки системні виклики, крім того, що read()можна було б викликати з обробника сигналів, вони також можуть займати ідентичний набір ресурсів read(). Щоб уникнути проблем, пов'язаних з рецензуванням, найпростіша, найбезпечніша конструкція - це зупиняти переривання read()щоразу, коли сигнал відбувається під час його запуску.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.